From e77b777a270d0cb4a85265ee256c44057ba22ea9 Mon Sep 17 00:00:00 2001 From: "fuzeone-bot[bot]" Date: Fri, 17 Jul 2026 07:32:40 +0300 Subject: [PATCH 1/2] fix(deploy): bump email/sms tags in release, wire security verification env WIP checkpoint. [skip ci] Co-Authored-By: Claude Claude-Session-Id: cf830721-b1ef-4fe0-a024-035ad280dcf7 --- .github/workflows/release.yml | 16 +++++++++++--- .../fuzefront/templates/email-service.yaml | 7 +++++- deploy/helm/fuzefront/templates/security.yaml | 22 +++++++++++++++++++ .../helm/fuzefront/templates/sms-service.yaml | 6 ++++- deploy/helm/fuzefront/values.yaml | 5 +++++ 5 files changed, 51 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9e84e7f1..01e5be96 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -253,16 +253,26 @@ jobs: /repository: ghcr\.io\/izzywdev\/fuzefront-security-service\r?$/ { hot=1 } /repository: ghcr\.io\/izzywdev\/fuzefront-applications-service\r?$/ { hot=1 } /repository: ghcr\.io\/izzywdev\/fuzefront-clock-app\r?$/ { hot=1 } + /repository: ghcr\.io\/izzywdev\/fuzefront-email-service\r?$/ { hot=1 } + /repository: ghcr\.io\/izzywdev\/fuzefront-sms-service\r?$/ { hot=1 } { print } END { print n+0 > "/tmp/bump-count" } ' /tmp/vp-current.yaml > /tmp/vp-new.yaml # Guard against silent no-ops: if the file layout drifts (quoted # values, reordered keys, renamed registry path) the awk matches # nothing and we would otherwise "succeed" while deploying stale - # tags (review finding). Exactly 5 core-app tags must be rewritten. + # tags (review finding). Exactly 7 core-app tags must be rewritten. + # The anchor list MUST cover every image this workflow BUILDS. It + # previously listed only 5 while the build steps produced 7: the + # email-service and sms-service images were built and pushed on every + # release but their values-prod.yaml tags were never rewritten. The + # count guard did not catch it (5 anchors == 5 rewrites == "pass"), + # so it failed silently: email-service froze at a stale SHA and + # sms-service kept tag "" — i.e. it was NEVER deployed at all. If you + # add a build step above, add its anchor here AND bump this number. COUNT=$(cat /tmp/bump-count) - if [ "$COUNT" -ne 5 ]; then - echo "::error::expected 5 core-app tag rewrites in ${FILE}, got ${COUNT} — file layout changed; update the bump step" + if [ "$COUNT" -ne 7 ]; then + echo "::error::expected 7 core-app tag rewrites in ${FILE}, got ${COUNT} — file layout changed; update the bump step" exit 1 fi if cmp -s /tmp/vp-current.yaml /tmp/vp-new.yaml; then diff --git a/deploy/helm/fuzefront/templates/email-service.yaml b/deploy/helm/fuzefront/templates/email-service.yaml index e81e7194..6d58e5ac 100644 --- a/deploy/helm/fuzefront/templates/email-service.yaml +++ b/deploy/helm/fuzefront/templates/email-service.yaml @@ -43,12 +43,17 @@ spec: value: {{ .Values.emailService.email.provider | quote }} - name: EMAIL_FROM value: {{ .Values.emailService.email.from | quote }} - {{- if .Values.secret.sendgridApiKey }} + # Gate on existingSecret TOO (same reason as sms-service): in prod the + # inline value is empty and the real key lives in the SealedSecret. + # `optional: true` so the pod still starts on the SMTP provider path, + # where SENDGRID_API_KEY is legitimately absent from the secret. + {{- if or .Values.secret.existingSecret .Values.secret.sendgridApiKey }} - name: SENDGRID_API_KEY valueFrom: secretKeyRef: name: {{ include "fuzefront.secretName" . }} key: SENDGRID_API_KEY + optional: true {{- end }} readinessProbe: httpGet: diff --git a/deploy/helm/fuzefront/templates/security.yaml b/deploy/helm/fuzefront/templates/security.yaml index 7e6ff9e5..106b314b 100644 --- a/deploy/helm/fuzefront/templates/security.yaml +++ b/deploy/helm/fuzefront/templates/security.yaml @@ -96,6 +96,28 @@ spec: key: INTERNAL_PROVISION_SECRET - name: KAFKA_BROKERS value: {{ .Values.securityService.kafka.brokers | quote }} + # In-cluster dispatch targets for the verification flows. These MUST + # be set explicitly: the code's built-in fallbacks are + # http://email-service:3000 / http://sms-service:3000, which match + # NEITHER the real Service names (fuzefront-email-service / + # fuzefront-sms-service) NOR the real ports (3003 / 3004) — so the + # fallback silently never resolves. Rendered from the same values the + # Services are built from, so a port change cannot drift them apart. + {{- if .Values.emailService.enabled }} + - name: EMAIL_SERVICE_URL + value: "http://fuzefront-email-service:{{ .Values.emailService.port }}" + {{- end }} + {{- if .Values.smsService.enabled }} + - name: SMS_SERVICE_URL + value: "http://fuzefront-sms-service:{{ .Values.smsService.port }}" + {{- end }} + # Release/kill switch for signup email verification. The code reads + # the STRING "true" and additionally requires EMAIL_SERVICE_URL to be + # non-empty; if either is missing it DEGRADES to auto-verifying every + # account. That degrade is silent, which is why this is pinned from + # values per-env rather than left unset. + - name: REQUIRE_EMAIL_VERIFICATION + value: {{ .Values.securityService.requireEmailVerification | default false | quote }} {{- if .Values.authentik.oidc.enabled }} - name: AUTHENTIK_CLIENT_ID valueFrom: diff --git a/deploy/helm/fuzefront/templates/sms-service.yaml b/deploy/helm/fuzefront/templates/sms-service.yaml index b23d1b57..e7ea69f0 100644 --- a/deploy/helm/fuzefront/templates/sms-service.yaml +++ b/deploy/helm/fuzefront/templates/sms-service.yaml @@ -36,7 +36,11 @@ spec: value: "production" - name: PORT value: {{ .Values.smsService.port | quote }} - {{- if .Values.secret.twilioAccountSid }} + # Gate on existingSecret TOO, not just the inline value: in prod + # secret.twilioAccountSid is empty by design (the real values live in + # the fuzefront-secrets SealedSecret), so an inline-only gate rendered + # a Twilio-less pod in exactly the environment that needs Twilio. + {{- if or .Values.secret.existingSecret .Values.secret.twilioAccountSid }} - name: TWILIO_ACCOUNT_SID valueFrom: secretKeyRef: diff --git a/deploy/helm/fuzefront/values.yaml b/deploy/helm/fuzefront/values.yaml index 58d7222e..f7ef8300 100644 --- a/deploy/helm/fuzefront/values.yaml +++ b/deploy/helm/fuzefront/values.yaml @@ -169,6 +169,11 @@ securityService: tag: local port: 3002 replicas: 1 + # Enforce signup email verification. Default false: the code DEGRADES to + # auto-verifying every account when this is off, so turning it on is only safe + # once a working email path (email-service + a proven SMTP/SendGrid sender) is + # verified — otherwise every new signup is locked out at the door. + requireEmailVerification: false kafka: brokers: "fuzeinfra-kafka.fuzeinfra.svc.cluster.local:9092" resources: From 97c0e6fc7d3d86d4423b68da629ba71e79f83a77 Mon Sep 17 00:00:00 2001 From: "fuzeone-bot[bot]" Date: Fri, 17 Jul 2026 07:40:10 +0300 Subject: [PATCH 2/2] fix(deploy): build+wire sms-service, and give security the email/SMS/verification env it needs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The release bump step's awk anchor list covered 5 images while the workflow BUILDS 7. email-service and sms-service were built and pushed on every release but their values-prod.yaml tags were never rewritten, and the count guard could not catch it (5 anchors == 5 rewrites == pass). Result: email-service froze at a stale SHA and sms-service kept tag "" — never deployed at all. - release.yml: anchor email-service + sms-service, guard 5 -> 7. - security.yaml: add EMAIL_SERVICE_URL / SMS_SERVICE_URL (rendered from the same values the Services use) + REQUIRE_EMAIL_VERIFICATION from values. The code's fallbacks (http://email-service:3000) match neither the real Service names nor ports, so they never resolved. - sms/email-service.yaml: gate Twilio/SendGrid env on existingSecret too — prod keeps those values empty by design, so the inline-only gate stripped Twilio env in exactly the env that needs it. - values-prod.yaml: requireEmailVerification: false (email path unproven). Co-Authored-By: Claude Claude-Session-Id: cf830721-b1ef-4fe0-a024-035ad280dcf7 --- deploy/contabo/SEAL_PROD_SECRETS.md | 56 ++++++++++++++++++++++++++ deploy/helm/fuzefront/values-prod.yaml | 36 +++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/deploy/contabo/SEAL_PROD_SECRETS.md b/deploy/contabo/SEAL_PROD_SECRETS.md index 8e75647a..49ca8fe0 100644 --- a/deploy/contabo/SEAL_PROD_SECRETS.md +++ b/deploy/contabo/SEAL_PROD_SECRETS.md @@ -89,6 +89,62 @@ git add deploy/contabo/sealed/fuzefront-secrets.yaml && git commit && git push > is owned by **feature-flags-engineer**; the Unleash DEPLOY (Helm/Argo) is devops. > This recipe only SEALS the token feature-flags-engineer hands over. +## Adding the Twilio keys (phone 2FA / `sms-service`) — REQUIRED before enabling SMS + +`smsService.enabled` is **false** in `values-prod.yaml` and **must stay false until +these four keys are sealed**. Unlike `FEATURE_FLAGS_CLIENT_TOKEN` above, these are +NOT optional: `templates/sms-service.yaml` mounts `SMS_AUTH_SECRET` via a hard +`secretKeyRef` (no `optional: true`), so a missing key means the pod never starts — +kubelet holds the container in `CreateContainerConfigError` / `CrashLoopBackOff`. + +Seal all four into the SAME `fuzefront-secrets` SealedSecret. The key names below +are what the chart reads — they must match EXACTLY: + +| Key | Where to get it | Used by | +|-----|-----------------|---------| +| `TWILIO_ACCOUNT_SID` | Twilio console → Account Info (starts `AC…`) | sms-service → Twilio Verify | +| `TWILIO_AUTH_TOKEN` | Twilio console → Account Info (rotate-able) | sms-service → Twilio Verify | +| `TWILIO_VERIFY_SERVICE_SID` | Twilio console → Verify → Services (starts `VA…`) | sms-service → Twilio Verify | +| `SMS_AUTH_SECRET` | **Generate a fresh random value** — `openssl rand -hex 32` | Authentik + security-service authenticating TO sms-service | + +`SMS_AUTH_SECRET` is not issued by any vendor: it is an internal shared secret you +mint yourself. It is consumed by the Authentik SMS stage blueprint +(`deploy/helm/fuzefront/authentik/blueprints/stages-sms.yaml`), so the value sealed +here must be the same one that stage uses. + +```bash +# Seal each key in place (does not disturb the other keys in the manifest). +for KEY in TWILIO_ACCOUNT_SID TWILIO_AUTH_TOKEN TWILIO_VERIFY_SERVICE_SID; do + read -rsp "value for ${KEY}: " V; echo + printf '%s' "$V" | tr -d '[:space:]' > /tmp/sms-val.txt + deploy/scripts/seal-secret.sh "$KEY" \ + --in /tmp/sms-val.txt \ + --scope fuzefront/fuzefront-secrets \ + --manifest deploy/contabo/sealed/fuzefront-secrets.yaml + rm -f /tmp/sms-val.txt +done + +# SMS_AUTH_SECRET — minted here, not copied from a vendor. +openssl rand -hex 32 | tr -d '\n' > /tmp/sms-auth.txt +deploy/scripts/seal-secret.sh SMS_AUTH_SECRET \ + --in /tmp/sms-auth.txt \ + --scope fuzefront/fuzefront-secrets \ + --manifest deploy/contabo/sealed/fuzefront-secrets.yaml +rm -f /tmp/sms-auth.txt + +git add deploy/contabo/sealed/fuzefront-secrets.yaml +git commit -m "secrets(prod): seal Twilio + SMS_AUTH_SECRET for phone 2FA" +git push +``` + +Then, as a SEPARATE GitOps commit in a deploy window, flip +`smsService.enabled: true` in `values-prod.yaml`. Keep the two steps apart so the +secret is provably present before the Deployment renders. + +> Only the **owner** can perform this sealing — it needs the real Twilio credentials +> and the cluster's sealing cert. Agents scaffold the wiring and the key names; they +> never hold or commit the values. + ## ⚠️ Resealing a single value (e.g. `AUTHENTIK_BOOTSTRAP_TOKEN`) To rotate or repair ONE key without retyping the rest, use the offline merge helper — diff --git a/deploy/helm/fuzefront/values-prod.yaml b/deploy/helm/fuzefront/values-prod.yaml index b1e03d2f..0b94021f 100644 --- a/deploy/helm/fuzefront/values-prod.yaml +++ b/deploy/helm/fuzefront/values-prod.yaml @@ -49,6 +49,15 @@ securityService: # Fresh address (absent in prod) so the idempotent seed-admin hook does a clean # insert with the sealed FUZEFRONT_ADMIN_PASSWORD. See PR #213 (password reseal). adminEmail: admin@fuzefront.com + # Signup email verification (PR #275) — DELIBERATELY OFF. + # While false the code auto-verifies every new account, i.e. verification is + # effectively not enforced. That is the SAFE state right now: email-service is + # enabled but its SMTP sender is NOT yet proven end-to-end (no SMTP_HOST is + # wired; it defaults to a mailhog-style localhost:1025 and silently drops mail). + # Flipping this to true before a real sender is proven would bounce EVERY new + # signup — they would be created unverified and never receive the mail needed to + # verify. Sequence: prove the sender → then flip this to true via GitOps. + requireEmailVerification: false image: repository: ghcr.io/izzywdev/fuzefront-security-service tag: 215bd6a95f97 @@ -83,12 +92,39 @@ clockApp: repository: ghcr.io/izzywdev/fuzefront-clock-app tag: 215bd6a95f97 +# email-service — transactional sender behind the signup email-verification flow +# (PR #275). Its image IS built by release.yml on every release, but until this PR +# its repository was missing from the bump step's anchor list, so the tag below sat +# frozen at a stale SHA while the built image moved on. The anchor is now present, +# so the next release rewrites this tag automatically — do not hand-pin it. emailService: + enabled: true image: repository: ghcr.io/izzywdev/fuzefront-email-service tag: 1fbd206c5b2a + # Provider stays "smtp" (chart default) until a sender is proven end-to-end. + # NOTE: securityService.requireEmailVerification MUST stay false until then — + # see the security block below. +# sms-service — Twilio Verify dispatcher behind phone 2FA (SMS/voice, PR #274). +# Its image is built by release.yml, but its repository was likewise absent from +# the bump anchor list, so this tag stayed "" — an EMPTY TAG, meaning the service +# was never actually deployed in prod even though the security routes answered. +# The anchor now exists, so the next release writes the real SHA here. +# +# STILL DISABLED, deliberately. Enabling it now would CrashLoop the pod: the +# container requires SMS_AUTH_SECRET (a hard secretKeyRef, no `optional`) and the +# Twilio credentials, and NONE of those keys exist in the fuzefront-secrets +# SealedSecret yet (verified against deploy/contabo/sealed/fuzefront-secrets.yaml). +# GO-LIVE for phone 2FA is exactly two steps, in this order: +# 1. Owner seals the 4 keys listed in deploy/contabo/SEAL_PROD_SECRETS.md +# (TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_VERIFY_SERVICE_SID, +# SMS_AUTH_SECRET) into fuzefront-secrets. +# 2. Flip `enabled` to true here via GitOps in a deploy window. +# Until step 1 lands this MUST stay false — the empty tag hid a missing-secret +# failure that would otherwise surface as a CrashLoopBackOff on the umbrella sync. smsService: + enabled: false image: repository: ghcr.io/izzywdev/fuzefront-sms-service tag: "" # CI's release sed writes the real image SHA here