VPS & Server security checklist

SSH & Access Security Audit: 11 Checks for Linux Servers

An SSH security audit checks your server's SSH daemon and access controls for the misconfigurations attackers look for first. This page documents all 11 checks in the CtrlOps SSH & Access audit: what each one reads, the exact value that passes, the values that warn or fail, and the command to fix it.

Hiren KalariyaLast reviewed: Aug 22, 202618 min read
11
checks in this audit
4
rated high severity
14s
automated run time
2
need root or sudo

Every pass, warning and failure threshold on this page is transcribed from the vps-security-ssh-access audit script that CtrlOps runs, and a build check fails if the two ever disagree. See how the audit runs.

An SSH security audit reviews your server's SSH daemon configuration, user permissions and authentication rules to find the gaps an attacker looks for first. This page documents the 11 checks in the CtrlOps SSH & Access audit, in the order the audit runs them.

Every threshold below is transcribed from the audit script itself, not from general advice. Where the script passes a value, this page says it passes; where the script warns rather than fails, this page says so and explains why the distinction matters.

Key takeaways

Eleven settings separate a hardened SSH configuration from an exposed one. Each takes a minute or two to check by hand, which is fine for one server and stops being fine somewhere around the fifth.

  • Four checks are rated HIGH: root login, authorized keys, extra UID 0 accounts and login restrictions. If you only have ten minutes, do those four.
  • Two need root: reading other accounts' authorized_keys files and reading /etc/sudoers. The other nine run as any user.
  • The whole audit takes about 14 seconds when CtrlOps runs it over your existing SSH connection, against roughly 11 minutes by hand.
  • The real problem is drift, not the first audit. A setting relaxed "just for testing" in March is still relaxed in September unless something re-checks it.
#CheckSeverityWhat it catchesRoot needed
1Root loginHIGHDirect root access over SSHNo
2Password authMEDIUMBrute-forceable password loginsNo
3SSH portLOWScanner noise on port 22No
4Idle timeoutLOWSessions that never expireNo
5Authorized keysHIGHUnknown or writable key filesYes
6Extra UID 0 accountsHIGHHidden superuser accountsNo
7Sudo group membersMEDIUMOver-privileged accountsNo
8Passwordless sudoMEDIUMUnrestricted NOPASSWD grantsYes
9Login restrictionsHIGHEmpty passwords, no allow-listNo
10Crypto algorithmsMEDIUMSHA-1 key exchange, CBC ciphersNo
11Password ageingLOWPasswords that never expireNo
How the config is read. Checks 1, 2, 3, 4, 9 and 10 parse /etc/ssh/sshd_config and follow any Include drop-ins. Match blocks are not evaluated, so a setting that is overridden only inside a Match block will not be reflected here. When in doubt, confirm with sudo sshd -T on the host.

Check 1: Is root login over SSH disabled?

High severityNo sudo needed

Allowing direct root login means an attacker needs to guess one credential for the most powerful account on the system. Disabling it forces two separate steps: authenticate as an ordinary user, then escalate with sudo. That second step is also where your audit trail comes from, because sudo logs who ran what while a direct root session does not.

How to check manually

grep -i "^PermitRootLogin" /etc/ssh/sshd_config /etc/ssh/sshd_config.d/*.conf 2>/dev/null
Result thresholds this check applies
ResultWhenWhat it means
PASSThe effective value is noRoot login is disabled entirely.
WARNThe value is prohibit-password or without-passwordRoot can still log in with a key. Better than a password, but you lose the sudo audit trail.
FAILAny other value, including yesRoot login is allowed. Set PermitRootLogin no.
SKIP/etc/ssh/sshd_config is not readableThe audit could not read the file. Check the path and its permissions.

No output at all does not mean you are safe. It means no explicit directive exists and you are inheriting the package default, which on most modern distributions is prohibit-password - the warning case, not the passing one.

How to fix it

# Back up first
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak

# Set this line in /etc/ssh/sshd_config
PermitRootLogin no

# Validate the syntax before restarting
sudo sshd -t

sudo systemctl restart sshd
Before you restart. Confirm you have a non-root account with working sudo and a tested SSH key. Locking out root without a second path in means losing the server. sudo sshd -t catches syntax errors before they take down your only way back.

Check 2: Is SSH password authentication off?

Medium severityNo sudo needed

Passwords are the single most automated attack surface on an internet-facing server. Microsoft's 2025 Digital Defense Report attributes 97% of identity attacks to password spray, which is exactly this: automated tools throwing common passwords at every account name they can find. SSH keys remove the entire category, because there is nothing guessable to spray.

This check is rated MEDIUM rather than HIGH for one reason: turning it off wrongly locks people out faster than any other setting on this list.

How to check manually

grep -i "^PasswordAuthentication" /etc/ssh/sshd_config /etc/ssh/sshd_config.d/*.conf 2>/dev/null
Result thresholds this check applies
ResultWhenWhat it means
PASSThe effective value is noKey-based authentication only.
FAILAny other value, or no directive at allPassword auth is enabled. Test key login first, then set PasswordAuthentication no.
SKIP/etc/ssh/sshd_config is not readableThe audit could not read the file.

How to fix it

sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak

# In /etc/ssh/sshd_config
PasswordAuthentication no
KbdInteractiveAuthentication no
UsePAM yes

sudo sshd -t
sudo systemctl restart sshd

Keep UsePAM yes. PAM is still what carries MFA, account policy, OS Login and cloud IAM integration even when passwords are off.

Version note. KbdInteractiveAuthentication only exists on OpenSSH 8.7 and later (2021 onward). On CentOS 7 and older LTS releases, sshd -t will reject it as an unsupported option. Check with sshd -V first and use ChallengeResponseAuthentication no on older builds.

Check 3: Is SSH running on a non-default port?

Low severityNo sudo needed

This is the one check on the list that is explicitly not about security. A port scan finds SSH on any port in seconds, so moving off 22 stops exactly zero targeted attacks. What it does is drop the automated background noise from thousands of attempts a day to nearly none, which makes a real intrusion attempt visible in your auth log instead of buried in it.

That is why the audit warns rather than fails here. Port 22 with Fail2Ban and key-only auth is a perfectly defensible configuration.

How to check manually

grep -i "^Port" /etc/ssh/sshd_config /etc/ssh/sshd_config.d/*.conf 2>/dev/null
Result thresholds this check applies
ResultWhenWhat it means
PASSA non-default port is configuredAutomated scanners will mostly miss this host.
WARNPort 22, or no directive at allExpect brute-force noise in the logs. Keep Fail2Ban active.
SKIP/etc/ssh/sshd_config is not readableThe audit could not read the file.

How to fix it

sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak

# In /etc/ssh/sshd_config, pick a high port
Port 2222

# Open the new port BEFORE restarting sshd
sudo ufw allow 2222/tcp

sudo sshd -t
sudo systemctl restart sshd
Order matters. Open the new port in the firewall first, restart, confirm a connection on the new port from a second terminal, and only then close port 22. Reversing those steps locks you out.

Check 4: Do idle SSH sessions time out?

Low severityNo sudo needed

An SSH session left open on an unlocked laptop is an authenticated session anyone with physical access inherits. ClientAliveInterval tells sshd how often to probe a quiet client, and ClientAliveCountMax how many unanswered probes to tolerate before hanging up.

The recommended pair is 300 and 2, which drops a dead session after 600 seconds.

How to check manually

grep -i "^ClientAlive" /etc/ssh/sshd_config /etc/ssh/sshd_config.d/*.conf 2>/dev/null
Result thresholds this check applies
ResultWhenWhat it means
PASSClientAliveInterval is set to a non-zero valueIdle sessions drop after interval x count-max seconds.
WARNClientAliveInterval 0, or no directive at allNo idle timeout. Set ClientAliveInterval 300 and ClientAliveCountMax 2.
SKIP/etc/ssh/sshd_config is not readableThe audit could not read the file.

How to fix it

sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak

# In /etc/ssh/sshd_config
ClientAliveInterval 300
ClientAliveCountMax 2

sudo sshd -t
sudo systemctl restart sshd

With 300 and 2, sshd sends a probe after five minutes of silence, another five minutes later, and closes the session at the ten-minute mark if neither is answered.


Check 5: Which SSH keys can log in?

High severityNeeds sudo

Every line in an authorized_keys file is a working credential that never expires and never prompts. They accumulate quietly: a contractor who finished last year, a CI pipeline that was rebuilt, a laptop that was replaced. Nothing on the server ever removes them.

The permissions half matters more than the count. A authorized_keys file that is writable by group or other means any user on that host can append their own public key and log in as that account whenever they like.

Why getent and not /home/*. The audit enumerates accounts with UID 0 or UID 1000 and above via getent passwd rather than globbing /home. Service accounts and admin accounts frequently have home directories outside /home, and a glob silently misses every one of them.

How to check manually

# Enumerate real account homes, count keys, read permissions
getent passwd | awk -F: '($3 == 0 || $3 >= 1000) {print $6}' | sort -u | while read -r home; do
  keyfile="$home/.ssh/authorized_keys"
  [ -f "$keyfile" ] || continue
  count=$(grep -cE '(ssh-(rsa|dss|ed25519)|ecdsa-sha2|sk-(ssh|ecdsa))' "$keyfile" 2>/dev/null)
  perms=$(stat -c '%a' "$keyfile" 2>/dev/null || stat -f '%Lp' "$keyfile" 2>/dev/null)
  echo "$keyfile: $count key(s), permissions: $perms"
done
Result thresholds this check applies
ResultWhenWhat it means
PASSKey files were found and none are group- or world-writableReview the reported key count per account and remove any key you do not recognize.
WARNNo readable authorized_keys file was found anywhereExpected only if this host uses another authentication method. Otherwise the audit could not see the files.
FAILAny authorized_keys file is writable by group or otherAnother user on this host can append their own key. Run chmod 600 on each file.

How to fix it

# Lock down permissions
chmod 600 /home/username/.ssh/authorized_keys
chmod 700 /home/username/.ssh

# Then remove keys you do not recognize
nano /home/username/.ssh/authorized_keys
Run this on every offboarding. Revoking someone's account is not the same as removing their key from every server they were ever given access to. One missed authorized_keys entry is a working credential for as long as the host exists. SSH key management best practices covers the rotation and naming side of this.

Check 6: Does any account other than root have UID 0?

High severityNo sudo needed

Linux grants superuser rights by UID, not by name. An account called backup-svc with UID 0 has exactly the same power as root while looking unremarkable in a user list. Creating one is a long-standing persistence technique after a compromise, precisely because it survives a password reset on the real root account.

The audit uses getent rather than reading /etc/passwd directly so that LDAP and SSSD-backed accounts are covered too. On a domain-joined host, reading the local file alone would miss them entirely.

How to check manually

# Covers local, LDAP and SSSD-backed accounts
getent passwd | awk -F: '$3 == 0 && $1 != "root" {print $1}'
Result thresholds this check applies
ResultWhenWhat it means
PASSNo outputOnly root holds UID 0.
FAILOne or more account names are printedEach one is a full superuser account. Investigate immediately.

How to fix it

Do not delete the account first. Establish how it got there:

passwd -S suspicious_account   # account status and password age
last suspicious_account        # login history
ps -u suspicious_account       # anything running as it right now

Then remove it once you understand the answer:

sudo userdel suspicious_account
This is an incident, not a cleanup task. An unexplained UID 0 account on a production host means assuming the server is compromised until you can show otherwise. Check auth logs, review recent changes, and treat any credential that touched the box as exposed.

Check 7: Who is in the sudo group?

Medium severityNo sudo needed

Sudo membership grows and never shrinks. Someone is added during an incident at 2am, the incident ends, and nobody removes them. Twelve months later that list is the real answer to "who can take this server down", and it rarely matches anyone's mental model.

The part people miss is primary-group membership. getent group sudo lists supplementary members only. An account created with sudo as its primary group never appears in that output and is invisible to the obvious one-liner.

How to check manually

# Supplementary members
getent group sudo wheel admin 2>/dev/null

# Primary-group members, which the line above does not show
SGIDS=$(getent group sudo wheel admin 2>/dev/null | cut -d: -f3 | tr '\n' '|' | sed 's/|$//')
getent passwd | awk -F: -v g="^($SGIDS)$" '$4 ~ g {print $1}'
Result thresholds this check applies
ResultWhenWhat it means
PASSOne or more accounts are in sudo, wheel or adminThe privileged accounts are listed. Confirm each still needs access.
WARNNo members in any of the three groupsNobody can escalate. Make sure you are not relying on direct root login instead.

A pass here is not automatically good news. The audit can list the accounts, but only you know whether deploy and backup should be able to run anything as root, or whether they should have scoped rules instead.

How to fix it

sudo deluser username sudo      # Debian, Ubuntu
sudo gpasswd -d username wheel  # RHEL, Rocky, Alma

Check 8: Which sudo rules skip the password?

Medium severityNeeds sudo

Scoped NOPASSWD is a legitimate pattern. A deployment user that needs systemctl restart myapp without an interactive prompt is a reasonable thing to configure, and the audit does not fail it.

Blanket NOPASSWD: ALL is different in kind, not degree. It means that whoever compromises that account, through a stolen key or a vulnerable web app running as that user, gets instant unrestricted root with nothing left to defeat.

How to check manually

sudo grep -rhE '^[^#]*NOPASSWD' /etc/sudoers /etc/sudoers.d/ 2>/dev/null
Result thresholds this check applies
ResultWhenWhat it means
PASSNo NOPASSWD rules existEvery sudo invocation requires a password.
WARNNOPASSWD rules exist, all scoped to specific commandsConfirm none of the named commands can be used to spawn a shell.
FAILOne or more rules grant NOPASSWD: ALLThat account is unrestricted root with no second factor. Scope it to the commands it actually needs.
SKIPThe audit could not read /etc/sudoersThis check needs root. Run the audit with sudo.

How to fix it

# visudo validates syntax before saving - never edit sudoers with a plain editor
sudo visudo

# Replace:  deploy ALL=(ALL) NOPASSWD: ALL
# With:     deploy ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart myapp
Scoped is not automatically safe. A NOPASSWD rule for /usr/bin/vim lets that user open a root shell from inside vim in two keystrokes. The same is true of less, find, awk and anything else with a shell escape. Restrict the list to non-interactive commands.

Check 9: Are SSH logins restricted?

High severityNo sudo needed

These three settings are evaluated as one check because they describe the same thing from different angles: who is permitted to attempt an SSH login, and how many attempts each connection gets.

The allow-list is the one most servers are missing. Without AllowUsers or AllowGroups, every account that exists on the host, including service accounts that were never meant to be interactive, is a valid login target.

How to check manually

grep -iE "^(PermitEmptyPasswords|MaxAuthTries|AllowUsers|AllowGroups)" \
  /etc/ssh/sshd_config /etc/ssh/sshd_config.d/*.conf 2>/dev/null
Result thresholds this check applies
ResultWhenWhat it means
PASSEmpty passwords refused, MaxAuthTries 6 or lower, and an allow-list is setAccess is limited to the named accounts or groups.
FAILPermitEmptyPasswords yesAny account with a blank password can log in over the network. Set it to no.
WARNMaxAuthTries is greater than 6Each connection gets that many guesses. Lower it to 3.
WARNNo AllowUsers and no AllowGroupsEvery account on the host may attempt SSH. Restrict it to the accounts that need it.
SKIP/etc/ssh/sshd_config is not readableThe audit could not read the file.

Two details worth knowing. The OpenSSH default for MaxAuthTries is 6, and the audit only warns above that, so an unconfigured host passes this sub-check. And the conditions are evaluated in order, so a host with PermitEmptyPasswords yes reports that failure and stops - fix it and re-run to see what else is waiting.

How to fix it

sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak

# In /etc/ssh/sshd_config
PermitEmptyPasswords no
MaxAuthTries 3
AllowUsers your_admin_user your_deploy_user

sudo sshd -t
sudo systemctl restart sshd
AllowUsers is exclusive. Once it exists, accounts not on the list cannot SSH in, including yours if you mistype it. Run sudo sshd -t, then open a second session and confirm it works before you close the one you are in.

Check 10: Are weak SSH ciphers still accepted?

Medium severityNo sudo needed

CBC-mode ciphers have known plaintext-recovery attacks against SSH specifically, and SHA-1 collisions are practical rather than theoretical. Neither belongs in a 2026 configuration.

The warning case is the one that catches people. Leaving all three directives unset feels neutral, but it means the effective algorithm list is whatever your distribution shipped. On older releases that list still enables SHA-1 key exchange and CBC ciphers, so "unconfigured" and "insecure" are frequently the same state.

How to check manually

grep -iE "^(KexAlgorithms|Ciphers|MACs)" \
  /etc/ssh/sshd_config /etc/ssh/sshd_config.d/*.conf 2>/dev/null
Result thresholds this check applies
ResultWhenWhat it means
PASSAlgorithms are pinned and none are weakNo SHA-1 key exchange, CBC ciphers or weak MACs are configured.
FAILA weak entry appears in any of the three listsSHA-1 key exchange and CBC ciphers are downgrade and plaintext-recovery material. Pin modern lists.
WARNNone of KexAlgorithms, Ciphers or MACs is setYou inherit the package default, which on older distributions still enables SHA-1 and CBC.
SKIP/etc/ssh/sshd_config is not readableThe audit could not read the file.

Specifically, the audit flags diffie-hellman-group1-sha1, diffie-hellman-group14-sha1 and diffie-hellman-group-exchange-sha1 in KexAlgorithms; 3des-cbc, arcfour, blowfish-cbc, cast128-cbc and any aes*-cbc in Ciphers; and hmac-md5, hmac-sha1, hmac-sha1-96 and umac-64@ in MACs.

How to fix it

sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak

# In /etc/ssh/sshd_config
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com

sudo sshd -t
sudo systemctl restart sshd
Test every client afterwards. Pinning algorithms is a compatibility change as much as a security one. SSH clients older than roughly 2019 may not negotiate these. If a legacy client breaks, add diffie-hellman-group14-sha256 as a fallback, never a SHA-1 variant.

Check 11: Do passwords ever expire?

Low severityNo sudo needed

Even with SSH password authentication disabled, /etc/login.defs still governs local console login and any PAM-backed service on the host. A password that never expires stays valid for as long as the account does, which means a credential leaked two years ago still works.

The important caveat is scope: PASS_MAX_DAYS binds new accounts only. Changing it does nothing to accounts that already exist.

How to check manually

grep -E "^PASS_MAX_DAYS|^PASS_MIN_LEN" /etc/login.defs 2>/dev/null
Result thresholds this check applies
ResultWhenWhat it means
PASSPASS_MAX_DAYS is 365 or lowerAn expiry policy exists. Note it binds new accounts only - chage -l confirms existing ones.
WARNPASS_MAX_DAYS is greater than 365Effectively no expiry. Set 90 if this host uses password auth.
WARNNo PASS_MAX_DAYS line existsPasswords never expire.
SKIP/etc/login.defs is not readableThe audit could not read the file.

How to fix it

# For new accounts, in /etc/login.defs
PASS_MAX_DAYS   90

# Existing accounts have to be updated individually
sudo chage -l username        # view the current policy
sudo chage -M 90 username     # set a 90-day maximum
Low severity, still worth setting. If you have already disabled SSH password authentication in check 2, this matters much less for remote access. It still governs console login and PAM-backed services, and costs nothing to get right.

What this audit does not cover

Being explicit about scope is part of the point of publishing thresholds. This audit reads configuration; it does not test behaviour from the outside.

  • Match blocks are not evaluated. Values are parsed from sshd_config and its Include drop-ins. A directive overridden inside a Match User or Match Address block will not be reflected. Confirm with sudo sshd -T when a host uses them heavily.
  • No live handshake. Check 10 reads the configured algorithm lists rather than negotiating a connection, so it reports what sshd is told to offer, not what a specific client ends up using.
  • No brute-force protection check. Fail2Ban, sshguard and CrowdSec are not in this audit. Firewall and network exposure are covered by the Firewall & Network audit instead.
  • Key strength is not evaluated. Check 5 counts keys and reads file permissions. It does not judge whether a given key is a 1024-bit RSA key that should have been retired.

The manual audit problem at scale

Running these 11 checks on one server takes about 11 minutes, including reading each output and deciding whether it is a pass, a warning or a failure. For one server that is a reasonable afternoon.

Then multiply it. A freelancer with 8 client servers: 88 minutes. An agency running 20 staging and production environments: over 3 hours. A startup with 12 servers across development, staging and production: more than 2 hours, and this is not a once-a-year job.

Here is what the manual version actually looks like:

  1. SSH into server #1
  2. Run 11 commands, one at a time
  3. Read each output and interpret whether it passed, warned or failed
  4. Write the findings down somewhere
  5. SSH into server #2
  6. Repeat steps 2 to 4
  7. Continue for every server in the fleet
  8. Compare the results to work out which servers need attention first
  9. Go back to the worst ones and start fixing

No score. No report you can hand a client. No record of what the state was last time, so no way to prove a fix actually improved anything. Just terminal scrollback and a spreadsheet you will forget to update.

The real cost is not the time, it is the drift. You audit thoroughly in January and fix everything. By March someone has re-enabled password auth on a staging box because a deployment script needed it, and nobody wrote that down. Without something that re-checks, every fix has a half-life. That is the same failure mode as managing multiple servers without a central view.


How CtrlOps runs all 11 checks in one click

Instead of running 11 commands on every server by hand, CtrlOps runs the whole SSH & Access audit over the SSH connection you already have open. Nothing is installed on the server: no agent, no daemon, no package, no new credentials. The checks are read-only POSIX shell that executes, streams its results back, and leaves nothing behind.

Watch it run

Two and a half minutes, start to finish: selecting the audit, watching the checks stream in, reading the score, and sending the failures to the AI Terminal for a fix. The written steps below cover the same ground if you would rather read than watch.

Step 1: Open the Audit tab

Connect to your server in CtrlOps and open the Audit tab in the left sidebar.

Step 2: Select SSH & Access

Choose the SSH & Access audit from the catalog. All 11 checks are listed with their descriptions, severity levels and the estimated run time (about 14 seconds in total). Toggle off any check you want to skip.

Step 3: Run it

The audit runs over your existing SSH session. Results stream in as they complete, so you watch each check pass, warn or fail rather than waiting for a final report.

Step 4: Read the report

You get a summary containing:

  • A hardening score out of 100
  • A severity breakdown: how many HIGH, MEDIUM and LOW findings
  • Pass, warning, failed and skipped counts
  • A findings table you can sort and filter

Step 5: Fix what failed

Select the failed findings and send them to the AI Terminal. Each proposed fix command appears with an explanation of what it changes. Nothing runs until you review and approve it.

Step 6: Re-run and compare

Run the same audit again after applying fixes and watch the hardening score move. That delta is the thing a spreadsheet never gave you: evidence that the fix landed.

The whole cycle, audit then fix then verify, happens in one window. No switching terminals, no copy-pasting commands out of a checklist, no scoring by hand.

For fleet-wide work, the Audit Reports view on the Home screen runs the same audit across every connected server at once. Each server gets its own report, and the hardening scores line up side by side so you can see which box to open first.

TaskBy hand, 8 serversCtrlOps, 8 servers
Run all 11 checksAbout 88 minutesAbout 2 minutes
Get a scored reportNot availableAutomatic
Export a PDF for a clientNot availableOne click
Fix what failedCopy-paste from your notesAI-generated commands behind an approval gate
Re-audit after fixingAnother 88 minutesAnother 2 minutes

Conclusion

For a proper security audit, CtrlOps is the best option. While manual checks using commands are helpful for single servers, managing configurations at scale requires a centralized, automated approach.

The CtrlOps Security Audit feature handles all 11 SSH checks and more across your entire fleet in one click. It gives you a clear hardening score, client-ready PDF reports, and AI-assisted commands to fix vulnerabilities instantly. Use CtrlOps to eliminate configuration drift and keep your servers secure.


Frequently asked questions

An SSH security audit checklist is a structured set of configuration checks run against a server's SSH daemon and access controls. It covers root login permissions, password authentication, the listening port, idle timeouts, authorized keys, accounts holding UID 0, sudo group membership, passwordless sudo rules, login restrictions, cryptographic algorithms and password ageing. The goal is to find misconfigurations before someone else does.

Run one after every change to who has access: a new hire, a departure, a contractor finishing, or a server being provisioned. Beyond that, monthly is a reasonable baseline for production. The reason is configuration drift rather than new vulnerabilities: settings relaxed for a quick test are rarely reverted, and nothing on the server reminds you.

PermitRootLogin no blocks all root login over SSH, both passwords and keys. prohibit-password (previously without-password) blocks password-based root login but still allows key authentication as root. The audit passes no and warns on prohibit-password, because key-based root login still bypasses the sudo audit trail that records who ran which command.

It reduces automated brute-force noise substantially but adds no meaningful protection against a targeted attacker, who will find the new port with a scan in seconds. That is why this check warns rather than fails. Treat it as log hygiene that makes real intrusion attempts visible, not as a security control, and never as a substitute for key-only authentication.

It breaks any workflow that types a password to connect, which in practice means some CI/CD pipelines, older deployment scripts and SFTP clients configured with passwords. Before disabling it, confirm every human and service account that connects has a working key, and test key login in a separate terminal while your current session is still open.

It reads ~/.ssh/authorized_keys in every account's home directory, and those files are readable only by their owner and root. Without root the audit can see your own file and nothing else, which would report a clean result on a server that has unknown keys sitting in another account. The check is skipped rather than guessed when it cannot read them.

It means that account can run any command as root with no password prompt. If an attacker gets that account, through a stolen key, a vulnerable application running as that user, or a hijacked session, they have unrestricted root immediately and nothing else to defeat. Scoped NOPASSWD rules naming specific non-interactive commands are a different matter and only draw a warning.

Use curve25519-sha256 for key exchange, chacha20-poly1305@openssh.com or aes256-gcm@openssh.com for ciphers, and hmac-sha2-512-etm@openssh.com for MACs. Remove all SHA-1 key exchange, every CBC-mode cipher, RC4 variants and MD5 or SHA-1 MACs. Test each client that connects to the server before rolling the change out, because clients older than about 2019 may not negotiate these.

Yes. Every check works on any Linux host reachable over SSH: EC2, DigitalOcean Droplets, Linode, Vultr, Hetzner, bare metal or a VPS from any other provider. The audit scripts detect the distribution family (Debian, Ubuntu, RHEL, Rocky, Alma, Amazon Linux, Fedora, Oracle, SUSE, Alpine) and adapt the commands accordingly.

If your workloads run on Kubernetes, serverless platforms such as Lambda or Cloud Functions, or a managed PaaS where you do not control the SSH layer, this audit does not apply: those platforms handle access differently and you have no sshd to configure. It is for servers you access and administer over SSH.

No. The 11 checks are read-only POSIX shell scripts executed over the SSH session you already have open. Nothing is installed, no agent or daemon runs afterwards, and nothing is written to the target host. Two of the checks need root to read other accounts' files, and are reported as skipped rather than guessed if you run without it.

ssh-audit connects from outside and evaluates the algorithms a server actually negotiates, which is a genuinely different angle from reading the config. Lynis is installed on the host and covers far more than SSH. This audit runs agentlessly over an existing connection, publishes the exact thresholds it applies, and feeds failed findings into a guided fix step. They complement each other rather than replacing one another.

Audit your fleet

Run all these checks on every server, in one click

CtrlOps runs this audit over your existing SSH connection - no agents, no scripts to manage. $7/user/month after a 1 month free trial - no credit card required.

✓ Start instantly·✓ No credit card·✓ No sneaky autorenewals