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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
kubectl for Real-Time Security InvestigationCheck 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.
ImagePullBackOff, Failed to pull image: rpc error: code = Unknown desc = failed to resolve image.root despite runAsUser: 1001: securityContext.runAsUser is at the container level, but the PodSecurity policy expects it at the pod level.cosign verify returns ERROR: failed to verify signature: invalid claim: audience mismatch, but the --certificate-oidc-audience was not set to cosign.--exit-code 1 was forgotten in CI. Use trivy image --exit-code 1 --severity HIGH,CRITICAL to catch high-impact issues.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.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy