Security Intermediate 14 min

Hardening .NET Containers for Production

Stop running as root. Build secure, minimal container images for .NET applications using multi-stage builds and distroless/alpine bases.

By Victor Robin Updated:

Introduction

The first time I ran docker scan on BlueRobin’s API image, it found 247 vulnerabilities—including 12 critical CVEs in the base image’s OS packages. The image was 890MB, running as root, with a full shell and package manager available. An attacker who exploited any RCE vulnerability would have had a fully-equipped Linux environment to work from. Rebuilding the images with multi-stage builds, Alpine bases, and non-root users reduced the image to 89MB and the vulnerability count to 3 (all low severity).

“It runs on my machine” is not a security strategy. When deploying to Kubernetes, your container image acts as the first line of defense. [CIS Docker Benchmark] — Center for Internet Security , 2023 A bloated image running as root is a gift to attackers.

Why Container Hardening?

What We’ll Build

  1. Multi-Stage Build: Separating the SDK (heavy) from the Runtime (light).
  2. Non-Root User: Configuring the container to run as app (UID 1000).
  3. Read-Only Filesystem: Preparing the app to run without write access to disk.

Architecture Overview

flowchart TB
    subgraph BuildStage["🏗️ Build Stage (SDK Image)"]
        Src["Source Code"] --> Restore["dotnet restore"]
        Restore --> Publish["dotnet publish"]
        Publish --> Artifacts["/app/publish"]
    end

    subgraph RuntimeStage["🚀 Runtime Stage (Alpine/Chiseled)"]
        Base["Base Image"] --> User["Create User (app)"]
        User --> Copy["Copy Artifacts"]
        Copy --> Entry["ENTRYPOINT"]
    end

    Artifacts --> Copy

    classDef primary fill:#7c3aed,stroke:#fff,color:#fff
    classDef secondary fill:#06b6d4,stroke:#fff,color:#fff
    classDef db fill:#f43f5e,stroke:#fff,color:#fff
    classDef warning fill:#fbbf24,stroke:#fff,color:#fff

    class RuntimeStage primary
    class BuildStage secondary

Section 1: The Secure Dockerfile

Does your Dockerfile look like this?

# Dockerfile
# 1. Build Stage
FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build
WORKDIR /src
COPY . .
RUN dotnet publish -c Release -o /app/publish /p:UseAppHost=false

# 2. Runtime Stage
FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine AS runtime
WORKDIR /app

# 3. Create a non-root group and user
RUN addgroup -g 1000 appgroup && \
    adduser -u 1000 -G appgroup -h /app -D appuser

# 4. Copy artifacts
COPY --from=build /app/publish .

# 5. Switch to non-root user
USER 1000

# 6. Expose port (must be > 1024 for non-root)
EXPOSE 8080
ENV ASPNETCORE_URLS=http://+:8080

ENTRYPOINT ["dotnet", "MyApp.Api.dll"]
[.NET Container Images] — Microsoft , 2024

Section 2: Kubernetes Security Context

The Dockerfile is only half the battle. You must enforce security in your Kubernetes manifest. [Kubernetes Pod Security Standards] — Kubernetes , 2024

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000
        runAsGroup: 1000
        fsGroup: 1000
      containers:
        - name: api
          image: myapp-api:latest
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: ["ALL"]
          volumeMounts:
            - name: tmp
              mountPath: /tmp
      volumes:
        - name: tmp
          emptyDir: {}

Conclusion

By combining a hardened Dockerfile with a restrictive Kubernetes SecurityContext, you create a “defense in depth” posture. Even if an attacker finds a Remote Code Execution (RCE) vulnerability in your API, they are trapped in a container with no root privileges, no shell, a read-only filesystem, and no capabilities.

Every BlueRobin service now follows this template. The CI pipeline runs trivy image scan on every build, and any image with a critical or high CVE is blocked from deployment. The combination of Alpine base images, non-root users, read-only filesystems, and dropped capabilities means that even a zero-day in the .NET runtime would give an attacker almost nothing to work with. Container security isn’t glamorous, but it’s the foundation that every other security layer depends on.

Next Steps:

Further Reading

  • [CIS Docker Benchmark] — Center for Internet Security , 2023 — Industry-standard benchmark for securing Docker containers, covering image building, runtime configuration, and orchestration.
  • [Kubernetes Pod Security Standards] — Kubernetes , 2024 — Official Kubernetes documentation defining Privileged, Baseline, and Restricted security profiles for pods.
  • [OWASP Docker Security Cheat Sheet] — OWASP , 2024 — Practical checklist covering Dockerfile best practices, secret management, and network security for containers.