Security RBAC Pod Security Admission and Supply Chain

Reading time
10 min read
Word count
1967 words
Diagram count
2 diagrams

Source: Victor Bona's Obsidian Compendium snapshot, Knowledge base/kubernetes/09 Security RBAC Pod Security Admission and Supply Chain.md.

Purpose: explain how Kubernetes security controls compose across identity, admission, pod hardening, secrets, and software supply chain enforcement.

Security, RBAC, Pod Security Admission, and Supply Chain

This note expands Kubernetes, 01 Kubernetes Mental Model and Architecture, 06 Configuration Secrets ServiceAccounts and Runtime Identity, and 09 Security RBAC Pod Security Admission and Supply Chain with production security guidance. Kubernetes security is layered. RBAC answers who can call the API. Admission answers what objects are allowed. Pod security settings reduce container escape and privilege escalation risk. Supply chain controls reduce the chance that unsafe images reach the cluster. None of these layers is enough alone.

Rendering diagram...

Security model

LayerMain questionKubernetes mechanismFailure mode
AuthenticationWho is calling the APIClient certs, OIDC, ServiceAccount tokens, webhook authShared credentials, long lived tokens, weak issuer validation
AuthorizationIs the caller allowedRBAC Roles, ClusterRoles, bindingsOverbroad verbs, wildcard resources, cluster admin bindings
AdmissionIs the requested object acceptableBuilt in admission, ValidatingAdmissionPolicy, mutating webhooks, validating webhooksPolicy bypass by namespace gaps, fail open webhooks
Pod sandboxingWhat can a container do on a nodeSecurityContext, seccomp, AppArmor, SELinux, capabilitiesPrivileged containers, host namespaces, writable root filesystems
Supply chainCan the image be trustedRegistry policy, image scanning, SBOMs, signatures, provenanceMutable tags, unsigned images, vulnerable bases
SecretsCan sensitive data be readSecret RBAC, encryption at rest, external secrets, short lived tokensNamespace readers can exfiltrate secrets
TenancyCan one tenant affect anotherNamespace isolation, quotas, network policy, node isolationSoft isolation mistaken for hard isolation

RBAC primitives

RBAC is additive. There is no deny rule in native RBAC. A subject receives the union of all permissions granted by all matching bindings.

ObjectScopeGrants permissions forBinds to
RoleNamespaceNamespaced resources inside one namespaceRoleBinding
ClusterRoleClusterCluster resources, or reusable namespaced permissionsRoleBinding or ClusterRoleBinding
RoleBindingNamespaceSubjects in a namespace contextRole or ClusterRole
ClusterRoleBindingClusterSubjects across the whole clusterClusterRole

Minimal namespace reader:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: pod-reader
  namespace: app-prod
rules:
  - apiGroups: [""]
    resources: ["pods", "pods/log"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: pod-reader-for-oncall
  namespace: app-prod
subjects:
  - kind: Group
    name: platform-oncall
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io

Reusable ClusterRole bound only inside one namespace:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: workload-debug-reader
rules:
  - apiGroups: [""]
    resources: ["pods", "pods/log", "events", "services", "endpoints"]
    verbs: ["get", "list", "watch"]
  - apiGroups: ["apps"]
    resources: ["deployments", "replicasets", "statefulsets", "daemonsets"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: workload-debug-reader
  namespace: app-prod
subjects:
  - kind: ServiceAccount
    name: diagnostics-bot
    namespace: platform-tools
roleRef:
  kind: ClusterRole
  name: workload-debug-reader
  apiGroup: rbac.authorization.k8s.io

High risk permissions:

PermissionWhy it is dangerousSafer pattern
* on *Equivalent to unbounded future access as APIs are addedName exact resources and verbs
create pods/execRemote command execution into containersRestrict to break glass groups, audit every use
create pods/ephemeralcontainersCan inject debug containers into PodsRestrict to incident responders
get secretsSecret values are exposed through the APIUse narrow names or external secret access
create podsCan run arbitrary workloads under allowed ServiceAccountsCombine with PSA and admission policy
impersonate users/groups/serviceaccountsCan become another identityRestrict to control plane automation
bind and escalate on RBAC resourcesCan grant privileges the caller does not already haveAvoid outside trusted platform controllers
update validatingwebhookconfigurationsCan disable admission controlsPlatform admin only
update nodesCan disrupt scheduling or trust boundariesCluster operator only

ClusterRoles and aggregation

Aggregated ClusterRoles let Kubernetes combine labeled ClusterRoles into a parent ClusterRole. This is how built in roles such as admin, edit, and view can gain permissions for custom resources.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: widgets-view
  labels:
    rbac.authorization.k8s.io/aggregate-to-view: "true"
rules:
  - apiGroups: ["apps.example.com"]
    resources: ["widgets"]
    verbs: ["get", "list", "watch"]

Aggregation tradeoffs:

ChoiceBenefitRisk
Aggregate CRD read permissions to viewNormal readers can inspect new resourcesSensitive CRD fields may become broadly visible
Aggregate write permissions to editApp teams can operate CRDs with familiar rolesedit may become more powerful than intended
Avoid aggregation and create explicit rolesClear least privilegeMore bindings to maintain

Review every aggregated role as part of CRD review. A CRD that stores credentials, connection strings, policy decisions, or tenant state should not be blindly aggregated into view.

ServiceAccount permissions

Pods authenticate to the API as a ServiceAccount. If serviceAccountName is omitted, Pods use the namespace default ServiceAccount.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: invoice-worker
  namespace: payments
automountServiceAccountToken: false
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: invoice-worker
  namespace: payments
spec:
  replicas: 2
  selector:
    matchLabels:
      app: invoice-worker
  template:
    metadata:
      labels:
        app: invoice-worker
    spec:
      serviceAccountName: invoice-worker
      automountServiceAccountToken: false
      containers:
        - name: worker
          image: registry.example.com/payments/invoice-worker@sha256:1111111111111111111111111111111111111111111111111111111111111111

Production guidance:

PracticeReason
Create one ServiceAccount per workloadAvoid shared blast radius
Set automountServiceAccountToken: false unless the app calls the APIReduces token theft impact
Bind ServiceAccounts by namespace and namePrevents broad group grants
Prefer short lived projected tokensReduces long lived credential exposure
Avoid mounting cloud provider credentials directlyPrefer workload identity integrations
Audit default ServiceAccount bindingsMany accidental privileges start there

Useful checks:

kubectl auth can-i get pods --as=system:serviceaccount:payments:invoice-worker -n payments
kubectl auth can-i create pods/exec --as=system:serviceaccount:payments:invoice-worker -n payments
kubectl get rolebinding,clusterrolebinding -A -o wide | rg invoice-worker
kubectl describe serviceaccount invoice-worker -n payments

Pod Security Standards

Pod Security Standards define three policy profiles: privileged, baseline, and restricted.

ProfileIntentCommon use
privilegedAllows almost everythingSystem namespaces, CNI, CSI, node agents
baselineBlocks known privilege escalation paths while preserving common workloadsTransitional default for app namespaces
restrictedStrongest built in pod hardening profileProduction application namespaces

Pod Security Admission is the built in admission controller that enforces Pod Security Standards. It became stable in Kubernetes v1.25. PodSecurityPolicy was removed in Kubernetes v1.25, so new clusters should use Pod Security Admission plus policy engines when built in standards are not expressive enough.

Namespace labels:

apiVersion: v1
kind: Namespace
metadata:
  name: payments
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: v1.30
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/audit-version: v1.30
    pod-security.kubernetes.io/warn: restricted
    pod-security.kubernetes.io/warn-version: v1.30

Rollout pattern:

PhaseLabelsGoal
Discoverwarn=restricted, audit=restrictedShow users and audit logs what would fail
Enforce baselineenforce=baseline, warn=restrictedBlock obvious host and privileged escapes
Enforce restrictedenforce=restrictedRequire hardened defaults for app namespaces
Exception handlingIsolated namespace with explicit owner and expiryKeep system or legacy exceptions visible

SecurityContext hardening

Restricted application Pod:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hardened-api
  namespace: payments
spec:
  replicas: 3
  selector:
    matchLabels:
      app: hardened-api
  template:
    metadata:
      labels:
        app: hardened-api
    spec:
      serviceAccountName: hardened-api
      automountServiceAccountToken: false
      securityContext:
        runAsNonRoot: true
        runAsUser: 10001
        runAsGroup: 10001
        fsGroup: 10001
        seccompProfile:
          type: RuntimeDefault
      containers:
        - name: api
          image: registry.example.com/payments/api@sha256:2222222222222222222222222222222222222222222222222222222222222222
          ports:
            - containerPort: 8080
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: ["ALL"]
          volumeMounts:
            - name: tmp
              mountPath: /tmp
      volumes:
        - name: tmp
          emptyDir: {}

Key settings:

SettingProduction defaultWhy
runAsNonRoottruePrevents root user execution when image metadata is safe
runAsUserExplicit high UIDAvoids image default surprises
allowPrivilegeEscalationfalseBlocks setuid and privilege escalation paths
capabilities.drop["ALL"]Removes Linux capabilities not needed by normal apps
seccompProfile.typeRuntimeDefaultBlocks many dangerous syscalls
readOnlyRootFilesystemtrueLimits filesystem tampering
privilegedfalseAvoids host level access
hostNetwork, hostPID, hostIPCfalseKeeps workload out of host namespaces

Capability examples:

NeedAvoidSafer option
Bind to port 80Add NET_BIND_SERVICE by habitListen on 8080 and map Service port 80
Packet capturePrivileged containerDedicated debug namespace and short lived tooling
Filesystem ownership fixRun as root foreverInitContainer with narrow permission, then nonroot app

Seccomp, AppArmor, and SELinux

ControlWhat it governsKubernetes usageNotes
seccompLinux syscallssecurityContext.seccompProfileRuntimeDefault is the common baseline
AppArmorLinux program access profileLocalhost profiles through annotations or security fields depending on versionNode profile availability matters
SELinuxMandatory access control labelsseLinuxOptionsStrong when platform manages labels correctly

Overview:

securityContext:
  seccompProfile:
    type: RuntimeDefault

SELinux is most common on distributions where the node OS and container runtime are configured together. AppArmor is common on Ubuntu based nodes. Do not enable custom profiles by copy and paste. Roll them out on a node pool where profile installation, drift detection, and workload scheduling are controlled.

Admission controllers and policy engines

Native admission controls run after authorization and before persistence. They can reject, mutate, or validate API objects depending on the controller.

Important categories:

ControlPurposeExamples
Built in admissionCore Kubernetes behaviorPod Security Admission, ResourceQuota, LimitRanger, NamespaceLifecycle
Mutating webhookDefaults objects before validationSidecar injection, label injection
Validating webhookRejects objects that violate policyOPA Gatekeeper, Kyverno validate rules
ValidatingAdmissionPolicyNative CEL based validationSimple policy without external webhook runtime

ValidatingAdmissionPolicy with CEL:

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: require-image-digest
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE"]
        resources: ["pods"]
  validations:
    - expression: "object.spec.containers.all(c, c.image.contains('@sha256:'))"
      message: "containers must use immutable image digests"
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: require-image-digest
spec:
  policyName: require-image-digest
  validationActions: ["Deny"]

Gatekeeper constraint example:

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
  name: require-owner-label
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Namespace"]
  parameters:
    labels:
      - key: owner

Kyverno image policy example:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-signed-images
spec:
  validationFailureAction: Enforce
  background: true
  rules:
    - name: verify-cosign-signature
      match:
        any:
          - resources:
              kinds: ["Pod"]
      verifyImages:
        - imageReferences:
            - "registry.example.com/*"
          attestors:
            - entries:
                - keyless:
                    issuer: "https://token.actions.githubusercontent.com"
                    subject: "https://github.com/example/*"

Policy engine tradeoffs:

ApproachStrengthCost
Pod Security AdmissionBuilt in, stable, simple namespace labelsLimited to Pod Security Standards
ValidatingAdmissionPolicy with CELNo external webhook service for simple checksCEL is not ideal for complex inventory or mutation
OPA GatekeeperStrong constraint model and auditRego learning curve and webhook operations
KyvernoKubernetes native YAML policy, mutation, image verificationPolicy complexity can grow quickly
Custom webhookFull flexibilityHighest operational and safety burden

Image provenance and supply chain

Supply chain security should produce evidence at build time and enforce evidence at deploy time.

Rendering diagram...

Core practices:

PracticeWhy it mattersEnforcement point
Pin images by digestTags are mutableCI manifests and admission
Scan imagesFinds known vulnerabilities and malware indicatorsCI, registry, admission
Generate SBOMsGives dependency inventory for responseCI artifact store and registry
Sign images with CosignLinks image digest to trusted identityAdmission policy
Attach provenanceShows builder, source, and workflowAdmission and release review
Restrict registriesPrevents unknown image sourcesAdmission policy and runtime config
Rebuild base imagesReduces stale CVE exposureCI cadence

Cosign commands:

cosign sign --key cosign.key registry.example.com/payments/api@sha256:2222222222222222222222222222222222222222222222222222222222222222
cosign verify --key cosign.pub registry.example.com/payments/api@sha256:2222222222222222222222222222222222222222222222222222222222222222
cosign attest --predicate sbom.spdx.json --type spdxjson registry.example.com/payments/api@sha256:2222222222222222222222222222222222222222222222222222222222222222

Registry policy:

RuleRecommended default
Allowed registriesPrivate registry plus approved public mirrors
TagsDisallow mutable tags in production
LatestDisallow
SignaturesRequire for first party images
Vulnerability thresholdBlock exploitable critical findings, warn on lower risk with SLA
SBOMRequire for promoted production images
Build identityRequire trusted CI issuer and repository subject

Secrets security

Kubernetes Secrets are base64 encoded API objects, not a complete secret management system. Anyone with get secrets in a namespace can retrieve values.

Production controls:

ControlReason
Enable encryption at rest for Secrets in etcdReduces impact of raw datastore exposure
Restrict Secret RBAC separately from ConfigMapsConfigMap read is often broader
Avoid putting secrets in environment variables when possibleEnvironment values can leak through process inspection and dumps
Mount secrets as files with narrow pathsEasier rotation and narrower exposure
Use external secret operators carefullyThey create Kubernetes Secrets unless configured otherwise
Rotate after incident and after broad access grantsSecrets are often copied by workloads and humans
Avoid secret values in annotations, labels, and eventsMetadata is broadly visible

Secret volume example:

apiVersion: v1
kind: Pod
metadata:
  name: secret-consumer
  namespace: payments
spec:
  containers:
    - name: app
      image: registry.example.com/payments/app@sha256:3333333333333333333333333333333333333333333333333333333333333333
      volumeMounts:
        - name: db-password
          mountPath: /var/run/secrets/db
          readOnly: true
  volumes:
    - name: db-password
      secret:
        secretName: db-password
        defaultMode: 0400

Multi-tenant isolation limits

Namespaces are administrative boundaries, not hard security boundaries. Multi-tenant Kubernetes can be practical, but only when the threat model matches the controls.

ConcernNamespace onlyStronger control
API accessRBAC per namespaceSeparate clusters for hostile tenants
Network accessNetworkPolicy if CNI enforces itDefault deny, egress controls, service mesh policy
Node kernel sharingShared nodesDedicated node pools or clusters
Resource contentionQuotas and limitsPriority classes, separate node pools
Secret exposureNamespace RBACExternal secret boundaries and separate control planes
Admission exceptionsLabels per namespaceCentral policy with exception review

Use separate clusters for untrusted tenants, regulated workloads that require hard boundaries, or workloads that need privileged host access. Use namespaces for teams with shared trust, internal environments, and soft isolation.

Troubleshooting security denials

RBAC Forbidden:

kubectl auth can-i list pods -n payments
kubectl auth can-i list pods -n payments --as=system:serviceaccount:payments:invoice-worker
kubectl describe rolebinding -n payments
kubectl describe clusterrolebinding
kubectl get events -n payments --sort-by=.lastTimestamp

Admission denied:

kubectl apply -f deployment.yaml --dry-run=server
kubectl describe namespace payments
kubectl get validatingadmissionpolicy,validatingadmissionpolicybinding
kubectl get validatingwebhookconfiguration,mutatingwebhookconfiguration
kubectl get events -A --field-selector reason=FailedCreate

Pod security denied:

  1. Read the rejection message and identify the violated field.
  2. Check namespace labels for pod-security.kubernetes.io/enforce.
  3. Compare Pod spec against the target profile.
  4. Remove privileged, host namespace, host path, added capabilities, root user, or missing seccomp settings.
  5. If the workload is a node agent, move it to an explicit privileged namespace with owner review.

Common mistakes

MistakeConsequenceFix
Binding cluster-admin to CICI compromise becomes cluster compromiseCreate narrow deploy roles per namespace
Using the default ServiceAccountAccidental shared identityCreate named ServiceAccounts per workload
Leaving token automount enabledTokens appear in every PodDisable by default and opt in
Treating view as always safeSome CRDs expose sensitive dataReview aggregation labels and CRD schemas
Relying only on image scanningSigned vulnerable images can still passCombine scanning, signing, provenance, and runtime monitoring
Enforcing restricted without dry runWorkloads fail during rolloutStart with warn and audit labels
Allowing mutable tagsRollbacks and audits become ambiguousRequire image digests
Running debug tools as privilegedDebug path becomes escape pathUse ephemeral containers with controlled RBAC

Security review checklist

  • Namespace has Pod Security Admission labels with explicit version.
  • Workloads run as nonroot with allowPrivilegeEscalation: false.
  • Containers drop all Linux capabilities unless a reviewed exception exists.
  • seccompProfile.type: RuntimeDefault is set at Pod or container level.
  • Root filesystem is read only or the write paths are explicitly mounted.
  • ServiceAccount is workload specific.
  • ServiceAccount token automount is disabled unless required.
  • RBAC grants exact verbs and resources.
  • No wildcard RBAC exists outside platform controlled roles.
  • No app workload uses hostNetwork, hostPID, hostIPC, or privileged mode.
  • Production images are pinned by digest.
  • Image registry is approved.
  • Image signature, SBOM, and vulnerability scan evidence exist.
  • Secrets are not present in labels, annotations, command args, or logs.
  • Admission policy failures are monitored and routed to owning teams.