Fix "Host Key Verification Failed" in GitHub Actions Django Deploys
Fix Host key verification failed when a GitHub Actions workflow deploys Django over SSH: add the server’s host key to known_hosts from a secret, avoid StrictHostKeyChecking=no, handle rebuilt servers and custom ports, and verify fingerprints.
Problem statement
The deploy job reaches the SSH step and stops:
Host key verification failed.
Error: Process completed with exit code 255.
Every GitHub-hosted runner starts with an empty ~/.ssh/known_hosts. When ssh or rsync connects to your server, it cannot verify the server's identity, and in a non-interactive job it cannot ask you to accept it, so it refuses. The same happens after you rebuild a server: its host key changed, and a stored known_hosts entry no longer matches.
- Get the server's host key once from a trusted machine:
ssh-keyscan -t ed25519 -p 22 deploy.example.com. - Verify its fingerprint against the server (
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pubon the server). - Store the line in a repository secret, such as
SSH_KNOWN_HOSTS. - In the workflow, write it to
~/.ssh/known_hostsbefore connecting. - Do not use
StrictHostKeyChecking=no; it removes protection against a spoofed server.
Step-by-step solution
1. Capture and verify the host key
From your laptop or a machine that has already trusted the server:
ssh-keyscan -t ed25519 deploy.example.com
# with a custom port
ssh-keyscan -t ed25519 -p 2222 deploy.example.com
On the server, print the fingerprint and compare:
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub
ssh-keyscan -t ed25519 deploy.example.com | ssh-keygen -lf -
The SHA256 fingerprints must match. Most cloud consoles also show host key fingerprints in the instance's boot log.
2. Store it as a secret
Add a repository or environment secret named SSH_KNOWN_HOSTS with the full ssh-keyscan output line, for example:
deploy.example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...
For a custom port, the line starts with [deploy.example.com]:2222.
3. Write known_hosts in the workflow
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- name: Configure SSH
run: |
install -m 700 -d ~/.ssh
echo "${{ secrets.SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/known_hosts ~/.ssh/id_ed25519
- name: Deploy
run: ssh deploy@deploy.example.com "/srv/app/bin/deploy"
The host name in the ssh command must match the name in the known_hosts line. Connecting by IP when the entry uses the domain, or the other way round, fails verification. Include both if you use both.
4. Handle rebuilt or replaced servers
Recreating a server from scratch generates new host keys. The error then says:
WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!
or simply "Host key verification failed". Re-run step 1, verify the new fingerprint, and update the secret. To avoid this, preserve host keys when rebuilding (restore /etc/ssh/ssh_host_*) or set them through cloud-init.
5. Avoid disabling verification
ssh -o StrictHostKeyChecking=no or running ssh-keyscan inside the workflow makes the error go away by trusting whatever server answers. Anyone able to intercept DNS or routing could then receive your deploy key session and your code. A stored, verified key costs one secret.
Explanation
SSH authenticates both sides. The client proves who it is with the deploy key; the server proves who it is with its host key, which the client checks against known_hosts. Interactive sessions ask you to accept an unknown key the first time. Batch jobs cannot ask, so OpenSSH fails with "Host key verification failed". Supplying the expected key ahead of time makes the job both non-interactive and secure.
Edge cases / notes
- Bastion hosts. With a jump host, both the bastion and the target need known_hosts entries.
- Load-balanced SSH. Several servers behind one name need the same host keys, or one line per key.
- Third-party actions. Actions such as
appleboy/ssh-actionaccept afingerprintinput; set it rather than leaving verification off.
Next steps
- Build the full pipeline with Deploy Django With GitHub Actions CI/CD.
- If the server rejects the key instead, see Fix SSH Permission Denied (publickey) During Deploy.
- Review pipeline security with the Django CI/CD Pipeline Checklist.
FAQ
Why does it work from my laptop?
Your laptop has the server in ~/.ssh/known_hosts from an earlier interactive connection. Each CI runner starts empty.
Can I run ssh-keyscan in the workflow?
It works but trusts whatever responds at that moment, which defeats verification. Scan once from a trusted machine and store the result.
What changes the host key?
Rebuilding or reinstalling the server, or regenerating SSH host keys. Moving a domain to a new server also changes it.
Do self-hosted runners need this?
Yes, unless the runner user's known_hosts already contains the server. Store it explicitly so jobs are reproducible.