SSH key sprawl is the silent breach risk on every Linux fleet: a former employee's key still in ~/.ssh/authorized_keys, a 1024-bit DSA key from 2010 still trusted, a service-account key shared across three teams. The keys never expire, the audit trail is local to each host, and almost no one runs the inventory until the postmortem of a breach. This guide explains the audit techniques that scale to thousands of hosts, the weak-key detection patterns, and the policy enforcement that prevents new sprawl.
Where keys actually live
~/.ssh/authorized_keysper user β what the host trusts for inbound auth.~/.ssh/authorized_keys2β historic, sometimes still consulted; remove if present./etc/ssh/sshd_configβAuthorizedKeysFilemay redirect to a non-default path./etc/ssh/ssh_host_*_keyβ host keys, what clients verify on connect.~/.ssh/id_*β private keys held by users (audit for unencrypted).
Inventory all authorized_keys files
sudo find / -name authorized_keys -type f 2>/dev/null \
-exec sh -c 'echo "== $1 =="; cat "$1"' _ {} \;
sudo find / -name 'authorized_keys*' -type f 2>/dev/null \
-exec ls -l {} \;
Note both file paths and ownership. A file owned by root in a non-root user's home is a misconfiguration; a file world-readable is a smaller issue but worth fixing.
Show key details for every authorized entry
sudo find / -name authorized_keys -type f 2>/dev/null | while read f; do
echo "== $f =="
ssh-keygen -l -f "$f" 2>/dev/null
done
ssh-keygen -l prints fingerprint, bit-length, comment, and key type per entry. Output looks like:
256 SHA256:8x... user@laptop (ED25519)
3072 SHA256:Y2... bob@vault (RSA)
1024 SHA256:7Z... legacy (DSA)
Anything DSA, anything RSA shorter than 2048 bits, anything ECDSA P-256 with comment indicating a known weak source β flag for removal.
Detecting weak and deprecated key types
sudo find / -name authorized_keys -type f 2>/dev/null | while read f; do
ssh-keygen -l -f "$f" 2>/dev/null | awk -v file="$f" '
/\(DSA\)/ {print "DSA " file " | " $0}
/\(ECDSA\)/ {print "ECDSA " file " | " $0}
/\(RSA\)/ && $1+0 < 2048 {print "RSA-WEAK " file " | " $0}
/\(RSA\)/ && $1+0 == 2048 {print "RSA-2048 " file " | " $0}'
done
Modern recommendation: Ed25519 only, with RSA-3072 or larger as a compatibility fallback for ancient clients. DSA is broken, ECDSA P-256 has known issues, and RSA-1024 is computationally feasible to attack.
Identifying unused keys
An authorised key that has never been used is the most likely to be a leftover from a past employee. Use SSH's VERBOSE logging to record the fingerprint of every successful login:
# /etc/ssh/sshd_config.d/99-audit.conf
LogLevel VERBOSE
sudo grep 'Accepted publickey' /var/log/auth.log* 2>/dev/null \
| grep -oE 'SHA256:[A-Za-z0-9+/]+' | sort -u > /tmp/used-fingerprints.txt
sudo grep 'Accepted publickey' /var/log/auth.log* 2>/dev/null \
| awk '{print $9, $11}' | sort -u | head
Cross-reference each fingerprint in authorized_keys with the used set. Anything not seen in the past 90 days is a candidate for removal.
Centralised key management
For fleets, stop maintaining authorized_keys per host. Two scalable options:
- SSH certificates. Sign user keys with a CA; the SSH server trusts the CA, not individual keys. Revocation is centralised.
# /etc/ssh/sshd_config TrustedUserCAKeys /etc/ssh/ca.pub RevokedKeys /etc/ssh/revoked-keys - AuthorizedKeysCommand. sshd queries an external script β typically against LDAP, Vault, or an internal API β for the user's authorised keys at login time.
AuthorizedKeysCommand /usr/local/sbin/get-keys-for-user AuthorizedKeysCommandUser nobody
Either approach makes deprovisioning a single API call instead of an Ansible run across 500 hosts.
Enforcing key types in sshd
# /etc/ssh/sshd_config.d/99-keys.conf
PubkeyAcceptedAlgorithms ssh-ed25519,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-256
HostKeyAlgorithms ssh-ed25519,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-256
This refuses DSA, ECDSA, and RSA-SHA1 outright at the server. Validate with ssh-audit yourhost.com.
Restricting key capabilities
Per-key restrictions in authorized_keys drastically limit damage if a key leaks:
from="10.0.0.0/8",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,command="/usr/local/bin/backup.sh" ssh-ed25519 AAAAβ¦
Source IP restriction, no agent forwarding (prevents pivot), and command= to lock the key to a single script.
Detect tampering
sudo find / -name authorized_keys -newer /etc/ssh/sshd_config 2>/dev/null
sudo find / -name authorized_keys -newer /var/lib/dpkg/info/openssh-server.list 2>/dev/null
echo '-w /root/.ssh/authorized_keys -p wa -k ssh-keys' | sudo tee -a /etc/audit/rules.d/ssh.rules
echo '-w /home -p wa -k home-changes' | sudo tee -a /etc/audit/rules.d/ssh.rules
sudo augenrules --load
Combined with off-host log shipping, every key addition becomes a SIEM event you can review.
The audit script
#!/bin/bash
echo "== Authorized keys files =="
sudo find / -name 'authorized_keys*' -type f 2>/dev/null
echo
echo "== Per key fingerprints =="
sudo find / -name authorized_keys -type f 2>/dev/null | while read f; do
user=$(stat -c '%U' "$f")
echo "[$user] $f:"
ssh-keygen -l -f "$f" 2>/dev/null | sed 's/^/ /'
done
echo
echo "== Weak key types =="
sudo find / -name authorized_keys -type f 2>/dev/null -exec ssh-keygen -l -f {} \; \
| grep -E '\(DSA\)|\(ECDSA\)|^10[0-2][0-9]'
echo
echo "== Unencrypted private keys in user homes =="
sudo find /home /root -name 'id_*' -type f 2>/dev/null \
-exec sh -c 'head -1 "$1" | grep -q ENCRYPTED || echo " unprotected: $1"' _ {} \;
Common pitfalls
- Removing keys from
authorized_keysbut leaving the user account active and password-enabled. - Trusting
SHA256:fingerprints alone; the comment field in the key tells you what the key is used for. - Forgetting that
~/.ssh/authorized_keys2is still consulted by some sshd versions; remove if present. - Granting
PermitOpen anyor omitting it entirely; explicit allow lists prevent stolen keys from pivoting.
SSH key auditing is the kind of task that pays off once and prevents incidents forever. Run the audit script monthly across every host; remove keys not used in 90 days; reject DSA/ECDSA at the sshd level; and migrate to centralised key management before your fleet grows past a few dozen hosts.