FreeBSD Security Handbook: Hardening and Best Practices (2026)
FreeBSD ships with a security architecture that most operating systems cannot match: Capsicum capability mode, mandatory access controls in the kernel, the audit subsystem from TrustedBSD, GELI disk encryption, and jails that predate Linux containers by over a decade. The tools are there. The gap is almost always in configuration.
This handbook is the canonical reference for securing a FreeBSD production system. It covers every layer from SSH to disk encryption, with exact commands tested on FreeBSD 14.x and 15.0. It is written for sysadmins who already know their way around a Unix shell and want specific, actionable directives rather than theory.
Each section stands alone. You can read end-to-end for a full hardening pass or jump to a specific control. Cross-references to deeper tutorials are provided throughout.
1. SSH Hardening
SSH is the first service an attacker probes. A misconfigured sshd is an open invitation. The objective is to eliminate password-based authentication entirely, restrict access to named accounts, and harden the protocol configuration.
Key-Only Authentication and Certificate Authority
Disable passwords. Use Ed25519 keys at minimum.
Edit /etc/ssh/sshd_config:
shPermitRootLogin no PasswordAuthentication no KbdInteractiveAuthentication no PubkeyAuthentication yes AuthorizedKeysFile .ssh/authorized_keys # Protocol hardening HostKey /etc/ssh/ssh_host_ed25519_key KexAlgorithms sntrup761x25519-sha512@openssh.com,curve25519-sha256 Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com # Restrict to named users AllowUsers deployer admin # Timeouts and limits LoginGraceTime 30 MaxAuthTries 3 MaxSessions 5 ClientAliveInterval 300 ClientAliveCountMax 2
For organizations managing more than a handful of servers, SSH certificates eliminate the need to distribute authorized_keys files. Generate a CA key, sign host and user keys, and configure sshd to trust the CA:
sh# On the CA host ssh-keygen -t ed25519 -f /etc/ssh/ca_user_key -C "user-ca@example.com" # Sign a user key (valid 52 weeks, for the "admin" principal) ssh-keygen -s /etc/ssh/ca_user_key -I admin@example.com -n admin -V +52w /home/admin/.ssh/id_ed25519.pub
On each server, add to /etc/ssh/sshd_config:
shTrustedUserCAKeys /etc/ssh/ca_user_key.pub
Reload the daemon:
shservice sshd reload
See the full SSH hardening walkthrough at Secure SSH on FreeBSD.
2. PF Firewall
PF is the recommended firewall on FreeBSD. It is compiled into the GENERIC kernel and provides stateful filtering, NAT, rate limiting, and table-driven blocking in a readable syntax.
Default-Deny Ruleset
Enable PF:
shsysrc pf_enable="YES" sysrc pflog_enable="YES"
Create /etc/pf.conf:
sh# Macros ext_if = "vtnet0" tcp_services = "{ ssh, http, https }" icmp_types = "{ echoreq, unreach }" # Tables table <bruteforce> persist # Options set skip on lo0 set block-policy drop set loginterface $ext_if # Normalization scrub in on $ext_if all fragment reassemble # Default deny block in all block out all # Allow outbound pass out on $ext_if proto tcp from ($ext_if) modulate state pass out on $ext_if proto udp from ($ext_if) keep state pass out on $ext_if proto icmp from ($ext_if) keep state # Allow inbound services pass in on $ext_if proto tcp to ($ext_if) port $tcp_services keep state # SSH brute-force protection pass in on $ext_if proto tcp to ($ext_if) port ssh \ flags S/SA keep state \ (max-src-conn 10, max-src-conn-rate 5/60, \ overload <bruteforce> flush global) # Block brute-force offenders block in quick on $ext_if from <bruteforce> # ICMP pass in on $ext_if inet proto icmp icmp-type $icmp_types keep state
Load the ruleset:
shservice pf start pfctl -f /etc/pf.conf
Expire stale entries from the bruteforce table with a cron job:
sh# /etc/cron.d/pf-expire */30 * * * * root /sbin/pfctl -t bruteforce -T expire 86400
For a complete PF tutorial, see PF Firewall on FreeBSD. For a comparison with IPFW, see the dedicated guide. For fail2ban integration, see Fail2Ban on FreeBSD.
3. Securelevel
FreeBSD's kern.securelevel is a kernel state variable that restricts what even root can do. Once raised, it cannot be lowered without rebooting into single-user mode.
| Level | Behavior |
|-------|----------|
| -1 | Insecure mode. No restrictions. Default at boot. |
| 0 | No restrictions beyond normal security policies. |
| 1 | Secure mode. Immutable and append-only file flags cannot be removed. /dev/mem and /dev/kmem are read-only. Kernel modules cannot be loaded. |
| 2 | Highly secure mode. Adds: disks cannot be opened for writing (except by mounted filesystems). Time cannot be set backwards by more than one second. |
| 3 | Network secure mode. Adds: PF rules and NAT tables cannot be modified. |
Set the securelevel in /etc/rc.conf:
shsysrc kern_securelevel_enable="YES" sysrc kern_securelevel="2"
When to use which level:
- Level 1 -- suitable for most production servers. Prevents kernel module loading and protects immutable flags. Use this as your baseline.
- Level 2 -- appropriate for servers where disk writes should be restricted and time integrity matters (financial, logging infrastructure).
- Level 3 -- use on dedicated firewall appliances where PF rules must never change at runtime.
Before raising securelevel, mark critical files as immutable:
shchflags schg /boot/kernel/kernel chflags schg /sbin/init chflags schg /etc/rc.conf
At securelevel 1 or above, even root cannot clear these flags.
4. MAC Framework
FreeBSD's Mandatory Access Control framework (TrustedBSD MAC) allows fine-grained, policy-driven access control that overrides traditional Unix DAC permissions. MAC policies are loaded as kernel modules.
mac_bsdextended -- File System Firewall
mac_bsdextended implements a rule-based system analogous to a firewall but for filesystem access. Rules are managed with ugidfw.
shkldload mac_bsdextended sysrc kld_list+="mac_bsdextended"
Example: prevent the www user from accessing anything outside /usr/local/www:
shugidfw add subject uid www object not path /usr/local/www mode n
List active rules:
shugidfw list
mac_portacl -- Port Access Control
mac_portacl restricts which users or groups can bind to privileged ports (below 1024), allowing non-root daemons to bind to port 80 or 443 directly.
shkldload mac_portacl sysrc kld_list+="mac_portacl"
Allow the www user (UID 80) to bind to ports 80 and 443:
shsysctl security.mac.portacl.rules=uid:80:tcp:80,uid:80:tcp:443
Persist in /etc/sysctl.conf:
shsecurity.mac.portacl.rules=uid:80:tcp:80,uid:80:tcp:443 security.mac.portacl.port_high=1023
Other MAC Modules
- mac_seeotheruids -- prevents users from seeing processes owned by other users. Useful on shared hosting.
- mac_partition -- partitions process visibility by label.
- mac_biba -- integrity-based policy (Bell-LaPadula variant). Complex to deploy but powerful for high-security environments.
Load mac_seeotheruids for process isolation:
shkldload mac_seeotheruids sysrc kld_list+="mac_seeotheruids" sysctl security.mac.seeotheruids.enabled=1
5. Audit Subsystem
FreeBSD includes the BSM (Basic Security Module) audit subsystem from the TrustedBSD project. It records security-relevant events to a binary trail that can be analyzed with praudit and auditreduce.
Configuration
Enable the audit daemon:
shsysrc auditd_enable="YES"
Edit /etc/security/audit_control:
sh# Audit directory dir:/var/audit # Flags: lo=login/logout, aa=authentication, fw=file writes, ex=exec flags:lo,aa,fw,ex # Minimum free disk space before warning (percentage) minfree:5 # Audit policy policy:cnt,argv,arge # Trail file size limit (bytes) before rotation filesz:10M # Expire trails older than this expire-after:90d
Start the daemon:
shservice auditd start
Reading Audit Trails
Binary trails are stored in /var/audit/. Use praudit to render them human-readable:
sh# Read the current trail praudit /var/audit/current # Filter: show only failed login attempts auditreduce -c lo -m AUE_openssh /var/audit/current | praudit # Filter: all exec events by a specific user auditreduce -u admin /var/audit/current | praudit -x
Per-User Audit Policies
Override flags per user in /etc/security/audit_user:
sh# User:always-audit:never-audit root:lo,aa,ex,fw:no www:fw,ex:no
Rotate trails with audit -n or let newsyslog handle it by adding an entry in /etc/newsyslog.conf.d/audit.conf.
6. Jail Isolation
Jails are FreeBSD's native containment mechanism. They provide process, filesystem, and network isolation without the complexity of a hypervisor. Every internet-facing service should run in its own jail.
Security Benefits
- A compromised service inside a jail cannot access the host filesystem, host network stack, or other jails.
- Jails have their own user namespace. Root inside a jail is not root on the host.
- With VNET, each jail gets its own full network stack including routing table and firewall rules.
rctlenforces CPU, memory, and process count limits per jail.
VNET Jail Example
Create a jail with its own network stack:
sh# /etc/jail.conf webapp { host.hostname = "webapp.jail"; path = "/jails/webapp"; vnet; vnet.interface = "epair0b"; exec.prestart = "ifconfig epair0 create up"; exec.prestart += "ifconfig bridge0 addm epair0a"; exec.created = "ifconfig epair0b vnet webapp"; exec.start = "/bin/sh /etc/rc"; exec.stop = "/bin/sh /etc/rc.shutdown"; exec.poststop = "ifconfig epair0 destroy"; mount.devfs; devfs_ruleset = 4; enforce_statfs = 2; children.max = 0; securelevel = 2; allow.raw_sockets; }
Resource Limits with rctl
Enable rctl in the boot loader:
sh# /boot/loader.conf kern.racct.enable=1
Set limits:
sh# Max 512MB RAM, 50% CPU, 100 processes rctl -a jail:webapp:memoryuse:deny=512M rctl -a jail:webapp:pcpu:deny=50 rctl -a jail:webapp:maxproc:deny=100
Persist rules in /etc/rctl.conf:
shjail:webapp:memoryuse:deny=512M jail:webapp:pcpu:deny=50 jail:webapp:maxproc:deny=100
For a comprehensive jails walkthrough, see FreeBSD Jails Guide.
7. Disk Encryption
GELI Full Disk Encryption
GELI is FreeBSD's block-level disk encryption framework. It supports AES-XTS with up to 256-bit keys, multiple key slots, and key files.
Initialize and attach an encrypted partition:
sh# Generate a key file dd if=/dev/random of=/root/geli.key bs=64 count=1 # Initialize the provider geli init -b -s 4096 -e AES-XTS -l 256 -K /root/geli.key /dev/da1 # Attach (prompts for passphrase) geli attach -k /root/geli.key /dev/da1 # Create filesystem newfs -U /dev/da1.eli
For boot-time unlock, add to /etc/rc.conf:
shsysrc geli_devices="da1" sysrc geli_da1_flags="-k /root/geli.key"
Encrypted Swap
Encrypted swap prevents sensitive memory contents from persisting on disk:
sh# /etc/fstab /dev/da0s1b.eli none swap sw,ealgo=AES-XTS,keylen=256,sectorsize=4096 0 0
This uses one-time keys generated at each boot -- data from previous sessions is irrecoverable.
ZFS Native Encryption
ZFS native encryption (available since OpenZFS 2.0 on FreeBSD 13+) encrypts at the dataset level:
sh# Create an encrypted dataset zfs create -o encryption=aes-256-gcm -o keylocation=prompt -o keyformat=passphrase zroot/encrypted # Load key at boot (interactive) zfs load-key zroot/encrypted # Or use a key file zfs create -o encryption=aes-256-gcm -o keylocation=file:///root/zfs.key -o keyformat=raw zroot/encrypted
ZFS encryption is transparent to applications and supports key rotation:
shzfs change-key -o keyformat=passphrase zroot/encrypted
Choose GELI vs. ZFS encryption: Use GELI for full-disk encryption including the root filesystem. Use ZFS native encryption for selective dataset encryption on an already-running system, or when you need per-dataset keys and do not need to encrypt the boot partition.
8. sysctl Security Tunables
The following sysctl values should be set on every production FreeBSD system. Add them to /etc/sysctl.conf:
sh# Drop TCP packets to closed ports silently (no RST) net.inet.tcp.blackhole=2 # Drop UDP packets to closed ports silently (no ICMP unreachable) net.inet.udp.blackhole=1 # Randomize PID assignment kern.randompid=1 # Disable core dumps for setuid binaries kern.sugid_coredump=0 # Prevent users from seeing other users' processes security.bsd.see_other_uids=0 security.bsd.see_other_gids=0 # Restrict unprivileged users from reading kernel message buffer security.bsd.unprivileged_read_msgbuf=0 # Prevent unprivileged users from seeing jail information security.bsd.unprivileged_proc_debug=0 # Disallow unprivileged processes from using process debugging security.bsd.unprivileged_proc_debug=0 # Clear /tmp at boot kern.init_shutdown_timeout=120 # SYN cookies to mitigate SYN floods net.inet.tcp.syncookies=1 # Limit ICMP redirects net.inet.icmp.drop_redirect=1 net.inet.ip.redirect=0 # Disable source routing net.inet.ip.sourceroute=0 net.inet.ip.accept_sourceroute=0 # Strengthen TCP sequence numbers net.inet.tcp.always_keepalive=1
Clear /tmp at boot via /etc/rc.conf:
shsysrc clear_tmp_enable="YES"
Apply without reboot:
shsysctl -f /etc/sysctl.conf
9. User and Group Hardening
Password Policies via login.conf
Edit /etc/login.conf to enforce password requirements for the default class:
shdefault:\ :passwd_format=blf:\ :minpasswordlen=16:\ :mixpasswordcase=true:\ :passwordtime=90d:\ :warnpassword=14d:\ :umask=027:\ :priority=0:\ :tc=auth-defaults:
Rebuild the login database:
shcap_mkdb /etc/login.conf
Lock Down Root
Root should never log in directly. Use su or doas/sudo from a named account.
sh# Ensure root's shell is /usr/sbin/nologin for remote access # (keep /bin/sh for single-user mode via console) pw usermod root -s /usr/sbin/nologin
doas -- Minimal Privilege Escalation
doas is simpler and has a smaller attack surface than sudo. Install and configure:
shpkg install doas
Create /usr/local/etc/doas.conf:
sh# Allow wheel group to run commands as root, require password permit persist :wheel # Allow deployer to restart nginx without password permit nopass deployer as root cmd /usr/sbin/service args nginx restart # Deny all others deny :operator
Set permissions:
shchmod 0600 /usr/local/etc/doas.conf
Account Lockout
Use pam_tally or configure pam_unix to lock accounts after repeated failures. Edit /etc/pam.d/system:
shauth required pam_passwdqc.so min=disabled,disabled,disabled,16,16 enforce=users account required pam_login_access.so
Restrict which users and hosts can log in via /etc/login.access:
sh# Allow wheel members from anywhere +:wheel:ALL # Allow admin from specific subnet only +:admin:192.168.1.0/24 # Deny everyone else -:ALL:ALL
10. Automatic Security Updates
freebsd-update for Base System
Run freebsd-update as a cron job to fetch and install security patches:
shsysrc freebsd_update_enable="YES"
Create a cron entry:
sh# /etc/cron.d/freebsd-update @daily root /usr/sbin/freebsd-update cron
freebsd-update cron introduces a random delay (up to 3600 seconds) to avoid thundering herd on the update servers. To auto-install patches:
sh@daily root /usr/sbin/freebsd-update cron fetch install && service sshd restart
On critical servers, fetch and review before installing:
sh# Fetch only freebsd-update fetch # Review what will change freebsd-update updatesready # Install after review freebsd-update install
pkg audit for Third-Party Packages
Audit installed packages against the VuXML vulnerability database:
shpkg audit -F
Run this daily:
sh# /etc/periodic.conf daily_status_security_pkgaudit_enable="YES"
FreeBSD's periodic security scripts (run daily by default) will include pkg audit output in the daily security email if this is enabled.
Automated Package Updates
For non-critical systems, automate package updates:
shpkg upgrade -y
For production, use a staged approach:
sh# In a test jail or staging environment pkg upgrade -n # Dry run first pkg upgrade -y # Run test suite # If clean, repeat on production
Combine with AIDE file integrity monitoring to detect unauthorized changes between update cycles.
11. Incident Response Basics
When a breach is suspected, the order of operations matters. Preserve evidence first, contain second, remediate third.
Immediate Containment
Block the attacker's known IP addresses at the firewall:
shpfctl -t bruteforce -T add 203.0.113.50
If the compromise is severe, isolate the machine from the network but keep it powered on to preserve volatile evidence:
shifconfig vtnet0 down
Evidence Preservation
Capture volatile state before anything else:
sh# Running processes ps auxww > /root/incident/ps_$(date +%Y%m%d_%H%M%S).txt # Network connections sockstat -l > /root/incident/sockstat_$(date +%Y%m%d_%H%M%S).txt # Logged-in users w > /root/incident/users_$(date +%Y%m%d_%H%M%S).txt # Kernel modules kldstat > /root/incident/kldstat_$(date +%Y%m%d_%H%M%S).txt # Active PF state table pfctl -ss > /root/incident/pfstate_$(date +%Y%m%d_%H%M%S).txt
Log Preservation
Copy logs to a separate, trusted system immediately:
shtar czf /root/incident/logs_$(date +%Y%m%d).tar.gz \ /var/log/auth.log \ /var/log/messages \ /var/log/security \ /var/audit/ \ /var/log/utx.log
If audit trails are active, close the current trail and start a fresh one:
shaudit -n
Forensic Analysis
Use auditreduce to narrow down the audit trail to the timeframe of interest:
sh# Events from a specific time range auditreduce -a 20260409120000 -b 20260409180000 /var/audit/* | praudit -x # Events by a specific user auditreduce -u compromised_user /var/audit/* | praudit # All exec events (look for unexpected commands) auditreduce -c ex /var/audit/* | praudit
Check for rootkits and unexpected modifications:
sh# Compare installed packages against known checksums pkg check -s -a # Look for setuid/setgid binaries that should not exist find / -perm -4000 -o -perm -2000 -type f -exec ls -la {} \; # Check for unauthorized cron jobs ls -la /var/cron/tabs/ crontab -l -u root
Recovery
After evidence is preserved and analyzed:
- Rebuild the system from known-good media or a trusted image. Do not attempt to "clean" a compromised system.
- Restore data (not binaries) from backups predating the compromise.
- Rotate all credentials -- SSH keys, database passwords, API tokens, TLS certificates.
- Apply the patches or configuration changes that prevented the initial vector.
- Monitor closely for re-compromise.
For a broader hardening strategy, see Hardening a FreeBSD Server.
12. Frequently Asked Questions
Is FreeBSD more secure than Linux?
FreeBSD has several structural advantages: jails predate Linux containers and were designed as a security boundary from the start; the MAC framework provides kernel-enforced mandatory access controls; Capsicum offers capability-mode sandboxing that no Linux equivalent fully matches; and the base system is maintained as a single project with consistent code review. However, security depends on configuration. A hardened Linux system can be more secure than a default FreeBSD installation. The tools covered in this guide are what close that gap.
Should I use securelevel 3 on all servers?
No. Securelevel 3 prevents modification of PF rules at runtime, which means you cannot update firewall rules without rebooting into single-user mode. This is appropriate for dedicated firewall appliances but impractical for general-purpose servers where you need to adjust rules in response to incidents. Start with securelevel 1 or 2 for most workloads.
Can jails replace a full firewall?
Jails provide process and filesystem isolation, not network filtering. A jail with VNET gets its own network stack, but you still need PF or IPFW rules inside or outside the jail to filter traffic. Jails and firewalls are complementary layers, not substitutes.
How do I audit what happens inside a jail?
The host's audit subsystem captures events from all jails. Use auditreduce with the -j flag to filter events by jail ID:
shauditreduce -j 3 /var/audit/current | praudit
You can also run a separate auditd instance inside a jail with its own audit_control configuration, though this requires the allow.audit jail parameter.
Summary
A properly secured FreeBSD system stacks independent controls at every layer:
| Layer | Controls |
|-------|----------|
| Authentication | SSH key-only, AllowUsers, certificates, doas |
| Network | PF default-deny, rate limiting, brute-force tables |
| Kernel | Securelevel, sysctl tunables, MAC policies |
| Process | Jails with VNET, rctl resource limits |
| Storage | GELI or ZFS encryption, encrypted swap |
| Monitoring | auditd, pkg audit, file integrity checks |
| Updates | freebsd-update cron, periodic pkg audit |
| Response | Evidence preservation, log analysis, rebuild procedures |
No single measure is sufficient. Each layer exists so that when one fails -- and eventually one will -- the others hold.
Start with the controls that match your threat model. For most internet-facing servers, the highest-impact changes are: SSH key-only authentication, PF with default-deny, securelevel 1, process isolation with jails, and automated security updates. Everything else in this guide builds on that foundation.