The daemon logs a chroot failure and the profile looks generous already, so the tempting fix is to widen a path rule and restart. That usually changes nothing, because chroot is gated by a privilege the profile was never able to hand over. Deciding which layer to touch takes three checks, and they are cheap: what the process is allowed to hold, what the policy permits, and what the launcher removed before either of those mattered.

Two separate gates, and only one belongs to AppArmor

chroot needs CAP_SYS_CHROOT in the user namespace the process is running in. That is conventional Linux privilege, checked by the kernel before the call does anything with paths. Separately, an enforcing AppArmor profile can deny the capability even when the process holds it, because mandatory policy is consulted in addition to credentials rather than instead of them. The distinction matters in practice: the two layers live in different files and are usually maintained by different people.

Those two gates fail with the same errno. Both produce EPERM, and both surface as a generic permission complaint in the daemon log, which is why guessing at profile contents wastes so much time. The path rule you were about to add controls file access after the chroot succeeds; it has no bearing on whether the operation is permitted at all.

A third gate sits upstream of both. If the service manager or a wrapper started the process without the capability in its bounding set, no AppArmor change can put it back, and adding capabilities to the profile simply documents an intention the kernel will not honour.

Read the privilege state before editing policy

Start from the running process rather than from the configuration you believe is active. The status file exposes five capability masks and the no-new-privileges flag, and they answer different questions:

grep -E '^(CapInh|CapPrm|CapEff|CapBnd|CapAmb|NoNewPrivs):' /proc/$(pidof openvpn)/status
sudo aa-status | grep -i openvpn

CapEff is the set that matters for the call: a capability present in CapBnd but absent from CapEff is permitted by the ceiling and not currently held. CapAmb matters when the binary is not privileged in the usual setuid sense, because ambient capabilities are how a service keeps a privilege across exec without file capabilities. CapInh holds bits that could be inherited across exec, but only when the executed file's inheritable set allows it, so a bit there grants nothing on its own.

Then confirm which profile is actually attached. A process can be confined by a profile name you did not expect, especially when a distribution ships its own unit file and profile together, and reading /proc/PID/attr/current settles that in one line. Comparing your edited file against the profile the kernel reports is the difference between fixing the cause and editing a file nobody loads.

Add the capability rule only when the audit trail says so

Kernel and audit records name the refusing layer. Look for a denial that carries the operation capable and the name sys_chroot, along with the profile that produced it. If that record exists, the process already held the capability and AppArmor refused it, and the targeted change is a capability rule inside the real profile:

capability sys_chroot,

Keep it as a fragment in the profile that is genuinely attached, and check the keyword against the apparmor.d profile reference rather than copying a rule out of an unrelated tutorial. Dropping it into a shared include grants it to every confined program that pulls that file in, and switching profiles to complain mode hides the next denial instead of resolving this one. The profile rule removes a policy denial; it does not manufacture privilege, so a process that never had CAP_SYS_CHROOT in its effective set will still fail with the rule in place.

If the audit record shows no AppArmor denial at all, stop editing the profile. You are looking at a credentials problem, and the remaining work is in the unit file or the parent process, not in the policy language.

The service manager keeps its own ceiling

systemd sets a capability bounding set for each service, and by default that set is narrow. Anything excluded there is unavailable to the process no matter what the profile says, which is why hardened units can fail an operation that worked when the daemon was started by hand. The relevant directives are easy to read in the unit and easy to overlook when the failure is reported by the application. Drop-ins shipped by a distribution package are the usual source of a ceiling nobody remembers configuring.

Raising the ceiling and granting the capability are two changes, not one. A bounding set entry says the process may hold the capability; it does not add it to the effective set, and NoNewPrivs can prevent a later elevation from succeeding even when everything else lines up. Add only what the operation needs, keep the rest of the hardening intact, and resist the shortcut of granting every capability to make one call pass.

Order matters here as well. Change the unit ceiling first, verify that CapEff now contains the bit, and only then decide whether AppArmor still denies. Making both changes at once leaves you guessing again if the failure persists.

After the denial clears, check the jail itself

A successful chroot moves the root, so every path the daemon touches afterwards is resolved inside the new tree. Missing configuration, an absent device node or a certificate directory that did not get copied in will now appear as a file error, and that error is a layout problem rather than a capability problem. Read the next event instead of adding another broad permission. The move also changes what a relative path means, which is why hardening guides insist on absolute paths in the daemon configuration.

It also helps to remember what chroot does not do. It is a path boundary, not a sandbox: a process that keeps the privilege can escape it, which is why the capability is normally dropped after the call and why confinement should stay in place around the daemon. Retaining the AppArmor profile, the capability bounding set and the filesystem restrictions together is the point of the exercise, not a leftover to remove once the service starts.

The decision rule is short. If the audit record names AppArmor, add one capability rule to the attached profile. If the effective set is empty, fix the unit ceiling and the privilege-drop order. If the profile is not the one you edited, stop editing and find the real one.

Pennadoù liammet