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.

  • remoteUser set to a non-root user in every config.
  • A secret store for tokens/keys — never the repo or image.
  • trivy/grype scanning the image in CI, failing on criticals.
  • Editor workspace trust enabled so lifecycle hooks require confirmation.

Security prerequisitesYou need a non-root user, an external secret store, image scanning, and workspace-trust config.Non-root userremoteUser setSecret storehost/CodespacesImage scantrivy in CIWorkspace trustconfigured

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.

Security layersA hardened devcontainer runs non-root, injects secrets at runtime, scans images, and gates auto-run.Non-root by defaultremoteUser, least privilegeSecrets outside the imageinjected at runtimeScanned base + Featuresno known criticalsWorkspace trustno auto-run on open

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.

Hardening flowRun non-root, inject secrets from a store, scan in CI, and gate lifecycle auto-run.Set remoteUserleast privilegeInject secretsremoteEnv from storeScan in CIfail on criticalsGate trustreview before 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.

Secret handlingSecrets never belong in the image or config; inject them at runtime from a store.Never in the imageAPI tokensPrivate SSH keysCloud credentialsInject at runtimeremoteEnv from storeMounted secret filesShort-lived tokens

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.

Security validationConfirm no baked secrets, a non-root user, and a clean image scan.Any secret baked into the image orconfig?YESMove it to the secret storeDoes the container run as non-root?YESBase + Features scan cleanHardened devcontainer

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

Security pitfall triageA triage path from a leaked secret or auto-run risk to a least-privilege setup.Is a credential in the repo/image?YESRotate + move to the storeDoes opening the repo auto-run a hook?YESRequire workspace trustLeast-privilege, secret-safe

SymptomRoot CauseRemediation
Token found in image historySecret baked into a Dockerfile/configRotate it; inject via remoteEnv from a store
Container runs as rootremoteUser omittedSet a non-root remoteUser
Critical CVE ships to developersNo image scan gateAdd trivy --exit-code 1 on criticals in CI
Opening a repo runs code unexpectedlyWorkspace trust not enforcedRequire trust before lifecycle hooks run
Long-lived token leaked widelyStatic, broad-scope credentialUse 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.

Reduce and proveReduce privilege and baked secrets; prove the posture with scans and checks.ReduceRoot privilegesBaked secretsAuto-run surfaceProveImage scansNon-root checksSecret provenance

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.