Guardrails That Stop It
- Non-root enforcement:
runAsNonRoot: trueat both Pod and Container level. - No Escalation:
allowPrivilegeEscalation: false. - Dropped Capabilities:
capabilities.drop: [ALL]. - ReadOnly Filesystem:
readOnlyRootFilesystem: true. - Seccomp Profile:
seccompProfile: RuntimeDefault.
What a Hardened Baseline Looks Like
- Non-root User: No process should ever run as UID 0.
- ReadOnly Filesystem: The container cannot download and execute malware on its own filesystem.
- Explicit Writable Paths: Writable paths (like
/tmp) are explicitly mounted asemptyDirvolumes. - Namespace Consistency: Every container in the namespace follows the same baseline.
Hardened backend deployment
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
labels:
app: backend
app.kubernetes.io/name: backend
app.kubernetes.io/component: api
spec:
replicas: 1
revisionHistoryLimit: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
app.kubernetes.io/name: backend
app.kubernetes.io/component: api
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
prometheus.io/path: "/metrics"
spec:
imagePullSecrets:
- name: ghcr-credentials-docker
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: backend
image: backend:latest
imagePullPolicy: IfNotPresent
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
capabilities:
drop:
- ALL
ports:
- containerPort: 8080
name: http
protocol: TCP
env:
- name: PORT
value: "8080"
- name: NAMESPACE
value: "${NAMESPACE}"
- name: ENVIRONMENT
value: "${ENVIRONMENT}"
- name: LOG_LEVEL
value: "${LOG_LEVEL}"
- name: SERVICE_NAME
value: "backend"
- name: SERVICE_VERSION
value: "v1.0.0"
- name: DEPLOYMENT_ENVIRONMENT
value: "${ENVIRONMENT}"
- name: OTEL_RESOURCE_ATTRIBUTES
value: "k8s.cluster.name=${cluster_name}"
- name: UPTRACE_DSN
valueFrom:
secretKeyRef:
name: backend-secrets
key: uptrace-dsn
- name: OTEL_EXPORTER_OTLP_HEADERS
valueFrom:
secretKeyRef:
name: backend-secrets
key: uptrace-headers
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: backend-secrets
key: jwt-secret
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: app-postgres-app
key: username
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: app-postgres-app
key: password
- name: POSTGRES_HOST
value: app-postgres-rw
- name: POSTGRES_DB
value: app
livenessProbe:
httpGet:
path: /livez
port: http
initialDelaySeconds: 15
periodSeconds: 20
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /readyz
port: http
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
startupProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 0
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 30
resources:
requests:
cpu: 10m
memory: 32Mi
ephemeral-storage: 64Mi
limits:
cpu: 100m
memory: 128Mi
ephemeral-storage: 128Mi
volumeMounts:
- name: tmp
mountPath: /tmp
- name: cache
mountPath: /home/app/.cache
volumes:
- name: tmp
emptyDir: {}
- name: cache
emptyDir:
sizeLimit: 10Mi
Safe Workflow (Step-by-Step)
- Start from the Baseline: Always start from the hardened deployment manifest.
- Validate Context: Check the Pod Security Standard (PSS) baseline for the target namespace.
- Address Permissions: If the app fails to start, identify the specific path it needs to write to.
- Add Targeted Volumes: Use an
emptyDirvolume for the missing path. - Re-run & Confirm: Verify the app starts and remains non-root with dropped caps.
This builds on: Network isolation (Chapter 06) — security context adds pod-level defense. This enables: Resource management (Chapter 08) — hardened pods need resource contracts.