From 088cbe5e3a961a18d0102e9229583b99fe225fdf Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Wed, 19 Aug 2026 14:11:43 +0100 Subject: [PATCH 1/5] ak-nss: add apparmor profile override --- .../abstractions/nameservice.d/authentik | 9 ++++++ .../linux/nss/_deb/apparmor/local/unix-chkpwd | 15 ++++++++++ vpkg/linux/nss/_deb/postinst.sh | 28 +++++++++++++++++++ vpkg/linux/nss/nfpm.yaml | 17 +++++++++++ 4 files changed, 69 insertions(+) create mode 100644 vpkg/linux/nss/_deb/apparmor/abstractions/nameservice.d/authentik create mode 100644 vpkg/linux/nss/_deb/apparmor/local/unix-chkpwd diff --git a/vpkg/linux/nss/_deb/apparmor/abstractions/nameservice.d/authentik b/vpkg/linux/nss/_deb/apparmor/abstractions/nameservice.d/authentik new file mode 100644 index 000000000..932b353b1 --- /dev/null +++ b/vpkg/linux/nss/_deb/apparmor/abstractions/nameservice.d/authentik @@ -0,0 +1,9 @@ +# Drop-in for , which is included by every profile +# that performs user/group lookups. glibc loads libnss_authentik.so.2 into +# those processes, and it reads the authentik config and dials ak-sysd. + +# Allow NSS lookups to read the Authentik configuration file +/etc/authentik/config.json r, + +# Allow NSS lookups to read and write the Authentik UNIX socket. +@{run}/authentik/sys.sock rw, diff --git a/vpkg/linux/nss/_deb/apparmor/local/unix-chkpwd b/vpkg/linux/nss/_deb/apparmor/local/unix-chkpwd new file mode 100644 index 000000000..8903b6971 --- /dev/null +++ b/vpkg/linux/nss/_deb/apparmor/local/unix-chkpwd @@ -0,0 +1,15 @@ +# Included by /etc/apparmor.d/unix-chkpwd via `include if exists `. +# unix_chkpwd loads libnss_authentik.so.2 during NSS lookups, which reads the +# authentik config and connects to the ak-sysd socket. + +# Allow unix-chkpwd to read the Authentik configuration file +/etc/authentik/config.json r, + +# Allow unix-chkpwd to read and write to the Authentik UNIX socket +/run/authentik/sys.sock rw, + +# Disconnected-path variants, for when unix_chkpwd runs in a mount namespace +# (nspawn/containers). apparmor >= 5.0 declares the profile with +# flags=(attach_disconnected.path=/att/unix-chkpwd/); inert on apparmor 4.0.x. +/att/unix-chkpwd/etc/authentik/config.json r, +/att/unix-chkpwd/run/authentik/sys.sock rw, diff --git a/vpkg/linux/nss/_deb/postinst.sh b/vpkg/linux/nss/_deb/postinst.sh index 480b4e112..f209873a2 100755 --- a/vpkg/linux/nss/_deb/postinst.sh +++ b/vpkg/linux/nss/_deb/postinst.sh @@ -27,8 +27,36 @@ insert_nss_entry() { ' /etc/nsswitch.conf } +# reload AppArmor so it picks up the policy we ship under /etc/apparmor.d. +# The apparmor package registers no dpkg trigger on that directory, so without +# this the new rules only take effect after a reboot. Reload the whole policy +# rather than a single profile: the abstractions/nameservice.d drop-in is +# included by every profile that performs user/group lookups. +reload_apparmor() { + log "Checking AppArmor setup..." + if ! command -v apparmor_parser >/dev/null 2>&1; then + log "apparmor_parser not found, skipping." + return + fi + if ! aa-enabled --quiet 2>/dev/null; then + log "AppArmor is not enabled, skipping." + return + fi + if systemctl reload apparmor.service >/dev/null 2>&1; then + return + fi + log "Could not reload apparmor.service, falling back to unix-chkpwd only." + if ! [ -f /etc/apparmor.d/unix-chkpwd ]; then + log "Could not find /etc/apparmor.d/unix-chkpwd." + return + fi + apparmor_parser -r -T -W /etc/apparmor.d/unix-chkpwd \ + || log "Failed to reload the unix-chkpwd profile, changes apply after reboot." +} + action="$1" if [ configure = "$action" ]; then insert_nss_entry + reload_apparmor fi diff --git a/vpkg/linux/nss/nfpm.yaml b/vpkg/linux/nss/nfpm.yaml index 94d54b63a..36f08f9c4 100644 --- a/vpkg/linux/nss/nfpm.yaml +++ b/vpkg/linux/nss/nfpm.yaml @@ -22,6 +22,23 @@ contents: file_info: mode: 0644 packager: rpm + # AppArmor. Every confined profile that does user/group lookups includes + # , and glibc loads our NSS module into it, so the + # drop-in below is the general fix (the same place upstream wires up authd). + - src: ./nss/_deb/apparmor/abstractions/nameservice.d/authentik + dst: /etc/apparmor.d/abstractions/nameservice.d/authentik + file_info: + mode: 0644 + packager: deb + type: config + # unix_chkpwd additionally needs disconnected-path rules, which are specific + # to its profile and so cannot live in the nameservice drop-in. + - src: ./nss/_deb/apparmor/local/unix-chkpwd + dst: /etc/apparmor.d/local/unix-chkpwd + file_info: + mode: 0644 + packager: deb + type: config overrides: deb: depends: From 26372155d25f189542fab38d68f736c0eb1eacbb Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Wed, 19 Aug 2026 14:54:49 +0100 Subject: [PATCH 2/5] show all journalctl logs --- ak-platform-e2e/src/test_machine.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ak-platform-e2e/src/test_machine.rs b/ak-platform-e2e/src/test_machine.rs index fae7d6cb3..f9799a661 100644 --- a/ak-platform-e2e/src/test_machine.rs +++ b/ak-platform-e2e/src/test_machine.rs @@ -92,9 +92,7 @@ impl Drop for TestMachine { tracing::info!("Test machine cleanup"); tokio::runtime::Handle::current().block_on(async { for cmd in [ - "journalctl -u ak-sysd", - "journalctl -u ak-agent", - "journalctl -u ssh", + "journalctl", "systemctl stop ak-sysd", "systemctl stop ak-agent", ] { From d813df0b22fe45a127d0d24963f45d4289e017e7 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Wed, 19 Aug 2026 14:56:37 +0100 Subject: [PATCH 3/5] stderr? --- ak-platform-e2e/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ak-platform-e2e/src/lib.rs b/ak-platform-e2e/src/lib.rs index c967aeb45..066b458fa 100644 --- a/ak-platform-e2e/src/lib.rs +++ b/ak-platform-e2e/src/lib.rs @@ -278,7 +278,7 @@ pub async fn exec_command( let exit_code = result.exit_code().await.unwrap().unwrap(); if is_ci() { - println!("::group::{cmd} (Exit code {exit_code}"); + eprintln!("::group::{cmd} (Exit code {exit_code})"); } else { tracing::info!("[exec] {} exit={}", cmd, exit_code); } @@ -292,7 +292,7 @@ pub async fn exec_command( .for_each(|l| tracing::warn!("[stderr] {}", l)); if is_ci() { - println!("::endgroup::"); + eprintln!("::endgroup::"); } let output = format!("{}{}", stdout_str, stderr_str); From da8ca3ec3f983224a677b45404821469ea0323f3 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Wed, 19 Aug 2026 16:58:54 +0100 Subject: [PATCH 4/5] t --- .github/workflows/test.yml | 11 +++++++++++ ak-platform-e2e/src/lib.rs | 19 +++++++++++++++++++ ak-platform-e2e/tests/auth.rs | 12 ++++++++++-- ak-platform-e2e/tests/login.rs | 7 ++++++- .../abstractions/nameservice.d/authentik | 18 +++++++++++++----- .../linux/nss/_deb/apparmor/local/unix-chkpwd | 15 --------------- vpkg/linux/nss/_deb/postinst.sh | 16 +++++++++++----- vpkg/linux/nss/nfpm.yaml | 12 ++---------- 8 files changed, 72 insertions(+), 38 deletions(-) delete mode 100644 vpkg/linux/nss/_deb/apparmor/local/unix-chkpwd diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index de49f15fa..ebcc8e1a9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -219,6 +219,17 @@ jobs: run: | ./hack/authentik/wait.sh - run: make containers/e2e/ci-build + # AppArmor policy is host-global: the runner's unix-chkpwd profile confines + # unix_chkpwd inside the container, and the container cannot load policy of + # its own (no securityfs during docker build). Install the same drop-in the + # deb ships so the e2e run exercises it. + - name: Load authentik AppArmor drop-in + run: | + sudo install -m 0644 -D \ + vpkg/linux/nss/_deb/apparmor/abstractions/nameservice.d/authentik \ + /etc/apparmor.d/abstractions/nameservice.d/authentik + sudo systemctl reload apparmor.service + sudo aa-status | grep -q unix-chkpwd - env: AK_PASSWORD: ${{ steps.setup.outputs.admin_password }} AK_TOKEN: ${{ steps.setup.outputs.admin_token }} diff --git a/ak-platform-e2e/src/lib.rs b/ak-platform-e2e/src/lib.rs index 066b458fa..a7a9e7763 100644 --- a/ak-platform-e2e/src/lib.rs +++ b/ak-platform-e2e/src/lib.rs @@ -318,6 +318,25 @@ pub async fn must_exec( Ok(output) } +/// Fails if the kernel logged an AppArmor denial for an authentik path, which +/// means a confined process (typically unix_chkpwd) could not use the NSS +/// module. The policy is loaded on the CI host, not by the container, since +/// AppArmor policy is host-global -- see the e2e job in test.yml. +pub async fn assert_no_apparmor_denials(container: &ContainerAsync) -> Result<()> { + // Scoped to authentik paths so unrelated host denials don't fail the test. + let (exit_code, output) = exec_command( + container, + r#"journalctl --no-pager | grep 'apparmor="DENIED"' \ + | grep -E 'name="(/att/[^"]*)?/(etc|run)/authentik/'"#, + &[], + ) + .await?; + if exit_code == 0 { + bail!("AppArmor denied access to authentik paths:\n{}", output); + } + Ok(()) +} + /// A single parameterized command test case. pub struct CmdTestCase { pub name: String, diff --git a/ak-platform-e2e/tests/auth.rs b/ak-platform-e2e/tests/auth.rs index 42dc47aaf..bf2a7da07 100644 --- a/ak-platform-e2e/tests/auth.rs +++ b/ak-platform-e2e/tests/auth.rs @@ -1,6 +1,6 @@ use ak_platform_e2e::{ - CmdTestCase, TestMachine, agent_setup, cleanup_hosts, cmd_test, join_domain, must_exec, - test_init, + CmdTestCase, TestMachine, agent_setup, assert_no_apparmor_denials, cleanup_hosts, cmd_test, + join_domain, must_exec, test_init, }; #[tokio::test(flavor = "multi_thread")] @@ -32,6 +32,10 @@ async fn test_auth_identity_agent() { .await .expect("cmd test"); + assert_no_apparmor_denials(&tm.container) + .await + .expect("no apparmor denials"); + cleanup_hosts().await.expect("cleanup"); } @@ -101,5 +105,9 @@ async fn test_auth_local_only_user() { .await .expect("cmd test"); + assert_no_apparmor_denials(&tm.container) + .await + .expect("no apparmor denials"); + cleanup_hosts().await.expect("cleanup"); } diff --git a/ak-platform-e2e/tests/login.rs b/ak-platform-e2e/tests/login.rs index 75c2626a5..ff37291f3 100644 --- a/ak-platform-e2e/tests/login.rs +++ b/ak-platform-e2e/tests/login.rs @@ -1,5 +1,6 @@ use ak_platform_e2e::{ - TestMachine, authentik_creds, cleanup_hosts, exec_command, join_domain, must_exec, test_init, + TestMachine, assert_no_apparmor_denials, authentik_creds, cleanup_hosts, exec_command, + join_domain, must_exec, test_init, }; /// Verifies that a real local (non-SSH) login via the `login` PAM service is @@ -25,6 +26,10 @@ async fn test_local_login_success() { "expected successful local login, got: {output}" ); + assert_no_apparmor_denials(&tm.container) + .await + .expect("no apparmor denials"); + cleanup_hosts().await.expect("cleanup"); } diff --git a/vpkg/linux/nss/_deb/apparmor/abstractions/nameservice.d/authentik b/vpkg/linux/nss/_deb/apparmor/abstractions/nameservice.d/authentik index 932b353b1..2f5f20157 100644 --- a/vpkg/linux/nss/_deb/apparmor/abstractions/nameservice.d/authentik +++ b/vpkg/linux/nss/_deb/apparmor/abstractions/nameservice.d/authentik @@ -1,9 +1,17 @@ -# Drop-in for , which is included by every profile -# that performs user/group lookups. glibc loads libnss_authentik.so.2 into -# those processes, and it reads the authentik config and dials ak-sysd. +# Drop-in for , which is included by every profile that +# performs user/group lookups (unix-chkpwd, sshd, cron, ...). glibc loads +# libnss_authentik.so.2 into those processes, and it reads the authentik config +# and dials ak-sysd. -# Allow NSS lookups to read the Authentik configuration file +# Config file, read to discover the socket path. /etc/authentik/config.json r, -# Allow NSS lookups to read and write the Authentik UNIX socket. +# The ak-sysd control socket. AF_UNIX pathname sockets are mediated as files. @{run}/authentik/sys.sock rw, + +# Disconnected-path variants: apparmor >= 4.1 declares unix-chkpwd with +# flags=(attach_disconnected.path=/att/unix-chkpwd/), so paths resolved inside a +# mount namespace are reported under that prefix. Inert elsewhere. Upstream does +# the same for authd's socket. +/att/unix-chkpwd/etc/authentik/config.json r, +/att/unix-chkpwd/run/authentik/sys.sock rw, diff --git a/vpkg/linux/nss/_deb/apparmor/local/unix-chkpwd b/vpkg/linux/nss/_deb/apparmor/local/unix-chkpwd deleted file mode 100644 index 8903b6971..000000000 --- a/vpkg/linux/nss/_deb/apparmor/local/unix-chkpwd +++ /dev/null @@ -1,15 +0,0 @@ -# Included by /etc/apparmor.d/unix-chkpwd via `include if exists `. -# unix_chkpwd loads libnss_authentik.so.2 during NSS lookups, which reads the -# authentik config and connects to the ak-sysd socket. - -# Allow unix-chkpwd to read the Authentik configuration file -/etc/authentik/config.json r, - -# Allow unix-chkpwd to read and write to the Authentik UNIX socket -/run/authentik/sys.sock rw, - -# Disconnected-path variants, for when unix_chkpwd runs in a mount namespace -# (nspawn/containers). apparmor >= 5.0 declares the profile with -# flags=(attach_disconnected.path=/att/unix-chkpwd/); inert on apparmor 4.0.x. -/att/unix-chkpwd/etc/authentik/config.json r, -/att/unix-chkpwd/run/authentik/sys.sock rw, diff --git a/vpkg/linux/nss/_deb/postinst.sh b/vpkg/linux/nss/_deb/postinst.sh index f209873a2..6ff76f129 100755 --- a/vpkg/linux/nss/_deb/postinst.sh +++ b/vpkg/linux/nss/_deb/postinst.sh @@ -27,11 +27,17 @@ insert_nss_entry() { ' /etc/nsswitch.conf } -# reload AppArmor so it picks up the policy we ship under /etc/apparmor.d. -# The apparmor package registers no dpkg trigger on that directory, so without -# this the new rules only take effect after a reboot. Reload the whole policy -# rather than a single profile: the abstractions/nameservice.d drop-in is -# included by every profile that performs user/group lookups. +# Reload AppArmor so it picks up the drop-in we ship under /etc/apparmor.d. +# The apparmor package registers no dpkg trigger on that directory, so this only +# shortcuts the wait until the next apparmor reload or reboot; the rules +# themselves are picked up either way. Reload the whole policy rather than a +# single profile: the abstractions/nameservice.d drop-in is included by every +# profile that performs user/group lookups. +# +# Expected to no-op inside build containers: apparmor_parser is usually absent, +# and even when present /sys/kernel/security is not mounted during a container +# build, so no policy can be loaded. Note that AppArmor policy is host-global, +# so a container never confines its own processes -- the host's profiles do. reload_apparmor() { log "Checking AppArmor setup..." if ! command -v apparmor_parser >/dev/null 2>&1; then diff --git a/vpkg/linux/nss/nfpm.yaml b/vpkg/linux/nss/nfpm.yaml index 36f08f9c4..abe622b9b 100644 --- a/vpkg/linux/nss/nfpm.yaml +++ b/vpkg/linux/nss/nfpm.yaml @@ -23,22 +23,14 @@ contents: mode: 0644 packager: rpm # AppArmor. Every confined profile that does user/group lookups includes - # , and glibc loads our NSS module into it, so the - # drop-in below is the general fix (the same place upstream wires up authd). + # , and glibc loads our NSS module into it, so this + # single drop-in covers all of them (unix-chkpwd, sshd, cron, ...). - src: ./nss/_deb/apparmor/abstractions/nameservice.d/authentik dst: /etc/apparmor.d/abstractions/nameservice.d/authentik file_info: mode: 0644 packager: deb type: config - # unix_chkpwd additionally needs disconnected-path rules, which are specific - # to its profile and so cannot live in the nameservice drop-in. - - src: ./nss/_deb/apparmor/local/unix-chkpwd - dst: /etc/apparmor.d/local/unix-chkpwd - file_info: - mode: 0644 - packager: deb - type: config overrides: deb: depends: From 6083a3f0de07297d1d440b9079f9a01acd48bea3 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Wed, 19 Aug 2026 19:46:50 +0100 Subject: [PATCH 5/5] cleanup --- .github/workflows/test.yml | 6 ++---- ak-platform-e2e/src/lib.rs | 5 +---- .../apparmor/abstractions/nameservice.d/authentik | 12 ++++-------- vpkg/linux/nss/_deb/postinst.sh | 10 +--------- 4 files changed, 8 insertions(+), 25 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ebcc8e1a9..85caf75b4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -219,10 +219,8 @@ jobs: run: | ./hack/authentik/wait.sh - run: make containers/e2e/ci-build - # AppArmor policy is host-global: the runner's unix-chkpwd profile confines - # unix_chkpwd inside the container, and the container cannot load policy of - # its own (no securityfs during docker build). Install the same drop-in the - # deb ships so the e2e run exercises it. + # AppArmor policy is host-global: and the container cannot load policy of + # its own (no securityfs during docker build) - name: Load authentik AppArmor drop-in run: | sudo install -m 0644 -D \ diff --git a/ak-platform-e2e/src/lib.rs b/ak-platform-e2e/src/lib.rs index a7a9e7763..4c4a47814 100644 --- a/ak-platform-e2e/src/lib.rs +++ b/ak-platform-e2e/src/lib.rs @@ -318,10 +318,7 @@ pub async fn must_exec( Ok(output) } -/// Fails if the kernel logged an AppArmor denial for an authentik path, which -/// means a confined process (typically unix_chkpwd) could not use the NSS -/// module. The policy is loaded on the CI host, not by the container, since -/// AppArmor policy is host-global -- see the e2e job in test.yml. +/// Fails if the kernel logged an AppArmor denial for an authentik path pub async fn assert_no_apparmor_denials(container: &ContainerAsync) -> Result<()> { // Scoped to authentik paths so unrelated host denials don't fail the test. let (exit_code, output) = exec_command( diff --git a/vpkg/linux/nss/_deb/apparmor/abstractions/nameservice.d/authentik b/vpkg/linux/nss/_deb/apparmor/abstractions/nameservice.d/authentik index 2f5f20157..22996de6a 100644 --- a/vpkg/linux/nss/_deb/apparmor/abstractions/nameservice.d/authentik +++ b/vpkg/linux/nss/_deb/apparmor/abstractions/nameservice.d/authentik @@ -1,17 +1,13 @@ # Drop-in for , which is included by every profile that -# performs user/group lookups (unix-chkpwd, sshd, cron, ...). glibc loads -# libnss_authentik.so.2 into those processes, and it reads the authentik config -# and dials ak-sysd. +# performs user/group lookups -# Config file, read to discover the socket path. +# Config file, read to discover the socket path and debug config. /etc/authentik/config.json r, -# The ak-sysd control socket. AF_UNIX pathname sockets are mediated as files. +# The ak-sysd control socket. @{run}/authentik/sys.sock rw, # Disconnected-path variants: apparmor >= 4.1 declares unix-chkpwd with -# flags=(attach_disconnected.path=/att/unix-chkpwd/), so paths resolved inside a -# mount namespace are reported under that prefix. Inert elsewhere. Upstream does -# the same for authd's socket. +# flags=(attach_disconnected.path=/att/unix-chkpwd/) /att/unix-chkpwd/etc/authentik/config.json r, /att/unix-chkpwd/run/authentik/sys.sock rw, diff --git a/vpkg/linux/nss/_deb/postinst.sh b/vpkg/linux/nss/_deb/postinst.sh index 6ff76f129..a82bb2714 100755 --- a/vpkg/linux/nss/_deb/postinst.sh +++ b/vpkg/linux/nss/_deb/postinst.sh @@ -29,15 +29,7 @@ insert_nss_entry() { # Reload AppArmor so it picks up the drop-in we ship under /etc/apparmor.d. # The apparmor package registers no dpkg trigger on that directory, so this only -# shortcuts the wait until the next apparmor reload or reboot; the rules -# themselves are picked up either way. Reload the whole policy rather than a -# single profile: the abstractions/nameservice.d drop-in is included by every -# profile that performs user/group lookups. -# -# Expected to no-op inside build containers: apparmor_parser is usually absent, -# and even when present /sys/kernel/security is not mounted during a container -# build, so no policy can be loaded. Note that AppArmor policy is host-global, -# so a container never confines its own processes -- the host's profiles do. +# shortcuts the wait until the next apparmor reload or reboot. reload_apparmor() { log "Checking AppArmor setup..." if ! command -v apparmor_parser >/dev/null 2>&1; then