How to Harden a FreeBSD Server: Complete Security Checklist
A default FreeBSD installation is already more secure than most Linux distributions out of the box. But "more secure than default" is not the same as "production-hardened." Every exposed service, every permissive default, every unmonitored log is a surface an attacker can probe.
This guide is a comprehensive, command-by-command checklist for hardening a FreeBSD server. It assumes a fresh FreeBSD 14.x installation with root access. Every recommendation includes the exact configuration change and the reasoning behind it.
Work through each section in order. By the end, your server will implement defense in depth across authentication, network, filesystem, process isolation, and monitoring layers.
1. Hardening Philosophy: Defense in Depth
No single security measure is sufficient. Hardening works by stacking independent layers so that the failure of any one control does not compromise the system.
The layers covered in this guide:
- Authentication -- restrict who can log in and how.
- Network -- control what traffic reaches the system and what leaves it.
- Kernel -- tune the operating system to reject known attack patterns.
- Filesystem -- limit what can be written, executed, or modified.
- Process isolation -- confine services so a breach in one cannot spread.
- Monitoring -- detect anomalies before they become incidents.
Every recommendation here has been tested on FreeBSD 14.x. If you are running an older release, verify sysctl names and service options against your version's man pages.
2. SSH Hardening
SSH is almost always the first service exposed on a FreeBSD server, which makes it the first target. The goal is to reduce the attack surface to key-based authentication from known users only.
Disable Root Login and Enforce Key-Only Authentication
Edit /etc/ssh/sshd_config:
shellPermitRootLogin no PasswordAuthentication no KbdInteractiveAuthentication no PubkeyAuthentication yes AuthorizedKeysFile .ssh/authorized_keys
Restrict Access to Named Users
Only allow specific user accounts to connect:
shellAllowUsers deployer admin
This is a whitelist. Any user not listed is rejected before authentication is even attempted.
Limit Authentication Attempts and Idle Sessions
shellMaxAuthTries 3 LoginGraceTime 30 ClientAliveInterval 300 ClientAliveCountMax 2
LoginGraceTime 30 gives unauthenticated connections 30 seconds before the server drops them. ClientAliveInterval and ClientAliveCountMax together drop idle sessions after 10 minutes.
Disable Unnecessary Features
shellX11Forwarding no AllowTcpForwarding no AllowAgentForwarding no PermitTunnel no
Unless you specifically need TCP forwarding or X11, disable them. Each forwarding capability is a potential pivot point for an attacker who gains access.
The Port Change Debate
Changing SSH from port 22 to a high port (e.g., 2222 or 48222) is not a security measure -- it is an obscurity measure. It reduces noise in your logs from automated scanners but does not stop a targeted attacker. If you change the port, do it for log hygiene, not as a substitute for real controls:
shellPort 48222
Brute-Force Protection with fail2ban
Install and configure fail2ban to block IPs after repeated failed attempts. See our detailed fail2ban on FreeBSD guide for full setup instructions.
Quick setup:
shpkg install py311-fail2ban sysrc fail2ban_enable="YES"
Create /usr/local/etc/fail2ban/jail.local:
ini[sshd] enabled = true port = ssh filter = sshd logpath = /var/log/auth.log maxretry = 3 bantime = 3600 findtime = 600 banaction = pf
After all SSH changes, restart the service:
shservice sshd restart
Test your SSH access from another terminal before closing your current session. Locking yourself out of your own server is not a security improvement.
3. Firewall with PF
PF (Packet Filter) is FreeBSD's built-in stateful firewall. A properly configured PF ruleset is one of the most effective hardening measures available. For an in-depth walkthrough, see our PF firewall guide.
Enable PF
Add to /etc/rc.conf:
shpf_enable="YES" pf_rules="/etc/pf.conf" pflog_enable="YES"
Minimal Default-Deny Ruleset
Create /etc/pf.conf:
shell# Macros ext_if = "vtnet0" ssh_port = "48222" tcp_services = "{ 80, 443 }" # Tables table <bruteforce> persist # Options set skip on lo0 set block-policy drop set loginterface $ext_if # Scrub scrub in all # Default deny block in all block out all # Allow outbound traffic pass out on $ext_if proto tcp from ($ext_if) to any modulate state pass out on $ext_if proto udp from ($ext_if) to any keep state pass out on $ext_if proto icmp from ($ext_if) to any keep state # Allow inbound SSH with rate limiting pass in on $ext_if proto tcp to ($ext_if) port $ssh_port \ flags S/SA keep state \ (max-src-conn 10, max-src-conn-rate 3/30, \ overload <bruteforce> flush global) # Allow inbound HTTP/HTTPS pass in on $ext_if proto tcp to ($ext_if) port $tcp_services \ flags S/SA keep state # Block brute-force offenders block in quick on $ext_if from <bruteforce> # Allow ICMP ping (optional) pass in on $ext_if inet proto icmp icmp-type echoreq keep state
Key points about this ruleset:
set block-policy dropsilently drops blocked packets instead of sending RST, giving attackers no information.max-src-conn-rate 3/30limits any single IP to 3 new SSH connections per 30 seconds. Violators are added to thetable and blocked entirely.flags S/SAon TCP pass rules only allows new connections that begin with a proper SYN, blocking malformed packets.scrub in allnormalizes incoming packets, defeating fragmentation-based attacks.
Load and verify:
shservice pf start pfctl -f /etc/pf.conf pfctl -sr # Show active rules pfctl -ss # Show state table
To view and manage the bruteforce table:
shpfctl -t bruteforce -T show # List blocked IPs pfctl -t bruteforce -T delete 192.0.2.1 # Unblock an IP
4. sysctl Security Tunables
The FreeBSD kernel exposes dozens of security-relevant tunables via sysctl. Add the following to /etc/sysctl.conf:
shell# Prevent users from seeing other users' processes security.bsd.see_other_uids=0 security.bsd.see_other_gids=0 # Prevent unprivileged users from reading kernel message buffer security.bsd.unprivileged_read_msgbuf=0 # Prevent unprivileged users from seeing process debug info security.bsd.unprivileged_proc_debug=0 # Randomize PID assignment (makes PID prediction harder) kern.randompid=1 # Disable core dumps for setuid programs kern.sugid_coredump=0 # Blackhole packets to closed TCP/UDP ports (no RST/ICMP unreachable) net.inet.tcp.blackhole=2 net.inet.udp.blackhole=1 # Ignore ICMP redirects (prevents route manipulation) net.inet.icmp.drop_redirect=1 # Do not send ICMP redirects net.inet.ip.redirect=0 # Enable random IP IDs net.inet.ip.random_id=1 # Disable ICMP broadcast responses (smurf amplification prevention) net.inet.icmp.bmcastecho=0 # Log redirect packets net.inet.icmp.log_redirect=1 # Clear temporary files on startup kern.clean_temp=1
Explanation of the most important entries:
security.bsd.see_other_uids=0prevents non-root users from seeing processes owned by other users. On a shared server, this stops information leakage between accounts.net.inet.tcp.blackhole=2causes the kernel to silently drop TCP SYN packets sent to ports with no listening service. Without this, the kernel sends an RST, confirming to a scanner that the port is closed but the host is alive.net.inet.udp.blackhole=1does the same for UDP.net.inet.icmp.drop_redirect=1ignores ICMP redirect messages, which an attacker on your LAN could use to reroute your traffic through their machine.kern.randompid=1randomizes PID allocation, making it harder for an attacker to predict or target specific process IDs.
Apply without rebooting:
shsysctl -f /etc/sysctl.conf
5. User and Permission Management
Use the wheel Group for su Access
Only users in the wheel group can su to root. Verify your admin user is in the group:
shpw groupmod wheel -m admin
Confirm in /etc/group that wheel contains only accounts that genuinely need root escalation.
Configure sudo Instead of Direct Root Access
Install and configure sudo:
shpkg install sudo visudo
Add a restrictive entry:
shell# Allow wheel group members to sudo with password %wheel ALL=(ALL:ALL) ALL # Require password re-entry every 5 minutes Defaults timestamp_timeout=5 # Log all sudo commands Defaults logfile="/var/log/sudo.log"
Login Classes for Resource and Access Control
FreeBSD's login classes in /etc/login.conf let you enforce per-class resource limits and authentication requirements. Add a restricted class for service accounts:
shellrestricted:\ :tc=default:\ :cputime=1h:\ :memoryuse=512m:\ :openfiles=256:\ :maxproc=64:
Rebuild the login database after editing:
shcap_mkdb /etc/login.conf
Assign the class to a user:
shpw usermod serviceacct -L restricted
Remove Unnecessary Accounts
Review /etc/passwd for accounts you do not need. Lock them:
shpw lock toor pw lock games
6. File System Security
Securelevel
FreeBSD's securelevel kernel variable restricts what even root can do. At securelevel 1 and above, immutable flags cannot be removed, kernel modules cannot be loaded, and raw disk access is blocked.
Add to /etc/rc.conf:
shkern_securelevel_enable="YES" kern_securelevel="2"
Securelevel values:
| Level | Restrictions |
|-------|-------------|
| -1 | No restrictions (default) |
| 0 | Restrictions not enforced yet |
| 1 | Cannot remove immutable/append-only flags, no /dev/mem writes, no kernel module loading |
| 2 | All of level 1 + cannot write to mounted filesystems directly, cannot change time by more than 1 second |
| 3 | All of level 2 + cannot modify PF rules |
Warning: Set securelevel only after your firewall rules and system configuration are finalized. At securelevel 2+, you cannot modify PF rules without a reboot. At securelevel 3, even that is locked down.
Immutable and Append-Only Flags
Use chflags to protect critical files:
sh# Make system binaries immutable chflags schg /bin/* /sbin/* /usr/bin/* /usr/sbin/* # Make log files append-only chflags sappnd /var/log/auth.log chflags sappnd /var/log/messages
At securelevel 1+, even root cannot remove these flags without a reboot to single-user mode.
Mount Options: noexec, nosuid, nodev
Edit /etc/fstab to restrict what can happen on specific mount points:
shell/dev/ada0p3 /tmp ufs rw,nosuid,noexec,nodev 2 2 /dev/ada0p4 /var ufs rw,nosuid,nodev 2 2 /dev/ada0p5 /var/tmp ufs rw,nosuid,noexec,nodev 2 2 /dev/ada0p6 /home ufs rw,nosuid,nodev 2 2
noexecprevents execution of binaries on the partition. Apply to/tmpand/var/tmpto block a common attack vector where malware is downloaded to a temp directory and executed.nosuidprevents setuid/setgid bits from taking effect, blocking privilege escalation through planted binaries.nodevprevents interpretation of device special files.
7. Service Minimization
Every running service is an attack surface. The principle is simple: if you do not need it, disable it.
Audit Running Services
sh# List all enabled services service -e # List all listening network ports sockstat -l
Disable Common Unnecessary Services
Add to /etc/rc.conf:
sh# Disable services you do not need sendmail_enable="NONE" sendmail_submit_enable="NO" sendmail_outbound_enable="NO" sendmail_msp_queue_enable="NO" # Disable NFS if not used nfs_server_enable="NO" nfs_client_enable="NO" rpcbind_enable="NO" # Disable inetd (legacy service launcher) inetd_enable="NO"
Verify After Disabling
shservice sendmail stop service rpcbind stop # Confirm nothing unexpected is listening sockstat -l4
Review the output of sockstat -l regularly. Any listening port you do not recognize warrants investigation.
8. Automatic Security Updates
FreeBSD provides freebsd-update for binary security patches. Configure it to run automatically.
Enable Automatic Fetch
Add a cron job to fetch updates daily:
shcrontab -e
Add:
shell@daily root freebsd-update cron
The cron subcommand adds a random delay (up to 3600 seconds by default) to prevent all servers from hitting the update servers simultaneously.
Apply Updates Manually (Recommended)
Automatic fetching is safe. Automatic installation is a judgment call. For production servers, review updates before applying:
shfreebsd-update fetch freebsd-update install
Package Updates
For installed packages:
shpkg audit -F # Check for known vulnerabilities pkg upgrade # Apply available updates
Automate the vulnerability check:
shell@daily root pkg audit -F | mail -s "FreeBSD pkg audit" admin@example.com
This sends you a daily report of any installed packages with known CVEs.
9. Audit Framework (auditd)
FreeBSD includes a BSM (Basic Security Module) audit framework that can log security-relevant events at the kernel level. This is more tamper-resistant than application-level logging.
Enable auditd
Add to /etc/rc.conf:
shauditd_enable="YES"
Configure Audit Policy
Edit /etc/security/audit_control:
shelldir:/var/audit dist:off flags:lo,aa,ad,fw,fc,fd,fm minfree:5 naflags:lo,aa policy:cnt,argv filesz:10M expire-after:30d
Flag meanings:
| Flag | Logs |
|------|------|
| lo | Login/logout events |
| aa | Authentication and authorization |
| ad | Administrative actions |
| fw | File write |
| fc | File create |
| fd | File delete |
| fm | File attribute modify |
Start the service:
shservice auditd start
Review Audit Logs
sh# Read the current audit trail in human-readable form praudit /var/audit/current # Search for failed login events praudit /var/audit/current | grep "authentication" | grep "failure"
Audit logs are binary, which makes them harder for an attacker to tamper with than plain text logs. Use praudit to convert them for analysis.
10. Log Monitoring Essentials
Hardening without monitoring is guesswork. You need to know when something abnormal happens.
Key Log Files
| File | Contents |
|------|----------|
| /var/log/auth.log | Authentication events (SSH, su, sudo) |
| /var/log/messages | General system messages |
| /var/log/security | Security events |
| /var/log/pflog | PF firewall log (binary, read with tcpdump) |
Configure Log Rotation
Edit /etc/newsyslog.conf to ensure logs are rotated and retained appropriately:
shell/var/log/auth.log 600 12 1000 * JC /var/log/messages 644 12 1000 * JC /var/log/sudo.log 600 12 1000 * JC
This keeps 12 rotated copies, rotates at 1000 KB, and compresses old logs (J = bzip2, C = create new file after rotation).
Read PF Firewall Logs
shtcpdump -n -e -ttt -r /var/log/pflog
Periodic Security Checks
FreeBSD includes built-in periodic security scripts. Enable them in /etc/periodic.conf:
shdaily_status_security_enable="YES" daily_status_security_pkgaudit_enable="YES" daily_status_security_tcpwrap_enable="YES" daily_status_security_neggrpperm_enable="YES" daily_status_security_passwdless_enable="YES"
These scripts check for passwordless accounts, negative group permissions, package vulnerabilities, and other common misconfigurations. Output is emailed to root by default.
11. Jail Isolation for Services
FreeBSD jails provide operating-system-level virtualization. Running each service in its own jail means a compromise of one service does not give the attacker access to the host or other services. For a complete walkthrough, see our FreeBSD jails guide.
Basic Jail Configuration
Add to /etc/rc.conf:
shjail_enable="YES"
Create /etc/jail.conf:
shell# Global defaults exec.start = "/bin/sh /etc/rc"; exec.stop = "/bin/sh /etc/rc.shutdown"; exec.clean; mount.devfs; allow.raw_sockets = 0; allow.sysvipc = 0; webserver { host.hostname = "web.jail.local"; ip4.addr = "lo1|10.0.0.2/24"; path = "/usr/jails/webserver"; exec.start = "/bin/sh /etc/rc"; exec.stop = "/bin/sh /etc/rc.shutdown"; } database { host.hostname = "db.jail.local"; ip4.addr = "lo1|10.0.0.3/24"; path = "/usr/jails/database"; exec.start = "/bin/sh /etc/rc"; exec.stop = "/bin/sh /etc/rc.shutdown"; }
Create the Jail Filesystem
sh# Create the loopback interface for jail networking sysrc cloned_interfaces="lo1" sysrc ifconfig_lo1="inet 10.0.0.1/24" service netif cloneup # Install base system into jail mkdir -p /usr/jails/webserver bsdinstall jail /usr/jails/webserver
Jail Hardening Principles
- Each service gets its own jail.
- Jails have no raw socket access (
allow.raw_sockets = 0) unless required. - Jails cannot interact with System V IPC unless explicitly allowed.
- Use PF rules to control network access between jails and the outside world.
- Mount the jail's filesystem read-only where possible using
enforce_statfs.
12. Network Hardening
Beyond PF, the kernel has TCP/IP stack tunables that affect security.
TCP Security Tunables
Add to /etc/sysctl.conf:
shell# Enable SYN cookies to resist SYN flood attacks net.inet.tcp.syncookies=1 # Drop TCP connections after fewer retries on half-open connections net.inet.tcp.keepinit=30000 # Reduce TIME_WAIT duration (milliseconds) net.inet.tcp.msl=5000 # Limit the TCP reassembly queue net.inet.tcp.reass.maxsegments=2048 # Enable RFC 1323 TCP extensions (already default, ensures timestamps are on) net.inet.tcp.rfc1323=1
ICMP Controls
shell# Rate limit outgoing ICMP responses net.inet.icmp.icmplim=50 # Disable timestamp responses (prevents uptime fingerprinting) net.inet.icmp.tstamprepl=0 # Disable mask responses net.inet.icmp.maskrepl=0
IPv6 Hardening (If Not Used)
If you do not use IPv6, disable it entirely:
Add to /etc/rc.conf:
shipv6_enable="NO"
Add to /etc/sysctl.conf:
shellnet.inet6.ip6.auto_linklocal=0
If you do use IPv6, apply equivalent hardening: disable router advertisements unless needed, filter ICMPv6 carefully, and treat the IPv6 firewall rules with the same rigor as IPv4.
13. Quick Reference Checklist
Use this condensed checklist to verify your hardening is complete. Each item maps to a section above.
SSH
- [ ] Root login disabled (
PermitRootLogin no) - [ ] Password authentication disabled (
PasswordAuthentication no) - [ ]
AllowUsersconfigured with specific accounts - [ ]
MaxAuthTries 3set - [ ] Unnecessary forwarding disabled
- [ ] fail2ban or PF rate limiting active
Firewall
- [ ] PF enabled in
/etc/rc.conf - [ ] Default deny policy (
block in all,block out all) - [ ] Only required ports open
- [ ] Brute-force table with overload rules
- [ ]
set block-policy dropconfigured
Kernel
- [ ]
security.bsd.see_other_uids=0 - [ ]
net.inet.tcp.blackhole=2 - [ ]
net.inet.udp.blackhole=1 - [ ]
net.inet.icmp.drop_redirect=1 - [ ]
kern.randompid=1 - [ ]
net.inet.tcp.syncookies=1
Filesystem
- [ ] Securelevel set to 1 or higher
- [ ]
/tmpmounted withnoexec,nosuid - [ ] Critical binaries flagged
schg(immutable) - [ ] Log files flagged
sappnd(append-only)
Users
- [ ] Unnecessary accounts locked
- [ ] sudo configured and logging to
/var/log/sudo.log - [ ] Only required users in
wheelgroup
Services
- [ ] sendmail disabled
- [ ] inetd disabled
- [ ] NFS/rpcbind disabled if not needed
- [ ]
sockstat -lshows only expected listeners
Monitoring
- [ ] auditd enabled and configured
- [ ] Log rotation configured
- [ ] Periodic security checks enabled
- [ ]
pkg auditrunning daily
Isolation
- [ ] Public-facing services running in jails
- [ ] Jail-to-host access restricted
14. Frequently Asked Questions
Is FreeBSD more secure than Linux by default?
FreeBSD benefits from a smaller attack surface, an integrated security model (jails, securelevel, Capsicum), and a more conservative approach to default configurations. But "more secure by default" means nothing if you do not harden it. An unhardened FreeBSD box and an unhardened Linux box are both vulnerable -- just to different things.
Should I change the SSH port?
Changing the SSH port reduces log noise from automated scanners by approximately 90%, which makes real attacks easier to spot. It is not a security control. Treat it as a log hygiene measure and never rely on it as your primary defense. See our SSH guide for a complete configuration walkthrough.
What securelevel should I use in production?
Securelevel 2 is appropriate for most production servers. It prevents raw disk writes and direct filesystem modifications while still allowing PF rule changes at boot. Securelevel 3 locks PF rules as well, which is appropriate for high-security environments where firewall changes require a planned reboot.
Can I use iptables on FreeBSD?
No. iptables is Linux-specific. FreeBSD provides three firewall options: PF, IPFW, and IPF. PF is the most widely used and best documented option for FreeBSD. See our PF firewall guide for a full tutorial.
How do I check if my FreeBSD server has known vulnerabilities?
Run pkg audit -F to download the latest vulnerability database and check all installed packages against it. Run freebsd-update fetch to check for base system security patches. Both should run daily via cron.
Do I need fail2ban if PF already has rate limiting?
PF's overload mechanism handles connection-rate abuse well. fail2ban adds application-layer awareness -- it reads log files and can block attackers who cause authentication failures even at low connection rates. The two approaches complement each other. For most servers, PF rate limiting alone is sufficient. If you run services with password-based authentication (which you should avoid where possible), add fail2ban. See our fail2ban on FreeBSD guide for setup instructions.
How often should I reboot after applying updates?
Kernel updates and base system updates from freebsd-update install require a reboot to take effect. Package updates generally do not, though you should restart affected services. Run freebsd-version -k (running kernel) and freebsd-version -u (userland) -- if they differ, a reboot is pending.
Next Steps
Security is not a one-time task. After completing this checklist:
- Schedule quarterly reviews of your firewall rules, user accounts, and listening services.
- Subscribe to the FreeBSD Security Advisories mailing list for timely vulnerability notifications.
- Test your configuration with a port scan from an external host:
nmap -sS -sU -p- your.server.ip. - Consider implementing FreeBSD jails for all public-facing services if you have not already.
A hardened FreeBSD server is one of the most resilient platforms available for hosting production workloads. The time you spend on this checklist pays dividends every day the server runs without incident.