diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a62ddc5..8b08219 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -346,6 +346,16 @@ jobs: # demo-mode or api-key, so the overlay is acceptable here. COMPOSE_FILES=(-f docker-compose.yml -f docker-compose.demo.yml) + # Phase 2 SEC-H3 (2026-05-13): the demo overlay sets + # CERTCTL_DEMO_MODE_ACK=true; the SEC-H3 fail-closed guard + # requires a paired CERTCTL_DEMO_MODE_ACK_TS within the last + # 24h (a static YAML value would rot). The overlay reads + # ${CERTCTL_DEMO_MODE_ACK_TS:-} from the shell, so we mint a + # fresh timestamp here and export it for every compose + # invocation in this job (initial up-d AND the force-recreate + # at step 4). + export CERTCTL_DEMO_MODE_ACK_TS="$(date +%s)" + log "1/4 down -v --remove-orphans" docker compose "${COMPOSE_FILES[@]}" down -v --remove-orphans 2>&1 | tail -3 || true @@ -359,7 +369,15 @@ jobs: log "4/4 minting day-0 admin (proves migration ladder + bootstrap path)" TOKEN="$(openssl rand -base64 32 | tr -d '\n')" - echo "CERTCTL_BOOTSTRAP_TOKEN=$TOKEN" > /tmp/_smoke.env + { + echo "CERTCTL_BOOTSTRAP_TOKEN=$TOKEN" + # Re-emit the demo-mode ACK TS into the --env-file so the + # force-recreate at step 4 inherits it. `--env-file` REPLACES + # the shell-env source for variable interpolation on compose + # operations that use it, so omitting this line would re-trip + # the SEC-H3 guard. + echo "CERTCTL_DEMO_MODE_ACK_TS=$CERTCTL_DEMO_MODE_ACK_TS" + } > /tmp/_smoke.env docker compose "${COMPOSE_FILES[@]}" --env-file /tmp/_smoke.env up -d --force-recreate certctl-server 2>&1 | tail -2 sleep 5 wait_for_service_healthy certctl-server diff --git a/deploy/demo-up.sh b/deploy/demo-up.sh new file mode 100755 index 0000000..50c10d3 --- /dev/null +++ b/deploy/demo-up.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# deploy/demo-up.sh — boot the certctl demo stack with the fresh +# CERTCTL_DEMO_MODE_ACK_TS the Phase 2 SEC-H3 guard requires. +# +# The demo overlay sets CERTCTL_DEMO_MODE_ACK=true. Phase 2 SEC-H3 +# (2026-05-13) pairs that with a fail-closed requirement: the server +# refuses to start unless CERTCTL_DEMO_MODE_ACK_TS= is set +# and is within the last 24h (with 1-minute future clock-skew tolerance). +# +# A static value in docker-compose.demo.yml would rot the next day, so +# the overlay passthroughs the value from the shell environment. This +# helper mints a fresh TS at run time and forwards any extra args to +# `docker compose up`, so operators can use it as a drop-in replacement +# for the bare command. Example: +# +# ./demo-up.sh -d # cold boot in detached mode +# ./demo-up.sh -d --pull always # forward any flags through +# +# The cold-DB compose smoke in .github/workflows/ci.yml does the same +# thing inline; this script exists so local operators don't have to +# remember the export. + +set -euo pipefail + +# cd to the deploy/ dir so the relative `-f` paths resolve regardless +# of where the operator invokes this from. The script lives next to +# the compose files it references. +cd "$(dirname "$0")" + +export CERTCTL_DEMO_MODE_ACK_TS="$(date +%s)" + +echo "[demo-up] minting CERTCTL_DEMO_MODE_ACK_TS=$CERTCTL_DEMO_MODE_ACK_TS" +echo "[demo-up] running: docker compose -f docker-compose.yml -f docker-compose.demo.yml up $*" + +exec docker compose \ + -f docker-compose.yml \ + -f docker-compose.demo.yml \ + up "$@" diff --git a/deploy/docker-compose.demo.yml b/deploy/docker-compose.demo.yml index 6de9abc..10e939d 100644 --- a/deploy/docker-compose.demo.yml +++ b/deploy/docker-compose.demo.yml @@ -5,8 +5,15 @@ # Layered on top of the production-shaped base (docker-compose.yml) to give # operators a one-command, zero-config demo path: # -# docker compose -f deploy/docker-compose.yml \ -# -f deploy/docker-compose.demo.yml up -d --build +# deploy/demo-up.sh -d --build +# +# (which forwards args to `docker compose up` after exporting the fresh +# CERTCTL_DEMO_MODE_ACK_TS that Phase 2 SEC-H3 requires). Equivalent +# manual invocation: +# +# CERTCTL_DEMO_MODE_ACK_TS=$(date +%s) docker compose \ +# -f deploy/docker-compose.yml \ +# -f deploy/docker-compose.demo.yml up -d --build # # What this overlay does: # @@ -14,6 +21,10 @@ # request is served as the synthetic admin actor `actor-demo-anon`; # the server emits a prominent ⚠ DEMO MODE WARN banner at boot with # a production-promotion checklist (cmd/server/main.go::emitDemoBanner). +# Phase 2 SEC-H3 (2026-05-13) pairs DEMO_MODE_ACK with a required +# DEMO_MODE_ACK_TS within the last 24h. The overlay reads +# ${CERTCTL_DEMO_MODE_ACK_TS:-} from the shell — use deploy/demo-up.sh +# (which exports a fresh TS) instead of bare `docker compose up`. # # 2. Flips CERTCTL_KEYGEN_MODE=server (the demo issues + holds the key on # the server to keep the dashboard populated; production deploys must @@ -48,8 +59,7 @@ # To start fresh (wipe previous data): # docker compose -f deploy/docker-compose.yml \ # -f deploy/docker-compose.demo.yml down -v -# docker compose -f deploy/docker-compose.yml \ -# -f deploy/docker-compose.demo.yml up -d --build +# deploy/demo-up.sh -d --build services: postgres: @@ -67,6 +77,17 @@ services: # reminds the operator on every start. CERTCTL_AUTH_TYPE: none CERTCTL_DEMO_MODE_ACK: "true" + # Phase 2 SEC-H3 (2026-05-13): DEMO_MODE_ACK=true requires a fresh + # DEMO_MODE_ACK_TS within the last 24h. The overlay can't hardcode + # a timestamp (it would rot the next day), so we passthrough from + # the shell. Operators set this via: + # CERTCTL_DEMO_MODE_ACK_TS=$(date +%s) docker compose \ + # -f docker-compose.yml -f docker-compose.demo.yml up -d + # The cold-DB smoke + any helper script (deploy/demo-up.sh, when + # it lands) export this before invoking compose. Empty value + # fails the SEC-H3 guard with a clear operator-facing error + # message pointing at this line. + CERTCTL_DEMO_MODE_ACK_TS: "${CERTCTL_DEMO_MODE_ACK_TS:-}" # Server-side keygen so the demo can populate the dashboard with # full lifecycle history. Production deploys leave this at the # code default `agent` (CertctlAgent generates ECDSA P-256 keys