SSH: Remote Commands, ssh-keyscan, and Key-Based Auth

1 min readSystems & Networking

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.

shellsshsecurity

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

GotchaSecurity

ssh-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?

easy

The 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?