Guides

OpenBSD desktop hardening guide

Written by FlyWithMe (a SimpleX Chat user) -

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

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:

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.

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

  1. 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.
  2. Avoid installing ports as much as possible, every new package may be compromised and/or can increase the attack surface.
  3. Uninstall packages you don't need.
  4. Don't run services unnecessarily and stop services you don't use or need (check with rcctl ls on). Everything is attack surface.
  5. Use a reliable password manager, I recommend keepassxc.
  6. Chromium-based web browsers are more secure than Firefox-based ones.
  7. Use CWM (OpenBSD's own window manager) instead of FVWM, TWM or any other WM/DE.
  8. Try as much as possible to use OpenBSD's native tools/programs instead of relying on third-party projects.

Further reading

Hardening and Operational Safety

Taking OpenBSD Security to the Limit