Debugging a strange permission issue on Unbound

  • Posted on
  • 5 mins read

I recently upgraded a VM from Debian 9 to Debian 10 and found myself unable to start Unbound.

This is not uncommon when “dist-upgrading” and my experience told me it would probably not be a big deal, but the error message was quite a surprise:

unbound[429:0] error: Could not open /etc/unbound/unbound.conf: Permission denied
unbound[429:0] fatal error: Could not read config file: /etc/unbound/unbound.conf. Maybe try unbound -dd, it stays on the commandline to see more errors, or unbound-checkconf

Let’s see what the permissions are on that file:

# ls -ld /etc /etc/unbound /etc/unbound/unbound.conf
drwxr-xr-x 93 root    root    4096 Jun  4 14:22 /etc
drwxr-x---  3 unbound unbound 4096 May 29 06:03 /etc/unbound
-rw-r-----  1 unbound unbound   94 May 17 02:10 /etc/unbound/unbound.conf

Ok, that’s strange!

A bit of context

Before I dig into how I resolved this issue, I must signal that I use Puppet to manage my infrastructure and especially I use a module that I developed myself to manage Unbound, meaning everything is pretty much automated, from specific settings to access rights on configuration files.

I also already knew something was wrong with this module, as, months before I faced this issue myself, a kind contributor submitted a patch aiming at fixing a permission issue on Debian Buster.

The birth of a daemon

So what surprised me here has to do with the way daemons start on UNIX systems1.

Most daemons run as an unprivileged user, but they often start by running as root for a short period of time before forking and switching to another user. This allows the process to make actions otherwise restricted, like reading its configuration file, creating sockets or listening on privileged ports. Unbound is no exception and follows that exact same process, there’s no reason it would fail at reading its configuration file with EACCES (permission denied).

I couldn’t figure what was going on, and couldn’t reproduce this behavior in an interactive shell running as root:

# cat /etc/unbound/unbound.conf
# This file is managed by Puppet. DO NOT EDIT.

include: "/etc/unbound/unbound.conf.d/*.conf"
# touch /etc/unbound/unbound.conf; echo $?
0

The merge request I received on my Puppet module months ago quickly came to my mind: the contributor told me they had to set root as the owner of the configuration files, otherwise Unbound wouldn’t start.

Let’s give it a try:

# chown root: -R /etc/unbound /etc/unbound/unbound.conf
# unbound
[1591299980] unbound[10638:0] error: error for private key file: /etc/letsencrypt/live/rdns01.br0.fr/privkey.pem
[1591299980] unbound[10638:0] error: Error in SSL_CTX use_PrivateKey_file crypto error:0200100D:system library:fopen:Permission denied
[1591299980] unbound[10638:0] error: and additionally crypto error:20074002:BIO routines:file_ctrl:system lib
[1591299980] unbound[10638:0] error: and additionally crypto error:140B0002:SSL routines:SSL_CTX_use_PrivateKey_file:system lib
[1591299980] unbound[10638:0] fatal error: could not set up listen SSL_CTX

There was another issue here that needed attention, but at least Unbound was now able to read its configuration file and go further in its startup process.

Still, I couldn’t understand why it wouldn’t work in the first place and it seemed super strange that root would need explicit read access on these files. This was something I’d never encountered before.

I decided to search for similar issues on the Web and came across a post on Server Fault with an answer that could explain the certificates issue: I needed to tweak AppArmor in order to allow Unbound to read its certificates in /etc/letsencrypt.

AppArmor

AppArmor is a security mechanism that act kind of like a “sandbox” by restricting what a software can or can’t do on the system using per-process profiles. For example, it can limit what file or directory a software can read or write to, what POSIX capabilities are assigned to a process or what kind of network socket it can create. Profiles shipped in Linux distributions are pretty much transparent when running daemon close to their default configuration.

In this case, I fixed the certificates issue by adding the following two lines in /etc/apparmor.d/local/usr.sbin.unbound:

# Allows Unbound to recursively read files in those two directory
/etc/letsencrypt/archive/** r,
/etc/letsencrypt/live/** r,

After running apparmor_parser --replace /etc/apparmor.d/usr.sbin.unbound to reload the profile, Unbound was able to read the certificates used for DNS over TLS and it was finally able to start.

I could’ve stopped here, but I’m not the kind of person to be satisfied with workarounds, and I really wanted to understand what was going on in the first place. I started looking at Unbound’s AppArmor profile (in /etc/apparmor.d/usr.sbin.unbound) and I finally figured that my issue was related to POSIX capabilities.

POSIX capabilities

Besides limiting access to files and directory, AppArmor can assign or remove POSIX capabilities to limit what a process is allowed to do on the system.

In the case of Unbound, it is only granted the ones it really needs (for instance the NET_BIND_SERVICE capability allows it to listen ports lower than 1024). This means that even when Unbound is running as root, during the early phase of its startup process, it is limited to the capabilities that was explicitly granted.

Actually, this is the key difference between Unbound and my interactive shell, the later is basically granted with the full list of capabilities. There’s one specific capabilities that explains this difference: DAC_OVERRIDE allows a process to bypass access right checks on files.

That explains it all: Unbound can’t read a file that is owned by the unbound user without read permission for other users while running as root, because it doesn’t have that capability assigned.

Conclusion

To summarize, Unbound was unable to read its configuration file because AppArmor limited its capabilities such that even while running as root the kernel acted as if it was a unprivileged user. The fix was simply to set root the owner of Unbound’s configuration files.

In hindsight, I believe this could be a good practice: most daemons don’t need to have write access on their configuration file and preventing that can help mitigate the impact of some security issues.

It took a long time to debug this issue, and this is mainly due to my lack of experience with AppArmor to the point that I didn’t even think of it until I saw the post on Server Fault. I knew about AppArmor for a while but I only started experimenting with it recently (mainly by installing the apparmor-profiles package which ships with a lot of profiles).


  1. I use UNIX as a generic term here. ↩︎