# Devcontainer secret storage and the keyring tradeoff This page explains how secrets are stored inside the devcontainer, what the default storage does and does not protect against, and how to harden it if your threat model demands it. ## What is stored, and where The devcontainer keeps two kinds of secrets: - **docker/mcp secrets** — used by the `docker-pass` secrets engine. Depending on configuration, these live either in a GNOME Keyring inside the container or in an externally mounted secrets engine on the host (see below). - **`gh auth login` tokens** — GitHub CLI credentials, stored in the GNOME Keyring inside the container. The in-container keyring file is: ``` ~/.local/share/keyrings/login.keyring ``` It is brought up by `scripts/devcontainer-setup-keyring.sh`, which exposes the `org.freedesktop.secrets` Secret Service over a D-Bus session. ## The default tradeoff: an unencrypted keyring `devcontainer-setup-keyring.sh` creates `login.keyring` with an **empty password**. A GNOME Keyring with an empty password is stored **unencrypted** on disk — the secrets are effectively plaintext. This is deliberate. An empty password lets the keyring daemon start _headless_, without an interactive unlock prompt on every container start or window reload. ### What this protects against - Nothing at the file-encryption layer inside the container. ### What actually protects the secrets - **Host-level disk encryption** — on a typical single-user dev machine the container's filesystem lives on a host-encrypted disk (macOS FileVault, or the Docker Desktop VM disk). At rest, the keyring is protected by that layer. - **Container/host isolation** — the file is only reachable by processes with access to the container's filesystem. For a single-user devcontainer this is an acceptable tradeoff: any in-container encryption password would have to live nearby anyway, so encrypting the keyring file in place adds little real protection while adding startup complexity. ## Options ### 1. External mounted secrets engine (preferred, already implemented) When the host runs the `docker-secrets-engine` and mounts its socket, `.devcontainer/devcontainer-init.sh` detects it and sets `DOCKER_PASS_EXTERNAL_ENGINE=1`. In that mode the host engine is the source of truth and **docker/mcp secrets never persist inside the container** — the strongest real-world option. To use it, run the `docker-secrets-engine` on the host so its socket exists at the expected path (e.g. `~/Library/Caches/docker-secrets-engine/engine.sock` on macOS). `devcontainer-init.sh` flips `DOCKER_PASS_EXTERNAL_ENGINE=1` automatically when the socket is present; otherwise it falls back to the in-container keyring and an encrypted one-shot secret import. Note: this covers docker/mcp secrets only. `gh` tokens still land in the in-container keyring. ### 2. Password-encrypted keyring (hardening path, not currently enabled) If at-rest encryption inside the container becomes a requirement, switch from the hand-written empty-password keyring to `gnome-keyring-daemon --login`, which reads an unlock password on stdin and lets the daemon create/unlock an AES-encrypted `login.keyring`. The encryption is only as strong as wherever the unlock password lives. The recommended source is the **host keychain / 1Password**, fetched at init time and never written to a repo-local file. > **Do not** reuse `DOCKER_PASS_EXPORT_KEY` as the unlock password. It is > regenerated randomly on every `devcontainer-init`, so it cannot unlock a > keyring that persists across reloads. ### 3. Ephemeral tmpfs keyring (not currently enabled) Place the keyring on tmpfs so nothing touches disk, and re-import secrets each session. This removes the at-rest file entirely at the cost of a per-session re-import. ## Summary | Option | Secrets at rest in container | Headless start | Notes | | -------------------------------- | ---------------------------- | --------------------------------------------- | ---------------------------------- | | Empty-password keyring (default) | Yes, unencrypted | Yes | Relies on host disk encryption | | External mounted engine | No (docker secrets) | Yes | Preferred; `gh` tokens still local | | Password-encrypted keyring | Yes, encrypted | Yes, if password is sourced non-interactively | Use host keychain / 1Password | | Ephemeral tmpfs | No | Yes | Re-import every session |