Synopsis #
FreeBSD normally reserves low TCP and UDP ports for privileged processes. The mac_portacl(4) policy can authorize a numeric user or group ID to bind one specific port without leaving the complete service running as root.
This design changes two composed controls. The ordinary reserved-port range must stop denying the bind, and mac_portacl must then enforce an explicit allowlist over the same range. Disabling the reserved range without an active, verified MAC policy removes the intended protection. An application capable of dropping privilege after opening its socket, or PF redirection from a low port to an unprivileged high port, may be a smaller change.
Choose the privilege boundary first #
Prefer a service’s documented privilege-separation mode when a small root-owned supervisor opens the socket and the worker processes run under a service account. This keeps lifecycle and socket ownership within the application model.
PF redirection can keep the application entirely on a high port, but adds translation and firewall state to the packet path. Treat that as a firewall change and follow Change PF safely on a remote host .
Use mac_portacl when a direct low-port bind by a particular service identity is an explicit requirement. Record that identity, protocol, port, start order, and recovery owner in the security baseline
.
Understand the composed policy #
mac_portacl controls explicitly bound local TCP and UDP ports up to security.mac.portacl.port_high. It does not govern an automatically selected source port when an application binds port 0.
Rules have this syntax:
idtype:id:protocol:port[,idtype:id:protocol:port,...]
idtype is uid or gid; id, port, and group or user identifiers are numeric; and protocol is tcp or udp. Assigning security.mac.portacl.rules replaces the complete rules string, so an existing list must be preserved and reviewed rather than overwritten as if the setting appended one rule.
The related boundaries are:
| Setting | Effect |
|---|---|
security.mac.portacl.enabled | Enforces the policy when nonzero; default is enabled after the module loads |
security.mac.portacl.port_high | Sets the highest port governed by the policy; default is 1023 |
security.mac.portacl.suser_exempt | Allows root to bind protected ports without an ACL entry when nonzero; default is exempt |
security.mac.portacl.rules | Contains the complete numeric TCP and UDP allowlist |
net.inet.ip.portrange.reservedlow and reservedhigh | Separately impose the traditional privileged-port restriction |
MAC policies compose with other access checks; an ACL entry cannot override the reserved-port denial. Conversely, releasing the reserved range is safe only while the intended MAC policy covers that range.
Inventory the current state #
Before loading or changing the module on a production host, record existing low-port listeners and settings:
# sockstat -4 -6 -l
# sysctl net.inet.ip.portrange.reservedlow
# sysctl net.inet.ip.portrange.reservedhigh
# kldstat -n mac_portacl.ko
If the module is already loaded, also record its complete state:
# sysctl security.mac.portacl.enabled
# sysctl security.mac.portacl.port_high
# sysctl security.mac.portacl.suser_exempt
# sysctl security.mac.portacl.rules
The stock FreeBSD kernel includes the MAC framework, and the policy can be loaded as mac_portacl.ko. Loading a policy on a host whose reserved-port range was previously modified can immediately affect non-root binds, so the change belongs in a maintenance window with a console or equivalent recovery path.
Test one qualified rule #
The following example assumes a dedicated account named websvc has numeric UID 1001 and must bind TCP port 80. Confirm the actual account rather than assuming its UID:
$ id -u websvc
1001
Load the policy, establish its boundary and complete rule list, and only then release the separate reserved range:
# kldload mac_portacl
# sysctl security.mac.portacl.enabled=1
# sysctl security.mac.portacl.port_high=1023
# sysctl security.mac.portacl.suser_exempt=1
# sysctl security.mac.portacl.rules=uid:1001:tcp:80
# sysctl net.inet.ip.portrange.reservedlow=0
# sysctl net.inet.ip.portrange.reservedhigh=0
This is a syntax example for one new policy, not an instruction to replace an existing ACL. Multiple requirements belong in the one comma-separated rules value. suser_exempt=1 preserves the default root exemption; setting it to zero also subjects root-owned services to the allowlist and can break boot or recovery services that bind low ports.
Restart only the affected service, then verify the listener’s user, address, protocol, and port:
# sockstat -4 -6 -l
Also confirm that another unprivileged test identity cannot bind the protected port. A successful intended bind without a negative test does not prove that the allowlist is enforcing the boundary.
Make the tested policy persistent #
Load the module during boot with this line in /boot/loader.conf:
mac_portacl_load="YES"
Place the reviewed runtime values in /etc/sysctl.conf so they are applied before normal services start. For the example policy, the relevant assignments are:
security.mac.portacl.enabled=1
security.mac.portacl.port_high=1023
security.mac.portacl.suser_exempt=1
security.mac.portacl.rules=uid:1001:tcp:80
net.inet.ip.portrange.reservedlow=0
net.inet.ip.portrange.reservedhigh=0
The security.mac.portacl.rules value is a runtime sysctl rather than a loader tunable. Keep the module load and the runtime policy in their respective files. Review Make a system setting persistent
before rebooting.
After reboot, verify every saved value and repeat both the intended-bind and denied-bind tests. A running process is not sufficient if it inherited a socket opened before the policy took effect.
Roll back safely #
If the intended or negative test fails, first restore the captured reservedlow and reservedhigh values. That reinstates the prior privileged-port boundary independently of the MAC ACL. Then restore the previous rules, port_high, suser_exempt, and enabled values, and restart the affected service.
Remove the unaccepted lines from /etc/sysctl.conf and mac_portacl_load from /boot/loader.conf if the policy was newly introduced. A clean reboot during the maintenance window confirms that no unrecorded runtime state remains.
Do not respond to a lockout by widening the ACL or setting a larger port_high without identifying the failed access check. Restore the known state, then test the reserved-port and MAC layers separately.