Files
intotheeast-com/Makefile
T
m038andClaude Opus 4.8 9295914238 docs: capture history-rewrite-under-live-git-sync + secrets-audit tooling
Update the git-sync secret-exposure solution doc with today's operational
lesson: untracking an already-committed secret under a live bidirectional
sync. Covers the direction:both force-push-revert trap, the freeze-every-
server-first sequence, audit-before-reset (authoritative secret in env/),
the stale origin/main ref + sparse-checkout gotchas, and the ignore:-field
mechanism.

Add Makefile targets that supported the fix:
- remote-secrets-audit: secret-safe (existence + size + git ls-files, never
  contents) audit of config/ vs env/<host>/config secret locations
- remote-content-status: also show the .gitignore diff git-sync regenerates

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 19:25:32 +02:00

253 lines
14 KiB
Makefile

# Local/shared config — always loaded. Keep remote credentials OUT of here;
# those live in .env.test / .env.prod. (docker compose also reads .env directly
# for ${UID}/${GID} substitution and the travel-memories env_file.)
-include .env
# Remote config — loaded only when targeting an environment. ENV is set
# automatically by the env-suffixed remote targets (e.g. `make remote-install-prod`);
# each .env.<ENV> holds a full, self-contained set of remote vars.
ENV ?=
-include .env.$(ENV)
export
REMOTE_PORT ?= 22
SSH := ssh -p $(REMOTE_PORT) $(REMOTE_USER)@$(REMOTE_HOST)
WEBROOT ?= $(REMOTE_HOME)/public_html
SITE_CONFIG_DIR ?= $(REMOTE_HOME)/site-config
# Hostname Grav uses to pick its per-environment config (user/env/<host>/).
# Defaults to the SSH host; override in .env.<ENV> only if the web hostname
# Grav sees differs from the SSH host (e.g. an addon domain on a shared box).
WEB_HOST ?= $(REMOTE_HOST)
# ── Environment guard + generated per-env remote targets ──────────────────────
# Every remote-* target below gains `-test` / `-prod` variants, e.g.
# make remote-install-prod → runs remote-install with ENV=prod
# Calling a bare remote target (no ENV) fails via guard-env.
REMOTE_TARGETS := remote-env-setup remote-env-remove remote-wipe remote-install \
remote-fetch remote-fetch-content remote-install-plugins remote-update-plugins \
remote-upgrade-grav remote-git-sync-disable remote-git-sync-enable \
remote-content-status remote-clean remote-diag remote-apply-env \
remote-seed-api-salt remote-secrets-audit \
remote-gpm-install remote-maintenance-on remote-maintenance-off
ENVS := test prod
guard-env:
@test -n "$(ENV)" || { echo "ERROR: no environment. Use an env-suffixed target, e.g. 'make remote-install-prod'."; exit 1; }
@test -f ".env.$(ENV)" || { echo "ERROR: missing .env.$(ENV)"; exit 1; }
define make-env-target
$(1)-$(2): ; @$$(MAKE) --no-print-directory $(1) ENV=$(2)
endef
$(foreach t,$(REMOTE_TARGETS),$(foreach e,$(ENVS),$(eval $(call make-env-target,$(t),$(e)))))
# ── Tests ─────────────────────────────────────────────────────────────────────
# Local test account — auto-created, never committed (see user/.gitignore).
# Keep the password free of shell/Make/URL-special chars so every consumer agrees.
GRAV_TEST_USER ?= testrunner
GRAV_TEST_PASS ?= Testpass1234
test-account:
@docker exec intotheeast_grav sh -c 'test -f /var/www/html/user/accounts/$(GRAV_TEST_USER).yaml \
|| php bin/plugin login new-user -u $(GRAV_TEST_USER) -p "$(GRAV_TEST_PASS)" \
-e $(GRAV_TEST_USER)@example.test -N "Test Runner" -P b --admin-type both -s enabled -n'
test-config:
@bash scripts/test-form-config.sh
test-post: test-account
@bash scripts/test-post.sh
test-ui: test-account
@npx playwright test
test: test-config test-post test-ui
# ── Local dev ──────────────────────────────────────────────────────────────────
build:
docker compose build
build-assets:
docker run --rm \
-v $(PWD)/user/themes/intotheeast:/app \
-w /app node:20-alpine \
sh -c "npm install && npm run build"
start:
docker compose up -d
stop:
docker compose down
setup: build start install-plugins fix-perms
fix-perms:
docker exec intotheeast_grav bash -c "getent passwd 1000 > /dev/null || useradd -u 1000 -M hostuser"
docker exec intotheeast_grav chown -R 1000:1000 /var/www/html
docker exec intotheeast_grav apachectl graceful
install-plugins:
docker exec -w /var/www/html intotheeast_grav php bin/gpm install $(shell cat plugins.txt | tr '\n' ' ') -y
# ── Demo content ──────────────────────────────────────────────────────────────
demo-load:
# Load italy-2026-demo trip (create pages if absent)
docker exec intotheeast_grav bash -c "\
mkdir -p /var/www/html/user/pages/01.trips/italy-2026-demo/01.dailies /var/www/html/user/pages/01.trips/italy-2026-demo/04.stories && \
cp /var/www/html/user/docs/demo/trips/italy-2026-demo/trip.md /var/www/html/user/pages/01.trips/italy-2026-demo/trip.md 2>/dev/null || true && \
cp /var/www/html/user/docs/demo/trips/italy-2026-demo/stories.md /var/www/html/user/pages/01.trips/italy-2026-demo/04.stories/stories.md 2>/dev/null || true && \
cp -r /var/www/html/user/docs/demo/trips/italy-2026-demo/04.stories/. /var/www/html/user/pages/01.trips/italy-2026-demo/04.stories/ 2>/dev/null || true && \
cp -r /var/www/html/user/docs/demo/trips/italy-2026-demo/dailies/. /var/www/html/user/pages/01.trips/italy-2026-demo/01.dailies/ && \
cp /var/www/html/user/docs/demo/trips/italy-2026-demo/*.gpx /var/www/html/user/pages/01.trips/italy-2026-demo/ 2>/dev/null || true && \
chown -R 1000:1000 /var/www/html/user/pages/01.trips/italy-2026-demo && \
cd /var/www/html && php bin/grav clearcache"
demo-reset:
docker exec intotheeast_grav bash -c "rm -rf /var/www/html/user/pages/01.trips/italy-2026-demo && cd /var/www/html && php bin/grav clearcache"
pixelfed-import:
docker exec intotheeast_grav bash -c "which python3 || apt-get install -y python3 --no-install-recommends -q"
docker cp /home/mischa/Nextcloud/Downloads/pixelfed/pixelfed-statuses.json intotheeast_grav:/tmp/pixelfed-statuses.json
docker cp scripts/pixelfed-import.py intotheeast_grav:/tmp/pixelfed-import.py
docker exec -w /var/www/html intotheeast_grav python3 /tmp/pixelfed-import.py
# ── Content sync (user repo ↔ Gitea) ──────────────────────────────────────────
content-push:
git -C user push origin main
content-pull:
git -C user pull origin main
# ── Remote credentials ─────────────────────────────────────────────────────────
remote-env-setup: guard-env
@$(SSH) "printf 'GITEA_HOST=%s\nGITEA_USER=%s\nGITEA_TOKEN=%s\n' \
'$(GITEA_HOST)' '$(GITEA_USER)' '$(GITEA_TOKEN)' > ~/.env-intotheeast && chmod 600 ~/.env-intotheeast"
@echo "Credentials written to server. Run 'make remote-env-remove' when done."
remote-env-remove: guard-env
@$(SSH) "rm -f ~/.env-intotheeast"
@echo "Credentials removed from server."
# ── Remote: initial install ────────────────────────────────────────────────────
remote-wipe: guard-env
$(SSH) "cd $(WEBROOT) && rm -rf assets backup bin cache images logs system tmp vendor webserver-configs index.php .htaccess CHANGELOG.md LICENSE.txt README.md"
remote-install: guard-env
$(SSH) "WEBROOT=$(WEBROOT) \
SITE_CONFIG_DIR=$(SITE_CONFIG_DIR) \
USER_REPO=$(USER_REPO) \
MAIN_REPO=$(MAIN_REPO) \
GRAV_VERSION=$(GRAV_VERSION) \
PLUGINS='$(shell cat plugins.txt | tr '\n' ' ')' \
GITEA_HOST=$(GITEA_HOST) \
GITEA_USER=$(GITEA_USER) \
GITEA_TOKEN=$(GITEA_TOKEN) \
bash -s" < scripts/server-install.sh
# ── Remote: ongoing maintenance ────────────────────────────────────────────────
remote-fetch: guard-env
$(SSH) "git -C $(SITE_CONFIG_DIR) checkout main && git -C $(SITE_CONFIG_DIR) pull"
remote-fetch-content: guard-env
$(SSH) "git -C $(WEBROOT)/user fetch origin main && git -C $(WEBROOT)/user sparse-checkout disable && git -C $(WEBROOT)/user reset --hard origin/main"
remote-install-plugins: guard-env
$(SSH) "cd $(WEBROOT) && php bin/gpm index -f && php bin/gpm install $(shell cat plugins.txt | tr '\n' ' ') -y"
remote-update-plugins: guard-env
$(SSH) "cd $(WEBROOT) && php bin/gpm update -y && php bin/grav cache"
remote-upgrade-grav: guard-env
$(SSH) "cd $(WEBROOT) && php bin/gpm self-upgrade -y && php bin/grav cache"
remote-git-sync-disable: guard-env
$(SSH) "bash -s -- '$(WEBROOT)' false" < scripts/git-sync-toggle.sh
remote-git-sync-enable: guard-env
$(SSH) "bash -s -- '$(WEBROOT)' true" < scripts/git-sync-toggle.sh
remote-content-status: guard-env
$(SSH) "cd $(WEBROOT)/user && echo '--- HEAD ---' && git log -1 --oneline && echo '--- working tree ---' && git status --short && echo '--- config diff ---' && git diff -- config/ && echo '--- .gitignore diff ---' && git diff -- .gitignore"
remote-clean: guard-env
$(SSH) "cd $(WEBROOT) && php bin/grav clearcache"
# Install a single GPM package on the server (e.g. git-sync, which is
# intentionally NOT in plugins.txt — it is remote-only).
# Usage: make remote-gpm-install-prod PKG=git-sync
remote-gpm-install: guard-env
@test -n "$(PKG)" || { echo "ERROR: set PKG=<plugin-slug>"; exit 1; }
$(SSH) "cd $(WEBROOT) && php bin/gpm index -f && php bin/gpm install $(PKG) -y && php bin/grav clearcache"
# Deploy per-environment Grav config overrides to the server's
# user/env/<WEB_HOST>/config/ tree (deep-merged over the committed config).
# Source of truth: deploy/env/$(ENV)/system.yaml (version-controlled). This
# tree is outside the content repo, so it is NOT restored by content sync —
# re-run after any fresh install.
remote-apply-env: guard-env
@test -f deploy/env/$(ENV)/system.yaml || { echo "ERROR: missing deploy/env/$(ENV)/system.yaml"; exit 1; }
@host="$${WEB_HOST:-$(REMOTE_HOST)}"; \
test -n "$$host" || { echo "ERROR: WEB_HOST/REMOTE_HOST unresolved"; exit 1; }; \
$(SSH) "mkdir -p $(WEBROOT)/user/env/$$host/config && cat > $(WEBROOT)/user/env/$$host/config/system.yaml && cd $(WEBROOT) && php bin/grav clearcache" < deploy/env/$(ENV)/system.yaml; \
echo "Applied deploy/env/$(ENV)/system.yaml -> $(WEBROOT)/user/env/$$host/config/system.yaml"
# Seed a per-host popularity salt into the env override tree so the api plugin
# reads it there instead of appending one to the git-tracked config/plugins/
# api.yaml. That appended salt kept the content working tree dirty, which broke
# git-sync's auto-merge on webhook. Salt is generated server-side and never
# committed (a committed salt would be globally known). Idempotent: an existing
# salt is kept, so re-running never rotates it.
remote-seed-api-salt: guard-env
@host="$${WEB_HOST:-$(REMOTE_HOST)}"; \
test -n "$$host" || { echo "ERROR: WEB_HOST/REMOTE_HOST unresolved"; exit 1; }; \
$(SSH) "set -e; \
envfile=$(WEBROOT)/user/env/$$host/config/plugins/api.yaml; \
mkdir -p \$$(dirname \"\$$envfile\"); \
if grep -qE '^[[:space:]]*salt:' \"\$$envfile\" 2>/dev/null; then \
echo \"salt already present in \$$envfile — keeping it\"; \
else \
salt=\$$(openssl rand -hex 32); \
printf 'popularity:\n salt: %s\n' \"\$$salt\" > \"\$$envfile\"; \
echo \"seeded new per-host salt into \$$envfile\"; \
fi; \
git -C $(WEBROOT)/user checkout -- config/plugins/api.yaml 2>/dev/null || true; \
cd $(WEBROOT) && php bin/grav clearcache >/dev/null 2>&1 || true; \
echo '--- base api.yaml status (expect clean) ---'; \
git -C $(WEBROOT)/user status --short config/plugins/api.yaml; \
echo '(if the line above is empty, the tree is clean)'"
# Read-only health check: plugin install state, versions, key config, log tail.
remote-diag: guard-env
$(SSH) "cd $(WEBROOT) && \
echo '=== Grav version ==='; php bin/grav --version 2>/dev/null; \
echo '=== installed plugin versions ==='; for p in login admin2 flex-objects form api; do printf '%s: ' \"\$$p\"; grep -m1 '^version:' user/plugins/\$$p/blueprints.yaml 2>/dev/null || echo '(NOT installed)'; done; \
echo '=== what does GPM say about api? ==='; php bin/gpm info api 2>&1 | head -12; \
echo '=== api override (enabled/route/session) ==='; grep -nE '^enabled:|^route:|session_enabled:' user/config/plugins/api.yaml 2>&1; \
echo '=== per-env override present? ==='; for f in user/env/*/config/system.yaml; do echo \"\$$f:\"; cat \"\$$f\" 2>/dev/null | grep -E 'cache:|debug:|auto_reload:'; done; \
echo '=== twig cache populating? (non-empty => cache on) ==='; ls cache/twig/ 2>/dev/null | head -1 || echo '(empty)'; \
echo '=== git-sync config (secrets redacted) ==='; grep -vaiE 'password|token|secret' user/config/plugins/git-sync.yaml user/env/*/config/plugins/git-sync.yaml 2>/dev/null; \
echo '=== grav.log tail ==='; tail -8 logs/grav.log 2>/dev/null"
# Secret-safe audit: lists WHERE per-host secret/config files live (config/ vs
# env/<host>/config/) and their sizes — never prints contents. Used to decide
# whether a `reset --hard` would clobber a live runtime secret.
remote-secrets-audit: guard-env
$(SSH) "cd $(WEBROOT)/user && \
echo '=== tracked in git? (git ls-files) ==='; git ls-files config/security-private.php config/security.yaml config/versions.yaml config/plugins/api-private.php config/plugins/git-sync.yaml; \
echo '=== config/ copies (size only) ==='; ls -la config/security.yaml config/security-private.php config/versions.yaml config/plugins/api-private.php config/plugins/git-sync.yaml 2>&1; \
echo '=== env/<host>/config copies (size only) ==='; ls -la env/*/config/security.yaml env/*/config/security-private.php env/*/config/plugins/api-private.php env/*/config/plugins/git-sync.yaml 2>&1; \
echo '=== does security.yaml reference the private php? (key names only) ==='; grep -aoE '^[a-z_]+:' config/security.yaml 2>/dev/null; for f in env/*/config/security.yaml; do echo \"\$$f:\"; grep -aoE '^[a-z_]+:' \"\$$f\" 2>/dev/null; done; true"
remote-maintenance-on: guard-env
$(SSH) "bash -s on $(WEBROOT)" < scripts/server-maintenance.sh
remote-maintenance-off: guard-env
$(SSH) "bash -s off $(WEBROOT)" < scripts/server-maintenance.sh