Over time, server access accumulates sprawl. Team members rotate, contractors leave projects, emergency debugging keys get copied onto production machines, and old setup scripts drop keys that are never revisited. In many environments, authorized_keys files across multiple hosts become black boxes. Systems administrators often do not know who owns each key, whether weak algorithms like DSA or small RSA moduli are still permitted, or whether a single private key has been duplicated across several machines without restrictive options.
Auditing this state manually is tedious and prone to human error. Running ad-hoc shell pipelines across dozens of machines produces inconsistent text snippets that require manual cross-referencing. SSH KeyAudit addresses this maintenance problem. It is a single-file Python tool that inspects public keys, computes standard OpenSSH fingerprints, parses key sizes from public key wire structures, maps keys to known owners, flags security discrepancies, and computes state diffs across runs.
The Problem
Managing fleet access without centralized public key infrastructure or single sign-on often leads to several risks:
- Cryptographic obsolescence: Legacy keys such as 1024-bit DSA or short RSA keys remain accepted by default configurations unless explicitly vetted.
- Offboarded personnel access: When team members depart, their public keys often persist in user accounts or shared service accounts across various machines.
-
Missing command and source restrictions: Public keys configured in automated pipelines frequently lack restrictions such as
from="192.168.1.0/24"orcommand="...", leaving full shell access open if a private key is exposed. - Key reuse across separate endpoints: The same private key is sometimes shared across production, staging, and internal management servers, expanding the blast radius if an endpoint is compromised.
To address these risks systematically, an inventory tool must inspect the actual wire attributes inside public key files, compute predictable fingerprints, identify key locations across file paths, and evaluate compliance against explicit policies.
Install & Usage
The tool uses Python standard library modules exclusively (argparse, base64, datetime, hashlib, json, os, re, shlex, subprocess, sys, and tempfile). Because it relies on no third-party libraries, there are no external dependencies to install with a package manager.
To install the tool, download the single file, save it as ssh_keyaudit_fleet_key_inventory.py, and run it directly with Python:
python ssh_keyaudit_fleet_key_inventory.py --help
You can integrate its core routines directly into administrative scripts or run the file via Python's command line interface to view the built-in usage instructions and options.
The remote collection mechanism uses your local OpenSSH client (ssh) configured with batch mode and non-interactive switches. To scan hosts, ensure your local environment has standard network routing and SSH credentials (such as an SSH agent or identity key) available to connect to target hosts.
How It Works
The tool performs its work through four main steps: wire format parsing, remote collection, policy evaluation, and inventory diffing.
1. Decoding Key Wire Formats and Calculating Fingerprints
OpenSSH stores keys as base64-encoded wire structures. Rather than relying on external command executions for every entry, the tool parses the wire encoding using standard library utilities.
The read_wire_bytes function reads length-prefixed strings and numbers from raw byte arrays, while parse_ssh_key_data unpacks the structure, generates standard SHA256 fingerprints, and calculates bit lengths:
def parse_ssh_key_data(b64_key: str) -> Dict[str, Any]:
"""
Decodes base64 OpenSSH public key, parses wire structures,
calculates OpenSSH SHA256 fingerprint and algorithm bit length.
"""
try:
raw = base64.b64decode(b64_key.strip(), validate=True)
except Exception as exc:
return {
"error": f"Invalid base64 encoding: {exc}",
"fingerprint": "INVALID",
"type": "unknown",
"bits": 0,
}
# OpenSSH standard fingerprint: SHA256 of raw decoded wire bytes, unpadded base64
sha = hashlib.sha256(raw).digest()
fp_b64 = base64.b64encode(sha).decode("ascii").rstrip("=")
fingerprint = f"SHA256:{fp_b64}"
offset = 0
try:
key_type_bytes, offset = read_wire_bytes(raw, offset)
key_type = key_type_bytes.decode("ascii", errors="replace")
except Exception as exc:
return {
"error": f"Corrupt SSH wire header: {exc}",
"fingerprint": fingerprint,
"type": "corrupt",
"bits": 0,
}
bits = 0
if key_type == "ssh-rsa":
try:
# RSA format: string "ssh-rsa", mpint e, mpint n
_e, offset = read_wire_bytes(raw, offset)
n, _ = read_wire_bytes(raw, offset)
# Leading 0x00 is padding for positive signed mpint
bits = int.from_bytes(n, byteorder="big").bit_length()
except Exception:
bits = 0
elif key_type == "ssh-dss":
# DSA is deprecated and 1024-bit by specification
bits = 1024
elif key_type == "ssh-ed25519":
bits = 256
elif key_type.startswith("ecdsa-sha2-"):
if "nistp256" in key_type:
bits = 256
elif "nistp384" in key_type:
bits = 384
elif "nistp521" in key_type:
bits = 521
return {
"fingerprint": fingerprint,
"type": key_type,
"bits": bits,
"raw_b64": b64_key.strip(),
}
This decoder handles RSA public exponent and modulus arrays directly, determining if an RSA key meets modern minimums. It also extracts known bit sizes for elliptic curve and Ed25519 keys.
2. Multi-File Stream Remote Collection
When querying remote hosts, the tool initiates a subprocess calling the system ssh executable with options like BatchMode=yes and StrictHostKeyChecking=accept-new. It delivers a small Python command to the remote host that looks for authorized keys in /home/*/.ssh/authorized_keys, /root/.ssh/authorized_keys, and ~/.ssh/authorized_keys. Each discovered file is streamed back prefixed with a delimiter tag (###TARGET_FILE:), allowing the host parser to keep track of the source file for every record.
3. Policy and Inventory Auditing
Once the records are parsed, the audit engine checks each key against compliance rules and owner metadata loaded from an owners mapping file.
# 1. Weak algorithm & key length checks
if ktype == "ssh-dss":
findings.append("WEAK_ALGORITHM: Legacy DSA keys are cryptographically weak and deprecated.")
elif ktype == "ssh-rsa" and bits > 0 and bits < min_rsa_bits:
findings.append(f"WEAK_KEY: RSA length {bits}b is below policy threshold ({min_rsa_bits}b).")
# 2. Owner validation
matched_owner_meta = owners_map.get(fp) or owners_map.get(comment)
if not matched_owner_meta:
findings.append("UNKNOWN_OWNER: Key fingerprint and comment not mapped in owners directory.")
owner_name = "Unassigned"
owner_status = "unknown"
else:
owner_name = matched_owner_meta["owner"]
owner_status = matched_owner_meta["status"]
if owner_status == "offboarded":
findings.append(f"OFFBOARDED_OWNER: Key belongs to offboarded user '{owner_name}'.")
# 3. Missing security options
if flag_no_options and not options:
findings.append("NO_OPTIONS: authorized_keys line lacks restrictive options (from=, command=, etc.).")
# 4. Duplicate dispersion
instances = fingerprint_occurrences.get(fp, [])
if len(instances) > 1:
hosts_present = sorted(list({inst["host"] for inst in instances}))
if len(hosts_present) > 1:
findings.append(f"CROSS_HOST_DUPLICATE: Identical key found across {len(hosts_present)} hosts: {', '.join(hosts_present)}.")
The output compiles these findings into structured summaries indicating total keys, non-compliant keys, weak keys, offboarded keys, and unknown owners.
4. Diffing State Changes
The tool contains a diff function (diff_inventories) that compares two inventory snapshots. It indexes keys using a compound identifier of host, file path, and fingerprint (host:auth_file:fingerprint), tracking exactly which keys have appeared or disappeared across audit runs.
Limitations
Before deploying this script across your workflows, consider its architectural constraints:
-
Remote Python requirement: The remote discovery command relies on
python3being present on the remote targets to scan paths under/home/*/.ssh/and/root/.ssh/. Ifpython3is missing on the remote machine, the command falls back to runningcat ~/.ssh/authorized_keys, which only captures the current connecting user's keys. -
Permission boundaries: If the remote user running the collection script does not have root privileges, attempting to read
/home/*/.ssh/authorized_keysor/root/.ssh/authorized_keyswill be prevented by standard operating system file permissions. -
Non-standard file locations: Keys configured via custom OpenSSH daemon paths (such as keys stored in LDAP, database lookups, or custom
AuthorizedKeysFilepaths outside/homeand/root) are not discovered by the hardcoded glob patterns. -
Network execution model: Target hosts are contacted sequentially via
subprocess.run. Networks with significant latency or large numbers of targets will run synchronously unless wrapped in external concurrency.
FAQ
What format does the owners mapping file expect?
The load_owners_file function expects plain text lines formatted with whitespace separation: <fingerprint_or_comment_prefix> <owner_name> [status]. The status field defaults to active, but can be marked as offboarded or service. Lines starting with # are treated as comments and ignored.
How are OpenSSH SHA256 fingerprints calculated?
The tool decodes the base64 wire payload of the public key, computes the raw SHA256 cryptographic digest of those decoded bytes, base64-encodes that digest, strips any trailing padding characters (=), and prefixes the string with SHA256:. This matches the standard fingerprint representation produced by ssh-keygen -l.
What happens when a line in an authorized_keys file contains forced commands or options?
The parse_authorized_keys_line function scans the line for known key algorithm identifiers (such as ssh-rsa, ssh-ed25519, or ecdsa-sha2-*). Any tokens appearing before the algorithm identifier are parsed as the options field, while tokens following the base64 key payload are treated as the comment. If no options are present, the audit engine flags the entry with NO_OPTIONS if configured to do so.
Full version and the rest of the toolkit: SSH KeyAudit fleet key inventory on PyForge. Product page: https://py-forge.com/products/ssh-keyaudit-fleet-key-inventory?utm_source=devto&utm_medium=article&utm_campaign=ssh-keyaudit-fleet-key-inventory
Top comments (0)