Real-world Problem: The SSH Key Leak Nightmare in Docker Builds
Imagine you’re building a Docker image for a microservices project. Your application needs to pull code from several internal libraries located in Private Repositories on GitHub or GitLab.
The tough question is: How do you git clone that code into the image while ensuring security?
Many choose a “shortcut” by using the COPY command to put the SSH key file (id_rsa) directly into the Dockerfile. The project runs immediately, but the risk is enormous. This key file will reside permanently in the image layers. Anyone with permission to pull the image can use tools like dive to extract your private key in less than 60 seconds. A security vulnerability significant enough to cause the entire company’s source code to be compromised.
Why Old Methods Are Extremely Dangerous
1. Copying the SSH Key into the Image
# FATAL MISTAKE
COPY ~/.ssh/id_rsa /root/.ssh/id_rsa
RUN git clone [email protected]:company/private-core.git
RUN rm /root/.ssh/id_rsa
Many mistakenly believe that deleting the file with the rm command is enough. In reality, Docker records every change layer by layer. Even if the final layer doesn’t show the file, the previous COPY layer still stores it entirely in the storage driver.
2. Using Build Arguments (ARG)
# STILL NOT SECURE
ARG SSH_PRIVATE_KEY
RUN echo "$SSH_PRIVATE_KEY" > /root/.ssh/id_rsa
The value of ARG is recorded in the image metadata. Anyone typing the docker inspect command can read the entire content of the key you passed in. This is like trying to hide something behind your back while standing in front of a mirror.
3. Multi-stage build
This multi-stage build method is better because you can leave the key behind in the first build stage. However, intermediate images still exist on the CI/CD server. If this server is compromised, your key remains in the danger zone.
The Standard Solution: SSH Agent Forwarding with BuildKit
Since Docker version 18.09, Docker introduced BuildKit with the --mount=type=ssh feature. This is currently the most professional approach.
This mechanism is similar to how you use SSH Agent Forwarding to jump from one server to another. Docker creates a temporary Unix socket so the container can “borrow” the SSH Agent from the host machine.
Practical Benefits:
- Absolute Security: Not a single byte of SSH key data is written to the image layers.
- Convenience: No need to copy files, no need to manage complex passphrases in the Dockerfile.
- Control: You have the right to revoke access directly on the host machine or CI Runner at any time.
Detailed Implementation Guide
Below are the 3 steps I usually use to set up real-world enterprise projects.
Step 1: Prepare the SSH Agent on the Host Machine
First, make sure your SSH Agent is running and has the necessary key loaded.
# Start the agent
eval $(ssh-agent -s)
# Add the key (e.g., key used for GitHub)
ssh-add ~/.ssh/id_rsa_github
# Confirm the key is ready
ssh-add -l
Step 2: Write a BuildKit-compliant Dockerfile
In this step, you need to declare the SSH mount type right at the RUN command that performs the code clone. Remember to add github.com to known_hosts so the build process isn’t interrupted by fingerprint confirmation requests.
# syntax=docker/dockerfile:1
FROM node:18-slim
# Install git and openssh
RUN apt-get update && apt-get install -y git openssh-client && rm -rf /var/lib/apt/lists/*
# Scan fingerprint to avoid "Host key verification failed" error
RUN mkdir -p -m 0700 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts
WORKDIR /app
# Use ssh mount to clone the repo without storing the key
RUN --mount=type=ssh git clone [email protected]:your-org/private-lib.git .
RUN npm install
Pro Tip: The # syntax=docker/dockerfile:1 line at the top of the file is mandatory. Without it, Docker won’t understand advanced BuildKit features.
Step 3: Execute the Build
When running the build command, simply add the flag --ssh default. Docker will automatically connect the socket from your machine directly into the container.
# Enable BuildKit (if using older Docker versions)
export DOCKER_BUILDKIT=1
# Build the image securely
docker build --ssh default -t my-secure-app .
Real-world Experience & Troubleshooting
During the operation of large CI/CD systems, I’ve gathered a few important notes:
Using Multiple SSH Keys Simultaneously
If the project needs to pull code from both internal GitLab and GitHub, you can categorize them by ID:
# In Dockerfile
RUN --mount=type=ssh,id=gitlab git clone [email protected]:internal/core.git
When building, you map each corresponding key file: docker build --ssh gitlab=~/.ssh/id_rsa_gitlab ...
Deploying on GitHub Actions
In a CI environment like GitHub Actions, you should use the webfactory/ssh-agent action. It automatically manages the socket, helping BuildKit recognize keys more smoothly. Practice shows this method reduces pipeline configuration time by 20% compared to manual key file management.
Common Errors
If you encounter a “Permission denied” error, check the following:
- Have you performed
ssh-add? - Does the
$SSH_AUTH_SOCKenvironment variable exist on the host machine? - Is the
# syntaxline present at the top of the Dockerfile?
Conclusion
Switching from COPYing keys to SSH Agent Forwarding is more than just changing a few lines of code. It’s a professional and secure DevOps mindset. This approach gives you peace of mind when pushing images to Docker Hub or any Registry without fear of exposing system secrets.
Good luck with your implementation. If you face difficulties configuring this on different CI/CD systems, don’t hesitate to leave a question below!

