Understanding the Temptation

Developers often bake SSH private keys directly into Dockerfiles to simplify automated deployments. The approach feels convenient: a single image can pull code from a private repository, push artifacts, or run remote administration tasks without extra configuration steps.

While the method works, it silently creates a powerful attack surface. Once the image is built, the private key becomes part of every layer stored in a registry, cached on host machines, and potentially exposed to any user who can pull the image.

# Example of a risky Dockerfile
FROM alpine:3.18

# Copy the private key directly into the image (DO NOT DO THIS)
COPY id_rsa /root/.ssh/id_rsa
RUN chmod 600 /root/.ssh/id_rsa

# Install git and clone a private repo
RUN apk add --no-cache git openssh-client \
    && git clone [email protected]:example/private-repo.git /app

CMD ["sh"]

The snippet above demonstrates a common anti‑pattern. The key lives in the image’s filesystem and is immutable once the layer is created. Even if you later remove the key in a subsequent layer, the original layer remains in the image’s history.

How the Exposure Propagates

Container registries store each layer as a separate blob. When an image is pushed, the registry keeps the blob for as long as any tag references it. If a malicious actor gains read access to the registry, they can pull the image, extract the layer, and recover the private key with a few commands:

# Pull the image locally
docker pull myregistry.example.com/unsafe-image:latest

# Save the image as a tarball
docker save myregistry.example.com/unsafe-image:latest -o image.tar

# Extract the tarball and locate the key
tar -xf image.tar
grep -R "BEGIN RSA PRIVATE KEY" .

The key can then be used to access any resource that trusts the original host’s identity, including production servers, internal git repositories, and cloud APIs. Because the key is no longer under the developer’s control, revoking it requires rotating all dependent services—a costly and error‑prone process.

Detecting Embedded Keys Automatically

To guard against accidental inclusion, integrate a static analysis step into your CI pipeline. The following Bash script scans Docker image layers for patterns that resemble private keys. It can be run after the build but before the push.

# scan-ssh-keys.sh
#!/usr/bin/env bash
set -euo pipefail

IMAGE=$1
TMPDIR=$(mktemp -d)

# Export image to temporary directory
docker save "$IMAGE" | tar -C "$TMPDIR" -xvf - &>/dev/null

# Search for RSA, DSA, or EC private key markers
if grep -RIl "BEGIN RSA PRIVATE KEY" "$TMPDIR" \
   || grep -RIl "BEGIN DSA PRIVATE KEY" "$TMPDIR" \
   || grep -RIl "BEGIN EC PRIVATE KEY" "$TMPDIR"; then
  echo "⚠️  Private key detected in image $IMAGE"
  exit 1
else
  echo "✅ No private keys found"
fi

rm -rf "$TMPDIR"

Hook the script into a GitHub Actions workflow:

# .github/workflows/docker.yml
name: Docker Build
on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Build image
        run: |
          docker build -t myregistry.example.com/secure-image:${{ github.sha }} .
      - name: Scan for embedded keys
        run: |
          chmod +x scan-ssh-keys.sh
          ./scan-ssh-keys.sh myregistry.example.com/secure-image:${{ github.sha }}
      - name: Push image
        if: success()
        run: |
          docker push myregistry.example.com/secure-image:${{ github.sha }}

The workflow aborts the push if a key is detected, preventing the vulnerable artifact from ever reaching the registry.

Alternative Secure Patterns

Instead of embedding keys, use one of the following approaches:

  • SSH Agent Forwarding – Run the container with --ssh default and let the host’s SSH agent provide the key at runtime.
  • Mount Secrets at Runtime – Store the private key in a secret manager (AWS Secrets Manager, HashiCorp Vault) and mount it via a volume or environment variable when the container starts.
  • Deploy Keys with Limited Scope – Generate a read‑only deploy key for git operations, store it securely, and rotate it regularly.

Here’s an example using Docker’s --ssh flag with BuildKit:

# Dockerfile (no key included)
# syntax=docker/dockerfile:1.4
FROM alpine:3.18
RUN apk add --no-cache git openssh-client
WORKDIR /app
# The SSH key is provided by the builder, not stored in the image
RUN --mount=type=ssh git clone [email protected]:example/private-repo.git .

CMD ["sh"]

Build the image with the SSH agent forwarded:

# Enable BuildKit
export DOCKER_BUILDKIT=1
# Ensure your SSH agent has the key loaded
ssh-add ~/.ssh/id_rsa
# Build using the forwarded key
docker build --ssh default -t myregistry.example.com/clean-image:latest .

The resulting image contains no private material, yet the build process can still access the private repository securely.

Security and Best Practices

Never commit private keys to source control. Even a single accidental commit can propagate across forks and mirrors.

Enable image scanning. Many container registries offer built‑in vulnerability scanners; configure them to look for credential patterns as well.

Employ least‑privilege keys. Deploy keys should have only the permissions required for the task (e.g., read‑only access to a single repository).

“Embedding secrets in immutable artifacts is a convenience that trades security for speed; the cost is rarely worth it.”

Conclusion

The practice of baking SSH private keys into Docker images may look efficient, but it creates a hidden liability that can be exploited to compromise entire infrastructures. By treating container images as immutable artifacts and separating secret delivery from the build process, teams preserve the benefits of automation without exposing critical credentials.

Adopt automated scans, use SSH agent forwarding, and store secrets in dedicated managers. The extra steps pay off in reduced risk and simplified incident response when a key is ever suspected of compromise.

Created at: 2026-09-27T08:00:16Z