Container and Kubernetes Security: Production Best Practices
Container security is a multi-layered challenge that spans the entire container lifecycle - from base images to runtime. Kubernetes adds another dimension with its own security model for orchestration.
This guide covers practical security practices for production container environments.
Container Image Security
Secure Base Images
# Use distroless or minimal base images
FROM gcr.io/distroless/nodejs20-debian12:nonroot
# Or use specific Alpine version with security updates
FROM node:20.10.0-alpine3.18
# Apply security updates
RUN apk update && apk upgrade --no-cache
# Create non-root user
RUN addgroup -g 1001 -S appgroup && \
adduser -u 1001 -S appuser -G appgroup
# Set working directory
WORKDIR /app
# Copy only necessary files
COPY --chown=appuser:appgroup package*.json ./
RUN npm ci --only=production
COPY --chown=appuser:appgroup . .
# Remove unnecessary packages
RUN rm -rf /var/cache/apk/* /root/.npm
# Run as non-root
USER appuser
# Use specific UID
USER 1001
# Don't run as root even if image allows it
CMD ["node", "server.js"]Multi-Stage Build for Security
# Build stage
FROM node:20-alpine AS builder
WORKDIR /build
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build && npm prune --production
# Security scan stage
FROM aquasec/trivy:latest AS scanner
COPY --from=builder /build /scan
RUN trivy filesystem --exit-code 1 --severity HIGH,CRITICAL /scan
# Production stage
FROM gcr.io/distroless/nodejs20-debian12:nonroot
WORKDIR /app
# Copy only built artifacts
COPY --from=builder /build/dist ./dist
COPY --from=builder /build/node_modules ./node_modules
COPY --from=builder /build/package.json ./
USER nonroot
CMD ["dist/server.js"]Image Scanning Pipeline
# GitHub Actions workflow for image scanning
name: Container Security Scan
on:
push:
branches: [main]
pull_request:
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t app:${{ github.sha }} .
- name: Trivy vulnerability scan
uses: aquasecurity/trivy-action@master
with:
image-ref: 'app:${{ github.sha }}'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
exit-code: '1'
- name: Grype vulnerability scan
uses: anchore/scan-action@v3
with:
image: 'app:${{ github.sha }}'
fail-build: true
severity-cutoff: high
- name: Dockle lint
uses: erzz/dockle-action@v1
with:
image: 'app:${{ github.sha }}'
exit-code: '1'
failure-threshold: high
- name: Upload results to GitHub Security
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'Kubernetes Pod Security
Pod Security Standards
# Restricted Pod Security Standard
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: latest
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted
---
# Secure Pod specification
apiVersion: v1
kind: Pod
metadata:
name: secure-app
namespace: production
spec:
# Security context at pod level
securityContext:
runAsNonRoot: true
runAsUser: 1001
runAsGroup: 1001
fsGroup: 1001
seccompProfile:
type: RuntimeDefault
# Service account
serviceAccountName: app-service-account
automountServiceAccountToken: false
containers:
- name: app
image: myregistry/app:v1.0.0@sha256:abc123...
imagePullPolicy: Always
# Container security context
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1001
capabilities:
drop:
- ALL
# Resource limits
resources:
limits:
cpu: "1"
memory: "512Mi"
ephemeral-storage: "1Gi"
requests:
cpu: "100m"
memory: "128Mi"
# Health probes
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
# Volume mounts
volumeMounts:
- name: tmp
mountPath: /tmp
- name: secrets
mountPath: /secrets
readOnly: true
# Volumes
volumes:
- name: tmp
emptyDir: {}
- name: secrets
secret:
secretName: app-secrets
defaultMode: 0400
# Node affinity
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node-type
operator: In
values:
- production
# Tolerations
tolerations: []
# DNS policy
dnsPolicy: ClusterFirstDeployment Security
apiVersion: apps/v1
kind: Deployment
metadata:
name: secure-app
namespace: production
spec:
replicas: 3
revisionHistoryLimit: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 1
selector:
matchLabels:
app: secure-app
template:
metadata:
labels:
app: secure-app
annotations:
# Enable security scanning
container.apparmor.security.beta.kubernetes.io/app: runtime/default
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1001
fsGroup: 1001
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: myregistry/app:v1.0.0
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
ports:
- containerPort: 8080
protocol: TCP
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
resources:
limits:
cpu: "1"
memory: "512Mi"
requests:
cpu: "100m"
memory: "128Mi"
---
# Pod Disruption Budget
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: secure-app-pdb
spec:
minAvailable: 2
selector:
matchLabels:
app: secure-appNetwork Security
Network Policies
# Default deny all traffic
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
---
# Allow specific ingress
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-app-ingress
namespace: production
spec:
podSelector:
matchLabels:
app: secure-app
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: ingress-nginx
podSelector:
matchLabels:
app.kubernetes.io/name: ingress-nginx
ports:
- protocol: TCP
port: 8080
---
# Allow app to database
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-app-to-db
namespace: production
spec:
podSelector:
matchLabels:
app: secure-app
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchLabels:
app: postgresql
ports:
- protocol: TCP
port: 5432
# Allow DNS
- to:
- namespaceSelector: {}
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
---
# Deny egress to metadata service (AWS/GCP)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-metadata-access
namespace: production
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 169.254.169.254/32 # AWS metadata
- 169.254.0.0/16 # Link-local
- 10.0.0.0/8 # Internal (adjust as needed)Secrets Management
External Secrets Operator
# External Secrets configuration
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: aws-secrets-manager
namespace: production
spec:
provider:
aws:
service: SecretsManager
region: eu-west-1
auth:
jwt:
serviceAccountRef:
name: external-secrets-sa
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: app-secrets
namespace: production
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: SecretStore
target:
name: app-secrets
creationPolicy: Owner
template:
type: Opaque
data:
db-password: "{{ .db_password }}"
api-key: "{{ .api_key }}"
data:
- secretKey: db_password
remoteRef:
key: production/app/database
property: password
- secretKey: api_key
remoteRef:
key: production/app/api
property: keySealed Secrets
# Sealed Secret (encrypted, safe for git)
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: app-secrets
namespace: production
spec:
encryptedData:
db-password: AgBY8s9j3K...encrypted...
api-key: AgDJ2k4m8N...encrypted...
template:
metadata:
name: app-secrets
namespace: production
type: OpaqueRBAC Security
Least Privilege Service Account
# Service Account
apiVersion: v1
kind: ServiceAccount
metadata:
name: app-service-account
namespace: production
automountServiceAccountToken: false
---
# Role with minimal permissions
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: app-role
namespace: production
rules:
- apiGroups: [""]
resources: ["configmaps"]
resourceNames: ["app-config"]
verbs: ["get"]
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["app-secrets"]
verbs: ["get"]
---
# Role Binding
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: app-role-binding
namespace: production
subjects:
- kind: ServiceAccount
name: app-service-account
namespace: production
roleRef:
kind: Role
name: app-role
apiGroup: rbac.authorization.k8s.ioCluster-Wide RBAC Restrictions
# Deny privilege escalation cluster-wide
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: deny-privilege-escalation
rules:
- apiGroups: [""]
resources: ["pods/exec", "pods/attach"]
verbs: ["create"]
- apiGroups: [""]
resources: ["nodes/proxy", "pods/proxy", "services/proxy"]
verbs: ["*"]
---
# Audit role for security team
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: security-auditor
rules:
- apiGroups: ["*"]
resources: ["*"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["secrets"]
verbs: [] # No access to secretsRuntime Security
Falco Rules
# Falco rules for runtime detection
- rule: Terminal shell in container
desc: Detect shell execution in container
condition: >
spawned_process and container
and shell_procs
and proc.tty != 0
and container_entrypoint
output: >
Shell spawned in container
(user=%user.name container=%container.name
shell=%proc.name parent=%proc.pname)
priority: WARNING
tags: [container, shell]
- rule: Sensitive file access
desc: Detect access to sensitive files
condition: >
open_read and container
and sensitive_files
output: >
Sensitive file read
(user=%user.name file=%fd.name container=%container.name)
priority: WARNING
tags: [container, filesystem]
- rule: Crypto mining detection
desc: Detect potential crypto mining
condition: >
spawned_process and container
and (proc.name in (crypto_miners) or
proc.args contains "stratum" or
proc.args contains "xmr")
output: >
Crypto mining detected
(user=%user.name command=%proc.cmdline container=%container.name)
priority: CRITICAL
tags: [container, crypto]
- rule: Outbound connection to unusual port
desc: Detect outbound connections to unusual ports
condition: >
outbound and container
and not fd.port in (allowed_outbound_ports)
output: >
Unusual outbound connection
(container=%container.name connection=%fd.name)
priority: WARNING
tags: [container, network]Admission Controller Policies (Kyverno)
# Require resource limits
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-resource-limits
spec:
validationFailureAction: Enforce
background: true
rules:
- name: require-limits
match:
any:
- resources:
kinds:
- Pod
validate:
message: "Resource limits are required"
pattern:
spec:
containers:
- resources:
limits:
memory: "?*"
cpu: "?*"
---
# Require non-root
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-non-root
spec:
validationFailureAction: Enforce
rules:
- name: check-non-root
match:
any:
- resources:
kinds:
- Pod
validate:
message: "Containers must not run as root"
pattern:
spec:
securityContext:
runAsNonRoot: true
containers:
- securityContext:
runAsNonRoot: true
---
# Require approved registries
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-approved-registries
spec:
validationFailureAction: Enforce
rules:
- name: check-registry
match:
any:
- resources:
kinds:
- Pod
validate:
message: "Images must come from approved registries"
pattern:
spec:
containers:
- image: "gcr.io/myproject/* | myregistry.azurecr.io/*"Security Monitoring
Prometheus Security Metrics
# ServiceMonitor for security metrics
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: security-metrics
namespace: monitoring
spec:
selector:
matchLabels:
app: security-metrics
endpoints:
- port: metrics
interval: 30s
path: /metrics
---
# PrometheusRule for security alerts
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: security-alerts
namespace: monitoring
spec:
groups:
- name: container-security
rules:
- alert: PrivilegedContainerDetected
expr: sum(kube_pod_container_info{container_privileged="true"}) > 0
for: 5m
labels:
severity: critical
annotations:
summary: Privileged container detected
- alert: ContainerRunningAsRoot
expr: sum(kube_pod_container_info{container_run_as_user="0"}) > 0
for: 5m
labels:
severity: warning
annotations:
summary: Container running as root
- alert: HighCVEVulnerabilities
expr: trivy_vulnerability_count{severity="CRITICAL"} > 0
for: 1m
labels:
severity: critical
annotations:
summary: Critical CVE detected in imageConclusion
Container and Kubernetes security requires a defense-in-depth approach across the entire lifecycle. From secure base images to runtime protection, each layer adds to the overall security posture.
Key takeaways:
- Secure images - Use minimal bases, scan regularly
- Least privilege - Non-root, minimal capabilities, read-only filesystem
- Network segmentation - Default deny, explicit allow
- Secrets management - External secrets, encryption at rest
- Runtime protection - Monitor, detect, respond
At DeviDevs, we help organizations secure their Kubernetes deployments with comprehensive security implementations. Contact us to discuss your container security needs.