SSH: Remote Commands, ssh-keyscan, and Key-Based Auth
SSH runs remote commands by passing them after the host argument. ssh-keyscan fetches a server's public host key without authenticating — used in CI/CD to pre-populate known_hosts and prevent the 'authenticity of host' prompt from blocking automation. Key-based auth replaces password prompts with public/private key cryptography.
Running remote commands
# Run a single command on a remote host
ssh user@host "command"
# Run multiple commands
ssh user@host "cd /var/log && tail -100 app.log | grep ERROR"
# Run a complex script block
ssh user@host << 'EOF'
set -e
cd /app
git pull origin main
docker compose up -d --build
EOF
# Example: flush Redis remotely
ssh redis-user@redis-host \
"redis-cli -h redis-host -p 6379 FLUSHALL"
The command after the host runs in a non-interactive shell on the remote. Use 'EOF' (single-quoted heredoc) to prevent local variable expansion.
ssh-keyscan: pre-populating known_hosts
When SSH connects to a host for the first time, it asks:
The authenticity of host 'github.com (140.82.114.4)' can't be established.
ED25519 key fingerprint is SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU.
Are you sure you want to continue connecting (yes/no/[fingerprint])?
In CI/CD pipelines, this interactive prompt blocks automation. ssh-keyscan fetches the host's public key to pre-populate known_hosts:
# Fetch GitHub's host keys and add to known_hosts
ssh-keyscan github.com >> ~/.ssh/known_hosts
# Fetch specific key types
ssh-keyscan -t ed25519,rsa github.com >> ~/.ssh/known_hosts
# In CI/CD (CircleCI, GitHub Actions):
mkdir -p ~/.ssh
ssh-keyscan github.com >> ~/.ssh/known_hosts
# Now git clone [email protected]:... works without the prompt
ssh-keyscan connects to port 22, receives the server's public key(s), and prints them in known_hosts format:
github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAA...
github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTIt...
github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOM...
ssh-keyscan is not secure by itself — you must verify the fetched fingerprint against a trusted source
GotchaSecurityssh-keyscan fetches the key from the host, but it can't verify the key is legitimate. A man-in-the-middle attacker could serve a different key. For GitHub and other known services, compare the fingerprint from ssh-keyscan against the official published fingerprints. GitHub publishes its SSH key fingerprints at github.com/security/documentation. For internal servers, distribute the known_hosts file through a configuration management system (Ansible, Puppet, Chef) or use a CA-signed host certificate to eliminate per-host key management.
Prerequisites
- SSH key pairs
- Man-in-the-middle attacks
- PKI
Key Points
- ssh-keyscan alone doesn't verify the key is authentic — it just fetches whatever the server presents.
- Verify fingerprints against a trusted source (official documentation, verified TLS channel).
- ssh-keygen -lf known_hosts to see fingerprints of stored keys.
- StrictHostKeyChecking=no in SSH config disables host key checking entirely — never do this in production.
SSH config file
Avoid repeating connection options with ~/.ssh/config:
# ~/.ssh/config
Host prod-web
HostName 203.0.113.10
User ubuntu
IdentityFile ~/.ssh/prod-key.pem
ForwardAgent yes
Host bastion
HostName 203.0.113.5
User ec2-user
IdentityFile ~/.ssh/bastion-key.pem
# Jump through bastion to reach internal hosts
Host internal-*
User ubuntu
ProxyJump bastion
IdentityFile ~/.ssh/internal-key.pem
ssh prod-web # uses the config above
ssh internal-db # connects via bastion automatically
ssh -J bastion internal-db # explicit jump host (command line)
ProxyJump (SSH 7.3+) replaces the older -o ProxyCommand='ssh bastion -W %h:%p' pattern.
A CircleCI job runs `git clone [email protected]:org/repo.git` and hangs with 'Are you sure you want to continue connecting?'. How do you fix it?
easyThe CI container has no known_hosts file. SSH's default behavior is to prompt for host key confirmation on first connection.
AAdd `GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no'` to skip the check
Incorrect.This works but disables host key verification entirely, making every connection vulnerable to MITM attacks. Better to pre-populate known_hosts with verified keys than to disable the check.BAdd `ssh-keyscan github.com >> ~/.ssh/known_hosts` as a step before the git clone, then verify the fetched fingerprint against GitHub's published keys
Correct!ssh-keyscan fetches GitHub's host key and writes it to known_hosts before the first connection. SSH finds the key in known_hosts, skips the prompt, and connects. The verification step (comparing fingerprint against GitHub's documented fingerprints) ensures the key is authentic and not from a MITM. Most CI platforms have pre-built steps for this: CircleCI's checkout step handles it automatically; for custom jobs you add ssh-keyscan explicitly.CUse HTTPS for git clone instead of SSH
Incorrect.Switching to HTTPS works, but requires a Personal Access Token for private repos and doesn't use SSH key auth. The question is about fixing the SSH flow.DIncrease the SSH connection timeout
Incorrect.The hang isn't a timeout issue — it's an interactive prompt waiting for user input. No timeout setting resolves an interactive prompt in non-interactive mode.
Hint:Why does SSH prompt on first connection? What file stores accepted host keys?