DevContainer Security & Secrets Management
A devcontainer is executable configuration — opening a repository can build an image and run lifecycle commands — so its security posture matters as much as its ergonomics. This guide covers the four pillars of a hardened devcontainer: running non-root, keeping secrets out of the image, scanning the base and Features, and gating lifecycle auto-run behind workspace trust. It builds on the permission and registry discipline from the architecture overview.
The mindset is least privilege plus provenance: give the container only the access it needs, never bake a secret into an image or config, and be able to prove — via scans and checks — that both hold. Every recommendation here reduces what a compromised or malicious repository could do.
Prerequisites
You need an explicit non-root remoteUser, a secret store (the host or Codespaces secret store) to inject credentials at runtime, image scanning wired into CI, and workspace-trust configured so opening a repo doesn't silently run code.
remoteUserset to a non-root user in every config.- A secret store for tokens/keys — never the repo or image.
trivy/grypescanning the image in CI, failing on criticals.- Editor workspace trust enabled so lifecycle hooks require confirmation.
Architecture & Configuration Deep Dive
A hardened devcontainer has four layers. It runs non-root by default (remoteUser), so a process compromise is contained to an unprivileged user. Its secrets live outside the image, injected at runtime via remoteEnv from a store, so nothing sensitive is baked into a layer or committed. Its base image and Features are scanned, so no known-critical vulnerability ships. And workspace trust gates lifecycle auto-run, so merely opening an untrusted repository does not execute its postCreateCommand.
The secrets layer is where mistakes are most costly. A token baked into a Dockerfile ENV or committed to .devcontainer/ persists in image layers and git history, so even after deletion it is recoverable. The rule — enforced across this site — is that secrets are injected at runtime from a store and never written into an image or config, matching the remoteEnv guidance in the property reference.
Step-by-Step Implementation
Set a non-root user, inject secrets from the store at runtime, scan in CI, and require trust before hooks run.
{
"name": "Hardened DevContainer",
"image": "mcr.microsoft.com/devcontainers/base:ubuntu@sha256:PINNED",
"remoteUser": "vscode",
"remoteEnv": {
"NPM_TOKEN": "${localEnv:NPM_TOKEN}"
},
"postCreateCommand": "npm ci"
}
# Scan the pinned image in CI, failing the build on criticals
trivy image --severity CRITICAL --exit-code 1 mcr.microsoft.com/devcontainers/base@sha256:PINNED
Safe secret injection is detailed in injecting secrets into a devcontainer safely, non-root execution in running devcontainers as a non-root user, and image scanning in scanning devcontainer images for vulnerabilities.
Performance & Resource Optimization
Security here is mostly about where secrets live, not raw performance — but the handling model has a clear right answer. Secrets never belong in the image or the committed config; they are injected at runtime from a store, ideally as short-lived tokens so a leak has a bounded blast radius.
Prefer runtime injection via remoteEnv pulling from ${localEnv:…} (backed by the host/Codespaces secret store) over any baked value, and mount secret files only from outside the build context. Short-lived, scoped tokens beat long-lived ones because they limit exposure — a discipline that costs nothing and pays off the day a laptop is lost.
Validation & Testing
Validate three invariants: no secret is baked into the image or config, the container runs as a non-root user, and the base plus Features scan clean of criticals. A repo scan (for example gitleaks) plus the image scan and a user check covers all three.
# No baked secrets, non-root at runtime, clean image
gitleaks detect --source . --no-banner
devcontainer exec --workspace-folder . -- id -u # non-zero (not root)
Common Pitfalls
The failures below are leaked secrets or excess privilege/auto-run. The triage below points at the fix.
| Symptom | Root Cause | Remediation |
|---|---|---|
| Token found in image history | Secret baked into a Dockerfile/config | Rotate it; inject via remoteEnv from a store |
| Container runs as root | remoteUser omitted | Set a non-root remoteUser |
| Critical CVE ships to developers | No image scan gate | Add trivy --exit-code 1 on criticals in CI |
| Opening a repo runs code unexpectedly | Workspace trust not enforced | Require trust before lifecycle hooks run |
| Long-lived token leaked widely | Static, broad-scope credential | Use short-lived, scoped tokens |
Conclusion
Treat the devcontainer as code that runs, and secure it accordingly: run non-root, keep every secret out of the image and injected at runtime from a store, scan the base and Features on each build, and require trust before hooks execute. Reduce privilege, remove baked secrets, and prove the posture with scans — and a shared devcontainer becomes a safe default rather than a quiet liability.
FAQ
Where should API tokens and keys live?
Never in the image or the committed config — both persist in layers and git history. Store them in the host or Codespaces secret store and inject them at runtime with remoteEnv referencing ${localEnv:…}, or mount secret files from outside the build context. Prefer short-lived, narrowly-scoped tokens so any leak is bounded.
Why run the container as non-root?
Least privilege: if a process is compromised, running as an unprivileged user limits what it can touch, and it keeps bind-mounted workspace files correctly owned on the host. Set an explicit remoteUser; the official base images provide a non-root user (like vscode) ready to use.
What is workspace trust protecting against?
A malicious repository can define a postCreateCommand that runs the moment you open it in a container. Workspace trust makes lifecycle-hook execution require explicit confirmation, so cloning and opening an unfamiliar repo does not silently execute its setup code. Combine it with scanning the base and Features you pull.
Related
- DevContainer Architecture & Core Tooling — the permission and registry model security builds on.
- Injecting Secrets into a DevContainer Safely — runtime injection patterns.
- Running DevContainers as a Non-Root User — least-privilege execution.
- Scanning DevContainer Images for Vulnerabilities — gating criticals in CI.
- Container Registry Best Practices for Dev Images — pinned, scanned, provenance-tracked images.