Initial OpenBSD setup and configuration
Table of Contents
Intro#
Having just ordered an OpenBSD VPS which is now online and exposed to the world, I want to lock it down a little by changing the SSH config as well as the firewall rules.
SSH#
I like to start by disabling direct root-login, as well as disabling password
logins. The reason being that there are bots constantly scanning every IPv4
address on the internet, looking for known vulnerabilities I guess, as well as
some bots trying to log in through SSH. By disabling password-logins
you efficiently stop their brute-force attempts on passwords, and if you have doas
set up like you should, there is no reason to log in directly as root either.
If you follow what I am doing here, be a little careful so you don’t inadvertently lock yourself out, or be prepared to log in on the console to fix your mistake. It’s probably a good idea to try the console login before making changes like this to make sure you can recover if something goes bad.
I edit /etc/ssh/sshd_config
with vi
using doas, and find the PermitRootLogin
line. As this is written after the fact, I don’t remember exactly what was necessary
to do, but the end result is that the line should be uncommented and contain the
value no
like this:
PermitRootLogin no
Then I found the PasswordAuthentication
line, uncommented it and again made sure the value was no
,
and finally did the same for the KbdInteractiveAuthentication
line.
I saved the file and restarted sshd to make the changes take effect: doas rcctl restart sshd
Here the fun begins. If I have made any mistakes I could lock myself out of the server. Fortunately the connection was kept, so I let that window be, and in another window tried to log in with SSH again. It worked! If not I could have used the previous window and connection to fix my mistake, but that was not necessary this time.
Firewall#
The next order of business I would say is to close down the firewall. The default rules are quite permissive, as they allow connections to every port as long as they are “correct” and would be able to establish a stateful-connection. TCP packets containing just the F-flag or something similar would not be allowed, as that is not how you establish a TCP connection.
set skip on lo
block return # block stateless traffic
pass # establish keep-state
# By default, do not permit remote connections to X11
block return in on ! lo0 proto tcp to port 6000:6010
# Port build user does not need network
block return out log proto {tcp udp} user _pbuild
The reason I’m putting this second is because OpenBSD doesn’t run many network services by default, so there aren’t many ports open to the internet. On this VM it’s only two, SSH and DHCP.
I have a somewhat static ip-address which I have for months at a time, so I’ve set up SSH to only be available from my address. DHCP only needs outgoing traffic to the best of my knowledge so I only allow that.
I also prefer to simply drop traffic and not return anything to make it seem like the IP of the server is not in use.
If your ip is very dynamic so you need to allow logins from everyone, replace
the ip address with the word any
.
My initial, revised rules then looks like this:
set skip on lo
block drop
pass out on vio0
## SSH
pass in from 192.0.2.2 to any port 22
# To allow ssh for everyone, remove the previous line and add this instead:
# pass in from any to any port 22
# Explicitly allow outgoing dhcp
pass out proto udp from any port 68 to any port 67
# Port build user does not need network
block return out log proto {tcp udp} user _pbuild
These rules so far break IPv6 connectivity, but I will fix next. It took a bit of experimentation though, so I feel it requires some more documentation.
The IP here is not my actual IP, but one made available for documentation purposes. It should not be routed on the internet.
pf
rules follows the “last match wins” paradigm, that is why the block rule
is so high up in the rules. Since I now have a “default block” rule, I removed
the X11 rule for ports 6000-6010 as it is not needed.
Now to allow incoming SSH connections using IPv6 I’ll add a rule like this:
# If I have a /64 subnet on IPv6 which is like one IPv4 address:
pass in proto tcp from 2001:db8:3003:4004::/64 to any port 22
# Or, since IPv6 address space is so large it isn't being scanned:
pass in proto tcp from any to 2001:db8:1001:2002::2 port 22
Now I have two rules to allow incoming tcp to port 22. There’s a way to avoid that by using tables, but I’ll get back to that later.
Having the allow rule for IPv6 is a start, but to actually get IPv6 connectivity working again, or to have it keep working, I’ve come up with these rules:
pass in quick inet6 proto icmp6 from fe80::/64 to ff02::1:ff00:0/104 icmp6-type 135 no state
pass out quick inet6 proto icmp6 from fe80::/64 to ff02::1:ff00:0/104 icmp6-type 135 no state
pass in quick inet6 proto icmp6 from fe80::/64 to fe80::/64 icmp6-type { 135 136 } no state
pass out quick inet6 proto icmp6 to fe80::/64 icmp6-type { 135 136 } no state
pass in quick inet6 proto icmp6 from 2001:db8:1001:2002::1 to 2001:db8:1001:2002::2 icmp6-type 135 no state
pass out quick inet6 proto icmp6 from 2001:db8:1001:2002::2 to 2001:db8:1001:2002::1 icmp6-type 136 no state
Icmp6-type 135 is neighbor solicitation and type 136 is neighbor advertisement. These types are easily
found with man icmp6
, but it just struck me, why do I need to find and input them?
Looking at the rules that are in effect with pfctl -sr
shows “neighbrsol” and “neighbradv” instead
of 135 or 136. I did a quick check and learned I can indeed use the words instead of
the type id, so I’ll be changing my rules to use the words instead from now on.
For some reason pf
and tcpdump
seems to use different abbreviations, so I can’t
simply copy to text from tcpdump unfortunately.
Capturing icmp6 traffic with tcpdump looks something like this:
fe80::my:mac > ff02::1:ff00:1: icmp6: neighbor sol: who has 2001:db8:1001:2002::1
fe80::router:mac > fe80::my:mac: icmp6: neighbor adv: tgt is 2001:db8:1001:2002::1
2001:db8:1001:2002::1 > 2001:db8:1001:2002::2: icmp6: neighbor sol: who has 2001:db8:1001:2002::2
2001:db8:1001:2002::2 > 2001:db8:1001:2002::1: icmp6: neighbor adv: tgt is 2001:db8:1001:2002::2
fe80::router:mac > fe80::my:mac: icmp6: neighbor sol: who has fe80::my:mac
fe80::my:mac > fe80::router:mac: icmp6: neighbor adv: tgt is fe80::my:mac
fe80::my:mac > fe80::router:mac: icmp6: neighbor sol: who has fe80::router:mac
fe80::router:mac > fe80::my:mac: icmp6: neighbor adv: tgt is fe80::router:mac
My understanding of IPv6 is quite limited so I can’t really explain how this works. I have done some research and found that addresses beginning with fe80 are “link local”, and thus aren’t routed on the internet. I could probably be more relaxed with them but I like to close down the firewall as much as possible. For my VM, they seem to contain my MAC address which is something I’m not sure I should give away, so that’s why I’ve removed it.
The address beginning with ff02 is a “solicited-node multicast address”, and I’m just going link to the Wikipedia article about it which is here. I guess it is sort of a broadcast address sent to everyone on the same link to discover who is there, and have them respond with their address.
All of this is part of the neighbor discovery protocol which I don’t really understand, but is needed for IPv6 to work.
The Wikipedia page on the protocol lists other types such as 133 and 134 for router solicitation and router advertisement, but I’ve not seen that on the network and don’t know why that is. Perhaps it’s due to how the provider has configured their network, I don’t know. In any case, that is why my rules only contain types 135 and 136.
The VM I’m using uses DHCP to get its IPv4 address, but while the IPv6 address is assigned statically, it needs this discovery protocol to work. I learned that the hard way by blocking icmp6 completely and while it worked fine for a while, all of a sudden I couldn’t reach the IPv6 address anymore, until I enabled this again.
So to iterate, having a rule allowing incoming traffic to some TCP port is not enough for IPv6. When someone tries to connect, the router will broadcast messages asking who has the destination IP, and if that is not answered, it won’t be possible to connect.
I could maybe use stateful rules (I removed that here by using “no state”) to limit the number of rules needed, but there was so much traffic and I didn’t like seeing it in the state table as it made the other traffic hard to see.
This is definately something I need to get back to. The firewall rules seems to be working for now, but I may have broken something I’ll only find out about later down the line.
Edit 2025-04-28: Yes I had, by only allowing neighbor sol incoming from default gateway, I had blocked neighbor adv which means my VM didn’t get those responses when it asked with “neighbor sol”. I discovered that as I saw my host sending multiple “neighbor sol” messages and the router seemingly responding fine. If my VM had received the answer properly instead of it being dropped by the firewall, my VM should have only asked once.
2001:db8:1001:2002::2 > 2001:db8:1001:2002::1: icmp6: neighbor sol: who has 2001:db8:1001:2002::1
2001:db8:1001:2002::1 > 2001:db8:1001:2002::2: icmp6: neighbor adv: tgt is 2001:db8:1001:2002::1
2001:db8:1001:2002::2 > 2001:db8:1001:2002::1: icmp6: neighbor sol: who has 2001:db8:1001:2002::1
2001:db8:1001:2002::1 > 2001:db8:1001:2002::2: icmp6: neighbor adv: tgt is 2001:db8:1001:2002::1
2001:db8:1001:2002::2 > 2001:db8:1001:2002::1: icmp6: neighbor sol: who has 2001:db8:1001:2002::1
2001:db8:1001:2002::1 > 2001:db8:1001:2002::2: icmp6: neighbor adv: tgt is 2001:db8:1001:2002::1
The fix was to add neighbradv to these rules so I allow both neighbor sol and neighbor adv incoming and outgoing to and from the default gateway:
pass in quick inet6 proto icmp6 from 2001:db8:1001:2002::1 to 2001:db8:1001:2002::2 icmp6-type { neighbrsol neighbradv } no state
pass out quick inet6 proto icmp6 from 2001:db8:1001:2002::2 to 2001:db8:1001:2002::1 icmp6-type { neighbrsol neighbradv } no state
I already have a rule that allows all outgoing traffic, but I like having explicit rules for this as well, in case I should change the “allow all out” rule later.
Now to verify that the syntax of the rules are ok with pfctl -nf /etc/pf.conf
,
and if so, cross fingers, hope for the best and load them with pfctl -f /etc/pf.conf
.
Phew, it worked. I wasn’t really concerned though, as long as my IP is the one being allowed to connect to SSH I should always be able to get back in.
Timezone#
Server time was set to Europe/Amsterdam which isn’t unnatural when you consider its location, but I prefer having time set to UTC to avoid the daylight savings mess. One reason: logs that will log the hour 2 in the morning twice when the clocks are turned back, or skip it when the clocks are turned forward.
Current timezone can be found with ls -l /etc/localtime
. Since it’s a symlink it can
be changed manually to any of the files in /usr/share/zoneinfo
but I use zic:
doas zic -l UTC
Prompt#
I use the default shell in OpenBSD which is ksh. I don’t like the default prompt
though so I change it by setting PS1 like this in ~/.profile
:
PS1="\t \u@\h:\w$ "
The result looks like this if I am in the docs directory in my home directory:
20:34:12 user@obsd-web:~/docs$
In other words: \t is current time, \u is current user, \h is the hostname and \w is current directory, shortened with ~ in front for anything in the home directory.