# Container Deployment Review

> 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.

- **Type:** Skill
- **Install:** `agentstack add skill-tunahanaliozturk-secure-dotnet-skills-container-deployment-review`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [tunahanaliozturk](https://agentstack.voostack.com/s/tunahanaliozturk)
- **Installs:** 0
- **Category:** [Cloud & Infrastructure](https://agentstack.voostack.com/c/cloud-infrastructure)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [tunahanaliozturk](https://github.com/tunahanaliozturk)
- **Source:** https://github.com/tunahanaliozturk/secure-dotnet-skills/tree/master/skills/container-deployment-review

## Install

```sh
agentstack add skill-tunahanaliozturk-secure-dotnet-skills-container-deployment-review
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# 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

1. **Review the Dockerfile build stages.** Confirm there is a multi-stage build: an SDK stage (`mcr.microsoft.com/dotnet/sdk:`) that runs `dotnet restore` and `dotnet publish`, and a separate slim runtime stage (`mcr.microsoft.com/dotnet/aspnet:` or a chiseled/distroless variant) that copies only the published output. Flag any single-stage image that ships the SDK layer into production.
2. **Check the runtime user and base image.** Confirm the runtime stage sets a non-root user (`USER $APP_UID` or `USER app`) before the `ENTRYPOINT`. Verify the base tag is pinned to a specific version (e.g. `8.0-jammy-chiseled`) or ideally a digest — never `latest`. Flag `USER root` or an absent `USER` directive. 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.
3. **Check configuration and secret handling.** Confirm no secret is present in `ENV` or `ARG` instructions, in a copied `appsettings.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. Verify `ASPNETCORE_ENVIRONMENT` is set correctly and that the app binds to port 8080 via `ASPNETCORE_HTTP_PORTS=8080` (not port 80, which requires root on Linux). Check `DOTNET_gcServer` and cgroup-limit awareness for container-appropriate GC behavior.
4. **Check health probes and graceful shutdown.** Confirm `MapHealthChecks("/healthz/live")` and `MapHealthChecks("/healthz/ready")` are wired in the app, and that the Kubernetes / Container Apps manifest maps these endpoints to `livenessProbe` and `readinessProbe`. Verify the app handles `SIGTERM` gracefully: `IHostApplicationLifetime.ApplicationStopping` used for in-flight draining and `ShutdownTimeout` configured slightly below the pod's `terminationGracePeriodSeconds` (e.g. `services.Configure(o => o.ShutdownTimeout = TimeSpan.FromSeconds(25))` for a 30 s grace period, leaving margin before `SIGKILL`).
5. **Check resource limits and supply-chain controls.** Confirm CPU and memory `requests` and `limits` are set in the manifest (a container without limits shares the node's resources with no bound). Verify `.dockerignore` excludes `bin/`, `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.
6. **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, `latest` tag, 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 restore` and `dotnet publish` steps must run in an image based on `mcr.microsoft.com/dotnet/sdk` (e.g. `mcr.microsoft.com/dotnet/sdk:8.0`). The runtime stage must be based on `mcr.microsoft.com/dotnet/aspnet` (includes the ASP.NET Core runtime) or `mcr.microsoft.com/dotnet/aspnet:8.0-jammy-chiseled` (chiseled / distroless: no shell, no apt, smaller attack surface, ~half the size of the standard image). The `COPY --from=build` instruction must copy only the `publish/` output, not the entire source tree. A single-stage Dockerfile that starts `FROM mcr.microsoft.com/dotnet/sdk` and 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 variable `APP_UID=1654` and configure the default user accordingly — a bare `USER $APP_UID` in the Dockerfile is sufficient. Chiseled images run as non-root by default and have no shell (no `bash`, `sh`, or `apt`), which is both a security benefit and an operational constraint (exec-based debugging is not available). The app must bind to port 8080 (set `ASPNETCORE_HTTP_PORTS=8080` or `ASPNETCORE_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.** Every `ENV` and `ARG` instruction is baked into the image layer and is visible in `docker inspect` and in the registry — including to anyone who pulls the image. Specifically flag: `ENV ConnectionStrings__*`, `ENV ApiKey`, `ARG SA_PASSWORD`, or a `COPY appsettings.Production.json .` that contains non-placeholder values. The correct pattern is to inject secrets at runtime via Container Apps secret references (`secretRef`), Kubernetes `Secret` objects mounted as environment variables or files, or the CSI Secrets Store driver mounting Key Vault secrets as a volume. The .NET app reads them via `IConfiguration` without 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=0` for single-core or memory-constrained containers (server GC creates one heap per CPU, which over-allocates in small containers). Kubernetes `resources.limits.memory` must 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` — returns `Healthy` as long as the process is functional; never queries downstream dependencies) and a readiness probe (`/healthz/ready` — returns `Healthy` only when the app is ready to serve traffic, including downstream dependency checks via `IHealthCheck` implementations). These map directly to `livenessProbe.httpGet.path: /healthz/live` and `readinessProbe.httpGet.path: /healthz/ready` in the Kubernetes manifest or Container Apps `probes` block. 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 sends `SIGTERM` before `SIGKILL` (default 30 s `terminationGracePeriodSeconds`). The .NET `IHost` handles `SIGTERM` and begins `IHostApplicationLifetime.ApplicationStopping`. Register a cancellation callback on `ApplicationStopping` for any in-flight work that must drain (e.g. message consumers, background queues). Set `ShutdownTimeout` in `HostOptions` to match (or be slightly less than) the pod's `terminationGracePeriodSeconds` so the host has time to drain before the process is killed. Failure to handle `SIGTERM` means 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 set `resources.requests` (scheduler hint) and `resources.limits` (enforced cgroup cap) for both CPU and memory. A missing `.dockerignore` risks copying `bin/`, `obj/`, local `.env` files, 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 via `trivy image`, Microsoft Defender for Containers in ACR/AKS) must be part of the pipeline. Where the app does not write to local disk, set `securityContext.readOnlyRootFilesystem: true` in 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/`](../../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](../azure-hardening-review/SKILL.md) — Bicep / App Service / Container Apps infrastructure hardening, Key Vault references, managed identity, and network exposure.

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [tunahanaliozturk](https://github.com/tunahanaliozturk)
- **Source:** [tunahanaliozturk/secure-dotnet-skills](https://github.com/tunahanaliozturk/secure-dotnet-skills)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-tunahanaliozturk-secure-dotnet-skills-container-deployment-review
- Seller: https://agentstack.voostack.com/s/tunahanaliozturk
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
