Skip to content

feat: expose container-level securityContext for api, frontend, taskProcessor, sse, and migrate jobs - #571

Merged
germangarces merged 2 commits into
Flagsmith:mainfrom
abbassoltanian-usmobile:feat/restricted-pss-container-securitycontext
Jul 31, 2026
Merged

feat: expose container-level securityContext for api, frontend, taskProcessor, sse, and migrate jobs#571
germangarces merged 2 commits into
Flagsmith:mainfrom
abbassoltanian-usmobile:feat/restricted-pss-container-securitycontext

Conversation

@abbassoltanian-usmobile

Copy link
Copy Markdown
Contributor
  • I have filled in the "Changes" section below?
  • I have filled in the "How did you test this code" section below?

Changes

pgbouncer is currently the only component in this chart whose container renders securityContext — it's the only one with both securityContext/defaultSecurityContext in values.yaml and the corresponding container-level block in its Deployment template. Every other component (api, frontend, taskProcessor, sse, the migrate-db/bootstrap init containers, jobs.migrateDb, jobs.migrateAnalyticsData) only exposes podSecurityContext/defaultPodSecurityContext (pod-level). Since allowPrivilegeEscalation and capabilities.drop are container-only fields, there is currently no way — via values.yaml or otherwise — to make any of those components satisfy Kubernetes' restricted Pod Security Standard, which requires both.

This PR mirrors the existing pgbouncer pattern onto the rest of the chart:

  • Adds securityContext: {} / defaultSecurityContext: {enabled: true, allowPrivilegeEscalation: false, capabilities.drop: [ALL]} to api, frontend, taskProcessor, sse, jobs.migrateDb, and jobs.migrateAnalyticsData in values.yaml.
  • Wires that into the container spec in deployment-api.yaml (main container + migrate-db and bootstrap init containers, which share api's image and now share its securityContext), deployment-frontend.yaml, deployment-task-processor.yaml, deployment-sse.yaml, jobs-migrate-db.yaml, and jobs-migrate-analytics-data.yaml.
  • jobs.migrateAnalyticsData was also missing podSecurityContext/defaultPodSecurityContext entirely (unlike jobs.migrateDb, which already had it) — added that too, so the Job now sets pod-level security context at all.

Defaults to enabled: true, matching pgbouncer's own default. allowPrivilegeEscalation: false + dropping all capabilities is safe regardless of which UID a container runs as, so this shouldn't be breaking — unlike runAsNonRoot (left commented out chart-wide, presumably because some image tags may still run as root).

Not included: the secret-creator container in the django-secret-init/sse-secret-init helper Jobs has the same gap but no existing securityContext values plumbing to extend (no podSecurityContext equivalent either), so it's a separate shaped change — happy to follow up in another PR if useful.

How did you test this code?

helm lint and helm template locally against:

  • The chart's default values
  • Each existing file under charts/flagsmith/ci/*.yaml
  • An ad-hoc values file enabling every optional component together (api.bootstrap, taskProcessor, sse, jobs.migrateDb, jobs.migrateAnalyticsData, databaseExternal) to render every container this PR touches in one pass

Then walked the rendered manifests and confirmed every targeted container (flagsmith-api, bootstrap, migrate-db init container, flagsmith-frontend, flagsmith-task-processor, flagsmith-sse, and both migrate-db/migrate-analytics-data Job containers) carries securityContext: {allowPrivilegeEscalation: false, capabilities: {drop: [ALL]}}, and that the default (no extra values) render is unaffected.

Motivated by a real deployment: we run this chart in an EKS namespace with pod-security.kubernetes.io/{audit,warn}=restricted, and flagsmith-frontend and the migrate-db init container currently show up as PodSecurity violations with no way to resolve them through values.yaml.

…rocessor, sse, and migrate jobs

pgbouncer is the only component that renders allowPrivilegeEscalation
and capabilities.drop on its container; every other component only
exposes pod-level podSecurityContext/defaultPodSecurityContext, which
can't set those two fields (allowPrivilegeEscalation and capabilities
are container-only fields). That leaves api, frontend, taskProcessor,
sse, jobs.migrateDb, the migrate-db/bootstrap init containers, and
jobs.migrateAnalyticsData unable to satisfy Kubernetes' "restricted"
Pod Security Standard, regardless of user-supplied values.

Adds securityContext/defaultSecurityContext (enabled: true by default,
same shape and defaults as pgbouncer's) to each of those components and
wires it into their container specs. jobs.migrateAnalyticsData also
gains the pod-level podSecurityContext/defaultPodSecurityContext it was
missing entirely.

Verified with `helm lint` and `helm template` against the chart's own
ci/*.yaml value files plus a spread of ad-hoc values enabling every
optional component (bootstrap, taskProcessor, sse, both migrate jobs) —
every container in the resulting manifests carries the new
securityContext except the pre-existing django-secret-init/
sse-secret-init helper jobs' `secret-creator` container, which has no
existing securityContext plumbing to hook into and is left for a
follow-up.

Defaults to `allowPrivilegeEscalation: false` and `capabilities.drop:
[ALL]`, which is safe regardless of the UID a container runs as, so
this is a non-breaking default (unlike runAsNonRoot, which the chart
already leaves opt-in per-component since some tags may still run as
root).
@abbassoltanian-usmobile

Copy link
Copy Markdown
Contributor Author

@germangarces Can I have a review here in this PR?
This fix is enabling PSS compliance for Flagsmith.
Thank you

@amir-haji-usmobile amir-haji-usmobile left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This unblocks completing a Pod Security Standards (restricted) rollout for downstream users: pod-level podSecurityContext is already exposed, but allowPrivilegeEscalation: false and capabilities.drop are container-level fields, so without this hook charts consumers can't fully satisfy the restricted profile. Additive and defaults-preserving across api, frontend, taskProcessor, sse, and the migrate jobs — we've verified the api/frontend images (2.238.0) run as nobody, so the stricter contexts are compatible.

@germangarces

germangarces commented Jul 29, 2026

Copy link
Copy Markdown
Member

Hi @abbassoltanian-usmobile , thanks for this!

Just a nit: the same 6-line merge block is now repeated 8 times across the templates. A named template in _helpers.tpl would collapse it:

{{/*
Security context: chart defaults with user values merged on top (user wins).
Usage: (dict "component" .Values.api "key" "securityContext"|"podSecurityContext")
*/}}
{{- define "flagsmith.mergedSecurityContext" -}}
{{- $defaultKey := printf "default%s" (.key | title) -}}
{{- $ctx := index .component .key | default dict | deepCopy -}}
{{- $defaults := index .component $defaultKey | default dict -}}
{{- if $defaults.enabled -}}
{{- $ctx = $ctx | merge (omit $defaults "enabled") -}}
{{- end -}}
{{- toYaml $ctx -}}
{{- end -}}

Call sites

securityContext: {{- include "flagsmith.mergedSecurityContext" (dict "component" .Values.api "key" "securityContext") | nindent 10 }}
securityContext: {{- include "flagsmith.mergedSecurityContext" (dict "component" .Values.api "key" "podSecurityContext") | nindent 8 }}

@abbassoltanian-usmobile

Copy link
Copy Markdown
Contributor Author

Hi @abbassoltanian-usmobile , thanks for this!

Just a nit: the same 6-line merge block is now repeated 8 times across the templates. A named template in _helpers.tpl would collapse it:

{{/*
Security context: chart defaults with user values merged on top (user wins).
Usage: (dict "component" .Values.api "key" "securityContext"|"podSecurityContext")
*/}}
{{- define "flagsmith.mergedSecurityContext" -}}
{{- $defaultKey := printf "default%s" (.key | title) -}}
{{- $ctx := index .component .key | default dict | deepCopy -}}
{{- $defaults := index .component $defaultKey | default dict -}}
{{- if $defaults.enabled -}}
{{- $ctx = $ctx | merge (omit $defaults "enabled") -}}
{{- end -}}
{{- toYaml $ctx -}}
{{- end -}}

Call sites

securityContext: {{- include "flagsmith.mergedSecurityContext" (dict "component" .Values.api "key" "securityContext") | nindent 10 }}
securityContext: {{- include "flagsmith.mergedSecurityContext" (dict "component" .Values.api "key" "podSecurityContext") | nindent 8 }}

Thank you @germangarces for your perfect feedback. I will fix it and update the PR.

Collapse repeated securityContext merge blocks into flagsmith.mergedSecurityContext
in _helpers.tpl per review feedback on PR Flagsmith#571.
@abbassoltanian-usmobile

Copy link
Copy Markdown
Contributor Author

@germangarces thanks again for your feedback.
I applied that change plus have some optimization in the charts/flagsmith/values.yaml file to avoid repeating the same block.
Please check the Updated PR.

@germangarces

Copy link
Copy Markdown
Member

@abbassoltanian-usmobile Thanks, helper's cleaner than what I sketched. Retested: all five pods admitted under enforce=restricted.

One blocker, all from the 79974a9 commit.

Per-component override doesn't work. values.yaml:12 documents api.defaultSecurityContext.enabled: false, but:

--set api.defaultSecurityContext.enabled=false    # still drop: [ALL]  ← no effect
--set global.defaultSecurityContext.enabled=false # {}                 ← works

_helpers.tpl:376 -> sprig merge treats false as empty, so the component's false is skipped and global's true wins. Truthy component values do win, so it's inconsistent rather than global-always-wins.

Also regresses main, same flags both sides:

--set api.defaultPodSecurityContext.enabled=false --set api.defaultPodSecurityContext.fsGroup=1111
main → securityContext: {}      PR → securityContext: {fsGroup: 1111}

That key is gone from values.yaml too, so anyone using it loses it silently.

The 79974a9 move is a separate concern from exposing container-level securityContext, and it's what pulled the regression in. The PR was ready before it. Would you prefer to drop it here and raise it on its own?

@abbassoltanian-usmobile
abbassoltanian-usmobile force-pushed the feat/restricted-pss-container-securitycontext branch from 79974a9 to a10667b Compare July 30, 2026 17:51
@abbassoltanian-usmobile

Copy link
Copy Markdown
Contributor Author

@germangarces good catch, thanks for the thorough retest. Agreed — dropping the global-defaults move rather than trying to patch the boolean-merge issue in place. Force-pushed the branch back to a10667b (the helper-extraction commit you already retested and approved), so 79974a9 is gone entirely. Happy to raise the global-defaults idea as its own PR later with the merge semantics sorted out properly.

@germangarces germangarces left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your work @abbassoltanian-usmobile

@germangarces
germangarces merged commit e8d443e into Flagsmith:main Jul 31, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants