Skip to content
Merged
Show file tree
Hide file tree
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
16 changes: 13 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
56 changes: 56 additions & 0 deletions deploy/contabo/SEAL_PROD_SECRETS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 —
Expand Down
7 changes: 6 additions & 1 deletion deploy/helm/fuzefront/templates/email-service.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
22 changes: 22 additions & 0 deletions deploy/helm/fuzefront/templates/security.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
6 changes: 5 additions & 1 deletion deploy/helm/fuzefront/templates/sms-service.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
36 changes: 36 additions & 0 deletions deploy/helm/fuzefront/values-prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions deploy/helm/fuzefront/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading