Troubleshooting
CI/CD pipelines

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.

Why SSH refusesRunner startsempty known_hostsssh connectsserver presents keyKey unknownno one to askVerification failsexit 255The runner has no record of your server’s host key, so SSH cannot trust it.
  1. Get the server's host key once from a trusted machine: ssh-keyscan -t ed25519 -p 22 deploy.example.com.
  2. Verify its fingerprint against the server (ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub on the server).
  3. Store the line in a repository secret, such as SSH_KNOWN_HOSTS.
  4. In the workflow, write it to ~/.ssh/known_hosts before connecting.
  5. 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.

one-time setup1ssh-keyscan the serverfrom a trusted machine2Compare fingerprintswith the server itself3Save as SSH_KNOWN_HOSTSrepository secret4Write known_hosts in jobbefore ssh or rsync5Deploy connectskey verifiedVerify the key once, then let every job use the stored copydjango-deployment.com
Scanning inside the workflow would trust whatever answers at that moment.

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.

two ways to make the error go awayStored known_hosts secret✓server identity verified✓fails loudly on change✓one-time setupStrictHostKeyChecking=no✕trusts any server✕spoofing goes unnoticed✕deploy key exposedVerification is the point of the error; keep itdjango-deployment.com
A changed key should stop the deploy until a person checks why.

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-action accept a fingerprint input; set it rather than leaving verification off.

Next steps

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.

Keep going