← Back to Wiki
Security / SSH

Fleet-Wide SSH Hardening

A subnet-wide vulnerability scan will very likely flag weak SSH MAC algorithms on most of your fleet — it's an OS-default list, not something anyone configured on purpose. Here's how to fix it across a mix of containers, VMs, and vendor NAS units without a lot of manual per-host work.

The fix itself

Restrict sshd_config's MACs directive to modern, SHA-2-based algorithms only:

MACs hmac-sha2-256,hmac-sha2-512,[email protected],[email protected]

An idempotent, safe rollout script

Across a whole fleet, a script that backs up before editing, validates before restarting, and restores on failure is worth the extra few lines:

set -uo pipefail
CONF=/etc/ssh/sshd_config
BAK="$CONF.bak.$(date +%s)"
cp "$CONF" "$BAK"
STRONG='MACs hmac-sha2-256,hmac-sha2-512,[email protected],[email protected]'
if grep -q '^MACs ' "$CONF"; then
  sed -i "s/^MACs .*/$STRONG/" "$CONF"
else
  echo "$STRONG" >> "$CONF"
fi
if sshd -t 2>/tmp/sshd_t_err; then
  systemctl restart ssh 2>/dev/null || systemctl restart sshd 2>/dev/null
  echo HARDEN_OK
else
  cp "$BAK" "$CONF"
  echo "HARDEN_FAIL_SYNTAX: $(cat /tmp/sshd_t_err)"
fi

Reaching containers without per-host SSH keys

If most of your containers never got a dedicated SSH keypair set up (only the ones that needed direct SSH for other reasons), don't generate and authorize a new keypair for every single one just for a one-off fleet change. If your hypervisor supports executing commands directly inside a container without SSH (Proxmox's pct exec, for example), use that instead — it's the better default for any one-off fleet-wide change, not just this one.

Vendor NAS gotchas

A trailing Match block breaks a naive append. Some vendor NAS sshd_config files end with Match User blocks. Appending a new global directive at end-of-file lands it inside the last Match block, which OpenSSH rejects outright — global directives must come before any Match block. Insert before the first Match line instead:
awk -v line="$STRONG" '
  !inserted && /^Match/ { print line; inserted=1 }
  { print }
  END { if (!inserted) print line }
' "$CONF" > "$CONF.new" && mv "$CONF.new" "$CONF"
Some vendor NAS units have no SFTP subsystem enabled — plain scp silently fails. Modern OpenSSH's scp defaults to the SFTP protocol. If the target doesn't support it, force the legacy protocol instead: scp -O.

Verifying the fix actually works

A naive test can pass even when the restriction isn't working. Trying to connect while forcing a weak MAC algorithm and expecting rejection can succeed anyway — because modern SSH clients and servers often negotiate an AEAD cipher (like ChaCha20-Poly1305) that has an implicit MAC baked in. The MACs directive is only consulted when a non-AEAD cipher gets selected. Force one to test for real:
ssh -o Ciphers=aes256-ctr -o MACs=hmac-md5 <host>
# should be correctly rejected:
# "Unable to negotiate with <host> port 22: no matching MAC found"

Handling hosts with no SSH access at all

Occasionally a VM will have neither a dedicated SSH key nor a working guest-agent shortcut from your hypervisor. Check whether the host's default user really is root before concluding a host is unreachable — password auth may be enabled with a non-root account nobody thought to try. Generate a dedicated keypair, get it installed once (manually, if needed), and treat it like every other host from then on.

Passwordless sudo for automation, once

For hosts that connect as a non-root user, sudo normally needs an interactive password — which no automated fleet-wide script can supply. Rather than running the fix by hand on each of these individually, consider adding a standing passwordless-sudo rule once per host, so future fleet-wide maintenance doesn't hit the same wall again:

echo '<user> ALL=(ALL) NOPASSWD: ALL' | sudo tee /etc/sudoers.d/090-nopasswd
sudo chmod 0440 /etc/sudoers.d/090-nopasswd
sudo visudo -c

This is a real security tradeoff (standing root-equivalent access), so weigh it against how often you expect to need automated fleet-wide changes versus doing occasional fixes by hand.

Always spot-check after restarting sshd fleet-wide: open a fresh connection to every host after the restart to confirm it didn't lock itself out, and keep the backup config file around — it's a rollback point, not cleanup debris.