Guides
OpenBSD desktop hardening guide
This is a guide on how to harden OpenBSD, and is dedicated to desktop users, although information here may be useful to anyone. This guide is just a copy and paste of my own setup, with a few minor modifications.
This guide is not intended to show any configuration related to OPSEC and anonymity, this will be dedicated to another guide.
Hardening on OpenBSD is not necessary for most users, but for those who want to make their system even more secure. The hardening process is extremely easy compared to any other operating system I've ever used, requiring minimal configuration, and that is because OpenBSD is an secure OS by default.
Let's keep this as KISS as possible.
Privilege escalation
In order to prevent a program to escalate privileges, remove yourself from the wheel group, and don't set any doas or sudo permission.
If you need root privileges, switch to a TTY using the root user or
(even better) only allow your user as root for specific and restricted
commands, and escalate privileges using doas, this can be
configured in /etc/doas.conf.
/etc/doas.conf
Replace "flywithme" with the name of the user you want to allow. That's the setting I use, but you might want to add or remove some things.
I do not recommend allowing pkg_add, as this can be used
to install malicious applications, and not necessarily applications from
the ports tree but also local malware. Do not add anything that allows
you to change system files, especially the firewall settings and the
doas.conf itself.
This setting should be as restricted as possible.
# Allow reboot and halt without password
# Warning: this will allow Denial of Service (DoS)
permit nopass flywithme as root cmd reboot
permit nopass flywithme as root cmd halt
# Minimal tor and i2p management
permit persist flywithme as root cmd rcctl args check i2p
permit persist flywithme as root cmd rcctl args restart i2p
permit persist flywithme as root cmd rcctl args restart tor
permit persist flywithme as root cmd rcctl args stop i2p
permit persist flywithme as root cmd rcctl args stop tor
# For virtual machines
permit persist flywithme as root cmd rcctl args restart vmd
permit persist flywithme as root cmd rcctl args stop vmd
permit persist flywithme as root cmd vmctl
# For system update/upgrade
permit persist flywithme as root cmd syspatch
permit persist flywithme as root cmd sysupgrade
permit persist flywithme as root cmd sysmerge
# Privilege separation for building ports (uncomment if you need it)
#permit keepenv nopass flywithme as _pbuild
#permit keepenv nopass flywithme as _pfetch
# Flysunaba
permit nopass divaythfyr as fsunaba
permit nopass divaythfyr as fsunaba-amnesic
Multi-user hardening
The permissions of the user directory should be 700, so only the owner and root could browse it.
Ideally, you should add umask 077 to your user
environment (often ~/.profile), so every new directory or file
permissions will be restricted to your user only. But umask
077 doesn’t change existing files.
To set existing regular files to 600 and directories to 700:
find /home/user -type f -exec chmod 600 {} +
find /home/user -type d -exec chmod 700 {} +
This removes execute permission from existing files, including scripts and programs. If you need those to remain executable, don’t run the first command blindly or add the permissions again later.
Firewall
We need a good firewall, and this is probably the most important part, all users should dedicate their time studying how to configure the firewall of their systems.
That's the setting I use. Block everything by default and clearly block out what's essential, then allows only what is necessary.
Adapt configuration variables to suit your system.
/etc/pf.conf
# DEFINITION RULES
# Put your network interface here
ext_if = "egress"
# For virtual machines, comment if you don't use it
dns_server = "127.0.0.1"
vm_if = "tap0"
## Set "100.64.1.2/31" for more range if you need it
vm_ip = "100.64.1.3"
# Put users you want to allow later
# Put _pkgfetch and _syspatch to allow
# pkg_* and syspatch commands
allowed_users = "{ flywithme, theo-de-raadt, _pkgfetch, _syspatch }"
# Put users you want to block later
#blocked_users = "{ joanna, linus, stallman }"
#--------------------------------------------------------------
# PRE BLOCK RULES
## Silently discard blocked packets
set block-policy drop
## Exempt loopback from PF filtering
set skip on lo0
## Packet-normalization rules
match in all scrub (no-df random-id max-mss 1440 reassemble tcp)
match out all scrub (random-id)
## Reject locally implausible source addresses
antispoof quick for $ext_if
## NAT for the VM
match out on $ext_if from $vm_ip to any nat-to ($ext_if)
## Redirect VM DNS to the host resolver
pass in quick proto {udp tcp} from $vm_ip to any port domain \
rdr-to $dns_server port domain
#--------------------------------------------------------------
# BLOCK RULES
## Block everything (in/out, any port) by default
block all
## By default, do not permit remote connections to X11
block return in quick on ! lo0 proto tcp to port 6000:6010
## Port build user does not need network
block return out quick log proto {tcp udp} user _pbuild
## Block users you want, change "bob" to the real name
## This blocks both ipv4 (inet) and ipv6 (inet6), specify
## with "proto inet" or "proto inet6" if you want
block out quick on egress proto {tcp udp} user stallman
#block out quick on egress inet proto {tcp udp} user joanna
#block out quick on egress inet6 proto {tcp udp} user joanna
## Block out all the users that you set up at the beginning
#block out quick on $ext_if proto { udp tcp } user $blocked_users
#--------------------------------------------------------------
# PASS RULES
## Pass out _unwind user for DNS resolving
## Comment if you want DNS-over-TLS only and no plaintext-DNS fallback
pass out on $ext_if proto { udp tcp } to port domain user _unwind
## Comment if you didn't choose DNS-over-TLS (see topic 10)
pass out quick on $ext_if inet proto tcp to \
{ 9.9.9.9, 149.112.112.112 } port 853 user _unwind
pass out quick on $ext_if inet6 proto tcp to \
{ 2620:fe::fe, 2620:fe::9 } port 853 user _unwind
## Allow VM traffic
pass in quick on $vm_if from $vm_ip to any
pass out quick on $ext_if received-on $vm_if
## Pass out specific user to ssh into VM
pass out quick on $vm_if inet proto tcp \
from self to $vm_ip port ssh \
user flywithme
### Same thing as before
### (Idk why, but this is necessary if you use securelevel=2)
pass out quick on tap0 inet proto tcp \
from 100.64.1.2 to 100.64.1.3 port 22 \
user flywithme
## Permit SLAAC (if you want IPv6)
pass in quick on $ext_if inet6 proto udp from \
fe80::/10 port dhcpv6-server to \
fe80::/10 port dhcpv6-client no state
## Pass out NTP for time synchronization
pass out quick on $ext_if proto { udp tcp } user _ntp
## Pass out the users that you set up at the beginning
pass out quick on $ext_if proto { udp tcp } user $allowed_users
Ensure pf is enabled and then load the rules:
# pfctl -e
# pfctl -f /etc/pf.conf
Disabling USB ports
Root is already required to mount a filesystem, this protects against ordinary users mounting USB storage, but it doesn’t make a plugged-in USB device harmless. A device could pretend to be a keyboard or network adapter, or target a bug in a USB driver, and neither needs to mount a disk.
If you need to protect your system from malicious USB devices (usually in an office environment), you should disable them in the BIOS/Firmware if possible. If it's not possible, then you could still disable the kernel drivers at boot time using this method.
Create the file /etc/bsd.re-config and add the content to it:
disable usb
disable xhci
This will disable the support for USB 3 and 2 controllers. On a desktop computer, you may want to use PS/2 peripherals in these conditions.
/etc/sysctl.conf
There's not much to do here. The OpenBSD malloc system allows you to enable some extra checks, like use after free, heap overflow or guard pages, they can be all enabled at once.
ddb.console: Controls whether keyboard console buttons can drop the system into the kernel debugger (DDB). Disabling this prevents local users from bypassing authentication.
ddb.panic: drops into DDB when the kernel panics. If ddb.panic is enabled, a panicked unattended machine may stay stopped in the debugger instead of continuing to reboot.
net.inet.ip.redirect: Controls whether the kernel accepts ICMP redirect packets. Disabling this prevents network redirects and MITM route hijackings.
# Memory allocator hardening
# S = Enable all options suitable for security auditing
# read "man malloc" for further information
vm.malloc_conf=S
# Disable Console Kernel Debugger Access
ddb.console=0
ddb.panic=0
# Block Route Redirect Attacks. This may break things
net.inet.ip.redirect=0
net.inet6.ip6.redirect=0
# UNSAFE
# IP forwarding (necessary for virtual machines)
#net.inet.ip.forwarding=1
Restart the system for the changes to be applied, or apply them
manually with the sysctl command.
Securelevel
The OpenBSD kernel provides four levels of system security, the default one is "1 Secure mode". Securelevel provides convenient means of “locking down” a system to a degree suited to its environment. It is possible to increase to level 2, which is the maximum.
$ man securelevel
[...]
1 Secure mode
- default mode when system is multi-user
- securelevel may no longer be lowered except by init
- /dev/mem and /dev/kmem cannot be opened
- raw disk devices of mounted file systems are read-only
- system immutable and append-only file flags may not be removed
- the hw.allowpowerdown, kern.allowkmem, kern.utc_offset,
net.inet.ip.sourceroute, and machdep.kbdreset sysctl(8)
variables may not be changed
- the ddb.console, ddb.panic, and machdep.allowaperture sysctl(8)
variables may not be raised
- gpioctl(8) may only access GPIO pins configured at system
startup
2 Highly secure mode
- all effects of securelevel 1
- raw disk devices are always read-only whether mounted or not
- settimeofday(2) and clock_settime(2) may not set the time
backwards or close to overflow
- pf(4) filter and NAT rules may not be altered
[...]
Highly secure mode may seem Draconian, but is intended as a last line of
defence should the superuser account be compromised. Its effects
preclude circumvention of file flags by direct modification of a raw disk
device, or erasure of a file system by means of newfs(8). Further, it
can limit the potential damage of a compromised “firewall” by prohibiting
the modification of packet filter rules. Preventing the system clock
from being set backwards aids in post-mortem analysis and helps ensure
the integrity of logs. Precision timekeeping is not affected because the
clock may still be slowed.
Because securelevel can be modified with the in-kernel debugger ddb(4), a
convenient means of locking it off (if present) is provided at
securelevels 1 and 2. This is accomplished by setting ddb.console and
ddb.panic to 0 with the sysctl(8) utility.
[...]
If you don't need to change the firewall rules constantly, this hardening may be good for you, but it is especially useful for dedicated firewalls. I don't see many advantages of this for a desktop user.
/etc/rc.securelevel
sysctl ddb.console=0
sysctl ddb.panic=0
sysctl kern.securelevel=2
Then reboot. You can also apply the change instantly with:
# sysctl kern.securelevel=2
But without the /etc/rc.securelevel, after rebooting the
system the securelevel returns to level 1.
/etc/fstab
By default in OpenBSD only the /usr/local partition is
mounted with the wxallowed parameter.
wxallowed is an exception to OpenBSD’s W^X protection:
normally, programs can’t use memory that is both writable and executable.
That helps stop some code-injection attacks.
/usr/local contains third-party packages, and some need
that exception to work. The rest of the system is kept without it to
preserve the protection wherever possible. So the setting doesn’t mean
everything under /usr/local is unsafe, it means programs
there are allowed to request writable-and-executable memory.
If you want to be more secure by accepting that this can and probably
will bring future incompatibility, edit the /etc/fstab file
and remove the wxallowed parameter from the
/usr/local line:
[...]
/usr/local ffs rw,nodev 1 2
[...]
It is necessary to reboot the system for the configuration to be
applied, and then check with mount.
$ mount
[...]
/dev/sd2h on /usr/local type ffs (local, nodev)
[...]
This will completely break the I2P (java) daemon/program, I don't know what other programs this can break.
Immutable files
To make any file be immutable, you can use chflags:
# chflags schg /path/to/file
schg makes the file immutable: it can’t be changed,
moved, or deleted even by root until the flag is cleared. Check it with
ls -lo. You can clear it with:
# chflags noschg /path/to/file
schg can be cleared only when OpenBSD is at security
level 0 or -1. If the system has raised its security level, changing it
may require booting into single-user mode.
What to protect? Usually, nothing essential:
immutable flags can interfere with updates and normal system tasks.
uchg can be useful for a specific file you rarely change,
such as a carefully reviewed configuration snapshot. Avoid recursively
flagging /etc, /var, or the whole system. These
flags are an extra obstacle, in case of a compromised root user.
In my opinion, this tool has its greatest usefulness when it comes to logs.
For active logs, OpenBSD’s append-only flag is what you want. Logging can continue, but existing log contents can’t be edited or deleted.
# chflags sappnd /var/log/authlog
But this breaks normal log rotation: newsyslog needs to rename and
replace log files. You’d have to clear the flag for rotation, then set it
again. Don’t apply it to all logs unless you’ve planned and tested that
process. Use sappnd for active log files that need to grow,
and schg for archived logs that should never change.
Read the complete guide on this, where it shows you to set up this process automatically: Why ISO 27001 Demands Immutable Logs (Without Actually Saying So)
For stronger protection against someone with root access, sending logs to a separate machine is a better setup.
Locking single-user mode
OpenBSD allows single-user mode without asking for the root password.
With insecure, it prompts for the root password before
giving access to the single-user shell.
Edit the console entry: /etc/ttys, then find the entry
for the actual system console (which is the first line:
console) and change its final flag from secure
to insecure.
console "/usr/libexec/getty std.9600" vt220 off insecure
This hardening is not very useful unless an attacker gets physical access to your device after decryption (if your disk is encrypted) and during the boot process. This setting can also prevent legitimate console recovery if the root password is unavailable, so keep a tested recovery route.
Unwind for DNS resolving
This one can be good for privacy, anonymity (if you forward over Tor) and/or security. It all depends on how you set it up.
/etc/unwind.conf
Here you can go with 4 options:
1- No custom configuration. Unwind uses its built-in defaults. It can resolve recursively itself and can also use resolvers learned through network configuration, and it may switch to learned resolvers if it detects that direct DNS queries are blocked. This avoids choosing a public DNS provider, but ordinary DNS traffic is unencrypted between your machine and the DNS servers involved.
2- Prioritizes unwind doing recursive resolution itself, rather than using configured forwarders or learned resolvers through the preference list. No single public resolver gets your entire query stream, but your DNS traffic is still unencrypted.
3- Plaintext forwarding, no transport encryption.
4- DNS-over-TLS (DoT). This is the best for security. The provider still receives the queries, and your network can still see that you're connecting to the provider, but not the DNS contents of that encrypted connection.
Quad9 is a good DNS provider, but you can choose whatever you want.
# To resolve DNS locally
#preference { recursor }
# If you want to use an external DNS resolver
# Here Quad9 is used as an example, choose only one of the options:
# Unencrypted DNS (IPv4)
#forwarder { 9.9.9.9 }
#preference { forwarder }
# Encrypted: DNS-over-TLS (IPv4 and IPv6)
forwarder {
9.9.9.9 authentication name "dns.quad9.net" DoT
149.112.112.112 authentication name "dns.quad9.net" DoT
2620:fe::fe authentication name "dns.quad9.net" DoT
2620:fe::9 authentication name "dns.quad9.net" DoT
}
preference { DoT }
Enable and start unwind:
# rcctl enable unwind
# rcctl start unwind
Building ports
If you prefer to build ports locally rather than installing with
pkg_add, you should do it safely.
Create /etc/mk.conf and add the following lines:
PORTS_PRIVSEP=Yes
WRKOBJDIR=/usr/obj/ports
DISTDIR=/usr/distfiles
PACKAGE_REPOSITORY=/usr/packages
SUDO=doas
Don't forget to uncomment these 2 lines in
/etc/doas.conf:
permit keepenv nopass flywithme as _pbuild
permit keepenv nopass flywithme as _pfetch
After that, just follow the official guide: OpenBSD Ports - Working with Ports
Screenlock and disabling X11 magic keystrokes
The Xorg and XFree86 X servers trap certain magic keystrokes before X11 client programs ever see them. If you care about keeping your screen locked, this is a big problem. These keystrokes allow to totally bypass any X11 screensaver.
These keystrokes are:
Ctrl+Alt+Backspace;
Ctrl-Alt-F1,
Ctrl-Alt-F2,
etc.; and
Ctrl-Alt-KP_Multiply.
To disable them, create this file:
/usr/X11R6/share/X11/xorg.conf.d/90-serverflags.conf and add
the following lines:
Section "ServerFlags"
Option "DontZap" "true"
Option "DontVTSwitch" "true"
Option "AllowClosedownGrabs" "false"
EndSection
To lock the screen automatically I use the xautolock
program, you need to download it from the ports. Then set this on
~/.xsession:
# If you use xscreensaver
#xscreensaver --no-splash &
# Lock the screen after 3 minutes idle
xautolock -time 3 -locker "xlock" &
Userland sandboxing with Flysunaba
Flysunaba is a utility to run X (or X11) applications in a rudimentary sandbox on OpenBSD, isolating them from your X session and from each other. The command is Fsunaba.
The sandbox consists of:
- A separate, less privileged, local user account under which the X application runs, restricting access to your user files (assuming appropriate permissions are set).
- A separate X session created and rendered into a window within your running X display by Xephyr, one per invocation, so the application cannot snoop on XEvents in the parent X session, in another Fsunaba session, or in another application of the same session.
Flysunaba is a creation of mine with the simple objective of ending once and for all the problem of Xorg's lack of GUI isolation.
Xenocara is OpenBSD’s adapted X.Org, with privilege separation to reduce the risk from bugs in the display server.
But its hardening is about the server, not the clients. The server
drops privileges to the _x11 user, keeps a small [priv] child confined
with pledge(2) and unveil(2), and disables TCP
by default. All of that protects the system from a compromised server. It
does not isolate clients from each other: every authenticated client
still shares the display, so it can read, keylog, inject into, and
screenshot every other application, exactly as on stock X.Org.
The goal of Flysunaba is to remove that problem for the applications you run through it. Each session is a separate unprivileged user inside its own nested Xephyr display, so an application cannot read or interfere with your X session, with another Fsunaba session, or with any other application.
That GUI isolation is the main goal, but Fsunaba also restricts more than the display:
- Separate user and filesystem: the application runs as the sandbox user, not as you, so Unix permissions keep your files out of reach.
- No audio: the sandbox has no sndio cookie, so it can neither play nor record until you explicitly copy one in.
- No clipboard or selection: nothing is shared by default; copying requires the explicit xclip bridge (see Shared Selection and/or Clipboard).
- No credentials: a fresh, minimal environment, with its home directory as the working directory.
Compared with Wayland:
Wayland isolates the GUI for native clients, and does nothing else by itself. Applications still run as your user with full access to your files; PipeWire and PulseAudio accept any client of your user, so audio is not blocked; and the clipboard is shared across the session and readable by any client, including through wlr-data-control/ext-data-control-v1.
-
Wayland only: native clients cannot read each other's surfaces or input, and there is no X11 socket. But there is no user, filesystem, audio, or clipboard isolation without extra sandboxing, and X11 applications do not run.
-
On environments outside of GNOME, KDE Plasma, Sway, and COSMIC, applications can access screen content of the entire desktop. This implicitly includes the content of other applications. In addition, GNOME also provides weak thumbnailer sandboxing in Gnome Files, and Thunar/Tumblerd on Sway also provide weak thumbnailer sandboxing, which is bad if you want protection against attacks via thumbnailers.
-
Wayland with XWayland: X11 applications keep working, but they all share one XWayland server and therefore the whole X11 problem among themselves, and XWayland carries X.Org's code and its vulnerabilities.
-
Xenocara with Flysunaba: X11 applications keep working, and every Fsunaba session gets its own X server and its own unprivileged user, with audio and clipboard off by default. The cost is software rendering only (no OpenGL acceleration), no display manager, and one doas rule.
In short, Xenocara + Flysunaba is even stronger than Wayland-only.
Flysunaba is very easy to use, simple and is pure shell scripting. Just read the manpage.
Good practices and more tips
- Obviously encrypt the disk and swap in the OpenBSD installation. If you haven't done it in the installation process, then you can do it after.
- Avoid installing ports as much as possible, every new package may be compromised and/or can increase the attack surface.
- Uninstall packages you don't need.
- Don't run services unnecessarily and stop services you don't use or
need (check with
rcctl ls on). Everything is attack surface. - Use a reliable password manager, I recommend keepassxc.
- Chromium-based web browsers are more secure than Firefox-based ones.
- Use CWM (OpenBSD's own window manager) instead of FVWM, TWM or any other WM/DE.
- Try as much as possible to use OpenBSD's native tools/programs instead of relying on third-party projects.