Docker containers run in production. Kubernetes orchestrates hundreds of pods. But who controls that the base image doesn’t contain critical CVE? That the container doesn’t run as root? That the pod doesn’t have access to the host filesystem? Container security is a critical topic in 2018.
Security starts with the base image¶
Most container security issues originate from the base image. A typical node:10 image is based on Debian and contains hundreds of packages — including those with known vulnerabilities.
- Use Alpine variants —
node:10-alpinehas ~5 MB vs ~900 MB of the full Debian image. Fewer packages = less attack surface. - Multi-stage builds — build dependencies don’t belong in the production image
- Pin versions —
FROM node:10.16.3-alpineinstead ofFROM node:10
# Multi-stage build — secure production image
FROM node:10-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:10-alpine
RUN addgroup -S app && adduser -S app -G app
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
USER app
EXPOSE 3000
CMD ["node", "server.js"]
Image scanning¶
Every image must undergo security scanning before deployment. Tools:
- Clair — open-source, integrated with CoreOS Quay registry
- Trivy — fast, simple, scans OS packages and application dependencies
- Anchore — policy-based scanning with custom rules
- Docker Hub — automatic security scan for paid plans
# Trivy scan in CI pipeline
$ trivy image myapp:latest
myapp:latest (alpine 3.8.4)
============================
Total: 3 (UNKNOWN: 0, LOW: 1, MEDIUM: 1, HIGH: 1, CRITICAL: 0)
+---------+------------------+----------+-------------------+
| LIBRARY | VULNERABILITY ID | SEVERITY | INSTALLED VERSION |
+---------+------------------+----------+-------------------+
| musl | CVE-2019-14697 | HIGH | 1.1.19-r10 |
| openssl | CVE-2018-0734 | MEDIUM | 1.0.2p-r0 |
| zlib | CVE-2018-14618 | LOW | 1.2.11-r1 |
+---------+------------------+----------+-------------------+
Don’t run as root¶
Surprisingly, many production containers still run as root. This means that a container escape vulnerability gives an attacker root access on the host.
- Always add a
USERinstruction in Dockerfile - Kubernetes Pod Security Policies (PSP) can enforce non-root at the cluster level
- Set
readOnlyRootFilesystem: truewhere possible
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
name: restricted
spec:
privileged: false
runAsUser:
rule: MustRunAsNonRoot
fsGroup:
rule: RunAsAny
volumes:
- 'configMap'
- 'emptyDir'
- 'secret'
- 'persistentVolumeClaim'
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
Network Policies — microsegmentation¶
By default, in Kubernetes every pod can communicate with every other pod. This is a security nightmare. Network Policies define who can talk to whom:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-server-policy
namespace: production
spec:
podSelector:
matchLabels:
app: api-server
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app: nginx-ingress
ports:
- port: 8080
egress:
- to:
- podSelector:
matchLabels:
app: postgres
ports:
- port: 5432
This policy says: api-server accepts traffic only from nginx-ingress on port 8080 and can only connect to postgres on port 5432. No other traffic is allowed.
Warning: Network Policies require a CNI plugin that supports them — Calico, Cilium, Weave Net. The default kubenet doesn’t support them.
Secrets management¶
Kubernetes Secrets are stored in etcd unencrypted by default (base64 ≠ encryption). Solutions:
- Encryption at rest — Kubernetes 1.7+ supports EncryptionConfiguration for encrypting secrets in etcd
- HashiCorp Vault — externalize secrets outside the cluster, inject via sidecar or init container
- Sealed Secrets — Bitnami tool for encrypted secrets in Git repository
- RBAC — limit who can read secrets at the namespace level
Runtime security¶
Image scanning catches known vulnerabilities. But what about zero-day exploits and anomalous behavior at runtime?
- Falco — open-source runtime security from Sysdig. Detects anomalous syscalls, unexpected processes, access to sensitive files.
- Seccomp profiles — limit available syscalls to the minimum necessary for the application
- AppArmor/SELinux — mandatory access control at the kernel level
# Falco rule — detect shell in container
- rule: Terminal shell in container
desc: Detect a shell opened in a container
condition: >
spawned_process and container and
proc.name in (bash, sh, zsh)
output: >
Shell opened in container
(user=%user.name container=%container.name
shell=%proc.name parent=%proc.pname)
priority: WARNING
Supply chain security¶
Docker Hub contains thousands of community images with unknown origins. How do you ensure you’re downloading a trusted image?
- Docker Content Trust — image signing using Notary
- Private registry — Harbor, GitLab Container Registry with integrated scanning
- Admission controllers — Kubernetes webhook that rejects pods without signed images
Defense in depth — no single layer is enough¶
Container security requires measures at every level: secure base image, scanning in CI pipeline, Pod Security Policies, Network Policies, secrets encryption, runtime monitoring. No single measure is sufficient. Implement them gradually — start with non-root containers and image scanning, then add additional layers.
Need help with implementation?
Our experts can help with design, implementation, and operations. From architecture to production.
Contact us