An open, yet tightly guarded wireless network with authpf

As always, there are other ways to configure the security of your wireless network than the one we have just seen. What little protection WEP encryption offers, security professionals tend to agree is barely enough to signal to an attacker that you do not intend to let all and sundry use your network resources.

A different approach appeared one day in my mail as a message from my friend Vegard Engen, who told me he had been setting up authpf. authpf is a user shell which lets you load PF rules on a per user basis, effectively deciding which user gets to do what.

To use authpf, you create users with the authpf program as their shell. In order to get network access, the user logs in to the gateway using ssh. Once the user successfully completes ssh authentication, authpf loads the rules you have defined for the user or the relevant class of users.

These rules, which apply to the IP address which the user logged in from, stay loaded and in force for as long as the user stays logged in via the ssh connection. Once the connection is terminated, the rules are unloaded, and in most scenarios all non-ssh traffic from the user's IP address is denied. With a reasonable setup, only traffic originated by authenticated users will be let through.

Vegard's annotated config follows below. His wireless network is configured without WEP encryption, preferring to handle the security side of things via PF and authpf:

Start with creating an empty /etc/authpf/authpf.conf. It needs to be there for authpf to work, but doesn't actually need any content.

The other relevant bits of /etc/pf.conf follow. First, interface macros:

int_if="sis1"
ext_if="sis0"
wi_if = "wi0"

The use of this address will become apparent later:

auth_web="192.168.27.20"

The traditional authpf table

table <authpf_users> persist 

We could put the NAT part in authpf.rules, but keeping it in the main pf.conf doesn't hurt:

match out on $ext_if from $int_if:network nat-to ($ext_if)

or in pre-OpenBSD 4.7 syntax:

nat on $ext_if from $wi_if:network to any -> ($ext_if)

Redirects to let traffic reach servers on the internal net. These could be put in authpf.rules too, but since they do not actually provide access without pass rules, keeping them here won't hurt anything.

match in on $wi_if proto tcp from any to $myaddr port $tcp_in rdr-to $server
match in on $wi_if proto udp from any to $myaddr port $udp_in rdr-to $server

or in pre-OpenBSD 4.7 syntax:

rdr on $wi_if proto tcp from any to $myaddr port $tcp_in -> $server
rdr on $wi_if proto udp from any to $myaddr port $udp_in -> $server

The next redirect sends all web traffic from non authenticated users to port 80 on $auth_web. In Vegard's setup, this is a web server which displays contact info for people who stumble onto the wireless net. In a commercial setting, this would be where you would put something which could handle credit cards and create users.

match in on on $wi_if proto tcp from ! <authpf_users> port 80 rdr-to $auth_web

or in pre-OpenBSD 4.7 syntax:

rdr on $wi_if proto tcp from ! <authpf_users> to any \
 port 80 -> $auth_web

Also make sure you have the authpf anchor:

anchor "authpf/*"

in pre-OpenBSD 4.7 PF, you need separate anchors in order to activate nat, binat or redirects in authpf:

nat-anchor "authpf/*"
binat-anchor "authpf/*"
rdr-anchor "authpf/*"

On to the filtering rules, we start with a sensible default

block all

Other global, user independent rules would go here. Next for the authpf anchor, we make sure non-authenticated users connecting to the wireless interface get redirected to $auth_web

anchor "authpf/*" in on wi0

pass in on $wi_if inet proto tcp from any to $auth_web \
 port 80 keep state

There are three things we want anyway on the wireless interface: Name service (DNS), DHCP and SSH in to the gateway. Three rules do the trick

pass in on $wi_if inet proto udp from any port 53 keep state

pass in on $wi_if inet proto udp from any to $wi_if port 67

pass in on $wi_if inet proto tcp from any to $wi_if \
 port 22 keep state

Next up, the we define the rules which get loaded for all users who log in with their shell set to /usr/sbin/authpf. These rules go in /etc/authpf/authpf.rules,

int_if = "sis1"
ext_if = "sis0"
wi_if = "wi0"
server = "192.168.27.15"
myaddr = "213.187.n.m"

# Services which live on the internal network 
# and need to be accessible
tcp_services = "{ 22, 25, 53, 80, 110, 113, 995 }"
udp_services = "{ 53 }"
tcp_in = " { 22, 25, 53, 80, 993, 2317, pop3}"
udp_in = "{ 53 }"

# Pass traffic to elsewhere, that is the outside world
pass in on $wi_if inet from <authpf_users> to ! $int_if:network \
   keep state

# Let authenticated users use services on 
# the internal network.

pass in on $wi_if inet proto tcp from <authpf_users> to $server \
  port $tcp_in keep state
pass in on $wi_if inet proto udp from <authpf_users> to $server \
  port $udp_in keep state

# Also pass to external address. This means you can access 
# internal services on external addresses.

pass in on $wi_if inet proto tcp from <authpf_users> to $myaddr \
    port $tcp_in keep state
pass in on $wi_if inet proto udp from <authpf_users> to $myaddr \
    port $udp_in keep state

At this point we have an open net where anybody can connect and get an IP address from DHCP. All HTTP requests from non-authenticated users get redirected to port 80 on 192.168.27.20, which is a server on the internal net where all requests are answered with the same page, which displays contact info in case you want to be registered and be allowed to use the net.

You are allowed to ssh in to the gateway. Users with valid user IDs and passwords get rule sets with appropriate pass rules loaded for their assigned IP address.

We can fine tune this even more by making user specific rules in /etc/authpf/users/$user/authpf.rules. Per user rules can use the $user_ip macro for the user's IP address. For example, if I want to give myself unlimited access, create the following /etc/authpf/users/vegard/authpf.rules:

wi_if="wi0"
pass in on $wi_if from $user_ip to any keep state