Gripes With Setting Up My Unifi UDM

I’ve recently purchased an UDM SE when it was on sale. I use it as my main router moving away from OpenWRT (for this role). Especially the “recent” improvements including the zone-based Firewall configuration as well as beginnings of “usable” IPv6 support are what allowed me to make the jump.

I first chose to import my old self-hosted Unifi Network setup, but chose to redo it from scratch, because it seemed “buggy.”

I’m happy overall. The new zone-based firewall is a HUGE improvement (there’re specifics below)!

My gripes1 with the setup (as of January 2026):

You cannot–under any circumstance–create a working allow rule from a network in the Guest zone … there seem to be “hidden rules” that prevent this. Finding this out ate a whole weekend.

Firewall rules can’t have “any” zone as a source or destination. E.g. you can’t create a pure WAN egress rule.

Using the object policies you can get an error, saying you can’t create any more “ACL rules.” … why?!? I’m not using ACL rules (knowingly)! How many can I use (IIRC I had four or five)? How do I find out which ones they are? They count even when they are all paused?!?!?! ☠️

If you want to use the zone-based Firewall to allow Internet access to specific domains only make sure your UDM/etc. is the device’s DNS server. It doesn’t work with external DNS servers.

The Intrusion Prevention System blocks connections (e.g. www.privacy-handbuch.de) even when it’s set to only “notify.” In the logs it doesn’t say what the reason for blocking was, I just found out by elimination. 🤮

It seems not all blocked connections are shown in the flows/logs. I’ve had to create firewall rules for devices and services that were blocked, but didn’t show up in the flows/logs view (even with all the extended logging settings set). I only found out because of my internal monitoring setup (yay, Prometheus Blackbox Exporter and Ping Exporter). 😱

You cannot use device groups in Firewall rules, only in object policies.

You can select devices as sources in Firewall policies, but not as destinations.

You can’t add comments to Port or IP lists. Neither on the whole list, nor on the individual entries.

“Add multiple” fields won’t filter duplicates automatically … they will nag you until you’ve removed them manually. 😞

There’s no way to bulk export or import for DNS records … or firewall rules.

You can’t use IPv6 in WireGuard VPNs! 🤬

You can’t change the settings of the WireGuard Server or Clients. I know why they don’t allow it, but it’s rubbing me the wrong way.

MLO is a lie!

  1. according to the principle: “if you want to nag at least have the courtesy to be specific!” ↩︎

Kubernetes Resource Requests Are a Massive Footgun

If you have Kubernetes workloads that configure Resource Requests on Pods or Containers there’s a footgun “hidden” in a sentence in the documentation (kudos if you spot it immediately):

[…] The kubelet also reserves at least the request amount of that system resource specifically for that container to use. […]

This means Resource Requests actually reserve the requested amount of resources exclusively. To emphasize: this is not a fairness measure in case of over-provisioning! So, if there are Resource Requests you can’t “overprovision” your node/cluster … hell, the new pod won’t even be scheduled although your node is sitting idle. 😵😓

By the time you find out why and have patched the offending resources you’ll be swearing up and down. 🤬

Oh … and wait till you see what the Internat has to say about Resource Limits. 😰

Simulating Statically Compiled Binaries in Glorified Tarballs

Containers won for one reason: they simulate a statically compiled binary that’s ergonomic for engineers and transparent to the application. A Docker image is a glorified tarball with metadata in a JSON document.

From Joseph’s comment on “Containers and giving up on expecting good software installation practices”

I hadn’t thought of it that way, but from a developer’s perspective it makes sense. It may not be incidental that the new programming languages of the 2010s (e.g. Go, Rust, Zig) produce statically linked binaries by default.

I always thought of containers as a way to add standardized interfaces to an application/binary that can be configured in a common way (e.g. ports, data directories, configurationenv vars, grouping and isolation). The only other ecosystem that does this and maybe even goes a little further is Nix.

Because the binary format itself is ossified and the ecosystem fragmented enough we missed the train for advanced lifecycle hooks for applications (think multiple entry points for starting, pausing, resuming, stopping, reacting to events, etc. like on Android, iOS, MacOS) … in Linux this is something that’s again bolted on from the outside: with e.g. D-Bus, Systemd, CRIU).

Configuring Custom Ingress Ports With Cilium

This is just a note for anyone looking for a solution to this problem.

While it’s extremely easy with the Kubernetes’ newer Gateway API via listeners on Gateway resources it seems the Ingress resources were always meant to be used with (global?) default ports … mainly 80 and 443 for HTTP and HTTPS respectively. So every Ingress Controller seems to have their own “side-channel solution” that leverages some resource metadata to convey this information. For Cilium this happens to be the sparsely documented ingress.cilium.io/host-listener-port annotation.

So your Ingress definition should look something like this:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ...
  namespace: ...
  annotations:
    ingress.cilium.io/host-listener-port: 1234
spec:
  ingressClassName: cilium
  rules:
  - http: ...

Fixing Dracut for Encrypted ZFS on Root on Ubuntu 25.10

I just upgraded from Ubuntu 25.04 to 25.10 … well it was more of a reinstall really. Because I knew the new release changed the initrd-related tools to Dracut I tried to understand all the changes from a test installation in a VM. Well, I still somehow broke Dracut’s ability to unlock my encrypted ZFS on root setup automatically.

Looking at journalctl it claimed it couldn’t find the key file:

dracut-pre-mount[940]: Warning: ZFS: Key /run/keystore/rpool/system.key for rpool/enc hasn't appeared. Trying anyway.
[...]
dracut-pre-mount[1001]: Key load error: Failed to open key material file: No such file or directory
[...]
systemd[1]: Mounting sysroot.mount - /sysroot...
mount[1007]: zfs_mount_at() failed: encryption key not loaded
systemd[1]: sysroot.mount: Mount process exited, code=exited, status=2/INVALIDARGUMENT
systemd[1]: sysroot.mount: Failed with result 'exit-code'.
systemd[1]: Failed to mount sysroot.mount - /sysroot.
systemd[1]: Dependency failed for initrd-root-fs.target - Initrd Root File System.

All I could do was mounting the keystore manually in the emergency console:

systemd-cryptsetup attach keystore-rpool /dev/zvol/rpool/keystore
mkdir -p /run/keystore/rpool
mount /dev/mapper/keystore-rpool /run/keystore/rpool

After pressing Ctrl-d Systemd continued booting as if everything was OK. This worked, but was HUGELY annoying, especially considering it was also using an English keyboard mapping. 🤬

After I was done setting up my desktop I took the time investigate the issue. I compared all the things between my real system and the freshly setup VM. After comparing the system startup plots (exported with systemd-analyze plot > plot.svg) I noticed that the systemd-ask-password.service would start quite late in my real system (after I manually mounted the keystore). I knew there was a bug report for teaching Dracut Ubuntu’s ZFS on root encryption scheme (i.e. putting the root ZFS dataset’s encryption keys in a LUKS container on a Zvol (rpool/keystore)). So I looked at the actual patch and tried to walk through of how it would behave on my system. There I noticed that the script actually assumes the ZFS encryption root to be the same as the Zpool’s root dataset (e.g. rpool). 😯 I moved away from this kind of setup years ago as it makes restoring from a backup quite cumbersome. So I was using a sub-dataset for the encrypted data (e.g. root/crypt) which messed up the logic which assumed it to only contain the pool name. 🤦‍♂️

Long story short the following patch determines the pool name of the encryption root before trying to open and mount the LUKS keystore:

--- zfs-load-key.sh.orig        2025-10-16 20:44:47.955349974 +0200
+++ zfs-load-key.sh     2025-10-16 20:55:00.229000464 +0200
@@ -54,9 +54,11 @@
     [ "$(zfs get -Ho value keystatus "${ENCRYPTIONROOT}")" = "unavailable" ] || return 0

     KEYLOCATION="$(zfs get -Ho value keylocation "${ENCRYPTIONROOT}")"
+    # `ENCRYPTIONROOT` might not be the root dataset (e.g. `rpool/enc`)
+    ENCRYPTIONROOT_POOL="$(echo "${ENCRYPTIONROOT}" | cut -d/ -f1)"
     case "$KEYLOCATION" in
-        "file:///run/keystore/${ENCRYPTIONROOT}/"*)
-            _open_and_mount_luks_keystore "${ENCRYPTIONROOT}" "${KEYLOCATION#file://}"
+        "file:///run/keystore/${ENCRYPTIONROOT_POOL}/"*)
+            _open_and_mount_luks_keystore "${ENCRYPTIONROOT_POOL}" "${KEYLOCATION#file://}"
             ;;
     esac

🎉

Gebrauchsanmaßung ohne Zueignungsabsicht

Juristen haben echt grandiose Wörter 😂:

Zueignungsabsicht, f.

Die Absicht einer Person beschreibt, sich eine Sache wenigstens vorübergehend anzueignen bei gleichzeitigem Vorsatz, den Berechtigten um die Sache dauerhaft zu enteignen (siehe Diebstahl, Unterschlagung).
Wikipedia

Gebrauchsanmaßung, f.

Eine vorübergehende eigenmächtige (und damit unberechtigte) Nutzung von beweglichen Sachen unter zeitweiliger Brechung fremden Gewahrsams. Gemeint ist, dass die Sache zwar unberechtigt benutzt, dem Berechtigten später aber zurückgebracht wird.
Wikipedia

Ich bin durch einen News-Artikel über diese Wörter gestolpert.

Running k3s on Incus

I know the pain to manage a bunch of services on my own. Even with relying on Incus, Podman and Systemd as much as possible held together by lot’s of Ansible duct tape: it’s still arduous. I convinced myself change was in order: … something something Kubernetes.

My main criteria are basically:

  • Must be able to run on a single node (for now). i.e. no clustered services or databases. (k3s looks like it fits the bill)
  • Services must be able to be deployed with public service definitions (Helm FTW)
  • These service definitions must lend themselves to be version controlled
  • All relevant data directories must live on a separate ZFS datasets

Running k3s in an Incus container

You can run k3s in an Incus container, but it gets increasingly difficult. There’re reports of people getting it to run, but it gets increasingly difficult. Even public LXD/LXC definitions for microk8s or k3s are either quite old (as of 2025-08 3 and 6 years old respectively) and blast HUGE holes in the sandbox. ☹️ K3s “requires” access to /dev/kmsg, several places in /proc and /sys as well as modprobing several kernel modules (it checks for access to them and spams the logs with warnings and errors). 😶

It looks doable in a technical sense, but it’s a huge pain having to go though Incus, without any of the (sandboxing/security) benefits. So the general wisdom is to just use a VM. (No, I didn’t try k3s’ experimental rootless mode)

Running k3s in an Incus VM

I started with a fresh VM and could reuse my now much simplified Ansible tasks for setting um k3s. But my happiness got cut short by the k3s service spamming the journal with useless

level=error msg="failed to ping connection: disk I/O error: no such device"

error messages.After removing all the directories and files from /var/lib/rancher/k3s and starting the server by hand I got:

Error: preparing server: failed to bootstrap cluster data: creating storage endpoint: failed to create driver for default endpoint: setup db: disk I/O error: no such device

Some more mucking around with the k3s server config revealed a puzzling, but more useful

failed to mount overlay: invalid argument.

Looking at what dmesg had to say I got:

overlayfs: upper fs does not support tmpfile.
overlayfs: failed to set xattr on upper
overlayfs: …falling back to redirect_dir=nofollow.
overlayfs: …falling back to uuid=null.
overlayfs: …falling back to xino=off.
overlayfs: try mounting with 'userxattr' option
overlayfs: upper fs missing required features.

Long story short: it turns out in my eagerness I had mounted a custom Incus volume as k3s’ data directory (/var/lib/rancher/k3s). This being a VM (instead of a container) it mounted the volume using the virtiofs protocol. And it turns out the overlayfs doesn’t like being put on top of virtiofs devices (or NFS it seems). 😵‍💫 But good news: it was fixable, although hacky. I found out by grepping for “virtiofsd” processes that Incus vendors its own virtiofsd binary in /opt/incus/bin/virtiofsd. And it already runs it with the --posix-acl option with implies the required --xattr option. But Incus currently doesn’t support any way for configuring virtiofsd. 😓 So the only solution (by the main Incus maintainer none the less) is to replace /opt/incus/bin/virtiofsd with a shim script calling the real virtiofsd binary with the additional --modcaps=+sys_admin option. Basically something silly like:

#!/usr/bin/bash
exec /opt/incus/bin/virtiofsd.orig --modcaps=+sys_admin "$@"

Yeah also, “try mounting with ‘userxattr’ option” was not helpful and sent me down the wrong path. 🤐

All in all … all these stumbling blocks ate my weekend. Which was kind of in line with my prejudices against Kubernetes. 😅