# 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:
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
Restrict Access to Named Users
Only allow specific user accounts to connect:
AllowUsers deployer admin
This is a whitelist. Any user not listed is rejected before authentication is even attempted.
Limit Authentication Attempts and Idle Sessions
MaxAuthTries 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
X11Forwarding 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:
Port 48222
Brute-Force Protection with fail2ban
Install and configure fail2ban to block IPs after repeated failed attempts. See our detailed [fail2ban on FreeBSD](/blog/fail2ban-freebsd/) guide for full setup instructions.
Quick setup:
sh
pkg 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:
sh
service 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](/blog/pf-firewall-freebsd/).
Enable PF
Add to /etc/rc.conf:
sh
pf_enable="YES"
pf_rules="/etc/pf.conf"
pflog_enable="YES"
Minimal Default-Deny Ruleset
Create /etc/pf.conf:
# Macros
ext_if = "vtnet0"
ssh_port = "48222"
tcp_services = "{ 80, 443 }"
# Tables
table 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 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
# 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 drop** silently drops blocked packets instead of sending RST, giving attackers no information.
- **max-src-conn-rate 3/30** limits any single IP to 3 new SSH connections per 30 seconds. Violators are added to the table and blocked entirely.
- **flags S/SA** on TCP pass rules only allows new connections that begin with a proper SYN, blocking malformed packets.
- **scrub in all** normalizes incoming packets, defeating fragmentation-based attacks.
Load and verify:
sh
service pf start
pfctl -f /etc/pf.conf
pfctl -sr # Show active rules
pfctl -ss # Show state table
To view and manage the bruteforce table:
sh
pfctl -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:
# 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=0** prevents non-root users from seeing processes owned by other users. On a shared server, this stops information leakage between accounts.
- **net.inet.tcp.blackhole=2** causes 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=1** does the same for UDP.
- **net.inet.icmp.drop_redirect=1** ignores ICMP redirect messages, which an attacker on your LAN could use to reroute your traffic through their machine.
- **kern.randompid=1** randomizes PID allocation, making it harder for an attacker to predict or target specific process IDs.
Apply without rebooting:
sh
sysctl -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:
sh
pw 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:
sh
pkg install sudo
visudo
Add a restrictive entry:
# 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:
restricted:\
:tc=default:\
:cputime=1h:\
:memoryuse=512m:\
:openfiles=256:\
:maxproc=64:
Rebuild the login database after editing:
sh
cap_mkdb /etc/login.conf
Assign the class to a user:
sh
pw usermod serviceacct -L restricted
Remove Unnecessary Accounts
Review /etc/passwd for accounts you do not need. Lock them:
sh
pw 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:
sh
kern_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:
/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
- **noexec** prevents execution of binaries on the partition. Apply to /tmp and /var/tmp to block a common attack vector where malware is downloaded to a temp directory and executed.
- **nosuid** prevents setuid/setgid bits from taking effect, blocking privilege escalation through planted binaries.
- **nodev** prevents 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
sh
service 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:
sh
crontab -e
Add:
@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:
sh
freebsd-update fetch
freebsd-update install
Package Updates
For installed packages:
sh
pkg audit -F # Check for known vulnerabilities
pkg upgrade # Apply available updates
Automate the vulnerability check:
@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:
sh
auditd_enable="YES"
Configure Audit Policy
Edit /etc/security/audit_control:
dir:/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:
sh
service 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:
/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
sh
tcpdump -n -e -ttt -r /var/log/pflog
Periodic Security Checks
FreeBSD includes built-in periodic security scripts. Enable them in /etc/periodic.conf:
sh
daily_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](/blog/freebsd-jails-guide/) guide.
Basic Jail Configuration
Add to /etc/rc.conf:
sh
jail_enable="YES"
Create /etc/jail.conf:
# 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:
# 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
# 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:
sh
ipv6_enable="NO"
Add to /etc/sysctl.conf:
net.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)
- [ ] AllowUsers configured with specific accounts
- [ ] MaxAuthTries 3 set
- [ ] 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 drop configured
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
- [ ] /tmp mounted with noexec,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 wheel group
Services
- [ ] sendmail disabled
- [ ] inetd disabled
- [ ] NFS/rpcbind disabled if not needed
- [ ] sockstat -l shows only expected listeners
Monitoring
- [ ] auditd enabled and configured
- [ ] Log rotation configured
- [ ] Periodic security checks enabled
- [ ] pkg audit running 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](/blog/secure-ssh-freebsd/) 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](/blog/pf-firewall-freebsd/) 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](/blog/fail2ban-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:
1. Schedule quarterly reviews of your firewall rules, user accounts, and listening services.
2. Subscribe to the [FreeBSD Security Advisories mailing list](https://lists.freebsd.org/subscription/freebsd-security-notifications) for timely vulnerability notifications.
3. Test your configuration with a port scan from an external host: nmap -sS -sU -p- your.server.ip.
4. Consider implementing [FreeBSD jails](/blog/freebsd-jails-guide/) 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.