Console Login

Stop Running as Root: The 2023 Guide to Container Security in Production

You Are One Misconfiguration Away From a Crypto-Mining Disaster

I have spent the last decade cleaning up servers that looked 'secure enough' on paper. In 2023, the landscape isn't just about patching software; it is about architectural paranoia. If you are deploying containers with default settings, you are essentially handing over the keys to your infrastructure. The reality is harsh: attackers don't break in; they log in using the permissions you graciously left open.

Containerization revolutionized how we ship code, but it also introduced a massive, often ignored attack surface. We are going to fix that today. No buzzwords. Just hard configurations.

1. The "Latest" Tag is a Lie

Using the :latest tag in production is professional negligence. It breaks reproducibility and opens you up to supply chain attacks where a malicious update slips into your build pipeline unnoticed. You need deterministic builds.

The Fix: Distroless and SHAs

Stop using full OS images. You do not need curl or bash inside your production container unless you enjoy giving attackers a toolkit to explore your network. Use Google's Distroless images or Alpine, and pin by SHA256 digest.

# BAD
FROM node:latest

# GOOD
FROM node:18-alpine3.18 AS builder
WORKDIR /app
COPY . .
RUN npm ci && npm run build

# BETTER: Multi-stage + Distroless
FROM gcr.io/distroless/nodejs18-debian11
COPY --from=builder /app/dist /app
CMD ["server.js"]

This reduces your image size from ~900MB to ~50MB. Less code means fewer CVEs. It's simple math.

2. Rootless Execution is Non-Negotiable

By default, Docker containers run as root. If a process escapes the container, it has root on your host. I've seen this happen with a zero-day in a widely used image processing library earlier this year. The attacker escalated from a web shell to host root in under three minutes.

Enforce a non-root user in your Dockerfile immediately.

# Create a user group and user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

# Change ownership
RUN chown -R appuser:appgroup /app

# Switch user
USER appuser

In Kubernetes, you must enforce this at the Pod level using SecurityContext. If your orchestrator allows root containers, your cluster is compromised.

apiVersion: v1
kind: Pod
metadata:
  name: secure-app
spec:
  securityContext:
    runAsUser: 1000
    runAsGroup: 3000
    fsGroup: 2000
    runAsNonRoot: true
  containers:
  - name: app
    image: my-secure-image:1.0.4
    securityContext:
      allowPrivilegeEscalation: false
      readOnlyRootFilesystem: true

3. The Supply Chain: Trust Nothing

In 2023, the software supply chain is the primary vector. You push code, but do you know what your dependencies are pulling in? We use Trivy in our CI/CD pipelines. It is fast, comprehensive, and integrates with everything.

Pro Tip: Don't just scan the OS packages. Scan the language dependencies (npm, pip, go.mod). A vulnerability in a sub-dependency of a sub-dependency is still your problem.

Here is how you integrate a scan before you even think about deploying:

# Scan image for HIGH and CRITICAL vulnerabilities only
trivy image --severity HIGH,CRITICAL --exit-code 1 my-app:v1.0.0

If this command returns a non-zero exit code, the build fails. No exceptions. This saves you from explaining to the CTO why a known CVE made it to production.

4. Network Policies: Zero Trust Inside the Cluster

By default, all pods in a Kubernetes cluster can talk to each other. Your frontend has no business talking to your Redis cache directly if there is a backend API in between. Flat networks aid lateral movement.

Define a NetworkPolicy that denies all traffic by default, then whitelist only what is necessary.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress

Then, open up specific routes:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-api-access
spec:
  podSelector:
    matchLabels:
      app: backend
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: frontend
    ports:
    - protocol: TCP
      port: 8080

5. The Infrastructure Reality: Host Isolation & Norwegian Compliance

You can harden your container config until it is bulletproof, but if the underlying host is shared garbage, you are still at risk. This is where the "Noisy Neighbor" problem becomes a security flaw. Side-channel attacks on shared CPU caches are real.

We built CoolVDS on strictly isolated KVM instances for this exact reason. Unlike standard containers (LXC/OpenVZ) where you share the kernel with every other customer on the node, KVM provides hardware-level virtualization.

FeatureCheap VPS / Shared ContainerCoolVDS (KVM)
Kernel IsolationShared (High Risk)Dedicated Kernel (High Security)
Resource GuaranteeOversold / BurstableDedicated RAM & NVMe
Data ResidencyOften unknown cloud regionsStrictly Norway (Oslo)

The GDPR Angle

For those of us operating in Europe, and specifically dealing with the Norwegian Datatilsynet, data residency is not a suggestion. Schrems II killed the Privacy Shield. Storing customer data on US-owned cloud hyperscalers is a legal minefield in 2023.

Hosting on CoolVDS ensures your data stays physically in Norway, on Norwegian infrastructure, subject to Norwegian law. That is a compliance box you can tick instantly.

6. Runtime Security: Falco

Static analysis is great, but what happens when an exploit occurs in memory? You need runtime threat detection. Falco is the de facto standard here. It watches the syscalls.

If a shell spawns in a container that should only be serving HTTP traffic, Falco screams. Here is a custom rule we use to detect unexpected shell execution:

- rule: Terminal shell in container
  desc: A shell was used as the entrypoint for a container
  condition: >
    spawned_process and container
    and shell_procs and proc.tty != 0
    and container_entrypoint
  output: "Shell executed in container (user=%user.name %container.info)"
  priority: WARNING

Final Thoughts

Security is a process of friction. It makes development slightly harder initially, but it makes sleeping at night possible. Don't wait for a breach to justify the time spent on hardening.

If you need an environment where low latency meets high security, stop messing around with shared kernels. Your infrastructure deserves better than 'good enough'.

Ready to lock it down? Deploy a hardened KVM instance on CoolVDS today. NVMe speeds, Norwegian sovereignty, and zero noisy neighbors.