Here is the command you came for. Run this in your terminal and you will have a modern Ed25519 key pair with a passphrase in under ten seconds:

terminal
$ ssh-keygen -t ed25519 -C "your_comment_here"

When prompted, accept the default path (~/.ssh/id_ed25519) or specify your own with -f, then enter a strong passphrase twice. Two files appear: id_ed25519 (your private key -- never share this) and id_ed25519.pub (your public key -- copy this to servers). That is it for day one access. But if you want to understand why this is now the standard command, why the passphrase actually matters at the OS level, and how to manage keys the way people who do this professionally actually do it, keep reading.

Windows Users: OpenSSH Is Built In

Windows 10 (build 1809+) and Windows 11 ship with OpenSSH as an optional feature. Enable it in Settings > Optional Features, then run ssh-keygen from PowerShell or Command Prompt exactly as shown here. Keys land in C:\Users\YourName\.ssh\. For Windows Server environments or older systems, Win32-OpenSSH provides the same toolchain. PuTTY's puttygen also generates Ed25519 keys, but the .ppk format is PuTTY-specific -- if you need interoperability with standard OpenSSH servers, use the native OpenSSH client rather than PuTTY for new key generation.

Why Ed25519 Instead of RSA

For years, the default recommendation was ssh-keygen -t rsa -b 4096. That command still works, and RSA keys still function on every modern system. But Ed25519 is now the clear preference for new key generation, and the reasons go deeper than marketing.

RSA's security model rests on the difficulty of factoring large numbers. The larger the key, the harder the math -- which is why 4096-bit RSA was considered a solid hedge. Ed25519 uses an entirely different mathematical structure: Edwards-curve cryptography, specifically the Curve25519 elliptic curve designed by Daniel J. Bernstein. Because elliptic curve operations work in a fundamentally different problem space, a 256-bit Ed25519 key provides security comparable to a 3072-bit RSA key. Smaller keys, equivalent or better protection.

Algorithm Provenance

Ed25519 is the SSH implementation of EdDSA (Edwards-curve Digital Signature Algorithm) over Curve25519. The OpenSSH 6.5 release notes note that it offers stronger security than both ECDSA and DSA, with good performance. OpenSSH added Ed25519 support in version 6.5, released January 2014. Any modern Linux distribution, macOS, and Windows 10/11 with the built-in OpenSSH client support it without additional configuration.

Performance is another genuine advantage, not a footnote. Ed25519 signature operations are fast -- measurably faster than RSA at comparable security levels. In environments where SSH connections are frequent, automated, or part of CI/CD pipelines, that speed difference adds up. But the more compelling argument is implementation safety.

RSA signing relies on a source of randomness that, if it degrades or has subtle flaws, can weaken the key. DSA and ECDSA (using NIST curves) have an even more critical dependency on randomness: if the random nonce used during signing is reused even once, the private key can be recovered entirely. This is not a theoretical concern -- it has happened to real systems in production. Ed25519 uses a deterministic signing process. There is no per-signature random nonce to get wrong. Given the same private key and message, it produces the same signature every time, eliminating an entire category of implementation-level attacks.

There is also a trust question that security professionals take seriously. NIST curves -- the ones behind ECDSA -- were generated with seeds that have never been fully explained. The process is documented, but the seed origin is not. Curve25519 was designed with a fully transparent, auditable process, and that provenance is independently verifiable. For organizations operating in sensitive environments, that distinction matters.

Cryptsus: How to Secure Your SSH Server with Ed25519 (2024)  — covers NIST curve seed provenance and transparent parameter selection for Curve25519.

RSA vs Ed25519: Side by Side

Property RSA 4096-bit Ed25519
Key size 4096 bits 256 bits
Comparable security ~140-bit equivalent ~128-bit (NIST Category 1)
Signing speed Slower Significantly faster
Deterministic signing No (randomness dependent) Yes (no nonce risk)
Parameter transparency Standard Fully auditable
OpenSSH support All versions 6.5+ (January 2014)
Private key format PEM (legacy) or OpenSSH OpenSSH (bcrypt KDF)

The Command, Explained Flag by Flag

The basic command is straightforward, but understanding what each flag does lets you adapt it to your specific needs:

terminal
$ ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_work -C "[email protected]"

Generating public/private ed25519 key pair.
Enter passphrase (empty for no passphrase): [type your passphrase]
Enter same passphrase again: [retype passphrase]
Your identification has been saved in /home/user/.ssh/id_ed25519_work
Your public key has been saved in /home/user/.ssh/id_ed25519_work.pub
The key fingerprint is:
SHA256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx [email protected]
The key's randomart image is:
+--[ED25519 256]--+
|        .o+o.    |
|       ..o+.     |
|      . ++..     |
|       B.+..     |
|      o S+o      |
|       =.*o      |
|      E=.=+      |
|     .=*==.      |
|      =BB+.      |
+----[SHA256]-----+

What Is the Randomart Image?

The visual grid printed after key generation -- called a randomart image -- is a visual fingerprint of your key. It uses a simple "random walk" algorithm (Bishop's algorithm) to translate the key's binary fingerprint into a unique pattern of characters. Every key produces a different image, and the same key always produces the same image.

Its practical purpose is human recognition at a glance. If you have memorized the rough shape of a key's randomart, you can spot substitutions or errors faster than you can compare two 43-character SHA256 hashes character by character. Some operations let you display it again on demand:

terminal -- view randomart after the fact
# Display the randomart for any key file
$ ssh-keygen -lv -f ~/.ssh/id_ed25519_work.pub

Randomart is not a security control -- do not rely on it as a substitute for verifying fingerprints when onboarding a server or sharing a public key. It is an optional visual aid for people who work with many keys and find the hash comparison tedious. If your workflow never involves visual fingerprint comparison, you can ignore it entirely.

The -t ed25519 flag sets the key type. The -f flag specifies a custom output file path -- useful when you maintain multiple keys for different contexts (personal, work, a specific server, a GitHub account). The -C flag appends a comment to the public key. This comment appears in authorized_keys files on every server that trusts your key and is purely for human identification. A common and practical convention is to embed a year: [email protected]. That way, when the key shows up on a server years from now, you know when it was created.

Key Naming Convention

Name keys by their purpose, not by the algorithm. ~/.ssh/id_ed25519_github, ~/.ssh/id_ed25519_work, ~/.ssh/id_ed25519_homelab -- these names make it immediately clear what each key is for when you list the directory or audit access six months from now. One experienced sysadmin described it this way: when you receive an Ed25519 key with a meaningful comment, you already know you are working with someone who knows what they are doing.

One Key Per Server, or One Key for Everything?

The right answer depends on your threat model, not personal preference. Using a single key everywhere is convenient: one thing to manage, one passphrase to remember. But it means that if one server is compromised -- and an attacker extracts your public key from authorized_keys -- they now have a roadmap of every other server that trusts the same key. They do not have your private key, but they know where to look once they do.

Segmenting keys by context limits blast radius. A key used only for your home lab cannot be used to authenticate to your work infrastructure. A key for a personal VPS cannot pivot to a client's server. The ~/.ssh/config file makes this practical: define one key per Host block and the routing is automatic. You still only type one passphrase per session if you use ssh-agent.

A reasonable middle ground for individuals: one key per trust boundary (personal, work, side projects), and per-environment keys for anything sensitive (staging versus production). For organizations: never share a personal key with a service account, and never share a deploy key across environments with different security classifications.

The -a Flag: Hardening the KDF

There is an additional flag worth knowing when key security is a priority. The -a flag controls the number of bcrypt KDF rounds applied when encrypting the private key on disk. OpenSSH 9.4 (August 2023) increased the default by 50% — from 16 rounds to 24 — as a quiet but meaningful improvement against offline brute-force attacks. Increasing this further makes attacks against a stolen key file significantly more expensive at the cost of a slightly longer passphrase-entry delay. For keys protecting access to production systems or sensitive infrastructure, 100 rounds is a reasonable hardened setting.

terminal -- hardened KDF rounds
# Generate with 100 KDF rounds -- slower to unlock, much harder to brute-force
$ ssh-keygen -t ed25519 -a 100 -f ~/.ssh/id_ed25519_work -C "[email protected]"
OpenBSD ssh-keygen(1) manual page  — The manual notes that higher round counts slow passphrase verification while making stolen keys harder to crack offline. The original default was 16 rounds; OpenSSH 9.4 (August 2023) raised it to 24.

Why the Passphrase Actually Matters

A passphrase is not the same thing as an SSH password. It does not travel over the network. It does not authenticate you to the remote server. What it does is encrypt the private key file that lives on your local disk.

Without a passphrase, your private key is stored in plaintext on your filesystem. Any process running as your user -- or as root -- can read it directly. Any backup that includes your home directory ships the private key in the clear. Any attacker who gains access to your laptop, a filesystem snapshot, or a misconfigured cloud storage bucket gets immediate, unlimited use of that key to authenticate to every server that trusts it.

With a passphrase, the private key file is encrypted using a key derived from your passphrase through the bcrypt PBKDF algorithm. The file is useless without the passphrase to decrypt it. This is the critical distinction: the passphrase protects the private key at rest. If an attacker steals the file but not the passphrase, they have nothing actionable.

What Is Actually Happening on Disk

Ed25519 keys always use the modern OpenSSH private key format (rather than the older PEM format that legacy RSA keys used). That format uses bcrypt PBKDF to derive the symmetric encryption key from your passphrase before encrypting the private key material. The OpenSSH key file header begins with -----BEGIN OPENSSH PRIVATE KEY----- rather than -----BEGIN RSA PRIVATE KEY-----, and the internal structure includes the KDF name, a random salt, and the configured round count. Every time you type your passphrase to load the key, bcrypt runs those rounds to re-derive the decryption key.

The OpenSSH 6.5 release notes describe the new key format as using "a bcrypt KDF to better protect keys at rest," and specify that this format applies unconditionally to Ed25519 keys.

-- OpenSSH 6.5 Release Notes, openssh.org/txt/release-6.5

In OpenSSH 9.4 (released August 2023), the default bcrypt round count was increased by 50% -- from 16 to 24 rounds -- a quiet but meaningful improvement that makes newly generated keys harder to attack offline without any action required from users. This is exactly the kind of progressive hardening that makes Ed25519 with a passphrase the durable long-term choice.

Never Leave It Empty

The ssh-keygen prompt asks "Enter passphrase (empty for no passphrase)." The empty option exists for automated service accounts and pipelines that genuinely cannot interact with a passphrase. For any key associated with a human login, leaving this empty is the wrong call. A stolen plaintext private key grants access to every system that trusts it, immediately, with no further attack required.

Using ssh-agent to Avoid Typing It Repeatedly

The common objection to passphrases is friction: typing a long passphrase every time you SSH somewhere gets old fast. The answer is ssh-agent, which holds decrypted keys in memory for your session so you only enter the passphrase once per login.

terminal -- start agent and add key
# Start the agent (usually already running on desktop environments)
$ eval "$(ssh-agent -s)"
Agent pid 12345

# Add your key -- you will be prompted for the passphrase once
$ ssh-add ~/.ssh/id_ed25519_work
Enter passphrase for /home/user/.ssh/id_ed25519_work:
Identity added: /home/user/.ssh/id_ed25519_work ([email protected])

# List keys currently loaded in the agent
$ ssh-add -l

# Add a key with a session timeout (auto-removed after 8 hours)
$ ssh-add -t 8h ~/.ssh/id_ed25519_work

On most desktop Linux distributions, gnome-keyring or KDE Wallet integrates with the SSH agent automatically, prompting for the passphrase once at login and keeping the key available for the session. On macOS, the Keychain handles this. The result is strong at-rest encryption without the daily friction of typing a passphrase every time you open a connection. The -t flag adds a session timeout -- a useful extra layer for shared workstations or high-security environments where you want keys automatically evicted from memory after a period of inactivity.

The Attacker's View: What These Controls Actually Defend Against

Understanding why each control matters requires understanding how SSH keys are actually targeted in the wild. The MITRE ATT&CK framework documents these as real adversary behaviors, not theoretical concerns. Here is how the decisions made at key generation time connect to specific attack patterns.

Private Key Theft from Disk (T1552.004)

T1552.004 -- Unsecured Credentials: Private Keys documents adversaries scanning compromised systems for private keys stored in default locations: ~/.ssh/ on Linux and macOS, C:\Users\NAME\.ssh\ on Windows. Automated malware (TeamTNT, Skidmap, and others observed in cloud environments) sweeps these paths as a standard post-compromise step. A private key with no passphrase means an attacker can immediately authenticate to every server listed in authorized_keys with zero additional effort. A passphrase-protected key buys time -- an offline brute-force attack against bcrypt KDF with 100 rounds is computationally expensive. The attacker has the encrypted blob but not immediate access.

authorized_keys Injection for Persistence (T1098.004)

T1098.004 -- Account Manipulation: SSH Authorized Keys describes a technique used widely by APT groups and commodity malware alike: after gaining initial access, an attacker appends their own public key to a target account's authorized_keys file. This creates a persistent backdoor that survives password changes, user account audits, and even system reboots -- because it operates at the SSH layer, not the OS password layer. The countermeasures documented in this article directly address this: regular audits of authorized_keys content, ssh-keygen -lf ~/.ssh/authorized_keys to fingerprint what is present, LogLevel VERBOSE in sshd_config to log every key fingerprint used for authentication, and restricting which users can authenticate at all via AllowUsers.

Two controls that go significantly beyond the standard guidance: First, the AuthorizedKeysCommand directive in sshd_config redirects key lookups to a script or external system rather than a per-user flat file. Instead of checking ~/.ssh/authorized_keys on disk, the SSH daemon calls your script with the username and expects it to return the authorized public keys. This means an attacker who injects a key into the on-disk file gets nothing -- the file is not consulted. At scale, this enables centralized key inventory management through LDAP, a secrets manager, or a configuration management database. Second, auditd rules can fire an alert the moment any process writes to a .ssh/authorized_keys path, catching the injection in real time rather than during the next scheduled audit. The rule -w /home -p w -k authorized_keys_write combined with a log forwarding pipeline gives you near-instant detection of T1098.004.

Lateral Movement via Stolen Keys (T1021.004)

T1021.004 -- Remote Services: SSH covers adversaries using valid SSH credentials to move laterally through an environment. Once a private key is obtained -- from a compromised workstation, a misconfigured backup, a leaked dotfiles repository, or a container image that was built with keys embedded -- SSH provides authenticated, encrypted lateral movement that blends into normal administrative traffic. Salt Typhoon and APT40 both used SSH-based lateral movement extensively in documented intrusions. Segmenting key trust -- separate keys per environment, IdentitiesOnly yes in config, narrow authorized_keys scope -- limits the blast radius when one key is compromised.

Passphrase Offline Cracking (T1110 / T1110.001)

T1110 -- Brute Force and its sub-technique T1110.001 (Password Guessing) apply directly to SSH passphrase cracking. Tools like Hashcat and John the Ripper support the OpenSSH private key format. After stealing the encrypted key file, an attacker can run an offline cracking campaign against the passphrase at whatever speed their hardware allows -- no lockouts, no logging, no network noise. Bcrypt KDF is specifically designed to make this expensive: at 100 rounds, each passphrase candidate requires 100 bcrypt iterations to test. A short or common passphrase remains vulnerable; a long, random passphrase with high KDF rounds provides meaningful resistance even against GPU-accelerated attacks.

Agent Forwarding: A Capability Worth Understanding Carefully

SSH agent forwarding (ForwardAgent yes in your config, or ssh -A) allows a remote server to use your local agent's keys to authenticate onward to other hosts -- useful in jump-host setups, but it comes with a material risk. When agent forwarding is active, the remote server's root user (or any process that can access /tmp as your UID) can see your agent socket and use it to authenticate as you to any host your keys are trusted on. This is the mechanism behind MITRE ATT&CK T1563.001 (SSH Hijacking): an attacker with local root on an intermediate server can pivot silently through your key trust chain without ever touching your private key. Only enable agent forwarding when you understand and accept that risk, and consider hardware-backed keys for high-value hops instead.

Deploying the Public Key to a Server

Generating the key is half the work. You also need to place the public key on any server you want to access. The cleanest way is ssh-copy-id:

terminal
# Copy your public key to the remote server
$ ssh-copy-id -i ~/.ssh/id_ed25519_work.pub user@remote-server

# Verify it works
$ ssh -i ~/.ssh/id_ed25519_work user@remote-server

If ssh-copy-id is not available, the manual equivalent is straightforward:

terminal -- manual public key deployment
# On the remote server, create .ssh directory if needed and append the key
$ mkdir -p ~/.ssh && chmod 700 ~/.ssh
$ echo "$(cat ~/.ssh/id_ed25519_work.pub)" >> ~/.ssh/authorized_keys
$ chmod 600 ~/.ssh/authorized_keys
Permissions Matter

SSH is strict about file permissions. If ~/.ssh or authorized_keys is readable by other users, the SSH daemon will silently refuse to use the key without any clear error. The directory must be 700 (owner read/write/execute only) and authorized_keys must be 600 (owner read/write only). This is one of the most common reasons key authentication mysteriously fails after a manual deployment.

Host Key Verification and the known_hosts File

Generating a strong client key pair only addresses half of the authentication equation. The other half is whether you are actually talking to the server you think you are. When you connect to a host for the first time, OpenSSH shows you the server's host key fingerprint and asks whether to trust it. Most people type "yes" without checking. That is the Trust On First Use (TOFU) model, and it has a real weakness: if an attacker intercepts that first connection -- through DNS hijacking, ARP spoofing, or a compromised network path -- they can silently present their own key and you will have unknowingly accepted it. This is exactly what MITRE ATT&CK T1557 (Adversary-in-the-Middle) exploits in SSH contexts.

OpenSSH stores accepted host keys in ~/.ssh/known_hosts. On subsequent connections, it compares the server's presented key against the stored fingerprint. If they differ, SSH warns loudly -- a changed host key is either a server reinstall or a potential interception event, and you should investigate before proceeding.

terminal -- known_hosts management
# View the fingerprint of a server's host key before connecting
$ ssh-keyscan -t ed25519 remote-server | ssh-keygen -lf -

# Remove a stale or changed host key entry (e.g., after server reinstall)
$ ssh-keygen -R remote-server

# Hashed known_hosts: OpenSSH can store hostnames as hashes so the file
# doesn't reveal your access patterns if compromised
$ ssh-keyscan -H remote-server >> ~/.ssh/known_hosts

For a more rigorous approach than TOFU, verify the server's host key fingerprint out-of-band before connecting -- through the cloud provider's console, a configuration management system, or a colleague who has physical access. In enterprise environments, SSH certificates (using ssh-keygen -s to sign host keys with a Certificate Authority) eliminate the TOFU problem entirely by giving every server a verifiable identity from a trusted CA. That is a more advanced setup, but it closes the one gap that strong client keys alone cannot address.

Managing Multiple Keys with ~/.ssh/config

Once you have more than two or three keys, specifying -i on every connection becomes unwieldy. The ~/.ssh/config file maps hostnames to key files, usernames, and other options, letting you just type ssh work-server and have the right key selected automatically.

~/.ssh/config
# Work infrastructure
Host work-server
    HostName      192.168.1.50
    User          deploy
    IdentityFile  ~/.ssh/id_ed25519_work
    IdentitiesOnly yes

# GitHub
Host github.com
    HostName      github.com
    User          git
    IdentityFile  ~/.ssh/id_ed25519_github
    IdentitiesOnly yes

# Home lab jump host -- connect to any homelab-* through a jump server
Host homelab-*
    ProxyJump     jumphost.example.com
    IdentityFile  ~/.ssh/id_ed25519_homelab
    IdentitiesOnly yes

The IdentitiesOnly yes directive is worth noting. Without it, SSH will try every key currently loaded in the agent before falling back to the configured one. On systems with strict failed-authentication limits, that agent exhaustion can lock you out before the correct key is even attempted. Setting IdentitiesOnly yes tells SSH to use only the key explicitly specified for that host entry.

Changing or Adding a Passphrase to an Existing Key

You do not need to regenerate a key pair to update its passphrase. The -p flag to ssh-keygen lets you change the passphrase on an existing private key without touching the public key or needing to update authorized_keys on any server:

terminal
# Change the passphrase on an existing key
$ ssh-keygen -p -f ~/.ssh/id_ed25519_work
Enter old passphrase:
Enter new passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved with the new passphrase.

# Add -a to also increase KDF rounds while changing passphrase
$ ssh-keygen -p -a 100 -f ~/.ssh/id_ed25519_work

Combining -p with -a 100 lets you update an existing key's passphrase and increase its KDF strength at the same time -- useful for onboarding a key that was originally created without the hardened round count, or when rotating credentials as part of a security policy update.

What to Do When a Key Is Lost or the Passphrase Is Forgotten

There is no recovery path for a forgotten passphrase. This is by design. The passphrase-derived encryption key is never stored anywhere -- only your passphrase can regenerate it. If you forget the passphrase protecting a private key, that private key is gone. What this means practically: the private key file is unrecoverable, but the access it granted is not lost -- that access was always controlled by the public key on the server side.

The correct response when a passphrase is forgotten or a private key file is lost:

  1. Generate a new Ed25519 key pair immediately.
  2. Get the new public key onto every server that trusted the old one -- through a still-working console session, a cloud provider's recovery console, or by having a colleague or second key with access add it.
  3. Remove the old public key from authorized_keys on every server. A key whose private half is lost or inaccessible should not remain in authorized_keys -- it provides no benefit and creates unnecessary audit noise.
  4. If the device holding the private key was stolen rather than simply lost, treat it as a potential compromise. Move faster: remove the public key from all servers before the passphrase can be cracked offline. Audit recent SSH logs for any authentication events using that key's fingerprint.
This Is Why Backup Plans Matter

The practical protection against being locked out is not key recovery -- it is redundancy. Keep at least two independent access paths to any server you rely on: a primary key for daily use and a separate emergency key stored securely (an encrypted password manager, a hardware token, a physically secured offline backup). Cloud providers also offer serial console access and recovery modes that bypass SSH entirely; know how to use them for your environment before you need them under pressure.

If the key was used for a GitHub account, GitLab, or similar service, the path is the same: add a new key via the web interface using a recovery method (backup codes, email, TOTP), then remove the old key. GitHub's device authorization flow and GitLab's recovery codes exist precisely for this scenario.

Generating a strong key is the starting point, not the end state. Keys need to be managed across their entire lifecycle, which means rotation, audit, and revocation.

For individual keys, a practical rotation schedule is every one to two years. Embedding the year in the comment or filename creates a constant reminder in your normal workflow. Every time you use the key, you see when it was created. When the year looks dated, that is your cue to rotate.

Rotation does not mean deleting the old key immediately. The process is: generate the new key pair, deploy the new public key to every server's authorized_keys, verify the new key works on each of them, remove the old public key from every server's authorized_keys, and only then delete the old private key locally. Skipping the verification step is how people lock themselves out during rotations.

At fleet scale, the model of managing per-user authorized_keys files on each server becomes untenable for rotation. The professional solution is SSH certificates, which shift the trust model entirely. Instead of distributing a user's public key to every server, you run a Certificate Authority (ssh-keygen -s ca_key -I "[email protected]" -n alice -V +52w id_ed25519_work.pub), sign the user's key with the CA, and configure every server to trust the CA with a single TrustedUserCAKeys /etc/ssh/ca.pub line. When a key is compromised or an employee leaves, you issue a revocation certificate or simply let the signed certificate expire -- no authorized_keys updates needed across hundreds of hosts. This is how SSH key management works in environments with more than a handful of servers, and the infrastructure to implement it is built into OpenSSH with no additional software required.

Key Fingerprints for Auditing

You can check the fingerprint of any key file -- public or private -- without exposing the key material itself. This is useful for confirming that the key deployed on a server matches one in your local collection: ssh-keygen -lf ~/.ssh/id_ed25519_work.pub

Auditing What Is Authorized on a Server

The authorized_keys file on each server is the source of truth for who can authenticate. Reviewing it regularly is a basic hygiene task that is often skipped. Each line is one public key -- the comment at the end tells you whose key it is and when it was created, provided people used meaningful comments when generating their keys.

terminal -- audit authorized keys
# List all authorized keys with their comments
$ cat ~/.ssh/authorized_keys

# Check the fingerprint of each authorized key
$ ssh-keygen -lf ~/.ssh/authorized_keys

# Remove a specific key (by line) using sed -- example: remove line 3
$ sed -i '3d' ~/.ssh/authorized_keys

Detection: What Malicious SSH Key Activity Looks Like

Setting up strong key-based auth is the defensive side. The other side is knowing what to watch for in logs. SSH authentication activity leaves traces, and LogLevel VERBOSE in sshd_config makes those traces useful. With verbose logging, every successful authentication logs the key fingerprint used -- not just the username. That one change turns your SSH logs from a record of "who logged in" to "which specific key was used." When an unauthorized key gets injected (T1098.004), you can catch it: an unfamiliar fingerprint appearing in auth logs for a known account is a detection signal.

terminal -- reading SSH auth logs
# On systemd-based Linux: view SSH authentication events
$ journalctl -u sshd --since "24 hours ago" | grep "Accepted publickey"

# With LogLevel VERBOSE, each accepted connection logs the key fingerprint:
# Accepted publickey for alice from 10.0.1.5 port 54321 ssh2: ED25519 SHA256:xxxx

# Look for authentication attempts with unauthorized fingerprints
$ journalctl -u sshd | grep "Invalid user\|Failed\|Accepted" | tail -50

# On servers without systemd (e.g., Alpine, older Debian)
$ grep "sshd" /var/log/auth.log | grep "Accepted publickey"

Beyond individual server logs, audit the authorized_keys file on a schedule and compare fingerprints against an inventory of known, approved keys. Any key present in authorized_keys that does not appear in your approved inventory is an anomaly worth investigating. In environments with configuration management (Ansible, Chef, Puppet, Terraform), the authorized_keys file should be managed declaratively -- any drift from the declared state is automatically suspicious. That is a significantly stronger posture than manual audits.

Two detection layers that most guides skip: First, auditd filesystem watches. Adding -w /root/.ssh/authorized_keys -p wa -k ssh_key_tamper and equivalent rules for /home user directories to /etc/audit/rules.d/ causes auditd to log any write or attribute change to those files instantly -- before the SSH daemon is even consulted. This catches both manual injection and malware-based persistence attempts at the filesystem level, not after-the-fact in auth logs. Forward these events to a SIEM and alert on any write that does not correspond to a scheduled configuration management run. Second, fingerprint inventory correlation: export authorized fingerprints from each server regularly (ssh-keygen -lf ~/.ssh/authorized_keys output), store that inventory centrally, and diff it on a schedule. A fingerprint that appears on a server but was never in the approved inventory -- and was never added by a configuration management system -- is the signal you are looking for.

Correct File Permissions Reference

Getting permissions wrong is one of the most common reasons key authentication silently fails. Here is the complete reference:

terminal -- set correct permissions
# The .ssh directory itself -- owner rwx only
$ chmod 700 ~/.ssh

# Private key -- owner read/write only
$ chmod 600 ~/.ssh/id_ed25519_work

# Public key -- readable by others is fine, but tighten it anyway
$ chmod 644 ~/.ssh/id_ed25519_work.pub

# authorized_keys on the server -- owner read/write only
$ chmod 600 ~/.ssh/authorized_keys

# Confirm ownership -- everything should be owned by you
$ ls -la ~/.ssh/

When Key Authentication Fails: Debugging It

Key auth failures are common, often silent, and almost always caused by a small set of issues. The single most useful debugging tool is -v (or -vvv for maximum verbosity) on the SSH client. This shows exactly what the client is doing and where the process breaks down.

terminal -- debug SSH connection
# -v shows the negotiation; -vvv shows everything including raw packet details
$ ssh -v -i ~/.ssh/id_ed25519_work user@remote-server

# Key lines to look for in the output:
debug1: Offering public key: /home/user/.ssh/id_ed25519_work ED25519 ...
debug1: Server accepts key: ...
# If you see "Server accepts key" but auth still fails, the issue is on the server side

# If the key is never offered, the agent or IdentityFile config is wrong
debug1: No more authentication methods to try.

The causes of key auth failure follow a short list. Work through them in order:

Permissions on ~/.ssh or authorized_keys are wrong. This is the most common cause. SSH's StrictModes yes (the default) causes the daemon to silently refuse a key if the .ssh directory is group- or world-writable, or if authorized_keys is writable by anyone other than the owner. Run ls -la ~/.ssh/ on the server and confirm .ssh is 700 and authorized_keys is 600. If permissions are wrong, SSH will not tell you -- it will just fall through to password auth or fail entirely.

The wrong key is being offered. If your agent has several keys loaded and IdentitiesOnly is not set, SSH may be offering a different key than you intend. Use -i ~/.ssh/id_ed25519_work explicitly on the command line to test, then add IdentitiesOnly yes to the relevant ~/.ssh/config block.

The public key on the server does not match the private key. This can happen when a key is regenerated but the server is not updated, or when a copy-paste error truncated the public key during manual deployment. Verify with ssh-keygen -lf ~/.ssh/id_ed25519_work.pub (client side) and ssh-keygen -lf ~/.ssh/authorized_keys (server side). The fingerprints must match.

The server's sshd_config restricts the key type. If you applied the PubkeyAcceptedAlgorithms restriction and then tried to connect with an RSA key, authentication fails silently. The verbose output will show userauth_pubkey: key type ssh-rsa not in PubkeyAcceptedAlgorithms in the server log. Check journalctl -u sshd or /var/log/auth.log on the server alongside your -v client output to correlate both sides.

The authorized_keys file has the wrong line format. Each line must be the full public key in one of these forms: ssh-ed25519 AAAA... comment or the restricted form with options prefixed. A line break in the middle of the key data, a leading space, or a Windows-style CRLF line ending (common when copying from a text editor on Windows) all silently corrupt the entry. Use cat -A ~/.ssh/authorized_keys on the server to see raw line endings -- a ^M at the end of a line indicates CRLF that needs to be stripped with dos2unix or sed -i 's/\r//' ~/.ssh/authorized_keys.

Read Both Sides

Client-side -v output and server-side auth log output tell you different things. The client shows what it offered and whether the server accepted the key type. The server log (with LogLevel VERBOSE) shows whether the authentication ultimately succeeded and which fingerprint was used. Looking at both together eliminates most guesswork in under two minutes.

Going Further: Hardware-Backed Keys

For environments that require the highest assurance, hardware-backed SSH keys eliminate the remaining risk of private key theft entirely. The ed25519-sk key type stores the actual private key on a FIDO2 hardware token (such as a YubiKey) rather than on the filesystem. Cryptographic operations happen on the hardware itself. Even if an attacker compromises the machine, they cannot extract the private key -- there is nothing on disk to steal.

terminal -- hardware-backed ed25519 key
# Requires a FIDO2 hardware key attached and OpenSSH 8.2+
$ ssh-keygen -t ed25519-sk -C "[email protected]"
Generating public/private ed25519-sk key pair.
You may need to touch your authenticator to authorize key generation.
Enter file in which to save the key (~/.ssh/id_ed25519_sk):

# For resident keys (stored on the hardware token itself, not a key handle on disk)
$ ssh-keygen -t ed25519-sk -O resident -C "[email protected]"

The -sk suffix stands for "security key." When you use this key to authenticate, you must physically touch the hardware token to confirm the operation. This is two-factor by design: something you have (the hardware token) plus something you know (the PIN protecting the token, or the passphrase protecting the key handle on disk). For privileged access -- production servers, cloud infrastructure, CI/CD secrets -- the friction is worth it.

There are meaningful distinctions within hardware-backed key options that most documentation glosses over. By default, ed25519-sk requires a physical touch (user presence, or "UP" in FIDO terminology) to authorize each signing operation. Adding -O verify-required at generation time elevates the requirement to user verification -- meaning the token PIN must be entered, not just touched. This is the difference between "someone has the token" (touch-only) and "someone has the token and knows the PIN" (verify-required). For highly privileged access, verify-required is the stronger posture and is supported on YubiKey firmware 5.4+ and modern FIDO2 tokens. Conversely, -O no-touch-required disables the touch requirement entirely -- useful for automated scripts that use hardware-backed keys but cannot interact with a physical device, at the cost of the presence assurance. The resident key option (-O resident) stores the key directly on the token rather than creating a key handle file on disk. This means the key can be loaded onto any machine just by inserting the token (ssh-keygen -K exports it), but it also consumes limited on-device storage and slightly increases the risk profile if the token is lost without a PIN set.

OpenSSH and YubiKey Version Requirements

Hardware-backed ed25519-sk keys require OpenSSH 8.2 or later on the client side, and the SSH server must run OpenSSH 8.2+ as well. YubiKey hardware requires firmware 5.2.3 or newer for the ed25519-sk type specifically. Verify your client version with ssh -V before attempting this on older systems. The -O resident option (which stores the key on the token itself rather than a handle file on disk) additionally requires YubiKey firmware 5.2.3+ with FIDO2 support.

CI/CD Pipelines and Service Account Keys

Automated systems -- GitHub Actions, GitLab CI, Jenkins, deploy scripts -- frequently need SSH access to servers or code repositories. The temptation is to generate a key without a passphrase and embed it as a secret. That is functionally necessary (a pipeline cannot interactively type a passphrase), but it creates a key category that requires its own discipline.

A few principles that matter here. First, deploy keys should have the narrowest scope possible. GitHub's deploy keys are per-repository and can be set to read-only -- use that. A key that gives a CI pipeline access to one repository's deployment target should not be the same key that can read every repository in the organization. Second, embed these keys as environment secrets in the CI system itself (GitHub Actions secrets, GitLab CI variables, HashiCorp Vault), not committed to the repository. An accidentally-committed deploy key is one of the more common real-world credential leak vectors. Third, rotate CI keys on a more aggressive schedule than personal keys -- quarterly is reasonable for keys that grant deployment access to production. CI systems typically make rotation low-friction; it is just updating a secret value.

terminal -- generate a CI/CD deploy key
# Generate a deploy key (no passphrase -- must be usable non-interactively)
# The comment identifies the system and purpose for auditing
$ ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_github_deploy -C "github-actions-deploy-prod-2026" -N ""

# The public key goes into the server's authorized_keys or as a deploy key in the SCM
# The private key value goes into your CI secret store -- never into the repo
$ cat ~/.ssh/id_ed25519_github_deploy.pub

# On the authorized_keys side, scope the key with command restriction and no-agent-forwarding
# This limits what the key can do even if it is stolen
no-agent-forwarding,no-pty,no-X11-forwarding,command="/usr/bin/rsync --server ..." ssh-ed25519 AAAA...

The command= prefix in authorized_keys is a powerful control that is rarely used. It restricts a given key to executing only a specific command when it authenticates -- the key cannot be used to open a shell, only to run the one operation it was issued for. Combined with no-agent-forwarding and no-pty, it significantly limits what an attacker can do with a compromised deploy key. The key authenticates, but it can only do the one thing you authorized.

The from= option in authorized_keys is equally underused. Prefixing a key entry with from="10.0.0.50" or a CIDR range means the key is only valid when the connecting IP matches -- a compromised deploy key exfiltrated to an external attacker's system is rejected outright because it does not originate from the expected CI runner IP. This is not a substitute for other controls, but it meaningfully raises the cost of exploitation.

For environments that can support it, the more durable solution is to eliminate long-lived deploy keys entirely. GitOps workflows using tools like Flux can reduce or remove the need for interactive SSH deploy access altogether. HashiCorp Vault's SSH Secrets Engine issues short-lived signed SSH certificates on demand -- a CI pipeline authenticates to Vault using its own identity (OIDC, AWS IAM, GCP service account), receives a certificate valid for minutes rather than months, and uses that certificate to authenticate to the target server. There is no long-lived private key to steal, rotate, or audit. OpenSSH's native certificate support makes this feasible without additional agents on the server side; the target server needs only a TrustedUserCAKeys directive pointing at the Vault CA's public key. This model renders T1552.004 irrelevant for CI/CD contexts because the credential that would be stolen expires before an attacker can use it.

Locking Down the Server Side

Deploying a strong key pair only matters if the server is actually enforcing key-based authentication. Password authentication should be disabled once key access is confirmed working. For Ubuntu servers, you may also want to disable root SSH login entirely as a complementary hardening step. Edit /etc/ssh/sshd_config on the server:

/etc/ssh/sshd_config
# Disable password-based authentication
PasswordAuthentication           no
KbdInteractiveAuthentication     no

# Enable public key authentication (on by default, but make it explicit)
PubkeyAuthentication             yes

# Restrict root login to key-only, or disable it entirely
PermitRootLogin                  prohibit-password

# StrictModes enforces correct file permissions on .ssh/ and authorized_keys
# SSH refuses to use keys if permissions are too permissive
StrictModes                      yes

# Limit who can authenticate via SSH at all -- only named users or group members
# This is one of the highest-value controls against T1098.004 (unauthorized key injection)
AllowUsers                       deploy alice
# Or restrict by group: AllowGroups sshusers

# Limit failed authentication attempts before disconnect
MaxAuthTries                     3

# Verbose logging captures the fingerprint of every key used to authenticate
# Essential for detecting unauthorized key use (T1098.004, T1021.004)
LogLevel                         VERBOSE

Restricting Algorithms Server-Side

Disabling password auth is the most important change, but there are three more directives worth adding to sshd_config on servers you control. They ensure the server will not negotiate down to weaker algorithms even when connecting clients offer them:

/etc/ssh/sshd_config -- algorithm restrictions
# Accept only modern public key types for authentication
PubkeyAcceptedAlgorithms         ssh-ed25519,[email protected]

# Restrict key exchange to post-quantum hybrid and ECDH only
# mlkem768x25519-sha256 is the OpenSSH 10.0 default; include curve25519 as fallback
KexAlgorithms                    mlkem768x25519-sha256,curve25519-sha256,[email protected]

# Restrict ciphers to AEAD-only (ChaCha20-Poly1305 and AES-GCM)
Ciphers                          [email protected],[email protected],[email protected]

# Restrict MAC algorithms (only needed if not using AEAD-only ciphers, but explicit is safer)
MACs                             [email protected],[email protected]
Check Client Compatibility Before Locking Down

The PubkeyAcceptedAlgorithms restriction above rejects RSA keys entirely. If any legitimate user or service account connects with an RSA key, they will be locked out immediately after this change. Run ssh-keygen -lf ~/.ssh/authorized_keys on the server to audit what key types are currently in use before applying this restriction. Migrate RSA users to Ed25519 first.

ChallengeResponseAuthentication Is Deprecated

You may see older guides that include ChallengeResponseAuthentication no in this block. That directive was removed in OpenSSH 9.0 and replaced by KbdInteractiveAuthentication. Using the old directive on a modern system generates a deprecation warning during config validation. If you are running OpenSSH 8.x or earlier, use the old name; 9.0+ uses the new one.

Verify Access Before Restarting sshd

Do not restart sshd until you have confirmed your key-based login works in a separate open session. If you lock yourself out of the only access path to a remote server, recovery requires console access or a cloud provider's recovery mode. Test first, then apply: sudo sshd -t checks the config for syntax errors before you commit to a reload.

After verifying the config passes the syntax check, reload the daemon:

terminal
# Test the config first -- zero output means no errors
$ sudo sshd -t

# Reload (applies changes without dropping existing sessions)
$ sudo systemctl reload sshd

The Post-Quantum Picture in 2026

Ed25519 is not quantum-resistant. Neither is RSA. Both rely on mathematical problems that a sufficiently powerful quantum computer running Shor's algorithm could solve. In 2026, that threat remains theoretical for the key sizes in use today. No quantum computer capable of breaking these algorithms at relevant key sizes exists in any publicly accessible deployment.

The broader SSH ecosystem moved fast in 2024 and 2025. NIST finalized its first three post-quantum cryptographic standards in August 2024: ML-KEM (FIPS 203, derived from CRYSTALS-Kyber), ML-DSA (FIPS 204, derived from CRYSTALS-Dilithium), and SLH-DSA (FIPS 205, derived from SPHINCS+). These are available for deployment now.

On the SSH side specifically, OpenSSH has been ahead of the curve. Post-quantum key exchange became available by default in OpenSSH 9.0 (April 2022) via sntrup761x25519-sha512. OpenSSH 10.0, released April 9, 2025, made mlkem768x25519-sha256 — a hybrid of NIST's finalized ML-KEM with classical ECDH/X25519 — the new default key exchange algorithm. OpenSSH 10.1 (October 2025) went further: it now warns users by default when a connection falls back to a non-post-quantum key exchange. These hybrid algorithms protect the connection session against "harvest now, decrypt later" attacks; they do not replace the Ed25519 key pair used for authentication.

OpenSSH's post-quantum documentation explains that version 10.1 warns users when a non-post-quantum key exchange is negotiated, with the warning on by default and suppressed via the WarnWeakCrypto directive in ssh_config.

-- OpenSSH Post-Quantum Cryptography page, openssh.org/pq.html

That warning is not coming — it is already here. OpenSSH 10.1 (released October 2025) now displays the message ** WARNING: connection is not using a post-quantum key exchange algorithm by default when the negotiated key exchange is not one of the two standardized hybrid PQ algorithms (mlkem768x25519-sha256 or sntrup761x25519-sha512). If you are running an OpenSSH 10.1+ client connecting to an older server, you are already seeing this. The warning can be silenced per-host with WarnWeakCrypto no in ~/.ssh/config, but the correct fix is updating the server.

What does this mean for you right now? The Ed25519 key pair you generate today remains the right choice for authentication. The session-level post-quantum protection from mlkem768x25519-sha256 is active by default on OpenSSH 9.9+ clients connecting to OpenSSH 10.0+ servers. Keeping both client and server software current is the single most important post-quantum action you can take in 2026 — not switching away from Ed25519 for authentication, but ensuring the channel itself uses a hybrid PQ key exchange. Watch for NIST's and OpenSSH's release notes for when post-quantum authentication key types become available as operational guidance rather than experimental preview.

Quick Reference Summary

Everything covered here, distilled to the essential commands:

ed25519-key-cheatsheet.sh
# Generate a new Ed25519 key pair (hardened: 100 KDF rounds)
$ ssh-keygen -t ed25519 -a 100 -f ~/.ssh/id_ed25519_work -C "[email protected]"

# Deploy the public key to a server
$ ssh-copy-id -i ~/.ssh/id_ed25519_work.pub user@remote-server

# Add the key to ssh-agent (enter passphrase once)
$ ssh-add ~/.ssh/id_ed25519_work

# Add key with 8-hour session timeout
$ ssh-add -t 8h ~/.ssh/id_ed25519_work

# Check the fingerprint of your public key
$ ssh-keygen -lf ~/.ssh/id_ed25519_work.pub

# Change the passphrase and increase KDF rounds on an existing key
$ ssh-keygen -p -a 100 -f ~/.ssh/id_ed25519_work

# Audit authorized keys on a server
$ ssh-keygen -lf ~/.ssh/authorized_keys

# Set correct permissions
$ chmod 700 ~/.ssh && chmod 600 ~/.ssh/id_ed25519_work && chmod 600 ~/.ssh/authorized_keys

# Check your OpenSSH version (10.0+ uses mlkem768x25519-sha256 by default)
$ ssh -V

The shift from RSA to Ed25519 reflects a broader maturation in how the industry thinks about cryptographic defaults. Smaller keys with cleaner mathematical foundations, deterministic signing that eliminates whole classes of implementation errors, a transparent parameter selection process, bcrypt KDF that protects the key at rest, and a post-quantum session layer now enabled by default in OpenSSH 10.0. Pair all of that with a strong passphrase, a managed key lifecycle, and a hardened server configuration -- and SSH authentication stops being the thing you set up once and forget. It becomes something that actually holds up under scrutiny.