Someone asks who still has access to production, and the honest answer takes an afternoon: grepping authorized_keys across a dozen boxes, or worse, pointing to a spreadsheet last touched eight months ago that nobody trusts anymore. An SSH access review without spreadsheets in 2026 isn’t about buying an IAM platform, it’s about building an inventory that stays honest, and having a process that actually revokes what it finds instead of just documenting it.
Why the access review spreadsheet always goes stale
A spreadsheet drifts from reality the moment someone adds a key by hand and forgets to log it, which is most of the time. Contractors get offboarded and the row stays. A key gets copied to a new server during a deploy and nobody adds it to the sheet. Six months later the document is a historical record of what access looked like once, not what it looks like now, and everyone quietly knows it.
What does an SSH access review actually check?
A real review works across three axes: people, hosts, and keys. For every key that grants access, you need to answer who holds it, which hosts actually accept it, and whether that person still needs that specific host. Miss any one axis and the review is incomplete: a list of keys with no owner attached is an inventory, not a review.
This is specifically about classic, key-based SSH access, the kind that lives in an authorized_keys file and outlives whoever set it up. If your infrastructure already issues temporary, role-based credentials, through an IAM role or a short-lived certificate, a chunk of this problem is solved structurally: the credential expires on its own instead of waiting for someone to remember to remove it. Most small teams have a mix, some hosts on classic keys, some behind a cloud provider’s IAM layer, and the review below is for the first kind, the one that doesn’t clean itself up.
Building the inventory without an enterprise IAM tool
You don’t need a platform to build this, you need a script that runs the same check everywhere. On each host:
for f in /home/*/.ssh/authorized_keys /root/.ssh/authorized_keys; do
[ -f "$f" ] || continue
echo "== $f =="
while read -r line; do
[ -z "$line" ] && continue
echo "$line" | ssh-keygen -lf /dev/stdin 2>/dev/null
done < "$f"
done
Fingerprints, not raw key text, are what you compare across machines: the same public key can be pasted with different line breaks or comments, but its fingerprint is stable. Pull this from every host into one file, and you have the raw material for the inventory: a list of fingerprints, which hosts accept each one, and (from your own records, not the server) who each fingerprint actually belongs to.
That last part, matching a fingerprint to a name, is the step people skip because it’s manual and a little tedious, and it’s also the whole point of the exercise. Cross-reference the list against your actual current roster, not last quarter’s, including anyone who moved teams and no longer needs a host they used to. A fingerprint nobody on that list claims isn’t a mystery to investigate later, it’s a revocation candidate right now.
When grepping stops scaling
The loop above works fine for five or six hosts run by hand over SSH. Past that, running it host by host gets slow enough that people start skipping hosts, which defeats the point. The fix isn’t a new tool so much as running the same script through whatever you already use to reach every host at once, a for loop over a hosts file piped to ssh, an existing Ansible inventory, or a config management tool if one is already in place, and appending each host’s output into one dated file instead of eyeballing terminal output host by host. The script doesn’t get smarter, it just runs everywhere instead of somewhere.
Finding orphaned and stale access
Once you have the list, a handful of checks catch most of what matters:
- A key with no matching name in your current team or contractor list, meaning nobody claims it
- A key that hasn’t authenticated successfully in the last 90 days, checked against auth.log or journalctl, not against memory
- A key present on more hosts than that person’s current role touches, a sign it was copied once and never pruned
- A shared or generic account (deploy, admin, ops) with more than one key attached and no record of who added which one
None of these require special tooling, just the discipline to actually run the check instead of assuming the last review still holds.
How often should you actually run this?
Event-based reviews are not optional: the moment someone leaves or changes roles, their access gets revoked that day, not at the next quarterly review. Scheduled reviews catch what event-based ones miss: the key nobody remembered to remove, the host that quietly picked up a new authorized_keys entry during an incident. For a small team, quarterly is a reasonable default; tighten it if you’re working toward a SOC 2 audit, which typically wants evidence of a recurring cadence, not just a one-time cleanup.
Turning findings into action, not just a report
A review that ends in a document is homework, not security. Every orphaned or stale entry the review finds should end in a revocation, not a flag for later. If you want a grace period for safety, disable the key first (comment it out, confirm nothing breaks for a day) and delete it on a second pass, rather than leaving disabled-but-present entries to accumulate the same way the original clutter did.
Keep a short record of the review itself, separate from the access data: the date it ran, who ran it, how many entries were revoked. This isn’t the spreadsheet coming back in a different shape, it’s a log entry, not a live source of truth anyone edits between reviews, and it’s exactly the evidence an auditor or a future you will ask for when the question becomes not just “who has access” but “when did anyone last check.”
Keeping the next review cheap
The real fix is structural, not procedural: one key per person, never a key shared across two people or reused across accounts, so the mapping from fingerprint to human is trivial instead of guesswork. Managing SSH credentials across devices covers the per-person, per-device side of this. Grouping hosts by team or environment instead of keeping one flat list also matters: it turns “does this person still need this host” from an open-ended question into a five-second lookup.
Mistakes that quietly undo a review
A few patterns show up often enough to call out specifically:
- Reviewing the key list but not the group memberships that grant it access, so a person is removed from a host directly while a stale group still grants the same host to everyone in it
- Trusting a hostname instead of the actual server, after a rename or a reprovision the old inventory entry silently stops matching anything real
- Running the review from a jump host’s cached view of “every server I can see” instead of a source list that includes hosts nobody has jumped to in months, which is exactly where forgotten access tends to hide
- Treating the review as a one-time cleanup project instead of a recurring line item, which guarantees the exact same afternoon of grepping in another six months
Where termique fits into an access review
termique doesn’t run the review for you, but it removes two of the reasons reviews go stale in the first place. Host sharing grants access per person to a specific host, so revoking someone is one action instead of hunting down every place their key got copied. The per-command audit log turns “does this person still need this host” from a guess into a real answer, since you can see whether the access has actually been used. Both are part of Pro, alongside key rotation practices and the same encrypted key handling described in the SOC 2 audit trail guide, if a recurring review is part of what you’re building toward.
A spreadsheet can’t tell you a key was never removed. A running inventory, checked against real auth logs on a schedule, can.