Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 70 additions & 51 deletions .github/workflows/seal-sms-secrets.yml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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."
Expand All @@ -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
Expand Down
Loading