FreeBSD.software
Home/Blog/How to Secure SSH on FreeBSD: Complete Guide
tutorial2026-03-29

How to Secure SSH on FreeBSD: Complete Guide

Complete guide to securing SSH on FreeBSD. Covers sshd_config hardening, key-only authentication, certificate auth, jump hosts, port knocking, fail2ban, and advanced SSH security techniques.

# How to Secure SSH on FreeBSD: Complete Guide

SSH is the single most critical service on any FreeBSD server. It is your primary administration channel, and it is also the first thing attackers probe. A default OpenSSH configuration on FreeBSD is functional but not hardened. It allows password authentication, permits root login, and negotiates legacy ciphers that have no place on a production system.

This guide walks through every layer of SSH hardening on FreeBSD 14.x, from basic sshd_config directives to certificate-based authentication, jump hosts, two-factor authentication, and automated brute-force defense with fail2ban and PF. Each section includes exact commands and configuration blocks you can apply directly.

If you are building a new server, start with our [FreeBSD VPS setup guide](/blog/freebsd-vps-setup/) and [FreeBSD server hardening checklist](/blog/hardening-freebsd-server/) first. SSH hardening is one layer in a defense-in-depth strategy that includes firewall rules, kernel tunables, filesystem permissions, and monitoring.

1. Why SSH Defaults Are Not Enough

Out of the box, FreeBSD's OpenSSH daemon accepts password authentication, allows root login with a password, and negotiates a wide range of ciphers and key exchange algorithms for backward compatibility. Every one of these defaults is a liability.

**Password authentication** is vulnerable to brute force. Automated scanners hit port 22 within minutes of a server going online. Even strong passwords are weaker than a 256-bit Ed25519 key.

**Root login** means an attacker only needs one credential to own the entire system. Forcing login as an unprivileged user and then escalating with su or doas adds a second authentication barrier.

**Legacy ciphers and key exchange algorithms** like diffie-hellman-group1-sha1 or 3des-cbc have known weaknesses. Allowing them means a downgrade attack could force their use.

**No rate limiting** by default means the SSH daemon will accept authentication attempts as fast as the network can deliver them.

The goal is to reduce the attack surface to exactly what you need: key-based authentication, modern cryptography, restricted user access, and automated blocking of abusive connections.

2. Hardening sshd_config: Complete Walkthrough

Back up the existing configuration before making changes:

sh

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

Open /etc/ssh/sshd_config in your editor. The following subsections cover every important directive.

Disable Root Login


PermitRootLogin no

This forces administrators to log in as an unprivileged user and escalate privileges explicitly. If you need root-level automation (e.g., Ansible), use PermitRootLogin prohibit-password instead, which allows key-based root login but blocks password-based root login.

Disable Password Authentication


PasswordAuthentication no

KbdInteractiveAuthentication no

ChallengeResponseAuthentication no

With these three directives, the only way to authenticate is with a public key (or certificate, covered later). Set up your SSH keys before enabling this or you will lock yourself out.

Enable Public Key Authentication


PubkeyAuthentication yes

AuthorizedKeysFile .ssh/authorized_keys

This is the default on FreeBSD, but make it explicit. Explicit configuration survives upstream default changes.

Restrict Allowed Users and Groups


AllowUsers admin deploy

AllowGroups sshusers

Use AllowUsers to whitelist specific usernames, or AllowGroups to whitelist a group. Anyone not listed is denied SSH access entirely, regardless of whether they have valid credentials. Create the group and add users:

sh

pw groupadd sshusers

pw groupmod sshusers -m admin

pw groupmod sshusers -m deploy

Limit Authentication Attempts and Timing


MaxAuthTries 3

LoginGraceTime 30

MaxStartups 10:30:100

MaxSessions 3

MaxAuthTries 3 disconnects after three failed authentication attempts per connection. LoginGraceTime 30 gives 30 seconds to complete authentication before the server drops the connection. MaxStartups 10:30:100 starts dropping new unauthenticated connections probabilistically after 10 concurrent unauthenticated sessions, reaching 100% rejection at 100. MaxSessions 3 limits multiplexed sessions per connection.

Modern Ciphers, MACs, and Key Exchange

Strip out legacy algorithms. Use only algorithms with no known weaknesses:


KexAlgorithms sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org

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

HostKeyAlgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256

The sntrup761x25519-sha512 key exchange is a post-quantum hybrid algorithm available in OpenSSH 9.0+. FreeBSD 14.x ships with OpenSSH 9.x, so this works out of the box. The chacha20-poly1305 cipher is the strongest choice for most workloads. The AES-GCM ciphers provide hardware-accelerated alternatives on servers with AES-NI.

Only Encrypt-then-MAC (-etm) MAC algorithms are listed. Non-ETM MACs are vulnerable to theoretical attacks and should be excluded.

Disable Unnecessary Forwarding


X11Forwarding no

AllowAgentForwarding no

AllowTcpForwarding no

AllowStreamLocalForwarding no

GatewayPorts no

PermitTunnel no

Disable every forwarding feature you do not actively use. Agent forwarding is particularly dangerous -- if an attacker compromises a server where your agent is forwarded, they can use your key to authenticate to other systems. If you need forwarding for specific users, use a Match block:


Match User deploy

AllowTcpForwarding yes

Additional Hardening Directives


UseDNS no

PrintMotd no

PrintLastLog yes

TCPKeepAlive yes

ClientAliveInterval 300

ClientAliveCountMax 2

PermitEmptyPasswords no

PermitUserEnvironment no

StrictModes yes

Compression no

UseDNS no prevents reverse DNS lookups during authentication, which avoids delays and DNS-based attacks. ClientAliveInterval 300 with ClientAliveCountMax 2 drops idle sessions after 10 minutes. Compression no avoids the CRIME-class of compression oracle attacks. StrictModes yes ensures the server rejects keys from files with overly permissive permissions.

Restart and Validate

After saving changes, validate the configuration syntax before restarting:

sh

sshd -t

If it returns no output, the configuration is valid. Restart the daemon:

sh

service sshd restart

Keep your existing session open while testing a new connection in a separate terminal. This ensures you do not lock yourself out.

3. Key Management

Generate an Ed25519 Key Pair

Ed25519 keys are shorter, faster, and more secure than RSA keys. Generate one on your local machine:

sh

ssh-keygen -t ed25519 -a 100 -C "admin@freebsd-server" -f ~/.ssh/id_ed25519

The -a 100 flag sets 100 rounds of KDF (key derivation function) for the passphrase, making brute-force attacks against a stolen key file significantly harder.

Deploy the Public Key

Copy the public key to the server:

sh

ssh-copy-id -i ~/.ssh/id_ed25519.pub admin@your-server

Or manually append it to ~/.ssh/authorized_keys on the server. Verify permissions:

sh

chmod 700 ~/.ssh

chmod 600 ~/.ssh/authorized_keys

Use ssh-agent

Running ssh-agent avoids typing your passphrase repeatedly:

sh

eval "$(ssh-agent -s)"

ssh-add ~/.ssh/id_ed25519

On macOS, add AddKeysToAgent yes to your ~/.ssh/config to auto-load keys. On FreeBSD workstations, add the eval line to your shell profile.

Remove Legacy Keys

If you have old RSA or DSA keys on the server, remove them from authorized_keys. On the server side, remove unused host keys:

sh

rm /etc/ssh/ssh_host_dsa_key /etc/ssh/ssh_host_dsa_key.pub

rm /etc/ssh/ssh_host_ecdsa_key /etc/ssh/ssh_host_ecdsa_key.pub

Regenerate only the keys you need:

sh

ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N ""

ssh-keygen -t rsa -b 4096 -f /etc/ssh/ssh_host_rsa_key -N ""

Keep RSA as a fallback for older clients. If all your clients support Ed25519, remove RSA as well.

4. SSH Certificate Authentication

For teams managing multiple servers, SSH certificates eliminate the need to distribute individual public keys to every server. A central Certificate Authority (CA) signs user keys, and servers trust the CA rather than individual keys.

Create the CA Key

On a dedicated, air-gapped machine:

sh

ssh-keygen -t ed25519 -f /path/to/ca_user_key -C "SSH User CA"

Sign a User Key

sh

ssh-keygen -s /path/to/ca_user_key -I "admin@company" -n admin,deploy -V +52w ~/.ssh/id_ed25519.pub

This produces ~/.ssh/id_ed25519-cert.pub, valid for 52 weeks, authorized for the admin and deploy usernames. The -I flag sets an identifier that appears in logs.

Configure the Server to Trust the CA

Copy ca_user_key.pub to the server and add to sshd_config:


TrustedUserCAKeys /etc/ssh/ca_user_key.pub

Now any user key signed by this CA is accepted. No changes to authorized_keys are needed when onboarding or offboarding team members -- just issue or revoke certificates.

Revoke Certificates

Create a revocation list:

sh

ssh-keygen -k -f /etc/ssh/revoked_keys -s /path/to/ca_user_key public_key_to_revoke.pub

Add to sshd_config:


RevokedKeys /etc/ssh/revoked_keys

When a team member leaves or a key is compromised, add their key to the revocation list and redistribute the file.

5. Jump Hosts and ProxyJump

A jump host (bastion) is a single hardened entry point to your network. Internal servers are not directly accessible from the internet. All SSH access routes through the bastion.

Server-Side Architecture

The bastion server has a public IP. Internal servers have only private IPs. The bastion's sshd_config should be hardened with all directives from Section 2, plus:


AllowTcpForwarding yes

TCP forwarding is required on the bastion for proxying. Disable it on the internal servers.

Client-Side Configuration

In ~/.ssh/config on your workstation:


Host bastion

HostName bastion.example.com

User admin

IdentityFile ~/.ssh/id_ed25519

Port 22

Host internal-db

HostName 10.0.1.5

User admin

IdentityFile ~/.ssh/id_ed25519

ProxyJump bastion

Host internal-web

HostName 10.0.1.10

User deploy

IdentityFile ~/.ssh/id_ed25519

ProxyJump bastion

Now ssh internal-db automatically routes through the bastion. No agent forwarding is needed -- ProxyJump establishes a direct encrypted channel through the bastion without exposing your key to it.

PF Rules on the Bastion

On the bastion, use [PF](/blog/pf-firewall-freebsd/) to restrict which internal hosts can be reached:


pass in on egress proto tcp from any to (egress) port 22

pass out on int0 proto tcp from (egress) to 10.0.1.0/24 port 22

block out on int0 proto tcp from (egress) to 10.0.1.0/24 port != 22

6. Two-Factor Authentication

Google Authenticator (TOTP)

Install the PAM module:

sh

pkg install pam_google_authenticator

Run the setup as the target user:

sh

google-authenticator -t -d -f -r 3 -R 30 -w 3

This generates a TOTP secret, QR code, and emergency scratch codes. Scan the QR code with any TOTP app (Google Authenticator, Authy, FreeOTP).

Edit /etc/pam.d/sshd and add after the auth lines:


auth required /usr/local/lib/pam_google_authenticator.so

In sshd_config, enable challenge-response authentication:


ChallengeResponseAuthentication yes

AuthenticationMethods publickey,keyboard-interactive

The AuthenticationMethods directive requires both a valid key and a TOTP code. This is true two-factor: something you have (the key) and something you know (the TOTP code).

Restart sshd and test in a separate session before closing your current one.

YubiKey (FIDO2/U2F)

OpenSSH 8.2+ supports FIDO2 security keys natively. Generate a resident key on your YubiKey:

sh

ssh-keygen -t ed25519-sk -O resident -O application=ssh:freebsd -C "admin-yubikey"

The -sk suffix tells OpenSSH this is a security key credential. The private key never leaves the YubiKey. Add the resulting public key to authorized_keys on the server.

In sshd_config:


PubkeyAcceptedAlgorithms +sk-ssh-ed25519@openssh.com,sk-ecdsa-sha2-nistp256@openssh.com

When connecting, the YubiKey must be physically present and the user must touch it to complete authentication.

7. Fail2ban for SSH

Fail2ban monitors log files and bans IPs that show malicious signs. On FreeBSD, it integrates with PF to block offending addresses.

Install Fail2ban

sh

pkg install py311-fail2ban

sysrc fail2ban_enable="YES"

Configure the SSH Jail

Create /usr/local/etc/fail2ban/jail.local:

ini

[DEFAULT]

bantime = 3600

findtime = 600

maxretry = 3

banaction = pf[actiontype=]

backend = auto

[sshd]

enabled = true

port = ssh

filter = sshd

logpath = /var/log/auth.log

maxretry = 3

bantime = 86400

findtime = 600

This bans an IP for 24 hours after 3 failed attempts within 10 minutes. The pf action tells fail2ban to use PF tables for blocking, which is the correct approach on FreeBSD.

PF Integration

Fail2ban's PF action creates a table called . Ensure your /etc/pf.conf includes a rule to block traffic from this table:


table persist

block quick from

Place this rule before your pass rules. See our [PF firewall guide](/blog/pf-firewall-freebsd/) for the complete ruleset structure.

Start and Verify

sh

service fail2ban start

fail2ban-client status sshd

Check the PF table after a few blocked attempts:

sh

pfctl -t f2b-sshd -T show

8. Rate Limiting with PF

PF can rate-limit SSH connections independently of fail2ban. This provides a kernel-level defense that works even if fail2ban is down.

Add these rules to /etc/pf.conf:


table persist

block quick from

pass in on egress proto tcp to (egress) port 22 \

flags S/SA keep state \

(max-src-conn 5, max-src-conn-rate 3/30, \

overload flush global)

This allows a maximum of 5 simultaneous connections per source IP and 3 new connections per 30 seconds. Any IP that exceeds these limits is added to the ssh_bruteforce table and all its existing connections are flushed.

To automatically expire entries after 24 hours, add a cron job:

sh

echo '0 * * * * root /sbin/pfctl -t ssh_bruteforce -T expire 86400' >> /etc/crontab

Reload PF:

sh

pfctl -f /etc/pf.conf

This PF-based rate limiting catches attackers before they even reach the SSH daemon. Combined with fail2ban (which analyzes log patterns), you have two independent layers of brute-force protection.

9. SSH Auditing and Logging

Increase Log Verbosity

In sshd_config:


LogLevel VERBOSE

VERBOSE logs key fingerprints for every authentication attempt, which is essential for incident investigation. For even more detail during troubleshooting, use DEBUG -- but do not leave DEBUG on in production as it exposes sensitive information.

Monitor auth.log

SSH logs to /var/log/auth.log on FreeBSD. Monitor it in real time:

sh

tail -f /var/log/auth.log | grep sshd

For persistent monitoring, configure syslogd to send SSH logs to a remote syslog server so they survive a compromise of the local system:


auth.* @syslog.internal.example.com

Audit Key Usage

When using SSH certificates, every certificate has an identity string (-I flag during signing). This identity appears in the logs, making it straightforward to track which team member accessed which server and when.

Log File Rotation

FreeBSD's newsyslog rotates logs automatically. Verify /etc/newsyslog.conf includes:


/var/log/auth.log 600 7 1000 * JC

This keeps 7 rotated copies, compressed, rotating when the file exceeds 1000 KB.

10. Client-Side Hardening

Your ~/.ssh/config file controls how your SSH client connects. Harden it:


Host *

# Use only strong algorithms (match server config)

KexAlgorithms sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org

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

# Prefer Ed25519 keys

IdentityFile ~/.ssh/id_ed25519

IdentitiesOnly yes

# Verify host keys

StrictHostKeyChecking ask

UpdateHostKeys yes

VisualHostKey yes

# Connection settings

ServerAliveInterval 60

ServerAliveCountMax 3

AddKeysToAgent yes

HashKnownHosts yes

# Disable forwarding by default

ForwardAgent no

ForwardX11 no

Key points:

- **IdentitiesOnly yes** prevents the client from offering every key in ~/.ssh/ to every server. Without this, your client leaks information about which keys you possess.

- **HashKnownHosts yes** hashes hostnames in known_hosts so an attacker who steals this file cannot enumerate your infrastructure.

- **UpdateHostKeys yes** automatically rotates host keys when the server offers new ones, signed by the existing trusted key. This prevents the "host key has changed" error when servers are legitimately re-keyed.

- **StrictHostKeyChecking ask** prompts on first connection but refuses connections if a known host key changes unexpectedly (potential MITM).

11. Quick Reference: Hardened sshd_config

This is the complete, production-ready configuration. Adjust AllowUsers, AllowGroups, and Port for your environment.

# /etc/ssh/sshd_config -- hardened configuration for FreeBSD 14.x

# Network

Port 22

AddressFamily inet

ListenAddress 0.0.0.0

# Authentication

PermitRootLogin no

PubkeyAuthentication yes

AuthorizedKeysFile .ssh/authorized_keys

PasswordAuthentication no

KbdInteractiveAuthentication no

ChallengeResponseAuthentication no

PermitEmptyPasswords no

MaxAuthTries 3

LoginGraceTime 30

MaxStartups 10:30:100

MaxSessions 3

# Access control

AllowGroups sshusers

# Cryptography

KexAlgorithms sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org

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

HostKeyAlgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256

HostKey /etc/ssh/ssh_host_ed25519_key

HostKey /etc/ssh/ssh_host_rsa_key

# Forwarding (disabled by default)

X11Forwarding no

AllowAgentForwarding no

AllowTcpForwarding no

AllowStreamLocalForwarding no

GatewayPorts no

PermitTunnel no

# Logging

LogLevel VERBOSE

SyslogFacility AUTH

# Session

UseDNS no

PrintMotd no

PrintLastLog yes

TCPKeepAlive yes

ClientAliveInterval 300

ClientAliveCountMax 2

PermitUserEnvironment no

StrictModes yes

Compression no

# Subsystems

Subsystem sftp /usr/libexec/sftp-server -f AUTH -l VERBOSE

Note the SFTP subsystem line includes -f AUTH -l VERBOSE to log SFTP operations at the same verbosity level as SSH authentication. This is often overlooked and leaves a blind spot for file transfer activity.

12. Frequently Asked Questions

Should I change the default SSH port?

Changing from port 22 to a non-standard port reduces log noise from automated scanners. It is not a security measure by itself -- a port scan will find it in seconds. Treat it as a convenience to reduce noise, not as a defense. If you change the port, update your PF rules and fail2ban configuration accordingly.

How do I recover if I lock myself out?

Always test SSH changes in a separate terminal while keeping an existing session open. If you are on a VPS, use the provider's web console or VNC access as a fallback. On physical hardware, you need console access. Before applying changes, verify the config with sshd -t and confirm your public key is in authorized_keys.

Is Ed25519 better than RSA for SSH keys?

Yes for almost all use cases. Ed25519 keys are 256 bits, produce shorter signatures, and are faster to generate and verify than RSA-4096 keys. Ed25519 uses the Edwards curve, which is not vulnerable to the timing attacks that affect some NIST curves. The only reason to keep RSA is compatibility with very old clients or systems that do not support Ed25519.

Can I use SSH keys and two-factor authentication together?

Yes. The AuthenticationMethods publickey,keyboard-interactive directive in sshd_config requires both a valid key and a TOTP code or YubiKey touch. This is the recommended setup for high-security environments.

How often should I rotate SSH keys?

There is no universal answer, but a reasonable policy is to rotate user keys annually and host keys every two to three years. SSH certificates make this easier -- set certificate validity to 52 weeks and re-sign annually. For host keys, use UpdateHostKeys yes on the client side so rotations are seamless.

What is the difference between fail2ban and PF rate limiting?

PF rate limiting operates at the network level and blocks connections before they reach the SSH daemon. It is fast and efficient but can only count connections, not distinguish between successful and failed authentication attempts. Fail2ban reads logs, so it can detect actual failed login attempts and patterns like username enumeration. Use both for defense in depth.

How do I audit who accessed my server via SSH?

Set LogLevel VERBOSE in sshd_config. Every successful and failed authentication attempt is logged to /var/log/auth.log with the key fingerprint, username, and source IP. If using SSH certificates, the certificate identity is also logged. Forward these logs to a central syslog server for tamper-resistant storage.

Conclusion

SSH hardening is not a one-time task. The configuration in this guide is a strong starting point, but it requires ongoing maintenance: rotating keys, reviewing logs, updating fail2ban rules, and staying current with OpenSSH releases that patch newly discovered vulnerabilities.

The layered approach matters. Any single measure -- key-only authentication, PF rate limiting, fail2ban, modern ciphers -- can fail or be bypassed. When stacked together, an attacker must defeat every layer simultaneously. That is the difference between a server that survives an attack and one that does not.

Start with the hardened sshd_config from Section 11. Add fail2ban and PF rate limiting from Sections 7 and 8. Set up SSH certificates if you manage more than a handful of servers. And always, always test your changes in a separate session before closing the one you came in on.

For the complete server hardening picture, including kernel tunables, filesystem security, and process isolation, see our [FreeBSD server hardening checklist](/blog/hardening-freebsd-server/).