fix: harden owner bootstrap and auth persistence

This commit is contained in:
2026-06-23 18:33:51 +02:00
parent 5df5194651
commit a2272c5df6
13 changed files with 481 additions and 143 deletions
+4 -17
View File
@@ -43,20 +43,13 @@ jobs:
- name: Prepare .env
run: |
set -euo pipefail
HOST_OWNER_PASSWORD=$(docker run --rm -v "${DEPLOY_PATH}:/host-deploy:ro" alpine:latest sh -c "grep '^OWNER_PASSWORD=' /host-deploy/.env | cut -d= -f2-" 2>/dev/null || true)
if [ -z "${HOST_OWNER_PASSWORD}" ]; then
echo "ERROR: OWNER_PASSWORD not found in ${DEPLOY_PATH}/.env"
exit 1
fi
printf 'POSTGRES_DB=nexus\n' > "${ENV_TMPFILE}"
printf 'POSTGRES_USER=nexus\n' >> "${ENV_TMPFILE}"
printf 'POSTGRES_PASSWORD=%s\n' "${ENV_POSTGRES_PASSWORD}" >> "${ENV_TMPFILE}"
printf 'JWT_KEY=%s\n' "${ENV_JWT_KEY}" >> "${ENV_TMPFILE}"
printf 'JWT_ISSUER=nexus\n' >> "${ENV_TMPFILE}"
printf 'JWT_AUDIENCE=nexus-web\n' >> "${ENV_TMPFILE}"
printf 'OWNER_EMAIL=vmbao62@hotmail.de\n' >> "${ENV_TMPFILE}"
printf 'OWNER_PASSWORD=%s\n' "${HOST_OWNER_PASSWORD}" >> "${ENV_TMPFILE}"
printf 'OWNER_DISPLAY_NAME=\n' >> "${ENV_TMPFILE}"
printf 'BOOTSTRAP_OWNER_EMAIL=vmbao62@hotmail.de\n' >> "${ENV_TMPFILE}"
printf 'OPENCLAW_BASE_URL=http://host.docker.internal:18789\n' >> "${ENV_TMPFILE}"
printf 'OPENCLAW_GATEWAY_TOKEN=%s\n' "${ENV_OPENCLAW_TOKEN}" >> "${ENV_TMPFILE}"
printf 'OPENCLAW_GATEWAY_PASSWORD=\n' >> "${ENV_TMPFILE}"
@@ -82,16 +75,10 @@ jobs:
printf 'trap "rm -f /tmp/nexus-deploy-env" EXIT\n' >> "$SCRIPT"
printf 'cat > /tmp/nexus-deploy-env\n' >> "$SCRIPT"
printf '\n' >> "$SCRIPT"
printf '# ── Graceful shutdown (preserves DB volume integrity) ──\n' >> "$SCRIPT"
printf 'docker compose --env-file /tmp/nexus-deploy-env stop postgres 2>/dev/null || true\n' >> "$SCRIPT"
printf 'docker compose --env-file /tmp/nexus-deploy-env down --remove-orphans 2>/dev/null || true\n' >> "$SCRIPT"
printf 'docker rm -f nexus-postgres-1 nexus-api-1 nexus-web-1 2>/dev/null || true\n' >> "$SCRIPT"
printf '\n' >> "$SCRIPT"
printf 'PG_VOL=$(docker volume ls -q --filter name=nexus-postgres 2>/dev/null | head -1)\n' >> "$SCRIPT"
printf 'if [ -n "$PG_VOL" ]; then\n' >> "$SCRIPT"
printf ' echo "Checking postgres WAL integrity..."\n' >> "$SCRIPT"
printf ' docker run --rm -v "$PG_VOL:/var/lib/postgresql/data" --entrypoint sh postgres:17-alpine -c "pg_resetwal -f /var/lib/postgresql/data && echo WAL reset OK" 2>&1 || echo "pg_resetwal failed (may be benign)"\n' >> "$SCRIPT"
printf 'else\n' >> "$SCRIPT"
printf ' echo "Postgres volume not found - will be created fresh"\n' >> "$SCRIPT"
printf 'fi\n' >> "$SCRIPT"
printf 'echo "Postgres volume preserved (nexus-postgres) — no WAL reset"\n' >> "$SCRIPT"
printf '\n' >> "$SCRIPT"
printf 'echo "Deploying all services"\n' >> "$SCRIPT"
printf 'docker compose --env-file /tmp/nexus-deploy-env build --no-cache\n' >> "$SCRIPT"
+12 -35
View File
@@ -52,9 +52,8 @@ jobs:
ENV_POSTGRES_PASSWORD: ${{ secrets.ENV_POSTGRES_PASSWORD }}
ENV_JWT_KEY: ${{ secrets.ENV_JWT_KEY }}
ENV_OPENCLAW_TOKEN: ${{ secrets.ENV_OPENCLAW_TOKEN }}
# OWNER_PASSWORD is read from the host's persistent .env — NOT from a Gitea secret.
# This ensures the password stays consistent across deploys and the DB is the
# single source of truth after initial seed (enforced by SeedAudit guard).
# Owner password is not injected at deploy time.
# After first seed, the database is the only password source.
steps:
# ═══════════════════════════════════════════════════
@@ -113,37 +112,25 @@ jobs:
echo "mutated_main=false" >> "$GITEA_OUTPUT"
# ═══════════════════════════════════════════════════
# Step 4: Build .env from secrets + host .env (SAFE)
# Step 4: Build .env from secrets (SAFE)
#
# Secrets are written to /tmp/nexus-deploy-env — NEVER
# to a file inside the workspace that gets rsync'd to
# the host. The temp file is deleted immediately after
# compose operations complete.
#
# OWNER_PASSWORD is read from the host's persistent .env
# to ensure it stays the single source of truth. Other
# secrets (POSTGRES_PASSWORD, JWT_KEY, OPENCLAW_TOKEN)
# Owner password is deliberately omitted so production deploys
# cannot overwrite the persisted DB password.
# Other secrets (POSTGRES_PASSWORD, JWT_KEY, OPENCLAW_TOKEN)
# come from Gitea secrets.
# ═══════════════════════════════════════════════════
- name: Prepare .env (secrets + host .env → temp file)
- name: Prepare .env (secrets → temp file)
run: |
set -euo pipefail
# Read OWNER_PASSWORD from the host's persistent .env
HOST_OWNER_PASSWORD=""
if [ -f "${DEPLOY_PATH}/.env" ]; then
HOST_OWNER_PASSWORD=$(grep '^OWNER_PASSWORD=' "${DEPLOY_PATH}/.env" | cut -d= -f2- || true)
fi
if [ -z "${HOST_OWNER_PASSWORD}" ]; then
echo "❌ OWNER_PASSWORD not found in ${DEPLOY_PATH}/.env"
echo " The host .env is the single source of truth for the owner password."
echo " Ensure OWNER_PASSWORD is set in the deploy-path .env before deploying."
exit 1
fi
cat > "${ENV_TMPFILE}" <<EOF
# Nexus Production Environment — auto-generated by CD pipeline
# Managed via Gitea Secrets + host .env → do NOT edit manually on the host.
# Managed via Gitea Secrets → do NOT edit manually.
# This file lives in /tmp and is removed after deploy completes.
POSTGRES_DB=nexus
POSTGRES_USER=nexus
@@ -151,9 +138,7 @@ jobs:
JWT_KEY=${ENV_JWT_KEY}
JWT_ISSUER=nexus
JWT_AUDIENCE=nexus-web
OWNER_EMAIL=vmbao62@hotmail.de
OWNER_PASSWORD=${HOST_OWNER_PASSWORD}
OWNER_DISPLAY_NAME=
BOOTSTRAP_OWNER_EMAIL=vmbao62@hotmail.de
OPENCLAW_BASE_URL=http://host.docker.internal:18789
OPENCLAW_GATEWAY_TOKEN=${ENV_OPENCLAW_TOKEN}
OPENCLAW_GATEWAY_PASSWORD=
@@ -205,18 +190,10 @@ set -e
trap 'rm -f /tmp/nexus-deploy-env' EXIT
cat > /tmp/nexus-deploy-env
# ── Clean up zombie containers ──
# ── Graceful shutdown (preserves DB volume integrity) ──
docker compose --env-file /tmp/nexus-deploy-env stop postgres 2>/dev/null || true
docker compose --env-file /tmp/nexus-deploy-env down --remove-orphans 2>/dev/null || true
docker rm -f nexus-postgres-1 nexus-api-1 nexus-web-1 2>/dev/null || true
# ── WAL recovery ──
PG_VOL=$(docker volume ls -q --filter name=nexus-postgres 2>/dev/null | head -1)
if [ -n "$PG_VOL" ]; then
echo "Checking postgres WAL integrity..."
docker run --rm -v "$PG_VOL:/var/lib/postgresql/data" --entrypoint sh postgres:17-alpine -c "pg_resetwal -f /var/lib/postgresql/data && echo 'WAL reset OK'" 2>&1 || echo "pg_resetwal failed (may be benign)"
else
echo "Postgres volume not found - will be created fresh"
fi
echo "Postgres volume preserved (nexus-postgres) — no WAL reset"
BUILD_ARGS="${DEPLOY_BUILD_ARGS:-}"
SERVICE="${DEPLOY_SERVICE:-}"
+3 -15
View File
@@ -94,22 +94,12 @@ jobs:
fi
# ═══════════════════════════════════════════════════
# Step 3: Prepare .env from secrets + host .env (safe temp file)
# Step 3: Prepare .env from secrets (safe temp file)
# ═══════════════════════════════════════════════════
- name: Prepare .env (secrets + host .env → temp file)
- name: Prepare .env (secrets → temp file)
run: |
set -euo pipefail
# Read OWNER_PASSWORD from the host's persistent .env
HOST_OWNER_PASSWORD=""
if [ -f "${DEPLOY_PATH}/.env" ]; then
HOST_OWNER_PASSWORD=$(grep '^OWNER_PASSWORD=' "${DEPLOY_PATH}/.env" | cut -d= -f2- || true)
fi
if [ -z "${HOST_OWNER_PASSWORD}" ]; then
echo "❌ OWNER_PASSWORD not found in ${DEPLOY_PATH}/.env"
exit 1
fi
cat > "${ENV_TMPFILE}" <<EOF
# Nexus Production Environment — auto-generated by CD pipeline
POSTGRES_DB=nexus
@@ -118,9 +108,7 @@ jobs:
JWT_KEY=${ENV_JWT_KEY}
JWT_ISSUER=nexus
JWT_AUDIENCE=nexus-web
OWNER_EMAIL=vmbao62@hotmail.de
OWNER_PASSWORD=${HOST_OWNER_PASSWORD}
OWNER_DISPLAY_NAME=
BOOTSTRAP_OWNER_EMAIL=vmbao62@hotmail.de
OPENCLAW_BASE_URL=http://host.docker.internal:18789
OPENCLAW_GATEWAY_TOKEN=${ENV_OPENCLAW_TOKEN}
OPENCLAW_GATEWAY_PASSWORD=