Lesson  in  DevSecOps in Practice: Container & Kubernetes Security

Docker Image Hardening

A typical production Docker image runs as root and carries hundreds of CVEs. Using only standard Dockerfile techniques, you'll shrink it from 921 MB to under 60 MB, eliminate root, and lock down the filesystem.

The Insecure Image

The Problem with "Works on My Machine" Images

A typical Docker image pulled from a tutorial is nearly 1 GB, runs as root, and carries hundreds of unpatched CVEs. Let's build one and prove it.

Create the app

mkdir -p ~/demo-01 && cd ~/demo-01

cat > app.py << 'EOF'
from http.server import HTTPServer, BaseHTTPRequestHandler

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.end_headers()
        self.wfile.write(b"Hello from DevSecOps Demo!")
    def log_message(self, format, *args):
        pass

HTTPServer(("", 8080), Handler).serve_forever()
EOF

Write the insecure Dockerfile

cat > Dockerfile.insecure << 'EOF'
FROM python:3.8

COPY app.py .

RUN apt-get update && apt-get install -y curl vim

EXPOSE 8080
CMD ["python", "app.py"]
EOF

Build it

docker build -f Dockerfile.insecure -t demo-app:insecure .

Check the damage

docker images demo-app:insecure
docker run --rm demo-app:insecure whoami

Expected:

REPOSITORY   TAG        SIZE
demo-app     insecure   ~900MB

root

Root user. Nearly 1 GB. A compromised process has full container privileges.

The Hardened Image

Three Changes, Dramatic Difference

The hardened Dockerfile makes three key decisions:

  1. Alpine base — 5 MB instead of 900 MB, far fewer CVEs
  2. Multi-stage build — build tools never reach the final image
  3. Non-root user — a dedicated appuser with no special privileges

Write the hardened Dockerfile

cat > Dockerfile.hardened << 'EOF'
FROM python:3.8-alpine AS builder
WORKDIR /app
COPY app.py .

FROM python:3.8-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --from=builder /app/app.py .
RUN chown -R appuser:appgroup /app
USER appuser
EXPOSE 8080
CMD ["python", "app.py"]
EOF
docker build -f Dockerfile.hardened -t demo-app:hardened .

Compare

docker images demo-app
docker run --rm demo-app:hardened whoami

Expected:

REPOSITORY   TAG       SIZE
demo-app     hardened  ~49MB
demo-app     insecure  ~900MB

appuser

Apply runtime hardening flags

docker run -d \
  --name demo-hardened \
  --read-only \
  --cap-drop=ALL \
  --security-opt no-new-privileges:true \
  --tmpfs /tmp \
  -p 8888:8080 \
  demo-app:hardened

curl http://localhost:8888

Expected: Hello from DevSecOps Demo!

Prove the read-only filesystem

docker exec demo-hardened sh -c "echo test > /app/test.txt" 2>&1

Expected: sh: can't create /app/test.txt: Read-only file system

Cleanup

docker stop demo-hardened && docker rm demo-hardened

What You Achieved

TechniqueInsecureHardened
Base imagepython:3.8 Debian (~900 MB)python:3.8-alpine (~49 MB)
Userroot (UID 0)appuser (non-root)
Build tools in runtimeYes (curl, vim)No (multi-stage)
FilesystemRead-write--read-only
Linux capabilitiesAll inherited--cap-drop=ALL