Linux has no built-in way to bind packet filter rules to an authenticated
user session and revoke them at logout. pam_authnft fills that gap: it
loads nftables rules when a PAM session opens and removes them when it
closes. If close_session never runs (daemon crash, OOM kill, kernel
panic, hard reset), the session's set element carries a 24-hour timeout
and stops matching traffic within a day. The per-session chain, sets and
jump rule have no timeout of their own; they persist until a later
login's PID recycles onto the leaked names, which trips the self-heal at
open_session, or until an administrator removes them by hand.
OpenBSD's authpf has had this for years: named anchors loaded per session via pfctl and torn down when the session ends. authnft brings the same model to Linux, as a PAM module. Under 2,000 lines of C, auditable in a day or two. Named nftables sets stand in for the anchors, and the cgroupv2 inode of a systemd transient scope replaces the authenticated shell as the session identity.
No dedicated shell, no setuid binary, no kernel patches.
A session's entire runtime footprint is ordinary nftables state,
inspectable with standard nft tooling:
# nft list table inet authnft
table inet authnft {
set session_alice_1127936_v4 {
typeof socket cgroupv2 level 0 . ip saddr
flags timeout
elements = { "authnft.slice/authnft-alice-1127936.scope" . 192.0.2.1
timeout 1d expires 23h55m56s comment "alice (PID:1127936)" }
}
set session_alice_1127936_v6 {
typeof socket cgroupv2 level 0 . ip6 saddr
flags timeout
}
set session_alice_1127936_cg {
typeof socket cgroupv2 level 0
flags timeout
}
chain filter {
type filter hook input priority filter - 1; policy accept;
ct state established,related ct mark & 0x00ffffff == 0x00000000 accept
ct state established,related ct mark & 0x00ffffff == @live_sessions accept
jump session_alice_1127936
}
chain session_alice_1127936 {
socket cgroupv2 level 2 . ip saddr @session_alice_1127936_v4 accept
}
}
The quoted path is the session's cgroupv2 scope under authnft.slice;
the kernel resolves it to an inode when the element is inserted. When a
packet is classified, socket cgroupv2 level 2 reads the socket's
originating cgroup and matches it against the set. That binds the rule
to the session without referencing PIDs, UIDs or usernames. The 24-hour
timeout is a safety net; explicit deletion at logout is the primary
cleanup.
See docs/CONCEPTS.md for how it works and docs/ADMIN_GUIDE.md for installation and configuration.
- Needs the cgroupv2 unified hierarchy. Hybrid setups are untested.
- Requires systemd. Other init systems are not supported.
- Syntax errors in a fragment are caught at load time and logged. Semantic mistakes, rules that parse but do the wrong thing, are the administrator's responsibility.
- If cleanup fails at logout, say because nftables is unavailable, the session's set element expires on its own after 24 hours. Leftover session JSON files are removed after 7 days by systemd-tmpfiles.
- The match only applies to sockets created inside the session. A socket that existed before the session opened, such as the SSH control connection, keeps the cgroup it was created in and is never matched. The shared chain carries that traffic on its untagged arm, which is also why closing a session does not disturb it.
- The established-accept gate carries a flow only if conntrack was already tracking it when it opened. On a host with no stateful rules at all, where pam_authnft introduces the first one, a connection that predates it is not retroactively covered (issue #111).
All documentation is indexed in docs/; see the roadmap and CONTRIBUTING.txt. Report security issues privately via GitHub Security Advisories (SECURITY.md).
GPL-2.0-or-later. See LICENSE for details.
Every source file carries an SPDX-License-Identifier: GPL-2.0-or-later
tag. Recipients may redistribute and/or modify the software under the
terms of GPL-2.0, or, at their option, any later version published by
the Free Software Foundation.
Copyright (C) 2025-2026 Avinash H. Duduskar.