Skip to content

Hello Security

Run nginx with secure container defaults: non-root user, read-only filesystem, no capabilities.

Time: ~5 minutes Difficulty: Beginner

  • Setting runAsNonRoot: true to block root execution
  • Using readOnlyRootFilesystem: true to prevent filesystem writes
  • Dropping all Linux capabilities with capabilities.drop: [ALL]
  • Setting allowPrivilegeEscalation: false
  • Using non-root base images like nginxinc/nginx-unprivileged
  • Mounting writable volumes for cache and runtime directories

Navigate to the demo directory:

Terminal window
cd demos/hello-security

Deploy the resources:

Terminal window
kubectl apply -f manifests/namespace.yaml
kubectl apply -f manifests/
Terminal window
# Check pods are running
kubectl get pods -n hello-security
# Check the service
kubectl get svc -n hello-security
# Access the app
kubectl port-forward svc/secure-nginx-service 8080:80 -n hello-security

Open http://localhost:8080 in your browser. You should see the default nginx welcome page.

Verify the security settings:

Terminal window
# Confirm the pod is running as non-root
kubectl exec deployment/secure-nginx -n hello-security -- id
# uid=101 gid=101
# Try to write to the filesystem (should fail)
kubectl exec deployment/secure-nginx -n hello-security -- touch /test
# touch: /test: Read-only file system
# Writable volumes still work
kubectl exec deployment/secure-nginx -n hello-security -- touch /var/cache/nginx/test
# Works
manifests/
namespace.yaml # hello-security namespace
deployment.yaml # nginx with full security hardening
service.yaml # ClusterIP service on port 80

The Deployment runs nginx with all security best practices enabled:

SettingWhat It Does
runAsNonRoot: truePrevents running as UID 0 (root)
runAsUser: 101Sets the user ID to 101 (nginx user in unprivileged image)
readOnlyRootFilesystem: trueMakes the container filesystem read-only
allowPrivilegeEscalation: falseBlocks privilege escalation via setuid/setgid binaries
capabilities.drop: [ALL]Removes all Linux capabilities
seccompProfile: RuntimeDefaultRestricts syscalls to a safe set

Why use nginxinc/nginx-unprivileged?

The official nginx image runs on port 80, which requires root privileges. The unprivileged variant listens on port 8080 and is designed to run as a non-root user. This is the recommended approach for production workloads.

Why mount emptyDir volumes?

With readOnlyRootFilesystem: true, nginx cannot write to /var/cache/nginx or /var/run. We mount writable emptyDir volumes at these paths so nginx can store temporary files and its PID file.

  1. Compare with an insecure nginx pod:

    Terminal window
    kubectl run insecure-nginx --image=nginx:1.25-alpine -n hello-security
    kubectl exec insecure-nginx -n hello-security -- id
    # uid=0(root)
    kubectl delete pod insecure-nginx -n hello-security
  2. Try to escalate privileges (will fail):

    Terminal window
    kubectl exec deployment/secure-nginx -n hello-security -- cat /proc/1/status | grep Cap
    # All capabilities are 0 (none)
  3. View the security context in the running pod:

    Terminal window
    kubectl get pod -n hello-security -l app=secure-nginx -o jsonpath='{.items[0].spec.securityContext}' | jq
Terminal window
kubectl delete namespace hello-security

See docs/deep-dive.md for a detailed explanation of Linux capabilities, seccomp profiles, why read-only filesystems matter, and how these settings protect against container escapes and privilege escalation attacks.

This is the final demo in the series. To continue learning, explore:

  • Pod Security for Pod Security Standards and admission control
  • Kyverno for policy-based enforcement of these settings
  • Falco for runtime threat detection