Repository navigation
security: stop shipping placeholder k8s secrets; reject weak JWT_SECRET; bind settlement to gateway tenant - #69
Merged
Conversation
deploy/k8s/base/secrets.yaml was a kustomize base resource, so every overlay (dev, staging, production) rendered aex-secrets with JWT_SECRET=REPLACE_ME, Mongo root/root, AEX_API_KEY=dev-api-key and placeholder webhook and signing keys. Worse, kubectl apply -k overwrote a Secret the operator had already created. The README's create-secret step and deploy-eks.sh both did exactly that, so even careful deployments ran on public placeholders. Anyone could then forge gateway JWTs (issue #66). - Remove the Secret from the base. No overlay renders a Secret any more, and an existing aex-secrets is left untouched. - Rename the template to base/secrets.example.yaml. No kustomization references it, and it holds only "<generate: ...>" hints. - Add deploy/k8s/create-secrets.sh. It is idempotent and creates or updates aex-secrets with every key the manifests reference. Values come from the environment first, then the existing value if it is not a placeholder, then random generation. It never rotates on re-run, rejects short or placeholder JWT and webhook secrets, never logs a value, keeps an external MONGO_URI, and creates the mongodb-keyfile Secret the StatefulSet mounts. - The EKS and GKE scripts and Cloud Build call the helper instead of using fixed defaults. JWT_SECRET is now always set; no script set it before. - Update the README, DEPLOYMENT.md, docs/deployment.md, docs/PROJECT.md and demo/README.md to create the Secret before kubectl apply -k, and add a rotation note for clusters deployed before this change.
The gateway rejected only an empty JWT_SECRET, so a deployment running with "REPLACE_ME" accepted HS256 tokens anyone could sign, with any tenant_id and scopes ["*"] (issue #66). Unless ENVIRONMENT is development, startup now fails when JWT_SECRET is: - empty; - shorter than 32 bytes; - a known placeholder, compared trimmed and case-insensitively: REPLACE_ME, CHANGE_ME, changeme, secret, password and similar, including values that contain replace_me, change_me or do-not-use-in-production; - a template value wrapped in "<...>". In development these only log a warning, so the local compose and kind stacks keep starting. The secret value is never logged. config.ValidateJWTSecret holds the check and is covered by table tests.
GET /v1/balance, GET /v1/usage, GET /v1/usage/transactions and POST /v1/deposits took tenant_id from the query or body and ignored the X-Tenant-ID the gateway sets from the authenticated caller. Any authenticated tenant could read another tenant's balance and usage, or deposit into another tenant's account. When X-Tenant-ID is present, it now decides the tenant. tenant_id becomes optional, and naming a different tenant returns 403 TENANT_MISMATCH. Direct internal calls without the header behave as before.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #66.
What was wrong
deploy/k8s/base/secrets.yamlwas a kustomize base resource. Every overlay renderedaex-secretswithJWT_SECRET=REPLACE_ME, Mongoroot/root,AEX_API_KEY=dev-api-keyand placeholder webhook and signing keys. That includes production.JWT_SECRET, so anyone could sign an HS256 token withREPLACE_ME, anytenant_idandscopes: ["*"].kubectl apply -koverwrote a Secret the operator had already created. The README'skubectl create secretstep anddeploy/aws/deploy-eks.shboth create real secrets and then apply the overlay. So even a careful deployment ended up on the public placeholders.tenant_idfrom the query string or request body and ignored the gateway's authenticatedX-Tenant-ID. Any legitimate tenant could read another tenant's balance and usage, or deposit into another tenant's account.Changes
deploy(k8s): stop shipping placeholder secrets in every overlaykubectl kustomizeon all four. An existingaex-secretsis never overwritten.base/secrets.example.yamldocuments the keys. No kustomization references it.deploy/k8s/create-secrets.sh: idempotent, creates or updatesaex-secretswith every key the manifests reference.MONGO_URI.mongodb-keyfileSecret, which the StatefulSet mounts but which nothing created before.JWT_SECRETis now always set; no script ever set it before.DEPLOYMENT.md,docs/deployment.md,docs/PROJECT.mdanddemo/README.mdcreate the Secret beforekubectl apply -k. There's also a rotation section for clusters deployed before this change.fix(gateway): refuse placeholder or weak JWT_SECRET outside developmentUnless
ENVIRONMENT=development, startup fails whenJWT_SECRETis:REPLACE_ME,CHANGE_ME,secret,passwordand similar, plus values containingreplace_me,change_meordo-not-use-in-production);<...>template value.In development these only log a warning, so local compose and kind keep working. The secret is never logged.
fix(settlement): bind tenant-scoped endpoints to the gateway tenantOn
GET /v1/balance,GET /v1/usage,GET /v1/usage/transactionsandPOST /v1/deposits:X-Tenant-ID(set by the gateway, which strips client copies) decides the tenant, andtenant_idbecomes optional.tenant_idreturns 403TENANT_MISMATCH.Action for anyone who deployed with the old manifests
Rotate
JWT_SECRET, the Mongo root credentials,WEBHOOK_SECRETand any API keys. Treat tokens signed withREPLACE_MEas compromised. The rotation steps are indeploy/k8s/README.md.Test plan
kubectl kustomizerenders base, dev, staging and production with 0 Secrets and 0REPLACE_ME,root:rootordev-api-keyhits (before: 1 Secret and 6 placeholder hits each).GOWORK=off go build,go vetandgo testpass in every module.go test -racepasses for aex-settlement.go vet -tags integration,e2epasses inhack/integrationandhack/tests.create-secrets.shtested end to end against a fakekubectl: fresh run, re-run (values unchanged), upgrade from the old placeholder Secret, and rejection of placeholder or short values from the environment.bash -npasses on every changed script. shellcheck-S warningis clean apart from one SC2155 indeploy-gke.shthat was already onmain.Known follow-ups (not in this PR)
ENVIRONMENTunset, the gateway assumesdevelopment, anddeploy-eks.sh/deploy-gke.shdefault to the dev overlay on real clusters. Production and staging should be the safer defaults.kubectl apply -kon base and the overlays needs--load-restrictor=LoadRestrictionsNone, because the base references files outside its directory. This was already broken onmain.tenant_idfrom callers inside the cluster that don't sendX-Tenant-ID. A NetworkPolicy or service-to-service auth would close that.placeholder-update-mevalues from AWS and GCP secret seeding.