Signing commits inside ephemeral containers Jump to heading
Containers break signing in two predictable ways. Either the container has no key and no agent, so every commit fails with “gpg failed to sign the data”, or someone fixes that by copying a key into the image, where it is now readable by anyone who can pull the image, forever, in every layer cache. Neither is acceptable. The right answer depends on who is signing: a developer working in a dev container should sign with their own key held on the host, and an automated job should receive a key at runtime that dies with the container. This page covers both, within signed commits in CI pipelines.
When to use this approach Jump to heading
- Developers use dev containers or container-based development environments and commit from inside them.
- CI jobs run in custom images and need to create signed commits or tags.
- You found, or suspect, a private key inside a container image or its build context.
- The key holder is either a person on the host or a CI secret — never the image itself.
Step 1 — Check that no image contains a key Jump to heading
Before fixing signing, make sure nobody already “fixed” it the wrong way. Search the images you build for private key material, including in earlier layers that a later RUN rm did not really remove.
# Save the image and search every layer, not just the final filesystem
docker save my/dev-image:latest -o image.tar
mkdir layers && tar -xf image.tar -C layers
find layers -name '*.tar' -exec sh -c 'tar -tf "$1" | grep -E "(\.ssh/id_|\.gnupg/private-keys)" | sed "s|^|$1: |"' _ {} \; Any hit means the key is published to everyone who can pull the image. Rotate it, then rebuild the image without it, following responding to a leaked credential.
Step 2 — Dev containers: forward the host’s SSH agent Jump to heading
For a developer, the key belongs on their machine. Mount the host agent’s socket into the container and point Git at the developer’s public key. Signing requests travel out through the socket; the private key never enters the container.
// .devcontainer/devcontainer.json
{
"mounts": [
"source=${localEnv:SSH_AUTH_SOCK},target=/ssh-agent,type=bind"
],
"remoteEnv": { "SSH_AUTH_SOCK": "/ssh-agent" },
"postCreateCommand": "git config --global gpg.format ssh && git config --global commit.gpgSign true"
} # Inside the container: use the forwarded key by its public half
git config --global user.signingKey "key::$(ssh-add -L | grep signing | head -1)"
git commit --allow-empty -m "signed from container" && git log -1 --format='%G?' Many editors forward the agent automatically when they start a dev container, in which case only the Git configuration is needed. Check with ssh-add -L inside the container before adding a mount.
Step 3 — GPG in a dev container: forward the agent’s extra socket Jump to heading
GPG has a dedicated “extra” socket meant for exactly this: it allows signing but restricts some administrative operations. Forward that socket rather than the main one.
# On the host: find the extra socket
gpgconf --list-dirs agent-extra-socket {
"mounts": [
"source=${localEnv:HOME}/.gnupg/S.gpg-agent.extra,target=/home/vscode/.gnupg/S.gpg-agent,type=bind"
]
} The container needs the public key imported and the same user.signingKey subkey ID as the host. No secret key material is copied.
Step 4 — CI containers: inject a key at runtime Jump to heading
Automation has no host agent to forward. Pass the key in as a secret environment variable or mounted file at run time and load it into an agent inside the container, exactly as on a plain runner.
# GitHub Actions job running in a container image
jobs:
bump:
runs-on: ubuntu-latest
container: { image: ghcr.io/example/release-tools:1.8 }
environment: release-signing
steps:
- uses: actions/checkout@v4
- run: |
eval "$(ssh-agent -s)"
printf '%s\n' "$SIGNING_KEY" | ssh-add -
git config user.signingKey "key::$(ssh-add -L | head -1)"
git config gpg.format ssh
./ci/bump-and-commit.sh
env: { SIGNING_KEY: "${{ secrets.RELEASE_BOT_KEY }}" } The image needs ssh-keygen installed (openssh-client on Debian-based images, openssh-keygen on Alpine). Its absence produces a “cannot run ssh-keygen” error that Git reports, unhelpfully, as a signing failure.
# Verification inside the image
command -v ssh-keygen && ssh-keygen -Y sign 2>&1 | head -1 Step 5 — Never let the build context carry a key Jump to heading
Docker sends the build context to the daemon, and COPY . . copies all of it. A key sitting in the project directory ends up in the image even if no instruction mentions it. Exclude key material in .dockerignore, and use build secrets for anything a build step genuinely needs.
# .dockerignore
**/.ssh
**/.gnupg
**/*.pem
**/id_* # A build step that needs a key uses a secret mount: never stored in a layer
RUN git clone [email protected]:org/private-dep.git Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Is agent forwarding into a container risky? Jump to heading
Anything running in the container can ask the agent to sign while the socket is mounted. That is the same exposure as any process on the host, so it adds little risk for a developer’s own container. Do not forward a personal agent into containers running untrusted code.
Can I use keyless signing in containers instead? Jump to heading
Yes. Keyless signing needs network access to the identity provider and signing service, but no key at all, which suits CI containers well. See keyless commit signing with Sigstore gitsign.
Why does signing work in the container shell but fail in a post-create script? Jump to heading
Post-create scripts often run before the editor has set up agent forwarding, so SSH_AUTH_SOCK is empty at that moment. Move signing-dependent steps to a later lifecycle hook, or make them tolerate a missing agent.
Related Jump to heading
- Signed Commits in CI Pipelines — the parent topic.
- Protecting CI Signing Keys with Environment Secrets — scoping the secret you inject.
- Troubleshooting ‘gpg failed to sign the data’ — the error containers most often produce.
- Caching Docker Layers in CI — layer caches are another place a baked key would persist.