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.
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?
- Least Privilege: If an attacker compromises the app, they shouldn’t have root access to the filesystem. [OWASP Docker Security Cheat Sheet] — OWASP , 2024
- Attack Surface: Removing shells (
/bin/sh) and package managers (apk) prevents attackers from installing tools. [Distroless Container Images] — Google , 2024 - Size: Smaller images pull faster and scale quicker. [Chainguard Images] — Chainguard , 2024
What We’ll Build
- Multi-Stage Build: Separating the SDK (heavy) from the Runtime (light).
- Non-Root User: Configuring the container to run as
app(UID 1000). - 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:
- Implement network segmentation to isolate containers at the network level.
- Add Authelia SSO for identity-based access control to your containerized services.
- Expose hardened containers safely with Cloudflare Tunnel.
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.