Port ACLs on FreeBSD for ZNC

10 May 2018

Overall, I prefer FreeBSD (and BSD in general) to Linux-based operating systems. Ever since I played around with FreeBSD 9, I’ve generally preferred them in a server capacity. That isn’t to say I’d run them on my desktop/laptop machines, as I actually have to get things done, so on those I use macOS with a tweaked cloud working setup that I’ve detailed in the past.

For servers, though, where I can get away with it (read: where I don’t have to use Docker, where I’d use Alpine Linux instead), I use FreeBSD. This includes for my IRC bouncer, which is a small Vultr VPS costing me about $2.50/mo.

One thing that I like to do with IRC bouncers is run them over port 443 - the port commonly known for its association with HTTPS. This is for two reasons:

  1. Accessing the control panel now becomes mad easy - https://whatever
  2. A lot of places have network rules that prevent outbound connections to ports other than 80 and 443, which this bypasses

It’s not foolproof, but it’s ideal.

A long time ago when UNIX systems were actually multi-user, it became prudent to restrict a set of ports to root-only, so that the services could be clearly defined as legitimate to prevent spoofing and other semi-malicious behaviour. At least, I think that’s why. It was about 15 years before I was born and I’ve never had it come up in a job interview.

Linux has something called setcap which allows you to set the capability of a binary. For these distributions, it’s actually pretty easy:

setcap 'cap_net_bind_service=+ep' $(which znc)

FreeBSD (and presumably the other BSDs) have it a little more complex, as usual. Firstly, there’s no setcap functionality within the base kernel. It’s in a module called mac_portacl. Without spending more than fifteen seconds on the wiki I am under the assumption that the mac part refers to mandarory access control, and the portacl is quite literally ‘port access control list’. It kind of suits the functionality. Anyway, it’s not loaded by default and - to be honest - the documentation to actually load it is really annoying to follow if you know little about FreeBSD (or forgot).

One option is pretty easy: kldload mac_portacl will load the kernel extension. You can verify that it’s loaded by running sysctl security.mac.portacl.enabled, to which you should receive the following:

security.mac.portacl.enabled: 1

When you reboot this will helpfully disappear. Persistence requires adding the following to /boot/loader.conf:

mac_portacl_load="YES"

A reboot should allow you to verify that it’s enabled (sysctl security.mac.portacl.enabled), which will then allow you to actually set an ACL. Yes, one (maybe more) reboots and insofar we’ve only loaded the module. Luckily setting it requires no reboots.

Setting port ACLs on FreeBSD functions a bit differently from Linux. The most prominent difference to an end-user (i.e., you) is that it’s per-user, not per-binary. Additionally, you restrict it per-port, as opposed to merely overriding the exemption entirely as Linux does. This means that if you grant the ability for the znc user to run on port 443, any binary run under the znc user will have permission to bind to port 443, but only port 443 (and any unused port above 1024, of course). This set of trade-offs is honestly my preference. Now that I’ve told you why my opinions are correct let’s go on to explaining how to actually use this thing. For this, I’m assuming ZNC was installed via pkg, therefore the user ID is 897.

sysctl security.mac.portacl.rules=uid:897:tcp:443

Let’s break this down:

sysctl is sysctl(8), a utility which allows you to get or set kernel state.

security.mac.portacl.rules is the kernel tunable that is being set.

uid:897:tcp:443 is a trilogy in four parts:

  1. uid indicates that you’re specifying a user ID. You can use gid if you want to assign this ACL to an entire group.
  2. 897 is the user ID.
  3. tcp is the protocol that this ACL applies to. You can use udp as well.
  4. 443 is the port.

One final tuneable to modify is net.inet.ip.portrange.reservedhigh. Setting this tuneable to zero (sysctl net.inet.ip.portrange.reservedhigh=0) will disable the blanket reservation of ports below 1025. Despite what it looks like, the mac_portacl takes over from here.

ZNC has a hardcoded setting in its configuration initialiser that prevents you from setting it to run on ports below 1025. Luckily this doesn’t apply to anything beyond actual configuration creation. If you create a configuration with any random port and then modify the configuration after-the-fact, you can set it to run on port 443.

Finally, you should add these configuration directives into /etc/sysctl.conf, otherwise it’ll vanish again when you reboot.

Easy when there’s proper process, right?