Task · SFL-0070

helm: lease RBAC silently binds to the namespace `default` ServiceAccount when serviceAccount.create=false with no name

Status
To Do
Labels
followup, phase-3
Milestone
Security & deployment hardening
Updated
2026-08-14

Description

What

sf2loki.serviceAccountName (deploy/helm/templates/_helpers.tpl:49-55) falls back to the literal string "default" when serviceAccount.create is false and serviceAccount.name is empty:

{{- define "sf2loki.serviceAccountName" -}}
{{- if .Values.serviceAccount.create -}}
{{- default (include "sf2loki.fullname" .) .Values.serviceAccount.name -}}
{{- else -}}
{{- default "default" .Values.serviceAccount.name -}}
{{- end -}}
{{- end -}}

With ha.enabled: true, deploy/helm/templates/rbac.yaml:69 interpolates that helper straight into the lease RoleBinding subject. Verified render:

$ helm template sf2loki deploy/helm --set ha.enabled=true --set replicaCount=2 \
    --set serviceAccount.create=false
...
kind: RoleBinding
metadata:
  name: sf2loki-lease
subjects:
  - kind: ServiceAccount
    name: default          # <-- the namespace's shared default SA
    namespace: default
roleRef:
  kind: Role
  name: sf2loki-lease

The Role being bound (deploy/helm/templates/rbac.yaml:44-58) grants:

The chart already knows this SA is the wrong subject. The rbac.yaml:2-5 header comment gives the shared-default-SA hazard as the stated reason the chart creates its own SA by default: “Created by default so the chart doesn’t depend on a pre-existing ‘default’ SA (which may carry unrelated permissions in a shared namespace)”. The create: false path then binds to exactly that SA with no complaint.

Nothing catches the combination:

Why it matters

The misconfiguration is completely silent — sf2loki itself keeps working. deploy/helm/templates/deployment.yaml:47 uses the same helper, so the pod also runs as serviceAccountName: default and holds the permissions it needs. Nothing fails, nothing logs, and the operator has no signal.

The damage is blast radius. Every other pod in the namespace that omits serviceAccountName also runs as default, so an unrelated workload inherits:

A compromised or hostile co-tenant pod in a shared namespace is the realistic path; the operator’s intent (create: false to bind an externally-managed IRSA/workload-identity SA) is defeated silently rather than loudly.

Proposed approach

Fail the render on the ambiguous combination, mirroring the existing single-instance guard’s shape. In deploy/helm/templates/rbac.yaml, before the {{- if .Values.ha.enabled }} block at line 36:

{{- if and .Values.ha.enabled (not .Values.serviceAccount.create) (not .Values.serviceAccount.name) -}}
{{- fail "ha.enabled with serviceAccount.create=false requires an explicit serviceAccount.name: leaving it empty resolves to the namespace's shared `default` ServiceAccount, and the lease RoleBinding would grant create-on-any-Lease plus get/update on the leader Lease to every pod in the namespace that does not set serviceAccountName. Set serviceAccount.name to your externally-managed SA, or leave serviceAccount.create=true." -}}
{{- end -}}

Failing beats defaulting here: silently substituting the fullname would bind a Role to an SA that does not exist (the pod then cannot start), and there is no safe guess for an externally-managed SA’s name.

Supporting changes:

Consider whether externalSecrets.enabled with an empty serviceAccount.esoName deserves the same treatment — deploy/helm/templates/externalsecret.yaml:25-26,36-37 renders an empty serviceAccountRef.name, and deploy/helm/README.md:164-166 calls esoName required without enforcing it. Out of scope here unless bundled deliberately.


Imported from GitHub issue #154 on 2026-08-14, when this repo migrated from GitHub Issues to Backlog.md. The original issue has been deleted; its verbatim body, labels and comments are preserved in archive/issues-dump.json (jq '.[] | select(.number == 154)' archive/issues-dump.json).

Filed from the 2026-07-30 full-repo audit (11 finder lanes + adversarial verification per finding).

Acceptance Criteria

Definition of Done

References

View the source file on GitHub