SSH keys are a common pain point when your CI/CD pipeline needs to access private Git repositories. Here’s how to securely forward them so your builds can pull down proprietary code without exposing your credentials.

Imagine this: your build job needs to git clone git@github.com:my-org/my-private-repo.git. But your CI runner doesn’t have access to the SSH key that unlocks github.com for your organization. You can’t just paste the private key directly into your build script or a CI variable – that’s a massive security hole.

The solution is to use SSH agent forwarding. When your CI job starts, it can ask your local machine (or a bastion host) to handle the SSH authentication for it. Your private key never leaves your local machine; instead, a temporary, authenticated socket is created that the CI job can use to talk to the SSH agent.

Let’s walk through a typical scenario using GitLab CI.

The Problem:

Your .gitlab-ci.yml has a step like this:

build:
  stage: build
  script:
    - git clone git@github.com:my-org/my-private-repo.git
    - echo "Successfully cloned private repo!"

And it fails with an error like:

Cloning into 'my-private-repo'...
git@github.com: Permission denied (publickey).
fatal: Could not read from remote repository.

Please make sure you have the correct access rights and the repository exists.

This means the GitLab runner, executing this script, doesn’t have the SSH credentials to authenticate with GitHub.

The Solution: SSH Agent Forwarding

The core idea is to:

  1. Generate an SSH key pair specifically for your CI/CD access.
  2. Add the public key to your Git provider (GitHub, GitLab, Bitbucket).
  3. Add the private key to your local SSH agent.
  4. Configure your CI job to use SSH agent forwarding.

Step 1: Generate a Dedicated SSH Key Pair

On your local machine, create a new SSH key pair. It’s good practice to use a descriptive name.

ssh-keygen -t ed25519 -C "gitlab-ci-deploy-key" -f ~/.ssh/gitlab_deploy_key

This will create two files:

  • ~/.ssh/gitlab_deploy_key (your private key)
  • ~/.ssh/gitlab_deploy_key.pub (your public key)

Crucially, do NOT set a passphrase for this key if you intend to use it directly in CI without manual intervention. If you must have a passphrase, you’ll need a way to unlock it, which complicates the CI setup significantly. For automated builds, an unlocked key is standard.

Step 2: Add the Public Key to Your Git Provider

For GitHub: Go to your user settings -> SSH and GPG keys. Click "New SSH key" and paste the contents of ~/.ssh/gitlab_deploy_key.pub. Give it a title like "GitLab CI Deploy".

For GitLab: Go to your user settings -> SSH Keys. Click "Add key" and paste the contents of ~/.ssh/gitlab_deploy_key.pub. Give it a title like "GitLab CI Deploy".

For Bitbucket: Go to your personal settings -> SSH keys. Click "Add key" and paste the contents of ~/.ssh/gitlab_deploy_key.pub. Give it a title like "GitLab CI Deploy".

Alternatively, if this key is only for a specific repository, you can add it as a "Deploy Key" within that repository’s settings on GitHub/GitLab/Bitbucket. This is generally more secure as it limits the key’s scope.

Step 3: Add the Private Key to Your Local SSH Agent

Ensure your SSH agent is running. On most Linux/macOS systems, it’s started automatically.

Add your new private key to the agent:

eval "$(ssh-agent -s)" # Start the agent if not running
ssh-add ~/.ssh/gitlab_deploy_key

You won’t be prompted for a passphrase because we generated the key without one.

Step 4: Configure Your CI Job for SSH Agent Forwarding

This is where the magic happens in your .gitlab-ci.yml. You need to: a. Pass the private key to the runner securely. b. Instruct the runner to use the SSH agent.

Method A: Using GitLab CI’s secrets (Recommended for GitLab)

GitLab has a feature to securely inject secrets into jobs.

  1. Go to your project’s Settings > CI/CD.
  2. Expand the Secrets section.
  3. Click Add variable.
  4. Key: SSH_PRIVATE_KEY
  5. Value: Paste the entire contents of your ~/.ssh/gitlab_deploy_key file (including -----BEGIN OPENSSH PRIVATE KEY----- and -----END OPENSSH PRIVATE KEY-----).
  6. Protect variable: Check this box if your pipeline runs on protected branches or tags.
  7. Mask variable: Check this box to prevent it from appearing in job logs (though it’s still visible in the CI variables UI).

Now, modify your .gitlab-ci.yml:

build:
  stage: build
  before_script:
    # Install ssh-agent if not already installed
    - apt-get update -y && apt-get install openssh-client -y
    # Start ssh-agent
    - eval $(ssh-agent -s)
    # Add the private key to the agent
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
    # Create SSH directory and set permissions
    - mkdir -p ~/.ssh
    - chmod 700 ~/.ssh
    # Optional: Disable strict host key checking for the Git provider
    # This is less secure but often necessary in dynamic CI environments.
    # Alternatively, pre-populate known_hosts with the provider's key.
    - echo -e "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile=/dev/null" >> ~/.ssh/config
  script:
    - git clone git@github.com:my-org/my-private-repo.git
    - echo "Successfully cloned private repo!"
  after_script:
    # Clean up the SSH agent
    - eval $(ssh-agent -k)

Explanation:

  • eval $(ssh-agent -s): Starts the SSH agent process for the job’s environment.
  • echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -: Takes the private key from the CI variable, removes any stray carriage returns (common on Windows), and adds it to the running agent.
  • mkdir -p ~/.ssh && chmod 700 ~/.ssh: Creates the necessary directory for SSH configurations.
  • echo -e "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile=/dev/null" >> ~/.ssh/config: This is a common workaround for CI. It tells SSH to not verify the host’s identity (StrictHostKeyChecking no) and to not store host keys (UserKnownHostsFile=/dev/null). Be aware this reduces security. A more secure approach is to fetch the Git provider’s public SSH host key (e.g., ssh-keyscan github.com) and add it to ~/.ssh/known_hosts before attempting to clone.
  • eval $(ssh-agent -k): Shuts down the agent process.

Method B: Using Docker Executor with SSH Agent Forwarding (More Advanced)

If your GitLab runner uses the Docker executor, you can leverage Docker’s SSH agent forwarding capabilities. This is often cleaner as the key doesn’t need to be stored as a CI variable.

  1. Ensure your local SSH agent has the key: ssh-add ~/.ssh/gitlab_deploy_key (as in Step 3).

  2. Configure your .gitlab-ci.yml:

    build:
      stage: build
      image: docker:latest # Or your preferred image
      services:
        - docker:dind
      variables:
        DOCKER_HOST: tcp://docker:2375/
        DOCKER_TLS_CERTDIR: "" # Disable TLS
        GIT_STRATEGY: none # Prevent GitLab Runner from cloning automatically
      before_script:
        # Install git and ssh
        - apk add --no-cache git openssh-client
        # Add the Git provider's host key to known_hosts (more secure than disabling check)
        - ssh-keyscan github.com >> ~/.ssh/known_hosts
        - chmod 644 ~/.ssh/known_hosts
      script:
        # Use the SSH_AUTH_SOCK environment variable provided by the Docker daemon
        # when forwarding the agent.
        - ssh -A -T git@github.com # Test connection (optional)
        - git clone git@github.com:my-org/my-private-repo.git
        - echo "Successfully cloned private repo!"
    

Explanation:

  • services: - docker:dind: Starts a Docker-in-Docker service, allowing your job to run Docker commands.
  • GIT_STRATEGY: none: Prevents the GitLab runner from trying to clone the repository itself using its own mechanisms before your script runs.
  • ssh-keyscan github.com >> ~/.ssh/known_hosts: This is the more secure way to handle host key checking. It fetches the known public key for github.com and adds it to the known_hosts file.
  • ssh -A -T git@github.com: This is the key. The -A flag enables SSH agent forwarding. The Docker daemon, when configured to forward your local agent, makes SSH_AUTH_SOCK available inside the container. This command tests the connection and ensures the forwarded agent is working.
  • git clone ...: Now git can use the forwarded SSH agent to authenticate.

The Next Step:

Once you have private repo access working, the next hurdle is often managing multiple credentials or ensuring your build artifacts are securely uploaded.

Want structured learning?

Take the full Buildkit course →