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_keysfile 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-agentacts as a daemon that holds your decrypted private keys in memory. Thessh-addcommand 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
rsyncorsshattempts to connect, the SSH client consults thessh-agentfor the appropriate key to use for authentication.
The levers you control are:
- Key Generation: Using strong key types (
ed25519is generally preferred overrsafor its speed and security) and appropriate bit lengths. - Key Scope: Creating dedicated keys for CI/CD with limited privileges, rather than using personal keys or overly broad access.
- 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).
- 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.