Back to Blog
DockerKubernetesSecurityContainers

Container Security Best Practices for Production

Secure your containerized applications. From image hardening to runtime security to Kubernetes security configurations.

B
Bootspring Team
Engineering
October 12, 2024
5 min read

Containers provide isolation, but security requires deliberate effort. Here's how to secure containers from build to runtime.

Image Security#

Use Minimal Base Images#

1# ❌ Large attack surface 2FROM ubuntu:latest 3 4# ✅ Minimal image 5FROM node:20-alpine 6 7# ✅ Even smaller - distroless 8FROM gcr.io/distroless/nodejs20-debian11

Multi-Stage Builds#

1# Build stage 2FROM node:20-alpine AS builder 3 4WORKDIR /app 5COPY package*.json ./ 6RUN npm ci 7COPY . . 8RUN npm run build 9 10# Production stage - minimal image 11FROM node:20-alpine AS production 12 13# Don't run as root 14RUN addgroup -S app && adduser -S app -G app 15USER app 16 17WORKDIR /app 18COPY --from=builder --chown=app:app /app/dist ./dist 19COPY --from=builder --chown=app:app /app/node_modules ./node_modules 20 21EXPOSE 3000 22CMD ["node", "dist/index.js"]

Pin Versions#

1# ❌ Unpredictable 2FROM node:latest 3RUN npm install express 4 5# ✅ Pinned versions 6FROM node:20.10.0-alpine3.19 7COPY package-lock.json ./ 8RUN npm ci --only=production

Scan Images#

1# Trivy scan 2trivy image myapp:latest 3 4# Docker Scout 5docker scout cves myapp:latest 6 7# Snyk 8snyk container test myapp:latest 9 10# In CI/CD 11- name: Scan image 12 uses: aquasecurity/trivy-action@master 13 with: 14 image-ref: myapp:latest 15 exit-code: 1 16 severity: 'CRITICAL,HIGH'

Dockerfile Security#

Don't Run as Root#

1# Create non-root user 2RUN addgroup -S app && adduser -S app -G app 3 4# Set ownership 5COPY --chown=app:app . . 6 7# Switch to non-root user 8USER app 9 10# Or use numeric UID 11USER 1000:1000

Avoid Secrets in Images#

1# ❌ Secret in image layer 2RUN echo "password123" > /app/secret.txt 3COPY .env /app/.env 4 5# ✅ Use runtime secrets 6# Pass via environment variables or mounted secrets 7ENV DATABASE_URL="" 8 9# In Kubernetes 10spec: 11 containers: 12 - name: app 13 envFrom: 14 - secretRef: 15 name: app-secrets

Read-Only Filesystem#

1# Mark filesystem as read-only where possible 2FROM node:20-alpine 3 4# Create writable directories for runtime needs 5RUN mkdir -p /app/tmp /app/logs && \ 6 chown -R node:node /app 7 8USER node 9WORKDIR /app 10 11# Runtime: mount as read-only 12# docker run --read-only -v /app/tmp -v /app/logs myapp

Drop Capabilities#

1# In docker-compose.yml 2services: 3 app: 4 image: myapp 5 cap_drop: 6 - ALL 7 cap_add: 8 - NET_BIND_SERVICE # Only if needed 9 security_opt: 10 - no-new-privileges:true

Kubernetes Security#

Pod Security#

1apiVersion: v1 2kind: Pod 3metadata: 4 name: secure-pod 5spec: 6 securityContext: 7 runAsNonRoot: true 8 runAsUser: 1000 9 runAsGroup: 1000 10 fsGroup: 1000 11 seccompProfile: 12 type: RuntimeDefault 13 14 containers: 15 - name: app 16 image: myapp:latest 17 securityContext: 18 allowPrivilegeEscalation: false 19 readOnlyRootFilesystem: true 20 capabilities: 21 drop: 22 - ALL 23 24 resources: 25 limits: 26 memory: "256Mi" 27 cpu: "500m" 28 requests: 29 memory: "128Mi" 30 cpu: "100m" 31 32 volumeMounts: 33 - name: tmp 34 mountPath: /tmp 35 36 volumes: 37 - name: tmp 38 emptyDir: {}

Pod Security Standards#

1# Enforce restricted policy 2apiVersion: v1 3kind: Namespace 4metadata: 5 name: production 6 labels: 7 pod-security.kubernetes.io/enforce: restricted 8 pod-security.kubernetes.io/audit: restricted 9 pod-security.kubernetes.io/warn: restricted

Network Policies#

1# Default deny all traffic 2apiVersion: networking.k8s.io/v1 3kind: NetworkPolicy 4metadata: 5 name: default-deny 6 namespace: production 7spec: 8 podSelector: {} 9 policyTypes: 10 - Ingress 11 - Egress 12 13--- 14# Allow specific traffic 15apiVersion: networking.k8s.io/v1 16kind: NetworkPolicy 17metadata: 18 name: allow-api 19spec: 20 podSelector: 21 matchLabels: 22 app: api 23 policyTypes: 24 - Ingress 25 ingress: 26 - from: 27 - podSelector: 28 matchLabels: 29 app: frontend 30 ports: 31 - protocol: TCP 32 port: 3000

Secrets Management#

1# Use external secrets operator 2apiVersion: external-secrets.io/v1beta1 3kind: ExternalSecret 4metadata: 5 name: app-secrets 6spec: 7 refreshInterval: 1h 8 secretStoreRef: 9 name: vault-backend 10 kind: ClusterSecretStore 11 target: 12 name: app-secrets 13 data: 14 - secretKey: database-url 15 remoteRef: 16 key: prod/database 17 property: url

Runtime Security#

Container Runtime Protection#

1# Use gVisor or Kata Containers for additional isolation 2apiVersion: node.k8s.io/v1 3kind: RuntimeClass 4metadata: 5 name: gvisor 6handler: runsc 7 8--- 9apiVersion: v1 10kind: Pod 11spec: 12 runtimeClassName: gvisor 13 containers: 14 - name: untrusted-workload 15 image: untrusted:latest

Monitoring and Detection#

1# Falco rules for runtime detection 2- rule: Shell Spawned in Container 3 desc: Detect shell spawned in container 4 condition: > 5 spawned_process and container and 6 shell_procs and not shell_spawn_allowed 7 output: > 8 Shell spawned in container 9 (user=%user.name container=%container.name shell=%proc.name) 10 priority: WARNING 11 12- rule: Sensitive File Access 13 desc: Detect access to sensitive files 14 condition: > 15 open_read and container and 16 sensitive_files and not trusted_containers 17 output: > 18 Sensitive file accessed 19 (user=%user.name file=%fd.name container=%container.name) 20 priority: CRITICAL

Image Registry Security#

1# Use private registry with authentication 2apiVersion: v1 3kind: Secret 4metadata: 5 name: registry-credentials 6type: kubernetes.io/dockerconfigjson 7data: 8 .dockerconfigjson: <base64-encoded-config> 9 10--- 11apiVersion: v1 12kind: ServiceAccount 13metadata: 14 name: app 15imagePullSecrets: 16 - name: registry-credentials

Sign and Verify Images#

1# Sign with Cosign 2cosign sign --key cosign.key myregistry.com/myapp:v1.0.0 3 4# Verify before deploy 5cosign verify --key cosign.pub myregistry.com/myapp:v1.0.0 6 7# Kubernetes admission control 8apiVersion: policy.sigstore.dev/v1alpha1 9kind: ClusterImagePolicy 10metadata: 11 name: require-signatures 12spec: 13 images: 14 - glob: "myregistry.com/**" 15 authorities: 16 - key: 17 data: | 18 -----BEGIN PUBLIC KEY----- 19 ... 20 -----END PUBLIC KEY-----

Security Checklist#

1## Build Time 2- [ ] Use minimal base images 3- [ ] Multi-stage builds 4- [ ] Pin all versions 5- [ ] Scan for vulnerabilities 6- [ ] No secrets in images 7- [ ] Non-root user 8 9## Runtime 10- [ ] Read-only filesystem 11- [ ] Drop all capabilities 12- [ ] Resource limits 13- [ ] Network policies 14- [ ] Pod security standards 15- [ ] Runtime monitoring 16 17## Operations 18- [ ] Private registry 19- [ ] Image signing 20- [ ] Regular updates 21- [ ] Audit logging 22- [ ] Incident response plan

Conclusion#

Container security requires defense in depth: secure images, locked-down runtime, and continuous monitoring. Start with the basics—non-root users, minimal images, and vulnerability scanning—then add layers as your security requirements grow.

Automate security checks in CI/CD to catch issues early. Security is a process, not a one-time configuration.

Share this article

Help spread the word about Bootspring