diff --git a/.github/workflows/seal-sms-secrets.yml b/.github/workflows/seal-sms-secrets.yml index 997ac54b5..ee429e170 100644 --- a/.github/workflows/seal-sms-secrets.yml +++ b/.github/workflows/seal-sms-secrets.yml @@ -1,15 +1,15 @@ name: Seal Twilio + SMS secrets -# Dispatching service that seals the FOUR keys phone-2FA / sms-service needs into -# the existing `fuzefront-secrets` SealedSecret, WITHOUT any plaintext ever -# appearing in chat, a PR diff, or a build log. Same pattern as -# seal-consumer-secret.yml (kubeseal --merge-into preserves every other key). +# Dispatching service that seals the keys phone-2FA / sms-service needs into the +# existing `fuzefront-secrets` SealedSecret, WITHOUT any plaintext ever appearing +# in chat, a PR diff, or a build log (kubeseal --merge-into preserves every other +# key). # # WHO PROVIDES WHAT — the governance split (deploy/contabo/SEAL_PROD_SECRETS.md): -# - The Twilio values are EXTERNAL vendor credentials. They are read here -# from GitHub *repo secrets* the OWNER sets — never held by an agent, never -# pasted anywhere. Auth uses a Twilio API Key (SK… SID + secret), NOT the -# account Auth Token. Set them once (Settings → Secrets and variables → Actions): +# - The Twilio values are EXTERNAL vendor credentials, read here from GitHub +# *repo secrets* the OWNER sets — never held by an agent, never pasted +# anywhere. Auth uses a Twilio API Key (SK… SID + secret), NOT the account +# Auth Token. Set them once (Settings → Secrets and variables → Actions): # TWILIO_ACCOUNT_SID (starts AC…) # TWILIO_API_KEY_SID (starts SK…) # TWILIO_API_KEY_SECRET @@ -19,13 +19,21 @@ name: Seal Twilio + SMS secrets # (openssl rand -hex 32) and seals it, so both sides read the same value from # fuzefront-secrets. It is only minted when absent (or when rotate_sms_auth). # +# These are exactly the secretKeyRefs sms-service.yaml mounts: +# TWILIO_ACCOUNT_SID / TWILIO_API_KEY_SID / TWILIO_API_KEY_SECRET / +# TWILIO_VERIFY_SERVICE_SID / SMS_AUTH_SECRET. +# # This seal step is SAFE to run before go-live: adding the keys does NOT start any # pod — smsService.enabled is still false, so nothing renders. Enabling SMS is a # SEPARATE GitOps commit in a deploy window (flip smsService.enabled: true), kept # apart so the secret is provably present before the Deployment ever renders. # -# Requires the KUBE_CONFIG repo secret (base64 kubeconfig for the Contabo k3s -# cluster) — only to FETCH the sealed-secrets public cert; no decrypt key is used. +# NO cluster access is used: like deploy/scripts/seal-secret.sh, the sealed-secrets +# PUBLIC cert is fetched over HTTPS from CERT_URL, and the input Secret manifest is +# built inline (no kubectl). The commit to `master` goes through the same +# RELEASE_BUMP_SSH_KEY deploy key release.yml uses, because the "Protect Master" +# ruleset rejects a plain GITHUB_TOKEN push (Changes must be made through a pull +# request); the DeployKey is a bypass actor on that rule. on: workflow_dispatch: @@ -50,6 +58,11 @@ concurrency: permissions: contents: write +env: + # FuzeInfra publishes the sealed-secrets public cert here (same default as + # deploy/scripts/seal-secret.sh). Override via repo variable if the URL changes. + CERT_URL: https://sealed-secrets.prod.fuzefront.com/v1/cert.pem + jobs: seal: runs-on: ubuntu-latest @@ -82,19 +95,10 @@ jobs: fi # Light shape check (a wrong-shaped value is almost always a paste error). case "$TWILIO_ACCOUNT_SID" in AC*) :;; *) echo "::warning::TWILIO_ACCOUNT_SID does not start with 'AC' — double-check it.";; esac - case "$TWILIO_API_KEY_SID" in SK*) :;; *) echo "::warning::TWILIO_API_KEY_SID does not start with 'SK' — double-check it.";; esac + case "$TWILIO_API_KEY_SID" in SK*) :;; *) echo "::warning::TWILIO_API_KEY_SID does not start with 'SK' — double-check it.";; esac case "$TWILIO_VERIFY_SERVICE_SID" in VA*) :;; *) echo "::warning::TWILIO_VERIFY_SERVICE_SID does not start with 'VA' — double-check it.";; esac echo "::notice::All Twilio secrets are present." - - name: Set up kubectl - uses: azure/setup-kubectl@3e0aec4d80787158d6fc5badc4a6d671057d7b45 # v4 - - - name: Write kubeconfig - run: | - mkdir -p ~/.kube - echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > ~/.kube/config - chmod 600 ~/.kube/config - - name: Install kubeseal run: | KSVER=0.27.3 @@ -116,47 +120,50 @@ jobs: d=$(mktemp -d) trap 'rm -rf "$d"' EXIT - # Never echo values. Whitespace-trim each (paste artefacts are common). + # Never echo values. Whitespace-trim each (paste artefacts are common), + # then base64 for the Secret's data: block (base64 output is YAML-safe, + # so no quoting/escaping pitfalls regardless of the raw value). printf '%s' "$TWILIO_ACCOUNT_SID" | tr -d '[:space:]' > "$d/TWILIO_ACCOUNT_SID" printf '%s' "$TWILIO_API_KEY_SID" | tr -d '[:space:]' > "$d/TWILIO_API_KEY_SID" printf '%s' "$TWILIO_API_KEY_SECRET" | tr -d '[:space:]' > "$d/TWILIO_API_KEY_SECRET" printf '%s' "$TWILIO_VERIFY_SERVICE_SID" | tr -d '[:space:]' > "$d/TWILIO_VERIFY_SERVICE_SID" - # Always (re)seal the four Twilio keys. - FROM_FILE=( \ - --from-file=TWILIO_ACCOUNT_SID="$d/TWILIO_ACCOUNT_SID" \ - --from-file=TWILIO_API_KEY_SID="$d/TWILIO_API_KEY_SID" \ - --from-file=TWILIO_API_KEY_SECRET="$d/TWILIO_API_KEY_SECRET" \ - --from-file=TWILIO_VERIFY_SERVICE_SID="$d/TWILIO_VERIFY_SERVICE_SID" \ - ) - # SMS_AUTH_SECRET: mint only if absent, or if rotation was explicitly # requested. Re-minting rotates the shared token, so it must be opt-in — # Authentik and sms-service both restart to pick up a rotated value. + MINT_SMS=1 if grep -q 'SMS_AUTH_SECRET' "$MANIFEST" && [ "$ROTATE_SMS_AUTH" != "true" ]; then + MINT_SMS=0 echo "::notice::SMS_AUTH_SECRET already sealed — leaving it untouched (pass rotate_sms_auth=true to rotate)." else openssl rand -hex 32 | tr -d '\n' > "$d/SMS_AUTH_SECRET" - FROM_FILE+=( --from-file=SMS_AUTH_SECRET="$d/SMS_AUTH_SECRET" ) echo "::notice::SMS_AUTH_SECRET will be minted and sealed." fi - # Fetch the cluster's current public cert (no decrypt key needed). - kubeseal --fetch-cert \ - --controller-namespace kube-system \ - --controller-name sealed-secrets-controller \ - > "$d/cert.pem" - echo "::notice::Fetched sealed-secrets public cert." - - # Seal + merge into the existing SealedSecret (preserves every other key). - kubectl create secret generic fuzefront-secrets \ - -n fuzefront \ - "${FROM_FILE[@]}" \ - --dry-run=client -o yaml \ - | kubeseal \ - --cert "$d/cert.pem" \ - --format yaml \ - --merge-into "$MANIFEST" + # Fetch the sealed-secrets PUBLIC cert over HTTPS (no cluster/kubeconfig, + # no decrypt key) — the same source deploy/scripts/seal-secret.sh uses. + curl -fsSL "$CERT_URL" -o "$d/cert.pem" + echo "::notice::Fetched sealed-secrets public cert from $CERT_URL." + + # Build the input Secret manifest inline (no kubectl dependency). Only + # the keys being (re)sealed are listed; --merge-into leaves every other + # key in $MANIFEST untouched. + { + echo "apiVersion: v1" + echo "kind: Secret" + echo "metadata:" + echo " name: fuzefront-secrets" + echo " namespace: fuzefront" + echo "type: Opaque" + echo "data:" + echo " TWILIO_ACCOUNT_SID: $(base64 -w0 < "$d/TWILIO_ACCOUNT_SID")" + echo " TWILIO_API_KEY_SID: $(base64 -w0 < "$d/TWILIO_API_KEY_SID")" + echo " TWILIO_API_KEY_SECRET: $(base64 -w0 < "$d/TWILIO_API_KEY_SECRET")" + echo " TWILIO_VERIFY_SERVICE_SID: $(base64 -w0 < "$d/TWILIO_VERIFY_SERVICE_SID")" + [ "$MINT_SMS" = "1" ] && echo " SMS_AUTH_SECRET: $(base64 -w0 < "$d/SMS_AUTH_SECRET")" + } > "$d/secret.yaml" + + kubeseal --cert "$d/cert.pem" --format yaml --merge-into "$MANIFEST" < "$d/secret.yaml" echo "::notice::Sealed Twilio (+ SMS_AUTH_SECRET if applicable) and merged into $MANIFEST." - name: Show diff (dry run) @@ -165,14 +172,26 @@ jobs: - name: Commit sealed secret if: inputs.dry_run == 'false' + env: + SSH_KEY: ${{ secrets.RELEASE_BUMP_SSH_KEY }} run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add deploy/contabo/sealed/fuzefront-secrets.yaml - if git diff --cached --quiet; then + set -euo pipefail + if git diff --quiet deploy/contabo/sealed/fuzefront-secrets.yaml; then echo "::notice::No change to commit (keys already sealed with these values)." exit 0 fi + # Push via the RELEASE_BUMP_SSH_KEY deploy key — a bypass actor on the + # "Protect Master" ruleset, the same mechanism release.yml's GitOps bump + # uses. A plain GITHUB_TOKEN push 409s ("must be made through a PR"). + mkdir -p ~/.ssh + printf '%s\n' "$SSH_KEY" > ~/.ssh/release_bump + chmod 600 ~/.ssh/release_bump + ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null + export GIT_SSH_COMMAND="ssh -i ~/.ssh/release_bump -o IdentitiesOnly=yes" + git config user.name "fuzefront-release" + git config user.email "release-bump@users.noreply.github.com" + git remote set-url --push origin "git@github.com:${GITHUB_REPOSITORY}.git" + git add deploy/contabo/sealed/fuzefront-secrets.yaml git commit -m "secrets(prod): seal Twilio + SMS_AUTH_SECRET for phone 2FA" git push origin HEAD:master echo "::notice::Committed to master — ArgoCD syncs fuzefront-secrets within ~60s." @@ -187,7 +206,7 @@ jobs: if [ "$DRY_RUN" = "true" ]; then echo "- Dry run — sealed YAML NOT committed (see the diff step)." else - echo "- Sealed the 4 keys into \`fuzefront-secrets\` and committed to master." + echo "- Sealed the keys into \`fuzefront-secrets\` and committed to master." echo "- The keys are now present but INERT: \`smsService.enabled\` is still false." echo "- **Next (separate, deploy-window commit):** flip \`smsService.enabled: true\` in \`values-prod.yaml\`." fi