Container deployment review
Skill tunahanaliozturk/secure-dotnet-skills/skills/container-deployment-review
Aegis — 12 judgment-style agent skills for secure, production-grade .NET on Azure (security, design, performance, concurrency, observability). Works with Claude Code, Codex, Cursor, Gemini.
npx -y skills add tunahanaliozturk/secure-dotnet-skills --skill container-deployment-reviewAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use when reviewing how a .NET app is containerized and deployed — Dockerfile, base image, runtime user, configuration, health probes, resources, and secrets — before it runs in Kubernetes or Azure Container Apps.
SKILL.md
12.8 KB, as published. Nobody here has run it
Container Deployment Review
Directs the agent to audit the full container delivery chain — Dockerfile authoring, base image selection, runtime identity, secret injection, health probe wiring, and Kubernetes / Container Apps manifests — for the hardening gaps that most commonly cause incidents in production .NET workloads, and to produce a prioritized finding list with the exact Dockerfile instructions, environment variables, and manifest snippets required to fix each gap.
When to use
- Reviewing a Dockerfile PR or a Container Apps / Kubernetes manifest before it reaches a non-development environment.
- Auditing an existing containerized .NET service for security, reliability, or supply-chain posture.
- Pre-production checklist: confirming the image is non-root, secrets are not baked in, probes are wired, and resource limits are set before go-live.
- Post-incident hardening: validating that a root-running container, a leaked connection string, or an absent liveness probe has been remediated.
Process
- Review the Dockerfile build stages. Confirm there is a multi-stage build: an SDK stage (
mcr.microsoft.com/dotnet/sdk:<tag>) that runsdotnet restoreanddotnet publish, and a separate slim runtime stage (mcr.microsoft.com/dotnet/aspnet:<tag>or a chiseled/distroless variant) that copies only the published output. Flag any single-stage image that ships the SDK layer into production. - Check the runtime user and base image. Confirm the runtime stage sets a non-root user (
USER $APP_UIDorUSER app) before theENTRYPOINT. Verify the base tag is pinned to a specific version (e.g.8.0-jammy-chiseled) or ideally a digest — neverlatest. FlagUSER rootor an absentUSERdirective. Note that .NET 8+ standard runtime images define$APP_UID(UID 1654) and default to non-root; chiseled images (mcr.microsoft.com/dotnet/aspnet:8.0-jammy-chiseled) ship without a shell or package manager, further reducing attack surface. - Check configuration and secret handling. Confirm no secret is present in
ENVorARGinstructions, in a copiedappsettings.Production.json, or in any intermediate layer. Secrets must be injected at runtime via platform secrets (Container Apps secrets / K8s Secrets), Azure Key Vault with the CSI Secrets Store driver, or environment variables populated at deploy time — never baked into the image. VerifyASPNETCORE_ENVIRONMENTis set correctly and that the app binds to port 8080 viaASPNETCORE_HTTP_PORTS=8080(not port 80, which requires root on Linux). CheckDOTNET_gcServerand cgroup-limit awareness for container-appropriate GC behavior. - Check health probes and graceful shutdown. Confirm
MapHealthChecks("/healthz/live")andMapHealthChecks("/healthz/ready")are wired in the app, and that the Kubernetes / Container Apps manifest maps these endpoints tolivenessProbeandreadinessProbe. Verify the app handlesSIGTERMgracefully:IHostApplicationLifetime.ApplicationStoppingused for in-flight draining andShutdownTimeoutconfigured slightly below the pod'sterminationGracePeriodSeconds(e.g.services.Configure<HostOptions>(o => o.ShutdownTimeout = TimeSpan.FromSeconds(25))for a 30 s grace period, leaving margin beforeSIGKILL). - Check resource limits and supply-chain controls. Confirm CPU and memory
requestsandlimitsare set in the manifest (a container without limits shares the node's resources with no bound). Verify.dockerignoreexcludesbin/,obj/,*.user, and any local secrets files so they are never copied into the build context. Confirm image scanning is configured (Trivy in CI, Microsoft Defender for Containers in the registry / cluster). Flag the absence of a read-only root filesystem (securityContext.readOnlyRootFilesystem: true) where the app does not need to write to local disk. - Output a prioritized finding list. Group findings into High (active risk: secrets in layers, single-stage SDK image in production,
USER root), Medium (reliability/posture: no health probes, no resource limits,latesttag, no.dockerignore), and Low (defense-in-depth: no read-only root fs, no image scanning, GC not tuned). Each finding must include the exact Dockerfile line or manifest field and the corrected value.
.NET / Azure checks
- Multi-stage build: SDK stage → slim runtime stage. The
dotnet restoreanddotnet publishsteps must run in an image based onmcr.microsoft.com/dotnet/sdk(e.g.mcr.microsoft.com/dotnet/sdk:8.0). The runtime stage must be based onmcr.microsoft.com/dotnet/aspnet(includes the ASP.NET Core runtime) ormcr.microsoft.com/dotnet/aspnet:8.0-jammy-chiseled(chiseled / distroless: no shell, no apt, smaller attack surface, ~half the size of the standard image). TheCOPY --from=buildinstruction must copy only thepublish/output, not the entire source tree. A single-stage Dockerfile that startsFROM mcr.microsoft.com/dotnet/sdkand ships the SDK into production is always a High finding: the SDK surface area is ~3× larger and includes compilers, NuGet caches, and debugging tools. - Non-root user:
USER $APP_UID/USER app; port 8080, not 80. .NET 8+ runtime images define the environment variableAPP_UID=1654and configure the default user accordingly — a bareUSER $APP_UIDin the Dockerfile is sufficient. Chiseled images run as non-root by default and have no shell (nobash,sh, orapt), which is both a security benefit and an operational constraint (exec-based debugging is not available). The app must bind to port 8080 (setASPNETCORE_HTTP_PORTS=8080orASPNETCORE_URLS=http://+:8080) rather than port 80; binding to port 80 inside a Linux container requires root privileges and is a common root-escalation vector. Expose port 8080 in the Dockerfile (EXPOSE 8080). - No secrets in
ENV,ARG, or copied config files. EveryENVandARGinstruction is baked into the image layer and is visible indocker inspectand in the registry — including to anyone who pulls the image. Specifically flag:ENV ConnectionStrings__*,ENV ApiKey,ARG SA_PASSWORD, or aCOPY appsettings.Production.json .that contains non-placeholder values. The correct pattern is to inject secrets at runtime via Container Apps secret references (secretRef), KubernetesSecretobjects mounted as environment variables or files, or the CSI Secrets Store driver mounting Key Vault secrets as a volume. The .NET app reads them viaIConfigurationwithout any code change. - Container-aware GC and cgroup limits. In containers, the .NET runtime reads cgroup memory and CPU limits automatically (since .NET Core 3.0) and sizes the GC heap and thread pool accordingly — no flag is required to enable this. Set
DOTNET_gcServer=0for single-core or memory-constrained containers (server GC creates one heap per CPU, which over-allocates in small containers). Kubernetesresources.limits.memorymust be set; without it the runtime has no cgroup limit to read and defaults to the full node memory, causing GC under-pressure and potential OOM kills. MapHealthChecks→ liveness and readiness probes. The app must register at least two health check endpoints: a liveness probe (/healthz/live— returnsHealthyas long as the process is functional; never queries downstream dependencies) and a readiness probe (/healthz/ready— returnsHealthyonly when the app is ready to serve traffic, including downstream dependency checks viaIHealthCheckimplementations). These map directly tolivenessProbe.httpGet.path: /healthz/liveandreadinessProbe.httpGet.path: /healthz/readyin the Kubernetes manifest or Container Appsprobesblock. A missing liveness probe means Kubernetes cannot restart a deadlocked pod; a missing readiness probe means traffic is sent to a pod before it has finished startup.- Graceful shutdown on
SIGTERM. Kubernetes sendsSIGTERMbeforeSIGKILL(default 30 sterminationGracePeriodSeconds). The .NETIHosthandlesSIGTERMand beginsIHostApplicationLifetime.ApplicationStopping. Register a cancellation callback onApplicationStoppingfor any in-flight work that must drain (e.g. message consumers, background queues). SetShutdownTimeoutinHostOptionsto match (or be slightly less than) the pod'sterminationGracePeriodSecondsso the host has time to drain before the process is killed. Failure to handleSIGTERMmeans every rolling update kills in-flight requests. - CPU and memory requests and limits;
.dockerignore; pinned tags; image scanning; read-only root filesystem. Every container spec must setresources.requests(scheduler hint) andresources.limits(enforced cgroup cap) for both CPU and memory. A missing.dockerignorerisks copyingbin/,obj/, local.envfiles, or user secrets into the build context, increasing image size and potentially leaking secrets. Base image tags must be pinned to a specific version string (e.g.8.0-jammy-chiseled) and ideally to a digest (@sha256:…) to prevent upstream tag mutation from silently changing the deployed image. Image scanning (Trivy in CI viatrivy image, Microsoft Defender for Containers in ACR/AKS) must be part of the pipeline. Where the app does not write to local disk, setsecurityContext.readOnlyRootFilesystem: truein the pod spec to prevent an attacker from writing executables to the container filesystem at runtime.
Red flags
| Signal | Why it matters |
|---|---|
Single-stage FROM mcr.microsoft.com/dotnet/sdk in a production Dockerfile | Ships the full .NET SDK, NuGet caches, and build toolchain into production — roughly 3× the image size and a vastly larger attack surface. The SDK must never leave the build stage. |
USER root or no USER directive in the runtime stage | The process runs as UID 0 inside the container. If an attacker escapes the container or exploits the app, they have root on the host (with --privileged) or can write to the root filesystem. Non-root is the minimum bar. |
ENV ConnectionStrings__Default=Server=...;Password=... or equivalent | Bakes the secret into every image layer; visible in docker inspect, the registry manifest, and any CI log that prints environment variables. Even if the image is later deleted, the secret may remain in intermediate cache layers or registry history. |
COPY appsettings.Production.json . with non-placeholder secret values | Same root cause as ENV secrets — the values travel in the image layer and are visible to anyone with pull access to the registry. Use platform-managed secret injection instead. |
FROM mcr.microsoft.com/dotnet/aspnet:latest | The latest tag is mutable; a base image update will silently change the deployed image on the next build, potentially introducing breaking changes or unvetted CVEs. Pin to a specific version and digest. |
No livenessProbe or readinessProbe in the manifest | Kubernetes cannot detect a deadlocked process (no liveness probe) and will send traffic to pods that have not finished startup (no readiness probe), causing request failures during rolling updates and after restarts. |
No resources.limits on the container spec | The container has no cgroup memory cap; the .NET GC defaults to sizing heaps against the full node memory. Under load, the process can consume node resources until the OOM killer fires, affecting all pods on the node. |
Port 80 binding (ASPNETCORE_URLS=http://+:80) with a non-root user | Ports below 1024 require CAP_NET_BIND_SERVICE or root on Linux. An app that tries to bind port 80 as UID 1654 will fail to start unless the capability is explicitly granted, which widens the attack surface unnecessarily. |
Missing .dockerignore | bin/, obj/, .env, *.pfx, and user-secrets files may enter the build context and be copied into the image, leaking local secrets or bloating the final image with build artifacts. |
| No image scanning in CI or registry | Known CVEs in the base image or NuGet packages go undetected until they are exploited. Trivy or Defender for Containers catches high/critical CVEs before deployment. |
Example
See examples/container-deployment-review/ for a before/after walkthrough: a single-stage root-running Dockerfile with a connection string in ENV is transformed into a hardened multi-stage chiseled non-root image with platform-managed secrets and liveness/readiness probes.
Related skills
- azure-hardening-review — Bicep / App Service / Container Apps infrastructure hardening, Key Vault references, managed identity, and network exposure.