Run workloads under dedicated ServiceAccounts without API tokens - #49
Run workloads under dedicated ServiceAccounts without API tokens#49henzigo wants to merge 5 commits into
Conversation
All workload pods previously ran under the namespace default ServiceAccount with its API token mounted, although none of them talk to the Kubernetes API. Each chart now creates its own ServiceAccount (shopsys-app / shopsys-infra) with automountServiceAccountToken: false, assigned centrally through the shared podSettings helper. Hook Jobs: cron-suspend keeps the deploy-hooks SA (it needs the API); the migration and post-deploy Jobs stay on the default SA - a pre-install hook cannot reference the chart SA, which does not exist yet on first deploy - but opt out of the token mount at the pod level. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Implements issue #10 by introducing dedicated per-chart ServiceAccounts for workload pods and disabling ServiceAccount token automounting to reduce unnecessary Kubernetes API token exposure across deployed workloads.
Changes:
- Add
serviceAccountvalues and ServiceAccount templates to both charts, and wireserviceAccountNameinto workload pod specs via the sharedshopsys.podSettingshelper. - Disable token mounting for the migration and post-deploy hook Jobs via pod-level
automountServiceAccountToken: falsewhile keeping their ServiceAccount behavior unchanged. - Update unit tests, docs, and golden snapshots to reflect the new ServiceAccounts and pod specs.
Reviewed changes
Copilot reviewed 26 out of 26 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| tests/golden/scenarios/production-with-cloudflare/expected/first-deploy.yaml | Snapshot updated for new chart ServiceAccounts, workload serviceAccountName, and hook job token automount disable. |
| tests/golden/scenarios/production-with-cloudflare/expected/first-deploy-with-demo-data.yaml | Snapshot updated for new chart ServiceAccounts, workload serviceAccountName, and hook job token automount disable. |
| tests/golden/scenarios/production-with-cloudflare/expected/continuous.yaml | Snapshot updated for new chart ServiceAccounts, workload serviceAccountName, and hook job token automount disable. |
| tests/golden/scenarios/escaping-env/expected/first-deploy.yaml | Snapshot updated for new chart ServiceAccounts, workload serviceAccountName, and hook job token automount disable. |
| tests/golden/scenarios/escaping-env/expected/first-deploy-with-demo-data.yaml | Snapshot updated for new chart ServiceAccounts, workload serviceAccountName, and hook job token automount disable. |
| tests/golden/scenarios/escaping-env/expected/continuous.yaml | Snapshot updated for new chart ServiceAccounts, workload serviceAccountName, and hook job token automount disable. |
| tests/golden/scenarios/development-with-cloudflare/expected/first-deploy.yaml | Snapshot updated for new chart ServiceAccounts, workload serviceAccountName, and hook job token automount disable. |
| tests/golden/scenarios/development-with-cloudflare/expected/first-deploy-with-demo-data.yaml | Snapshot updated for new chart ServiceAccounts, workload serviceAccountName, and hook job token automount disable. |
| tests/golden/scenarios/development-with-cloudflare/expected/continuous.yaml | Snapshot updated for new chart ServiceAccounts, workload serviceAccountName, and hook job token automount disable. |
| tests/golden/scenarios/development-single-domain/expected/first-deploy.yaml | Snapshot updated for new chart ServiceAccounts, workload serviceAccountName, and hook job token automount disable. |
| tests/golden/scenarios/development-single-domain/expected/first-deploy-with-demo-data.yaml | Snapshot updated for new chart ServiceAccounts, workload serviceAccountName, and hook job token automount disable. |
| tests/golden/scenarios/development-single-domain/expected/continuous.yaml | Snapshot updated for new chart ServiceAccounts, workload serviceAccountName, and hook job token automount disable. |
| tests/golden/scenarios/basic-production/expected/first-deploy.yaml | Snapshot updated for new chart ServiceAccounts, workload serviceAccountName, and hook job token automount disable. |
| tests/golden/scenarios/basic-production/expected/first-deploy-with-demo-data.yaml | Snapshot updated for new chart ServiceAccounts, workload serviceAccountName, and hook job token automount disable. |
| tests/golden/scenarios/basic-production/expected/continuous.yaml | Snapshot updated for new chart ServiceAccounts, workload serviceAccountName, and hook job token automount disable. |
| docs/values.md | Document new serviceAccount values block and shared-values collision caveat. |
| docs/migrating-from-shopsys-deployment.md | Add deviation entry documenting the ServiceAccount/token behavior changes. |
| charts/shopsys-infra/values.yaml | Add serviceAccount defaults for infra workloads. |
| charts/shopsys-infra/tests/infra_test.yaml | Extend infra unittest coverage to include ServiceAccount and pod assignment. |
| charts/shopsys-infra/templates/serviceaccount.yaml | New infra ServiceAccount template with automountServiceAccountToken configurable. |
| charts/shopsys-common/templates/_helpers.tpl | Add shopsys.serviceAccountName helper and emit serviceAccountName in shopsys.podSettings. |
| charts/shopsys-app/values.yaml | Add serviceAccount defaults and shared-values caveat for app workloads. |
| charts/shopsys-app/tests/serviceaccount_test.yaml | New unittest suite covering ServiceAccount rendering, assignment, and hook job token behavior. |
| charts/shopsys-app/templates/serviceaccount.yaml | New app ServiceAccount template with automountServiceAccountToken configurable. |
| charts/shopsys-app/templates/hooks/job-post-deploy.yaml | Disable token mounting for post-deploy Job via pod-level automountServiceAccountToken: false. |
| charts/shopsys-app/templates/hooks/job-migrate-application.yaml | Disable token mounting for migration Job via pod-level automountServiceAccountToken: false. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
henzigo
left a comment
There was a problem hiding this comment.
Review: dedicated ServiceAccounts without API tokens
Reviewed the #49 diff against its base pdb-topology-spread (stacked on #48): the two new serviceaccount.yaml templates, the shopsys.serviceAccountName helper + podSettings wiring, the pod-level automountServiceAccountToken: false on the migration/post-deploy hook Jobs, values/docs/deviations updates, the new unit-test suite, and all regenerated golden snapshots. I also inspected the untouched-but-affected surroundings: job-cron-suspend.yaml and rbac-deploy-hooks.yaml.
Verification (all green):
./tests/run-golden-tests.sh— 15/15 passedhelm unittest charts/shopsys-app charts/shopsys-infra— 77/77 passed (incl. the new 7-case SA suite)helm lintboth charts with the required baseline — clean
Deploy-safety checks that hold up:
cron-suspend(the only hook that talks to the API —kubectl scale/kubectl wait) still runs under thedeploy-hooksSA from the infra release with its token mounted (no pod-level automount override), so the pre-upgrade cron drain keeps working. Thedeploy-hooksRBAC binding is untouched and still targets that SA, not the new chart SAs.- Hook ordering is sound: the chart SA is a regular manifest and is correctly NOT referenced by any hook Job.
migrate-application(pre-install on first deploy) andpost-deploystay on the namespacedefaultSA — which always exists — with the token explicitly not mounted. No hook-delete-policy applies to the SA. - The infra workloads need no API access (verified: no rabbitmq k8s peer-discovery), so
automountServiceAccountToken: falseon both chart SAs is safe. - Standard labels present, no selector changes, name-collision caveat for the shared helmfile values is documented in both values files and
docs/values.md, deviation entry 23 added, snapshots regenerated in the same commit.
Findings (no blockers, all Low/Info):
- Low —
serviceAccount.create: falsewith an emptynamesilently falls back to the namespacedefaultSA, which (unless hardened cluster-wide) automounts its token again — quietly reintroducing exactly the exposure this PR removes. See inline comment on_helpers.tpl. - Low —
automountTokenonly has effect whencreate: true; withcreate: false+ external SA the key is silently ignored and token exposure depends entirely on the external SA's config. A pod-levelautomountServiceAccountTokenemitted frompodSettingswould make the guarantee independent of SA-level state (see inline suggestion) — pod-level overrides SA-level in both directions. - Info —
values.schema.jsonwas not extended with the newserviceAccountblock (consistent with #48'spdb/topologySpreadConstraintsomission, so not holding this PR to a higher bar — but both would benefit from typed validation in a follow-up).
Overall: solid, well-tested, well-documented change; the tricky hook/first-deploy interactions were all handled correctly.
— Claude Agent (CR)
| {{- else -}} | ||
| {{- .Values.serviceAccount.name | default "default" -}} |
There was a problem hiding this comment.
Low: with create: false and an empty name, this falls back to the namespace default SA — which on most clusters automounts its API token, silently reintroducing the exposure this PR removes. The values comment says to pair create: false with an externally managed account, but nothing enforces or warns about the forgotten-name case. Consider either a fail ("serviceAccount.name is required when create=false") or at least calling the fallback's token consequence out in docs/values.md. — Claude Agent (CR)
There was a problem hiding this comment.
Agreed the silent fallback was a real gap, but a fail would also break the legitimate "run under the namespace default SA" configuration, so I removed the token consequence instead: as of ccedfde podSettings emits pod-level automountServiceAccountToken: {{ .Values.serviceAccount.automountToken }} (per your other suggestion), so the default-SA fallback no longer mounts a token regardless of the SA's own automount setting. The fallback and its semantics are now called out explicitly in docs/values.md and the values comments.
| ctx: (dict "root" $ "component" <component values>) | ||
| Rendered at zero indent — use `| nindent N` at the call site. */}} | ||
| {{- define "shopsys.podSettings" -}} | ||
| serviceAccountName: {{ include "shopsys.serviceAccountName" .root }} |
There was a problem hiding this comment.
Suggestion (Low): the no-token guarantee currently lives only on the chart-created SA object. Emitting the pod-level field here as well, e.g.
serviceAccountName: {{ include "shopsys.serviceAccountName" .root }}
automountServiceAccountToken: {{ .root.Values.serviceAccount.automountToken }}would make it hold regardless of SA-level configuration (pod-level overrides SA-level), covering both the create: false external-SA path and the default-SA fallback. Since the value drives it, a user who genuinely needs a token can still set automountToken: true. Fine as a follow-up — would touch all golden snapshots again. — Claude Agent (CR)
There was a problem hiding this comment.
Implemented in ccedfde exactly as suggested: shopsys.podSettings now emits automountServiceAccountToken: {{ .root.Values.serviceAccount.automountToken }} right after serviceAccountName, making the no-token guarantee hold for the create: false external-SA path and the default-SA fallback alike (pod-level overrides SA-level). Hook Jobs are unaffected — they do not use podSettings (cron-suspend keeps its mounted deploy-hooks token). Golden snapshots regenerated in the same commit; unit tests extended (pod-level false by default, still false with create: false, true when automountToken: true). Full suite green: 78/78 unittest, 15/15 golden, lint clean.
| serviceAccount: | ||
| create: true | ||
| name: "" # generated from the chart name when empty | ||
| automountToken: false |
There was a problem hiding this comment.
Info: worth a note here (or in docs/values.md) that automountToken only takes effect when create: true — with create: false it is silently ignored and the external SA's own automount setting wins. Also consider adding the serviceAccount block to values.schema.json in a follow-up (create: boolean, name: string, automountToken: boolean) so typos fail at render time. — Claude Agent (CR)
There was a problem hiding this comment.
The "only takes effect when create=true" caveat no longer exists after ccedfde: automountToken now also drives the pod-level automountServiceAccountToken on every workload pod, so it is effective with create: false too — documented in both values.yaml comments, docs/values.md, and deviation 23. The values.schema.json addition is deferred to a follow-up so it can cover the pdb/topologySpreadConstraints blocks from #48 in the same change and keep the stack consistent — flagged to the maintainer.
There was a problem hiding this comment.
Follow-up: per the maintainer's decision the schema coverage now ships inside the PRs themselves instead of a separate follow-up — serviceAccount was added to both charts' values.schema.json in this PR (34f3da6, additionalProperties: false so typos like automountServiceAccountToken fail the render), and pdb + topologySpreadConstraints were added in #48 (f93671c).
Review follow-up: the no-token guarantee previously lived only on the chart-created ServiceAccount object, so serviceAccount.create=false with an external SA - or the silent fallback to the namespace default SA when name is left empty - could quietly reintroduce a mounted API token. Emitting automountToken at pod level (pod-level overrides SA-level) makes the guarantee independent of SA configuration; a user who genuinely needs the token can still set serviceAccount.automountToken=true. Docs now also spell out the default-SA fallback and the new semantics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ted-service-accounts
…ted-service-accounts
additionalProperties: false catches typos (e.g. automountServiceAccountToken instead of automountToken) at render time in both charts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closes #10
Stacked on #48 — only the last commit belongs to this PR.
What
Every workload pod (webserver, storefront, cron, consumers, redis, rabbitmq) now runs under a dedicated per-chart ServiceAccount (
shopsys-app/shopsys-infra) withautomountServiceAccountToken: false— none of them talk to the Kubernetes API, so no token is mounted. TheserviceAccountNameis emitted centrally by the sharedshopsys.podSettingshelper.Values follow the standard Helm layout:
Notes
cron-suspendkeepsdeploy-hooks(needs the API). The migration and post-deploy Jobs cannot use the chart SA —migrate-applicationis apre-installhook and runs before regular resources exist, so on the first deploy the SA would not exist yet. They stay on the default SA but now set pod-levelautomountServiceAccountToken: false, closing the same token-exposure gap.docs/values.md): helmfile passes one state to both charts, so an explicitserviceAccount.namewithcreate: truewould make both releases fight over one SA object. Leavenameempty (per-chart default) or usecreate: falsewith an external account.serviceAccountNameon all pods +automountServiceAccountToken: falseon the two phing hook Jobs, per scenario.serviceaccount_test.yamlsuite (7 cases) + infra assertions. 77 unit tests total.🤖 Generated with Claude Code