diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index de49f15fa..85caf75b4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -219,6 +219,15 @@ jobs: run: | ./hack/authentik/wait.sh - run: make containers/e2e/ci-build + # 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 \ + 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 c967aeb45..4c4a47814 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); @@ -318,6 +318,22 @@ pub async fn must_exec( Ok(output) } +/// 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( + 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/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", ] { 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 new file mode 100644 index 000000000..22996de6a --- /dev/null +++ b/vpkg/linux/nss/_deb/apparmor/abstractions/nameservice.d/authentik @@ -0,0 +1,13 @@ +# Drop-in for , which is included by every profile that +# performs user/group lookups + +# Config file, read to discover the socket path and debug config. +/etc/authentik/config.json r, + +# 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/) +/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..a82bb2714 100755 --- a/vpkg/linux/nss/_deb/postinst.sh +++ b/vpkg/linux/nss/_deb/postinst.sh @@ -27,8 +27,34 @@ insert_nss_entry() { ' /etc/nsswitch.conf } +# 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. +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..abe622b9b 100644 --- a/vpkg/linux/nss/nfpm.yaml +++ b/vpkg/linux/nss/nfpm.yaml @@ -22,6 +22,15 @@ 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 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 overrides: deb: depends: