ssh-keygen Ed25519 Setup: 12 Steps, 35 Min [2026]

Your SSH keys are the front door to every server you run, and plenty of admins still generate them with habits from a decade ago. This ssh keygen tutorial builds a 2026 setup on top of OpenSSH 10.6, released October 6, 2026, which added a hybrid post-quantum signature algorithm named ssh-mldsa44-ed25519 while leaving the everyday Ed25519 workflow untouched.

You will finish in about 35 minutes with an Ed25519 key protected by a passphrase, an ssh-agent that forgets keys on a timer, a strict client config, a server that refuses passwords, an optional FIDO2 hardware key, a small certificate authority, and a script that audits all of it. That is 12 steps, and every command here is one you can paste into a terminal.

Passwords are the weak spot. Yubico’s own product pages say that more than 80% of hacking-related breaches come from stolen or weak passwords, and an SSH daemon that accepts passwords invites exactly that attack. If you also run Windows machines, our walkthrough on stopping brute-force attacks on Windows RDP applies the same lockdown logic to the other big remote-access door.

Google · Preferred Sources

Don't miss new tech stories on Google

Add Tech Insider once in the Google app and our stories appear in your news suggestions.

Add Now

Why ssh-keygen Still Matters in October 2026

Every serious SSH setup starts with ssh-keygen. The tool does more than make key pairs. It prints fingerprints, changes passphrases, signs user and host certificates, enrolls FIDO2 security keys, and builds revocation lists. Learning its handful of flags pays off on every Linux, macOS and Windows machine you touch, because the OpenSSH client ships almost everywhere.

Ed25519 remains the sensible default. The algorithm was standardized in RFC 8032 in 2017 and shows up in OpenSSH as the ssh-ed25519 key type. It is a specification, not a product, so there is no plan to buy and no price tag. Keys are short, signing is fast, and the format has no knobs to misconfigure. RSA still works for legacy gear, but it is now the exception.

What changed this autumn is the pace of OpenSSH itself. According to a WindowsForum write-up of the 10.6 release, the project shipped 10.4 and 10.5 before 10.6 arrived. That report says the maintainers plan to keep releasing more often to get fixes out faster, and a separate account says the project has been receiving many AI-assisted security reports. Treat both as reported rather than confirmed, and check which OpenSSH version your distribution actually ships.

OpenSSH 10.6 added several things that matter for this tutorial:

  • A new ssh-agent -A option that points the agent at directories holding agent sockets, with user: and shared: prefixes.
  • A server-side AgentSocketPath setting so administrators can control where forwarded agent sockets live.
  • The hybrid post-quantum signature algorithm ssh-mldsa44-ed25519.
  • SFTP support for mkdir -p and fractional-second ChannelTimeout values.
  • Reportedly, a change that turns off the LZ77 compression dictionary coder in both ssh and sshd to close a side-channel leak.

The post-quantum signature is the headline, and it is also the item to skip for now. Help Net Security’s headline on the release says it enables a post-quantum signature algorithm and that experimental keys need replacing, which is reason enough to wait. Generate plain Ed25519 keys today and revisit the hybrid type once distributions package it and your tooling understands it.

Your distribution will not ship 10.6 on day one, so check what you actually run. SUSE’s October 21, 2026 advisory for openSUSE Leap 15.6 lists openssh 9.6p1 builds, which shows how far packaged versions sit from upstream. Ubuntu 26.10 shipped on October 15, 2026 as a nine-month interim release supported until July 2027, and it reportedly carries OpenSSH 10.5. Debian 13 (trixie) remains the stable line, with 13.7 released on September 12, 2026. The commands below work on all of them.

Prerequisites and Versions

You need two machines, or one machine plus a cloud server you can reach through a provider console. The console matters. When you change sshd settings, a typo can lock you out, and a web console is the escape hatch that makes experimenting safe.

ComponentWhat to useNotes
SSH clientOpenSSH 9.x or 10.xRun ssh -V to see yours. Upstream is 10.6 as of October 6, 2026.
SSH serverDebian 13, Ubuntu 24.04 or later, or any supported LinuxThe service is named ssh on Debian and Ubuntu, sshd on RHEL-family systems.
PrivilegesA sudo-capable account on the serverYou will edit files under /etc/ssh.
Safety netA provider web console or physical accessNeeded if a config change blocks logins.
Optional hardwareA FIDO2 security key such as a YubiKey 5 series modelNeeded only for step 8. Your client must have FIDO support (libfido2).
TimeAbout 35 minutesStep 12 adds a few minutes if you run the audit on several machines.

The audit script in step 12 was run on a machine with OpenSSH 9.6p1, and the sshd syntax follows the current OpenSSH manual pages. If your version string differs, the commands still apply. Where a feature depends on a newer release, the step says so.

Open two terminal windows to the server before you begin. Keep the first one logged in for the entire tutorial. It is your lifeline while the second one tests every change.

Step 1: Audit Your Existing Keys and OpenSSH Version

Start by learning what you already have. Old keys are the usual source of surprises, and you cannot harden what you have not counted. Print the client version, then list every public key with its fingerprint and type.

ssh -V
ls -l ~/.ssh
for f in ~/.ssh/*.pub; do ssh-keygen -lf "$f"; done

A healthy machine prints something like this:

OpenSSH_10.x, OpenSSL 3.x
256 SHA256:Qm1xExampleFingerprintOnlyDoNotCopy0000000 denis@laptop (ED25519)
3072 SHA256:Zk3xExampleFingerprintOnlyDoNotCopy1111111 denis@oldbox (RSA)

The first column is the key size, the last is the type. Ed25519 always reports 256. Anything marked DSA is obsolete and should go. RSA below 3072 bits should go. ECDSA is acceptable but offers no advantage over Ed25519.

Write down which servers each old key can reach. You will remove those entries in step 10, after the new key works everywhere. Do not delete old keys yet, because you need them to log in while you roll out the replacement.

Also check the server side. On the server, count the entries in the authorized keys file and print their fingerprints:

ssh-keygen -lf ~/.ssh/authorized_keys

If the output lists keys you do not recognize, stop and find out whose they are before continuing. That is a finding, not a formatting quirk.

Step 2: Generate an Ed25519 Key With ssh-keygen

Now the main event. One command creates the pair. Use a descriptive file name and comment, because a folder full of keys called id_ed25519 and id_ed25519(1) tells you nothing at 2 a.m.

ssh-keygen -t ed25519 -a 100 -C "denis-laptop-2026-10" -f ~/.ssh/id_ed25519_work

The flags do the following. -t ed25519 picks the algorithm. -a 100 raises the key-derivation rounds above the default, which makes brute-forcing a stolen private key file much slower. -C sets a comment that travels with the public key, so use something that identifies the device and the date. -f names the output file.

When prompted, enter a passphrase. A long phrase of random words beats a short string of symbols. Expect output like this:

Generating public/private ed25519 key pair.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/denis/.ssh/id_ed25519_work
Your public key has been saved in /home/denis/.ssh/id_ed25519_work.pub
The key fingerprint is:
SHA256:Qm1xExampleFingerprintOnlyDoNotCopy0000000 denis-laptop-2026-10

Lock down the permissions, since OpenSSH refuses private keys that other users can read:

chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519_work
chmod 644 ~/.ssh/id_ed25519_work.pub

Which Key Type Should You Pick?

When you have a choice, the table compares the realistic options.

Key typeBest forMain drawback
ed25519Almost everything in 2026Very old servers may not accept it
rsa (3072 bits or more)Legacy systems that reject Ed25519Larger keys, slower signing
ecdsaRarely, for compatibilityNo real advantage over Ed25519
ed25519-skAdmin and production accessNeeds a FIDO2 token you can lose
ssh-mldsa44-ed25519Experiments onlyNew in 10.6, so tooling support is thin

Do not generate the hybrid post-quantum type for real accounts yet. It exists, it is documented in the 10.6 release notes, and it is worth watching. It is not worth a lockout.

Step 3: Install the Public Key and Test It From a Second Terminal

The public key goes to the server. The private key never leaves your machine. The standard helper, ssh-copy-id, appends the right line to the right file with the right permissions.

ssh-copy-id -i ~/.ssh/id_ed25519_work.pub [email protected]

This still uses your password one last time. If ssh-copy-id is missing, for example on a minimal macOS or Windows install, do the same by hand:

cat ~/.ssh/id_ed25519_work.pub | ssh [email protected] 'umask 077; mkdir -p ~/.ssh; cat >> ~/.ssh/authorized_keys'

Now the part people skip. Test the key in your second terminal, and force SSH to use only that key so you know it is the key doing the work:

ssh -i ~/.ssh/id_ed25519_work -o IdentitiesOnly=yes -o PreferredAuthentications=publickey [email protected]

You should be asked for the key passphrase, not the account password, and land at a shell prompt. If you are asked for the account password, the key was not accepted. Run the same command with -v and read the lines that mention offering and accepting public keys. The usual cause is wrong permissions on the server. Home directory, ~/.ssh and authorized_keys must not be writable by anyone else, and sshd checks this by default.

On the server, fix permissions like this:

chmod go-w ~
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys

Windows users get the same tools. The OpenSSH client is an optional Windows feature, and its agent service is disabled by default. Open an elevated PowerShell window and switch it on so keys survive between sessions:

Get-Service ssh-agent | Set-Service -StartupType Manual
Start-Service ssh-agent
ssh-add $env:USERPROFILE\.ssh\id_ed25519_work

On macOS, the system agent is already running, and adding UseKeychain yes to your config lets the keychain remember the passphrase. Linux desktops usually hand this job to the keyring.

Do not move on until key login works from a fresh terminal. Everything after this point assumes it does.

Step 4: Load the Key Into ssh-agent Safely

Typing a long passphrase for every connection gets old fast, and people solve it by removing the passphrase. Do not. Use ssh-agent instead, which holds the decrypted key in memory and signs on your behalf.

eval "$(ssh-agent -s)"
ssh-add -t 8h ~/.ssh/id_ed25519_work
ssh-add -l

The -t 8h flag is the point. The agent drops the key after eight hours, so a laptop left open overnight is not a standing invitation. ssh-add -l lists loaded keys by fingerprint:

Identity added: /home/denis/.ssh/id_ed25519_work (denis-laptop-2026-10)
Lifetime set to 28800 seconds
256 SHA256:Qm1xExampleFingerprintOnlyDoNotCopy0000000 denis-laptop-2026-10 (ED25519)

Two more commands belong in your muscle memory. ssh-add -D removes every key from the agent, and ssh-add -x locks the agent behind a password until you unlock it with ssh-add -X. Use the first when you step away from a shared machine and the second when you want a quick lock without unloading keys.

Agent security got attention this month. SUSE’s OpenSSH advisory dated October 21, 2026 lists three CVEs. CVE-2026-73281 concerns refused binding requests on locked SSH agents, CVE-2026-73282 covers unsynchronized remote forwarding updates in the client, and CVE-2026-73283 covers unenforced restriction keywords on tunnel forwarding. All three sit in the agent and forwarding code, which is a good reason to keep agent forwarding off by default and to patch your OpenSSH packages promptly.

On a desktop, your login session may already start an agent for you through GNOME Keyring, KDE Wallet or the macOS keychain. Check echo $SSH_AUTH_SOCK before starting a second one.

Step 5: Build a Strict SSH Client Config

A client config file turns long commands into short ones and, more importantly, bakes in safe defaults. Create or edit ~/.ssh/config:

Host *
    IdentitiesOnly yes
    AddKeysToAgent yes
    ForwardAgent no
    HashKnownHosts yes
    ServerAliveInterval 30

Host bastion
    HostName bastion.example.com
    User admin
    IdentityFile ~/.ssh/id_ed25519_work

Host app-*
    User deploy
    ProxyJump bastion
    IdentityFile ~/.ssh/id_ed25519_work

Each default earns its place. IdentitiesOnly yes stops SSH from offering every key in your agent to every server, which both leaks information and triggers the dreaded Too many authentication failures error. ForwardAgent no keeps your agent from being exposed on remote hosts. HashKnownHosts yes scrambles host names in the known_hosts file, so a stolen file does not map your whole infrastructure. ServerAliveInterval keeps idle sessions from silently dying behind a NAT.

The ProxyJump line replaces agent forwarding for the most common use case. Instead of logging into a bastion and hopping onward with a forwarded agent, your client connects to the bastion and tunnels straight through it, and the final server authenticates your local key. Nothing sensitive ever sits on the bastion.

Resist the urge to reuse one identity for every host. Give production, staging and personal machines their own IdentityFile lines, and a compromised laptop key then maps to a short list of servers instead of all of them. A Match block can apply settings by user or address when host patterns are not enough.

Check what SSH will actually do for a host without connecting:

ssh -G app-1 | grep -Ei 'identityfile|proxyjump|forwardagent|identitiesonly'

The expected output lists your work key, the bastion and the two security settings as yes or no:

forwardagent no
identitiesonly yes
proxyjump bastion
identityfile /home/denis/.ssh/id_ed25519_work

Set the file mode to 600 with chmod 600 ~/.ssh/config. OpenSSH is picky about it.

Step 6: Disable Password Authentication

This is the step that changes your security posture. Once passwords are off, an attacker who guesses or buys a credential gets nothing from the SSH port. Confirm you still have your first terminal logged in, then work on the server.

Modern Debian and Ubuntu read drop-in files from /etc/ssh/sshd_config.d/. Use one, because package upgrades will not overwrite it:

sudo tee /etc/ssh/sshd_config.d/00-keys-only.conf >/dev/null <<'EOF'
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
AuthenticationMethods publickey
EOF

The file name starts with 00 on purpose. sshd uses the first value it reads for each setting, and cloud images often ship a file such as 50-cloud-init.conf that sets PasswordAuthentication yes. If your file sorts later, the image’s value wins and you will wonder why passwords still work. Naming yours 00 puts it first.

Validate the syntax, reload, and then ask sshd what it actually believes:

sudo sshd -t && sudo systemctl reload ssh
sudo sshd -T | grep -Ei 'passwordauthentication|kbdinteractiveauthentication|permitrootlogin|authenticationmethods'

On RHEL-family systems, replace ssh with sshd in the service name. The second command should print lines like these, in any order:

permitrootlogin no
passwordauthentication no
kbdinteractiveauthentication no
authenticationmethods publickey

Now prove the lock works. From your second terminal, try to log in with a password only:

ssh -o PubkeyAuthentication=no -o PreferredAuthentications=password [email protected]

The answer you want is Permission denied (publickey), with no password prompt at all. Then confirm that your key still gets in. If it does, you are done with the riskiest change. If something went wrong, your first terminal is still open, so delete the drop-in file and reload.

Step 7: Harden sshd Beyond Passwords

Key-only login closes the largest hole. A few more directives shrink what a compromised account can do and slow down whatever remains. Create a second drop-in file:

sudo groupadd -f sshusers
sudo usermod -aG sshusers admin
sudo tee /etc/ssh/sshd_config.d/01-hardening.conf >/dev/null <<'EOF'
MaxAuthTries 3
LoginGraceTime 30
AllowGroups sshusers
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding no
PermitTunnel no
ClientAliveInterval 300
ClientAliveCountMax 2
LogLevel VERBOSE
EOF
sudo sshd -t && sudo systemctl reload ssh

Read the list as three groups. MaxAuthTries and LoginGraceTime cut the number of guesses and the time a connection can sit unauthenticated. AllowGroups means only members of sshusers can log in at all, so a stray service account cannot. The forwarding switches stop an attacker from using your server as a relay or from reaching into forwarded agents. Idle clients are dropped after about ten minutes of silence.

LogLevel VERBOSE matters for the audit trail. With it, sshd logs the fingerprint of the key used for each successful login, so you can tell which of your keys was used and spot one you did not expect.

Be careful with AllowGroups and with the two forwarding switches. Add yourself to the group before you reload, or you lock yourself out. And if you rely on ProxyJump through this host, leave AllowTcpForwarding enabled on bastions, because a jump host forwards TCP by design.

After you reload, test how sshd evaluates the rules for a specific login, including any Match blocks. The -C flag takes a connection specification:

sudo sshd -T -C user=admin,host=server.example.com,addr=203.0.113.10 | grep -Ei 'allowgroups|maxauthtries|allowtcpforwarding'

This prints the effective values for that user and address, which beats guessing when your config grows conditional sections.

You can also restrict which key algorithms the server accepts. This is optional, and it blocks every RSA and ECDSA client, so only do it when you control all your clients:

PubkeyAcceptedAlgorithms ssh-ed25519,[email protected],[email protected],[email protected]

Finally, keep the package current. With OpenSSH shipping on a faster rhythm, enabling automatic security updates on servers is cheap insurance. On Debian and Ubuntu that means the unattended-upgrades package, and on RHEL-family systems dnf-automatic.

Step 8: Add a FIDO2 Hardware Key With ed25519-sk

A passphrase protects a key file at rest. A hardware key goes further, because the private key never leaves the device and each login needs a physical touch. OpenSSH supports FIDO2 authenticators through the ed25519-sk and ecdsa-sk key types, and the OpenSSH manual pages document the flags.

Plug in the token and generate a resident key, which stores a handle on the device so you can recover it on a new machine:

ssh-keygen -t ed25519-sk -O resident -O verify-required -C "admin-yubikey-2026-10" -f ~/.ssh/id_ed25519_sk

The command asks for your token PIN and then for a touch. The -O verify-required option demands PIN or biometric verification on every use, so a stolen token alone is not enough. If your token does not support Ed25519, use -t ecdsa-sk instead.

Generating public/private ed25519-sk key pair.
You may need to touch your authenticator to authorize key generation.
Enter PIN for authenticator:
You may need to touch your authenticator again to authorize key generation.
Your identification has been saved in /home/denis/.ssh/id_ed25519_sk
Your public key has been saved in /home/denis/.ssh/id_ed25519_sk.pub

The file on disk is only a handle. Without the token it is useless, which is the point.

Recovering a Resident Key on a New Machine

To rebuild the handle on another computer, plug in the token and run ssh-keygen -K in an empty directory. It prompts for the PIN and writes the key handle files, which you then move into ~/.ssh with the right permissions. Do this once on a fresh laptop and you are productive in minutes, with nothing secret ever copied between machines.

Install the public key on the server and add the verify-required option so the server enforces the PIN too. In authorized_keys the line looks like this:

verify-required [email protected] AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29t... admin-yubikey-2026-10

Register a second token and store it somewhere safe. A single hardware key is a single point of failure.

On hardware, Yubico’s product page lists the YubiKey 5C NFC FIPS 140-3 at $88 at the time of writing, and any current YubiKey 5 series model with FIDO2 will do for this step. Yubico’s latest announced release is YubiKey 5.8, which it says improves passkey support, FIDO2 performance and cryptographic operations. The wider trend points the same way. The FIDO Alliance’s 2026 Passkey Index, published October 15, 2026, reports that passkeys made up 36% of successful sign-ins across participating services, up from 26% in 2025.

Step 9: Sign Keys With an SSH Certificate Authority

Once you manage more than a handful of servers, editing authorized_keys files everywhere becomes the bottleneck. SSH certificates solve it. You create a certificate authority key, tell servers to trust it, and then sign short-lived certificates for users. No file edits on servers, and expiry is built in.

Create the CA key and keep it off everyday machines. A hardware token or an offline encrypted volume is ideal:

ssh-keygen -t ed25519 -a 100 -C "user-ca-2026" -f ~/.ssh/ca_user

Sign your work key’s public half. The identity string shows up in server logs, the principal is the account name allowed to log in, and the validity window here is 12 hours:

ssh-keygen -s ~/.ssh/ca_user -I denis-laptop -n admin -V +12h ~/.ssh/id_ed25519_work.pub

That writes ~/.ssh/id_ed25519_work-cert.pub next to your key. Inspect it (output trimmed to the useful lines):

ssh-keygen -L -f ~/.ssh/id_ed25519_work-cert.pub
Type: [email protected] user certificate
Key ID: "denis-laptop"
Serial: 0
Valid: from 2026-10-24T09:00:00 to 2026-10-24T21:00:00
Principals:
        admin

On the server, copy the CA’s public key to /etc/ssh/ca_user.pub and trust it with one line in a drop-in file:

echo 'TrustedUserCAKeys /etc/ssh/ca_user.pub' | sudo tee /etc/ssh/sshd_config.d/02-ca.conf
sudo sshd -t && sudo systemctl reload ssh

Your client automatically offers the certificate that sits beside the key. From now on, access ends when the certificate does, with no cleanup on any server. Keep the CA private key safer than any server key, because whoever holds it can mint access to every machine that trusts it.

Certificates are optional for a small setup. For a team, they are the cleanest answer to offboarding, because a departed engineer’s access simply expires.

Step 10: Rotate, Restrict and Revoke Keys

Keys age badly. Laptops get replaced, contractors leave, and authorized_keys files accumulate entries nobody remembers adding. Build a rotation habit around four moves: add the new key, test it, remove the old one, and record the date.

Start with restrictions. A line in authorized_keys can carry options in front of the key. This entry only works from one network and cannot forward ports or agents:

from="203.0.113.0/24",no-port-forwarding,no-agent-forwarding,no-X11-forwarding ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... deploy-ci-2026-10

The address block above is a documentation range. Replace it with your own office or CI network. Restrictions like these turn a stolen key into a much smaller problem, because it only works from where you expect it.

To retire a key everywhere at once, even on servers you cannot reach quickly, build a key revocation list:

sudo ssh-keygen -k -f /etc/ssh/revoked_keys ~/old_laptop_key.pub
echo 'RevokedKeys /etc/ssh/revoked_keys' | sudo tee /etc/ssh/sshd_config.d/03-revoked.conf
sudo sshd -t && sudo systemctl reload ssh

The old fingerprint is now rejected regardless of what appears in authorized_keys. After you confirm the new key works on every host from step 1, remove the old public keys from each authorized_keys file and delete the old private keys from your machines. If you keep a long-lived private key you cannot replace yet, re-encrypt it with a stronger key-derivation setting:

ssh-keygen -p -a 100 -f ~/.ssh/id_old_key

Record rotation dates in your password manager or a simple spreadsheet. A key without an owner and a date is a liability.

Step 11: Watch the Logs and Block Brute Force

Disabling passwords does not make the port quiet. Bots will keep trying, and your logs will fill with failures. That noise is harmless to your accounts but useful as a signal, and it is worth rate limiting so it does not drown out real events.

On systemd machines, read sshd’s journal directly. The unit is ssh on Debian and Ubuntu and sshd on RHEL-family systems:

sudo journalctl -u ssh --since "1 hour ago" | grep -E 'Accepted|Failed|Invalid user'

A successful key login with LogLevel VERBOSE includes the key fingerprint:

Oct 24 09:14:02 server sshd[2231]: Accepted publickey for admin from 198.51.100.7 port 51742 ssh2: ED25519 SHA256:Qm1xExampleFingerprintOnlyDoNotCopy0000000

Match that fingerprint against the list from step 1. A fingerprint you do not recognize is an incident. Failed attempts look like Invalid user or Connection closed by authenticating user, and they come in bursts from a few addresses.

For automated blocking, a tool such as Fail2ban or CrowdSec reads those logs and adds firewall rules. Either one works alongside key-only SSH, and they cut log noise and CPU churn rather than protecting passwords you no longer accept. Keep the AllowGroups and MaxAuthTries limits in place as the first line of defense, and treat the log monitor as the second.

Quick history checks help during an investigation. The last command lists recent logins with source addresses, and ss -tn state established '( sport = :22 )' shows who is connected right now. Compare both against what you expect.

Two more habits help. First, move on from alerting on every failure. Alert on a successful login from a new address or a new fingerprint instead, since that is the event that matters. Second, forward sshd logs off the box. An intruder with root can edit local logs, but not the copy that already left. Our cybersecurity tutorials cover log forwarding and detection tooling in depth.

Step 12: Run the Working Project, a Key Audit Script

The last step ties everything together. This script checks your local SSH directory and, with a flag, the effective server policy. It flags legacy key types, weak RSA keys, private keys with no passphrase, loose permissions and a missing agent. It exits with a non-zero status when anything fails, so you can run it from cron or CI.

#!/usr/bin/env bash
# ssh-key-audit.sh: audit local SSH keys and, optionally, the effective sshd policy.
set -uo pipefail

SSH_DIR="${SSH_DIR:-$HOME/.ssh}"
fail=0
warn=0

ok()   { printf '  [ ok ] %s\n' "$*"; }
bad()  { printf '  [FAIL] %s\n' "$*"; fail=$((fail + 1)); }
note() { printf '  [warn] %s\n' "$*"; warn=$((warn + 1)); }

mode_of() { stat -c '%a' "$1" 2>/dev/null || stat -f '%Lp' "$1"; }

echo "== Directory and permissions"
if [ ! -d "$SSH_DIR" ]; then
  bad "$SSH_DIR does not exist"
else
  [ "$(mode_of "$SSH_DIR")" = "700" ] && ok "$SSH_DIR is 700" || bad "$SSH_DIR should be 700"
fi

echo "== Public keys"
shopt -s nullglob
for pub in "$SSH_DIR"/*.pub; do
  case "$pub" in *-cert.pub) continue ;; esac
  line="$(ssh-keygen -lf "$pub")"
  bits="${line%% *}"
  type="${line##* }"
  fp="$(printf '%s' "$line" | awk '{print $2}')"
  label="$(basename "$pub") $type $bits-bit $fp"
  case "$type" in
    "(ED25519)"|"(ED25519-SK)") ok "$label" ;;
    "(RSA)") [ "$bits" -ge 3072 ] && note "$label (RSA, prefer Ed25519)" || bad "$label (RSA below 3072 bits)" ;;
    "(ECDSA)"|"(ECDSA-SK)") note "$label (ECDSA, prefer Ed25519)" ;;
    *) bad "$label (legacy or unknown type)" ;;
  esac
done

echo "== Private keys"
for pub in "$SSH_DIR"/*.pub; do
  case "$pub" in *-cert.pub) continue ;; esac
  priv="${pub%.pub}"
  [ -f "$priv" ] || continue
  [ "$(mode_of "$priv")" = "600" ] && ok "$(basename "$priv") is 600" || bad "$(basename "$priv") should be 600"
  if grep -q 'sk-ssh\|sk-ecdsa' "$pub"; then
    ok "$(basename "$priv") is hardware-backed"
  elif ssh-keygen -y -P "" -f "$priv" >/dev/null 2>&1; then
    bad "$(basename "$priv") has NO passphrase"
  else
    ok "$(basename "$priv") is passphrase-protected"
  fi
done

echo "== ssh-agent"
if [ -n "${SSH_AUTH_SOCK:-}" ]; then
  n=$(ssh-add -l 2>/dev/null | grep -c SHA256 || true)
  ok "agent reachable, $n key(s) loaded"
else
  note "no agent running (SSH_AUTH_SOCK is empty)"
fi

if [ "${1:-}" = "--server" ]; then
  echo "== Effective sshd policy (needs sudo)"
  cfg="$(sudo sshd -T 2>/dev/null)" || { bad "could not run sshd -T"; cfg=""; }
  want() {
    printf '%s\n' "$cfg" | grep -qix "$1 $2" && ok "$1 $2" || bad "$1 should be $2"
  }
  want passwordauthentication no
  want kbdinteractiveauthentication no
  want permitrootlogin no
  want pubkeyauthentication yes
  want x11forwarding no
fi

echo "== Summary: $fail failure(s), $warn warning(s)"
[ "$fail" -eq 0 ]

Save it as ssh-key-audit.sh, make it executable with chmod +x ssh-key-audit.sh, and run it on your laptop. On a test directory with one good key, one key without a passphrase and one weak RSA key, the output looks like this:

== Directory and permissions
  [ ok ] /home/denis/.ssh is 700
== Public keys
  [ ok ] id_ed25519_scratch.pub (ED25519) 256-bit SHA256:dQSwQIRVL1zItdLtpZVF+AdAFXkwbK0Pf9lUZz63q88
  [ ok ] id_ed25519_work.pub (ED25519) 256-bit SHA256:CzwWoX3TFJjV1aKSKYBFlW0X7Lv7YY1khwVJbRJd1PA
  [FAIL] id_rsa.pub (RSA) 2048-bit SHA256:bE1o/qkcQz7rwQvoabTksn+FRtNOcp1CY28KKsi4/zo (RSA below 3072 bits)
== Private keys
  [ ok ] id_ed25519_scratch is 600
  [FAIL] id_ed25519_scratch has NO passphrase
  [ ok ] id_ed25519_work is 600
  [ ok ] id_ed25519_work is passphrase-protected
  [FAIL] id_rsa should be 600
  [ ok ] id_rsa is passphrase-protected
== ssh-agent
  [warn] no agent running (SSH_AUTH_SOCK is empty)
== Summary: 3 failure(s), 1 warning(s)

Fix each failure. Delete or re-encrypt the passphrase-free key, replace the weak RSA key with an Ed25519 one, and run chmod 600 on anything loose. Then rerun until the summary reads zero failures.

Running the Audit on a Server

On the server, run it with the flag to check the effective sshd policy through sshd -T. On a machine where the earlier checks are clean, the policy section and summary look like this:

./ssh-key-audit.sh --server
== Effective sshd policy (needs sudo)
  [ ok ] passwordauthentication no
  [ ok ] kbdinteractiveauthentication no
  [ ok ] permitrootlogin no
  [ ok ] pubkeyauthentication yes
  [ ok ] x11forwarding no
== Summary: 0 failure(s), 0 warning(s)

A Final Checklist

Before you call the job done, confirm each of these:

  • Your new Ed25519 key logs in to every server, and the audit script reports zero failures locally.
  • sshd -T shows passwordauthentication no on each server, and a password-only attempt gets Permission denied (publickey).
  • Old DSA, weak RSA and passphrase-free keys are deleted from your machines and removed from every authorized_keys file.
  • Agent forwarding is off by default, and you use ProxyJump for bastions.
  • A second FIDO2 token, if you use one, is enrolled and stored away from the first.
  • Rotation dates are written down, and sshd logs leave the host.

Run the audit again after every OS upgrade. Package updates sometimes drop in new config files, and an audit that takes two seconds is how you find out before an attacker does. That wraps the ssh keygen workflow from first key to monitored fleet.

Common Pitfalls That Break SSH Key Setups

These mistakes account for most of the lockouts and weak setups seen in the wild. Each has a simple prevention.

  • Closing your only session before testing. Keep one terminal open while you test from another. A web console is your backup.
  • Leaving the passphrase empty. A temporary passwordless key tends to become permanent. Use ssh-agent with a timeout instead.
  • Ignoring config file order. sshd uses the first value it finds. Cloud-image drop-ins such as 50-cloud-init.conf can silently re-enable passwords, so name your file 00-keys-only.conf and verify with sshd -T.
  • Forwarding your agent everywhere. ForwardAgent yes under Host * exposes your agent on every server you touch. Prefer ProxyJump.
  • Reusing one key for everything. One key per device and purpose limits the damage of a single theft and makes revocation precise.
  • Registering only one FIDO2 token. Lose it and you lose access. Enroll a second token and keep it in a safe place.
  • Following a tutorial written for an older OpenSSH. Defaults and available options change between releases. Check ssh -V and the manual pages for your version.
  • Never cleaning up authorized_keys. Old entries for departed staff and retired laptops are quiet backdoors. Audit them on a schedule.

Troubleshooting: 12 Errors and Their Fixes

When something fails, run the failing command again with -vvv and read the last twenty lines. The table below covers the errors that come up most often.

SymptomLikely causeFix
Permission denied (publickey) after installing the keyWrong permissions on home, ~/.ssh or authorized_keysRun chmod go-w ~, chmod 700 ~/.ssh, chmod 600 ~/.ssh/authorized_keys
Still asked for the account passwordA drop-in file overrides your settingRun sshd -T, find the winning file, rename yours so it sorts first
Could not open a connection to your authentication agentNo agent running in this shellRun eval "$(ssh-agent -s)" then ssh-add
Too many authentication failuresThe agent offers many keys and the server gives upSet IdentitiesOnly yes and a specific IdentityFile
WARNING: UNPROTECTED PRIVATE KEY FILEPrivate key readable by othersRun chmod 600 on the key
sign_and_send_pubkey: signing failed, agent refused operationAgent is locked, or a FIDO key needs touch or PINRun ssh-add -X to unlock, then touch the token when it blinks
Key enrollment failed or device not found for ed25519-skMissing FIDO support or udev rulesInstall libfido2 and your distro’s FIDO udev rules, then replug the token
no matching host key type found, or no mutual signature algorithmVery old server or client lacks the algorithmUpgrade the old side, or temporarily allow the older algorithm for that host only
Bad configuration option when running sshd -tTypo, or an option newer than your sshd versionCheck spelling and compare against the manual for your installed version
Locked out after a reloadYou closed the lifeline session or restricted AllowGroups wronglyUse the provider console, delete the drop-in file, reload, retest
Certificate invalid or principal mismatchExpired certificate or wrong -n principalRun ssh-keygen -L -f key-cert.pub and re-sign with the correct principal and window
Port change ignored on UbuntuSocket activation owns the listening portOn releases that use ssh.socket, run systemctl daemon-reload and restart ssh.socket

If none of these match, check the server log with sudo journalctl -u ssh -e. The server almost always writes down why it said no, even when the client message is vague.

Advanced Tips for Power Users

Once the basics are solid, these techniques add real capability without much complexity.

  • Sign Git commits with SSH keys. Git can use an SSH key for signing through git config gpg.format ssh and ssh-keygen -Y sign, which lets one key serve both login and commit signatures.
  • Use host certificates. ssh-keygen -s ca -h signs a server’s host key, so clients trust it through the CA and stop showing first-connection prompts.
  • Pull keys from a central source. AuthorizedKeysCommand lets sshd fetch keys from a directory service instead of a file on disk.
  • Confine file-transfer users. A Match Group sftponly block with ForceCommand internal-sftp and ChrootDirectory gives partners file access with no shell.
  • Control agent sockets on 10.6 and later. The new AgentSocketPath and ssh-agent -A options give administrators a say in where agent sockets live. Read the release notes before relying on them.
  • Tighten idle channels. Version 10.6 accepts fractional-second ChannelTimeout values, which helps when you want short, strict timeouts on specific channel types.
  • Stay ahead of the release cadence. With three releases between July and October, subscribe to the OpenSSH announcement list and test new versions on a staging host.
  • Plan for post-quantum without rushing. Watch the hybrid signature algorithm as it matures. When your distribution packages it and your Git host and clients support it, test it on a non-critical account first.

Frequently Asked Questions

Is Ed25519 better than RSA for SSH keys? For almost every new setup, yes. Ed25519 keys are short, fast to verify and hard to misconfigure, and RFC 8032 has standardized the algorithm since 2017. RSA still makes sense only when you must reach a system that rejects Ed25519, and then you should use at least 3072 bits.

Do I still need a passphrase if I use ssh-agent? Yes. The passphrase encrypts the key file at rest, and the agent only holds the decrypted key in memory for a limited time. Without a passphrase, anyone who copies the file gets your access.

Can I use one SSH key for GitHub and my servers? Technically yes, but it is a bad idea. One key per purpose limits the damage if a key leaks and makes revocation precise. Generate separate files with clear comments.

What is ed25519-sk and do I need a YubiKey? It is the OpenSSH key type backed by a FIDO2 security key, where the private key stays on the hardware and each login needs a touch. You do not need one to follow steps 1 through 7, but it is the strongest protection for admin accounts.

Should I use the new ssh-mldsa44-ed25519 algorithm? Not for production accounts. It arrived in OpenSSH 10.6 on October 6, 2026, and Help Net Security’s headline warns that experimental keys need replacing. Stay with plain Ed25519 until tooling and your Git hosts catch up.

How do I change a key’s passphrase or strengthen an old key? Run ssh-keygen -p -a 100 -f ~/.ssh/keyfile. It asks for the old passphrase and sets a new one with stronger key derivation.

Does disabling password login stop brute-force attacks? It stops password guessing from succeeding, which removes the real risk. Bots still connect and fill your logs, so keep rate limits and a log monitor in place.

How often should I rotate SSH keys? There is no universal interval. Rotate on every device replacement, every staff change and every suspected leak. If you use certificates with short validity such as 12 hours, rotation happens automatically.

Elias Virtanen

Elias Virtanen

Cybersecurity Analyst

Elias Virtanen is the Cybersecurity Analyst at Tech Insider, bringing hands-on expertise from his background in penetration testing and security consulting. He previously worked as a security researcher at F-Secure in Helsinki, where he focused on threat intelligence and vulnerability disclosure. Elias covers ransomware trends, zero-trust architecture, and the evolving regulatory landscape including NIS2 and the EU Cyber Resilience Act. He holds a CISSP certification and an MSc in Information Security from Aalto University.

View all articles