SSH keys are the backbone of secure automated deployments, but managing them in CI/CD pipelines is a surprisingly tricky dance between security and convenience.

Let’s see this in action. Imagine a simple deployment script, deploy.sh, designed to copy files to a staging server:

#!/bin/bash

# Ensure we have the SSH key and know the target
if [ -z "$SSH_PRIVATE_KEY" ] || [ -z "$DEPLOY_USER" ] || [ -z "$DEPLOY_HOST" ]; then
  echo "Error: SSH_PRIVATE_KEY, DEPLOY_USER, and DEPLOY_HOST must be set."
  exit 1
fi

# Create a temporary SSH key file
echo "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key

# Configure SSH to use the key
ssh-agent -s > /dev/null
ssh-add ~/.ssh/deploy_key

# Perform the deployment
rsync -avz --progress -e "ssh -o StrictHostKeyChecking=no" ./build/ "$DEPLOY_USER@$DEPLOY_HOST:/path/to/app/"

# Clean up the key
ssh-add -d ~/.ssh/deploy_key
rm ~/.ssh/deploy_key
echo "Deployment complete."

This script relies on an environment variable, SSH_PRIVATE_KEY, which holds the entire content of the private SSH key. This is a common pattern, but it’s also where many security pitfalls lie.

The core problem SSH solves in CI/CD is authentication without interactive passwords. When your CI/CD runner needs to SSH into a server to deploy code, it can’t ask a human for a password. SSH keys provide this passwordless, cryptographic authentication. A private key on the CI/CD runner is paired with a public key authorized on the target server. When the runner tries to connect, the server challenges it, and the private key proves its identity.

Here’s how it breaks down internally:

  • Key Pair Generation: You generate a standard SSH key pair (ssh-keygen). One key is private (keep it secret!), the other is public (share it freely).
  • Public Key Distribution: The public key is added to the ~/.ssh/authorized_keys file on the target server for the user that the CI/CD process will log in as.
  • Private Key in CI/CD: The private key is injected into the CI/CD environment, usually as a secret environment variable.
  • SSH Agent: The ssh-agent acts as a daemon that holds your decrypted private keys in memory. The ssh-add command loads the private key into the agent. This avoids repeatedly decrypting the key or having it lie unencrypted on disk for extended periods.
  • Connection: When rsync or ssh attempts to connect, the SSH client consults the ssh-agent for the appropriate key to use for authentication.

The levers you control are:

  1. Key Generation: Using strong key types (ed25519 is generally preferred over rsa for its speed and security) and appropriate bit lengths.
  2. Key Scope: Creating dedicated keys for CI/CD with limited privileges, rather than using personal keys or overly broad access.
  3. Key Storage: How the private key is stored and accessed within the CI/CD system (e.g., as a secret environment variable, a dedicated secrets manager).
  4. Key Lifecycle: How keys are rotated, revoked, and securely deleted.

The one thing most people don’t realize is that storing the entire private key content as a single environment variable, while common, is often the weakest link. Many CI/CD systems will log environment variables, and even if the variable name is SSH_PRIVATE_KEY, the value itself (the key content) might be visible in build logs if not handled with extreme care. A more robust approach is to store the key in a secrets manager and have the CI/CD runner fetch it securely, or use SSH certificates which can have a much shorter lifespan and be revoked granularly.

The next logical step is to consider how to manage SSH host key verification to prevent man-in-the-middle attacks.

Want structured learning?

Take the full Ssh course →