Container & Kubernetes Security

Secure container workloads. Covers image scanning, runtime protection, pod security standards, network policies, secrets in K8s, supply chain security, and admission controllers.

Container & Kubernetes security is the practice of hardening the lifecycle of containers and the orchestration platform that runs them—ensuring that images are trustworthy, workloads are isolated, configurations are consistent, and the cluster itself is resilient to attacks. It matters when deploying microservices, CI/CD pipelines, or cloud-native applications at scale, particularly when teams manage dozens of services across multiple environments, and when security is not an afterthought but a first-class concern.

Securing the Container Image Lifecycle

Build with Trusted Base Images

Use docker build with a Dockerfile that explicitly layers dependencies and sets a non-root user. The base image must be signed and verified.

FROM alpine:3.19 AS base
RUN apk add --no-cache ca-certificates tzdata
USER 1001:1001
WORKDIR /app

FROM base AS builder
COPY --from=registry.example.com/base-app:latest /app /app
RUN npm install --production && npm run build

FROM base AS runtime
COPY --from=builder /app /app
EXPOSE 8080
CMD ["/app/server.js"]

Build with docker build --pull --no-cache --progress=plain -t myapp:v1.2.0 ..

Verify image signatures using Cosign:

cosign verify --certificate-oidc-issuer=https://oidc.example.com --certificate-oidc-audience=cosign myapp:v1.2.0

Signature verification fails silently if the image has no signature, or if the OIDC claim is malformed. A missing cosign.crt file or a misconfigured --certificate-oidc-audience leads to ERROR: failed to verify signature: invalid claim: audience mismatch.

Scan for Vulnerabilities at Build Time

Use Trivy to scan images during build:

trivy image --exit-code 1 --severity HIGH,CRITICAL myapp:v1.2.0

Exit code 1 fails the build if any high or critical vulnerability is found. Trivy outputs a table of CVEs, including CVE-2023-1234, Severity: HIGH, Installed: openssl-1.1.1t-r1, Fixed: openssl-1.1.1u-r0.

Integrate Trivy into CI:

- name: Scan image
  run: |
    trivy image --exit-code 1 --severity HIGH,CRITICAL --format json --output trivy-report.json myapp:v1.2.0
    jq -r '.Vulnerabilities[] | .Vulnerability.ID' trivy-report.json

The --format json flag is essential: it enables tooling for dashboarding, alerts, and policy evaluation.

Deploying Secure Workloads to Kubernetes

Define Pod Security Policies with Best Practices

Use PodSecurity profiles in Kubernetes via PodSecurity admission controller. Define policies using PodSecurity objects.

apiVersion: policy/v1
kind: PodSecurity
metadata:
  name: restricted
spec:
  defaults:
    allowPrivilegeEscalation: false
    runAsUser:
      rule: MustRunAs
      ranges:
        - min: 1000
          max: 1000
    seLinux:
      rule: MustRunAs
    supplementalGroups:
      rule: MustRunAs
      ranges:
        - min: 1000
          max: 1000
    fsGroup:
      rule: MustRunAs
      ranges:
        - min: 1000
          max: 1000
  exemptions:
    users: []
    groups: []
  versions:
    - v1.28

Apply to namespace:

kubectl apply -f pod-security.yaml
kubectl label namespace production pod-security.kubernetes.io/level=restricted

A common failure: runAsUser set to rule: RunAsAny but ranges not defined. This allows any UID, but the runAsUser entry is ignored. The correct configuration must include rule: MustRunAs and ranges.

Configure Pod Security Contexts Explicitly

In Deployment manifests, define securityContext at the pod and container level.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 1001
        fsGroup: 1001
        supplementalGroups:
          - 1001
        seLinuxOptions:
          level: "s0:c123,c456"
      containers:
        - name: api
          image: myapp:v1.2.0
          ports:
            - containerPort: 8080
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop:
                - ALL
            runAsUser: 1001
            seLinuxOptions:
              level: "s0:c123,c456"

The readOnlyRootFilesystem: true flag prevents any write to / in the container—critical for preventing file-based attacks. A failure mode: docker exec into a pod, modify /etc/hosts, but the change is not persisted because readOnlyRootFilesystem was not set.

Use Image Pull Secrets and Registry Authentication

Authenticate to private registries using ImagePullSecrets:

apiVersion: v1
kind: Secret
metadata:
  name: regcred
type: kubernetes.io/dockerconfigjson
data:
  .dockerconfigjson: |
    {
      "auths": {
        "registry.example.com": {
          "auth": "base64(username:password)",
          "email": "[email protected]"
        }
      }
    }

Apply and reference in Pod spec:

spec:
  imagePullSecrets:
    - name: regcred

If the auth field is missing or the base64 encoding is malformed, Kubernetes logs failed to pull image "registry.example.com/myapp:v1.2.0": rpc error: code = Unknown desc = failed to pull image: failed to resolve image: failed to get auth config.

Enforce Image Verification via Admission Controllers

Use kyverno to validate pod specifications at creation time.

apiVersion: kyverno.io/v1
kind: Policy
metadata:
  name: verify-image-signature
spec:
  rules:
    - name: require-image-signature
      match:
        resources:
          kinds:
            - Pod
      verify:
        - jmespath: "spec.containers[*].image"
          jmespath: "contains(@, 'cosign')"
          message: "All containers must use cosign-signed images."
        - jmespath: "spec.containers[*].image"
          jmespath: "all(@, contains(@, 'myapp'))"
          message: "All containers must be from myapp registry."

Apply:

kubectl apply -f kyverno-policy.yaml

A common failure: the jmespath query contains(@, 'cosign') returns true if any image contains cosign in its string, but cosign is not a required prefix. A mismatch: myapp:cosign-v1.2.0 passes, but myapp:1.2.0-cosign fails. The correct query uses contains(@, 'cosign') but the message field is missing.

Securing the Kubernetes Control Plane

Harden the API Server

Configure kube-apiserver with TLS, client certificate authentication, and admission control.

--tls-cert-file=/etc/kubernetes/certs/apiserver.crt
--tls-private-key-file=/etc/kubernetes/certs/apiserver.key
--client-ca-file=/etc/kubernetes/certs/ca.crt
--authorization-mode=RBAC
--enable-admission-plugins=NodeRestriction,PodSecurity,ResourceQuota
--enable-bootstrap-token-auth=true
--authentication-token-webhook=true
--authentication-token-webhook-config-file=/etc/kubernetes/token-webhook.yaml

The --authentication-token-webhook-config-file points to a YAML file defining the webhook endpoint:

apiVersion: v1
kind: Config
clusters:
  - name: token-auth
    cluster:
      server: https://auth.example.com/token
      certificate-authority: /etc/kubernetes/ca.pem
users:
  - name: webhook
    user:
      client-certificate: /etc/kubernetes/webhook.crt
      client-key: /etc/kubernetes/webhook.key

A silent failure: the client-certificate is missing, or the server URL is unreachable. The API server logs failed to load webhook config: failed to connect to https://auth.example.com/token.

Enable Role-Based Access Control (RBAC)

Define ClusterRole and RoleBinding with precise permissions.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: app-deployer
rules:
  - apiGroups: [""]
    resources: ["pods", "services"]
    verbs: ["get", "list", "create", "update", "delete"]
  - apiGroups: ["apps"]
    resources: ["deployments", "statefulsets"]
    verbs: ["get", "list", "create", "update", "delete"]
  - apiGroups: [""]
    resources: ["configmaps"]
    verbs: ["get", "list", "create", "update", "delete"]
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: deployer-binding
  namespace: production
subjects:
  - kind: User
    name: [email protected]
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: app-deployer
  apiGroup: rbac.authorization.k8s.io

A frequent error: a user can create pods but not update them, because update is missing from the verbs list. The RoleBinding is applied, but the namespace is misspelled as production when the namespace is prod.

Monitoring and Responding to Security Incidents

Set Up Audit Logs

Enable audit logging in kube-apiserver:

--audit-policy-file=/etc/kubernetes/audit-policy.yaml
--audit-log-path=/var/log/kubernetes/audit.log
--audit-log-maxsize=104857600
--audit-log-maxbackup=10
--audit-log-maxage=30

The audit-policy.yaml file:

apiVersion: audit.k8s.io/v1
kind: Policy
rules:
  - level: Metadata
    resources:
      - group: ""
        resources: ["pods"]
  - level: Request
    resources:
      - group: "apps"
        resources: ["deployments"]
  - level: RequestResponse
    resources:
      - group: "batch"
        resources: ["jobs"]

A silent failure: RequestResponse level logs are stored, but Request level logs are not. The audit log contains level: Metadata but no Request fields.

Use kubectl for Real-Time Security Investigation

Check pod security context:

kubectl describe pod api-service-789d4f6c5b

Look for Security Context in output:

Security Context:
  RunAsNonRoot:     true
  RunAsUser:        1001
  ReadOnlyRootFilesystem: true
  Capabilities:
    Drop: ALL

Failures: a pod runs as root, but RunAsNonRoot is false in the manifest. Or, readOnlyRootFilesystem is true, but fsGroup is nil, causing /var to be writable.

Use kubectl get pods -o wide to correlate pod IPs with node labels and taints.

Troubleshooting Common Failures

Key Principles

This is container and Kubernetes security: a set of disciplined, repeatable practices that transform a cluster from a deployment platform into a security-hardened system.

This page was rewritten on 16 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.