identd-ng/pam_authnft
GitHub: identd-ng/pam_authnft
为 Linux 提供基于 PAM 与 nftables cgroup 绑定的按会话防火墙,解决认证会话与网络策略原子化联动问题。
Stars: 2 | Forks: 0
# pam_authnft
[](https://github.com/identd-ng/pam_authnft/actions/workflows/build.yml)
[](https://github.com/identd-ng/pam_authnft/actions/workflows/cifuzz.yml)
[](https://github.com/identd-ng/pam_authnft/actions/workflows/cppcheck.yml)
[](https://github.com/identd-ng/pam_authnft/actions/workflows/codeql.yml)
[](https://github.com/identd-ng/pam_authnft/actions/workflows/sanitizers.yml)
[](https://www.bestpractices.dev/projects/12496)
[](https://scan.coverity.com/projects/pam_authnft)
[](https://en.wikipedia.org/wiki/C_(programming_language))
[](LICENSE)
Linux 没有内置的方法将包过滤规则绑定到已认证的用户会话,并在注销时撤销它们。
pam_authnft 填补了这一空白。规则在注销时通过 `close_session` 移除,
依赖于通过 `pam_set_data` 存储的每个会话的状态。24 小时的元素超时是
一项安全兜底机制,用于应对 `close_session` 完全未执行的情况——例如
daemon 崩溃、OOM kill、kernel panic 或硬重置。
OpenBSD 的 pf 功能已有多年历史——它通过 pfctl 加载按会话划分的
命名 anchor,并在会话结束时将其销毁。pam_authnft 将相同的模型
引入 Linux:nftables 的命名 set 充当 anchor 的等价物,而 systemd
transient scope 的 cgroupv2 inode 取代已认证的 shell 作为会话标识。
无需专用 shell,无需 setuid 二进制文件,也无需打 kernel 补丁。
-.scope)
D-->>A: scope created under authnft.slice
A->>A: construct cg_path, per-session chain/set names
Note over A,N: nft transaction 1
A->>N: add table + shared filter chain
A->>N: add ct state established,related accept (pre-scope sockets)
A->>N: add per-session chain
A->>N: add 3 per-session sets (_v4, _v6, _cg)
A->>N: add element { cg_path . src_ip } in selected set
Note over A,N: nft transaction 2
A->>N: add jump rule from filter to per-session chain
N-->>A: jump rule handle (stored for cleanup)
Note over A,N: nft transaction 3
A->>A: read /etc/authnft/users/, validate ownership
A->>A: substitute @session_v4/v6/cg/chain placeholders
A->>N: execute substituted fragment
A->>F: write /run/authnft/sessions/.json
A->>J: AUTHNFT_EVENT=open (correlation token)
A-->>P: PAM_SUCCESS
Note over U,N: --- session active ---
packets matched by socket cgroupv2 level 2 U->>P: close_session P->>A: pam_sm_close_session A->>N: delete jump rule (by handle) A->>N: flush + delete per-session chain A->>N: delete 3 per-session sets A->>F: unlink session JSON A->>J: AUTHNFT_EVENT=close (same correlation token) A-->>P: PAM_SUCCESS ``` ### 包分类 当一个包进入 kernel 时,nftables 会遍历 `filter` 链。第一条 规则接受已建立/相关的流量(涵盖诸如 SSH 控制连接本身的 scope 前期 socket)。新连接跳转到按会话的链中;该链的规则 会检查该会话自己的按会话 set。每个会话都有其独立的链和 set—— alice 和 bob 由完全不同的规则匹配。 ``` incoming packet │ ▼ ┌──────────────────────────────────┐ │ chain filter │ │ hook input, priority filter - 1 │ ├──────────────────────────────────┤ │ ct state established,related │ ──▶ pre-scope sockets, accept │ jump session_alice_1127936 │ ──▶ alice's per-session chain │ jump session_bob_4321 │ ──▶ bob's per-session chain └─────────────────┬────────────────┘ │ ▼ (alice's session chain) ┌──────────────────────────────────┐ ┌──────────────────────────────────┐ │ chain session_alice_1127936 │ │ set session_alice_1127936_v4 │ ├──────────────────────────────────┤ ├──────────────────────────────────┤ │ socket cgroupv2 level 2 │ lookup │ { "authnft.slice/ │ │ . ip saddr │ ──────▶│ authnft-alice-1127936 │ │ @session_alice_1127936_v4 │ │ .scope" . 192.0.2.1 } │ │ accept │ └──────────────────────────────────┘ │ (loaded from alice's fragment │ │ with @session_v4 placeholder │ │ substituted at open_session) │ └──────────────────────────────────┘ key the set is matched on: socket's originating cgroup (set at socket creation) . packet source IP ``` 会话之间是相互隔离的:alice 的按会话链只引用 alice 的按会话 set,而该 set 仅包含一个元素(她的 cg_path . src_ip)。 在 close_session 时移除该元素,或者完全删除按会话链,都会 立即停止其规则的触发——bob 的链和 set 完全不受影响。 在打开会话时: 1. 规范化 `PAM_RHOST`:IPv4/IPv6 字面量直接通过,去除 zone 后缀, 主机名根据 `rhost_policy` 处理(参见[模块参数](#module-arguments))。 2. 使用 seccomp-BPF 白名单(默认 `SCMP_ACT_KILL`)锁定 PAM 进程。 3. 通过 D-Bus 在 `authnft.slice` 下创建一个命名的 transient `.scope`。 4. 构建 scope 的 cgroupv2 路径(`authnft.slice/.scope`),并将其
连同按会话的链/set 名称、规范化的源 IP 和一个相关性 token 一起
存储在 PAM data 中。
5. 创建一个按会话的链(`session__`)和三个按会话的
set(`_v4`, `_v6`, `_cg`)。根据解析出的 IP 族将一个会话元素
插入到其中一个 set 中。在共享的 `filter` 链中添加一条跳转规则。
6. 验证并加载位于 `/etc/authnft/users/` 且属主为 root 的
用户 fragment,将四个占位符
(`@session_v4`, `@session_v6`, `@session_cg`, `@session_chain`)
替换为当前的按会话名称。
7. 写入 `/run/authnft/sessions/.json`(0644 root:root),
以便非特权观察者能够将 cgroup 关联回所属会话——参见
[docs/INTEGRATIONS.txt](docs/INTEGRATIONS.txt) §5.6。
8. 发出带有相关性 token 的结构化 `AUTHNFT_EVENT=open` journal 条目——
参见 [docs/INTEGRATIONS.txt](docs/INTEGRATIONS.txt) §6.2。
在注销时,存储的会话状态将从 PAM data 中检索:跳转规则按句柄删除,
按会话链被刷新并删除,三个按会话 set 在单个事务中被删除。会话标识
JSON 被取消链接,并发出带有相同相关性 token 的匹配
`AUTHNFT_EVENT=close` journal 条目。共享的 `filter` 链和
`authnft` 表在会话之间持久存在。
有关完整的生命周期、信任模型和 seccomp 详细信息,请参见
[docs/ARCHITECTURE.txt](docs/ARCHITECTURE.txt)。
## 集成接口
pam_authnft 的设计刻意保持轻量和可组合。它公开了六个稳定的
接口;[集成契约](docs/INTEGRATIONS.txt) 记录了每一个接口的
MUST/SHOULD 要求和版本控制保证。
| Interface | 它是什么 | 适用对象 |
|---|---|---|
| **PAM** | 恰好有两个导出符号:`pam_sm_open_session`, `pam_sm_close_session`。读取 `PAM_USER`, `PAM_RHOST`,以及可选的两个环境变量(`claims_env=NAME`, `AUTHNFT_CORRELATION`)。 | PAM 模块作者,发行版打包者 |
| **nftables sets** | `table inet authnft` 下每个活跃会话有三个按会话的 set(`session___{v4,v6,cg}`),可通过 `nft list` 查看。 | 防火墙工具、策略引擎 |
| **按用户的 fragment** | 位于 `/etc/authnft/users/` 的纯 nftables 语法。可以使用 `include` 来组合共享的组级规则(§4.6)。 | 配置管理(Ansible/Salt/Puppet)、身份代理 |
| **systemd** | `authnft.slice` 下的 transient `.scope` 单元。可通过 `systemctl list-units 'authnft-*.scope'` 发现。支持所有 `systemd.resource-control(5)` 指令。 | 编排器、资源核算工具 |
| **claims_env** | 可选的 keyring-payload 通道:上游 PAM 模块通过 `add_key(2)` + `pam_putenv(3)` 写入一个标签;pam_authnft 读取、清理并将其嵌入到 nftables 元素的 comment 中。 | AAA/审计集成、身份代理 |
| **会话 JSON + journal 事件** | `/run/authnft/sessions/.json` 用于可观测性(§5.6);带有共享 `AUTHNFT_CORRELATION` token 的 `AUTHNFT_EVENT=open/close` journald 记录(§6.2)。 | SIEM 收集器、工作负载调度器、操作面板 |
该模块不是一个插件宿主。它没有共享库 ABI,也没有
回调注册表。每个契约都使用现有的 kernel 或用户空间原语
(PAM env、kernel keyring、文件系统、D-Bus、netlink、journald),
并带有严格的 schema。
```
flowchart LR
subgraph producers["Producers (write)"]
IB[Identity broker
OIDC PAM module] CM[Config management
Ansible/Salt/Puppet] OPS[Operator] end subgraph kernel["Kernel + userspace primitives"] KR[Kernel keyring
add_key/keyctl] FS["/etc/authnft/users/
fragments"] ENV[PAM env
claims_env, AUTHNFT_CORRELATION] end subgraph authnft["pam_authnft"] OPEN[pam_sm_open_session] CLOSE[pam_sm_close_session] end subgraph sinks["Consumers (read)"] NFT[nftables sets
nft list] JSON["/run/authnft/sessions/
JSON files"] JRNL[journald
AUTHNFT_EVENT] SCOPE[systemd scopes
systemctl] end subgraph audience["Who reads what"] FW[Firewall tooling] SIEM[SIEM / SOC] ORCH[Orchestrators] end IB -->|claims tag| KR IB -->|correlation token| ENV CM -->|writes| FS OPS -->|writes| FS KR --> OPEN FS --> OPEN ENV --> OPEN OPEN --> NFT OPEN --> JSON OPEN --> JRNL OPEN --> SCOPE CLOSE --> NFT CLOSE --> JRNL NFT --> FW JSON --> SIEM JRNL --> SIEM SCOPE --> ORCH ``` 生产者(左侧)独立于消费者(右侧)。pam_authnft 位于中间, 没有共享库或回调注册表——每个箭头都是记录在案的 kernel 或用户空间原语。 ## 快速开始 ### 前提条件 - Linux kernel >= 5.10,cgroupv2 统一层级 - 带有 D-Bus 的 systemd - nftables >= 1.0.6(`socket cgroupv2` 表达式需要 kernel >= 4.10) - 构建:`gcc`, `make`, `pkg-config` - 库:`libnftables`, `libseccomp`, `libsystemd`, `libcap`, `libaudit`, `pam` ### 构建并安装 ``` make # release build make debug # rebuild with -DDEBUG -g for stderr tracing make man # build pam_authnft(8) manpage (requires pandoc) sudo make install # installs pam_authnft.so, authnft.slice, tmpfiles.d sudo make install-man ``` 将模块安装到 `/usr/lib/security/pam_authnft.so`,将 `authnft.slice` 安装到 `/etc/systemd/system/`,并将用于在引导时 创建 `/run/authnft/sessions/` 的 tmpfiles.d 片段安装到 `/usr/lib/tmpfiles.d/authnft.conf`。 ### 最简工作配置 ``` # 创建 authnft 组(成员受 session firewall 规则约束) sudo groupadd authnft # 向组中添加用户 sudo usermod -aG authnft alice # 为该用户创建 root 拥有的 fragment sudo tee /etc/authnft/users/alice > /dev/null <<'EOF' add rule inet authnft @session_chain socket cgroupv2 level 2 . ip saddr @session_v4 accept EOF sudo chmod 644 /etc/authnft/users/alice ``` 添加到 `/etc/pam.d/sshd`(在 `pam_systemd.so` 之后): ``` session optional pam_authnft.so ``` 在打开会话时,没有有效 fragment 的组成员将被拒绝(并记录到 syslog)。非组成员将直接通过而不受影响。 请查看 `examples/examples_generator.sh -f` 以获取受限端口、masquerade 和限时 fragment 的变体。 ## 配置参考 ### 模块参数 | 参数 | 默认值 | 效果 | |---|---|---| | `host_policy=lax` | ✓ | 如果 PAM_RHOST 解析为 IP 则使用它,否则回退到仅 cgroup 的 set | | `rhost_policy=strict` | | 当 PAM_RHOST 不是可解析的 IP 字面量时拒绝会话(0.2 之前的行为) | | `rhost_policy=kernel` | | 通过 `NETLINK_SOCK_DIAG` 从会话进程自身的 ESTABLISHED TCP socket 推导出对端 IP(参见 `ss(8)`)。如果与 PAM_RHOST 存在差异,则记录警告。查找失败时回退到 `lax` | | `claims_env=NAME` | | 读取 PAM 环境变量 `NAME` 以获取 kernel-keyring 序列号;将经过清理的 keyed payload 嵌入到 nftables 元素的 comment 中。参见 [docs/INTEGRATIONS.txt](docs/INTEGRATIONS.txt) §2 | | `AUTHNFT_NO_SANDBOX=1` | | 禁用 seccomp sandbox。仅用于调试 | ### Kernel keyring 交接 (claims_env) 当设置了 `claims_env=NAME` 时,在同一会话中较早运行的上游 PAM 模块 可以通过 Linux kernel keyring 将会话元数据传递给 pam_authnft。 keyring 是一个由 kernel 管理的键/值存储,其作用域限定于 登录会话——参见 `keyrings(7)`。它不是文件、socket 或共享内存: kernel 分配 key,强制执行权限,并在会话结束时自动将其销毁。 ``` sequenceDiagram participant U as Upstream PAM module
(producer) participant K as Kernel keyring participant E as PAM env participant A as pam_authnft
(consumer) participant N as nftables Note over U,A: Both modules run in the same PAM session,
producer earlier in the stack than consumer U->>K: add_key("user", "", payload, SESSION_KEYRING)
K-->>U: serial number
U->>K: keyctl(SET_TIMEOUT, serial, ttl)
U->>K: keyctl(SETPERM, POSSESSOR view/read/search)
U->>E: pam_putenv("NAME=")
Note over K: claims live in kernel,
UID-locked, TTL-bounded A->>E: pam_getenv("NAME") E-->>A: ""
A->>K: keyctl(READ, serial)
K-->>A: payload bytes
A->>A: sanitize payload to safe charset
A->>N: insert element with claims as comment
```
生产者要求(key 类型、权限、payload 格式、
在 `SETPERM` 之前执行 `SET_TIMEOUT` 的顺序)记录在
[docs/INTEGRATIONS.txt §2](docs/INTEGRATIONS.txt) 中。pam_authnft 将
payload 视为不透明的可打印 ASCII——它不解析结构,只进行
清理和嵌入。这使得契约保持严谨,并允许任何生产者
(身份代理、AAA 栈、自定义模块)无需共享代码即可参与。
为什么选择 keyring 而不是仅仅使用文件或环境变量:
- **生命周期由 kernel 管理。** 当会话结束时,keyring
被销毁,claims 随之消失。无需清理代码。
- **在 kernel 级别进行 UID 锁定。** 主机上的其他进程无法
读取其他会话的 claims,即使是 root 用户,如果不首先获取
该 keyring(POSSESSOR 检查)也无法读取。
- **没有文件系统足迹。** 无需写入、同步或取消链接。不会
产生竞态条件,崩溃时也不会留下残留状态。
- **在 sshd privsep fork 中依然存活。** 与 shell 环境变量不同,kernel
keys 在 sshd 内部发生的 auth-worker → session-worker 过渡中
依然保持可读。
### PAM 栈选项
选项 A — 模块内部检查组成员身份;非组成员直接通过:
```
session optional pam_authnft.so
```
选项 B — PAM 根据组成员身份进行控制。没有有效 fragment 的成员将被
拒绝;非组成员将完全跳过该模块:
```
session [success=1 default=ignore] pam_succeed_if.so user notingroup authnft quiet
session required pam_authnft.so
```
### 按用户的 fragment
每个组成员都需要 `/etc/authnft/users/`,其属主为 root 且对
全局不可写。在加载之前,模块会对 fragment 路径调用 `stat(2)` 并
拒绝它,除非 `st_uid == 0` 并且全局可写位被清除——这与
`/etc/nftables.conf` 和 sudoers include 所使用的信任模型相同。
该 fragment 在顶层被包含,并作为 nftables 命令运行。
一个 fragment 可以使用 nftables 的 `include` 指令从其他文件中
提取共享规则——例如,位于 `/etc/authnft/groups/` 下、被属于
该组的每个用户引用的组级 fragment。
libnftables 会以传递方式解析 include。pam_authnft 仅对顶层的按用户
fragment 强制执行属主和模式;管理员需对每个传递包含的
文件的权限负责。请参阅
[docs/INTEGRATIONS.txt](docs/INTEGRATIONS.txt) §4.6 以了解组合
模式、安全说明和循环检测指南。
### 会话打开后的 nftables 状态
```
# 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 accept
jump session_alice_1127936
}
chain session_alice_1127936 {
socket cgroupv2 level 2 . ip saddr @session_alice_1127936_v4 accept
}
}
```
注意:`nft list` 将 set 类型规范化为 `level 0`;而规则
保留了配置的 `level 2`。这是预期的 nftables 行为——
level 是规则表达式的属性,而不是 set 类型的属性。
设置了 `claims_env=NAME` 并且栈中较早的模块生成了有效的 keyring 条目时,
元素的 comment 会扩展经过清理的 payload:
```
elements = { "authnft.slice/authnft-alice-1127936.scope" . 192.0.2.1 timeout 1d comment "alice (PID:1127936) [audit-session:7f3e9a]" }
```
引号中的路径是 `authnft.slice` 下会话的 cgroupv2 scope。
kernel 在插入时会将其解析为 cgroupv2 inode。在包分类
时刻,`socket cgroupv2 level 2` 会读取 socket 的起源 cgroup
并将其与 set 匹配——从而在不引用 PID、UID 或用户名的
情况下将防火墙规则绑定到会话。24 小时的超时是一项安全兜底机制;
在注销时显式删除才是主要的清理手段。
### 运行时可观测性(会话 JSON + 审计事件)
除了上述 nftables 状态之外,pam_authnft 还通过两个互补的
带外通道发布会话状态:
- **`/run/authnft/sessions/.json`** — 一个按会话的 JSON 文件
(0644 root:root),在打开时写入,在关闭时移除,包含带有版本号的
schema(`v=2`),其中含有 user、cgroup path、remote IP、fragment path、
claims tag、scope unit、correlation token 以及 RFC 3339 打开时间戳。
该目录由 `/usr/lib/tmpfiles.d/authnft.conf` 在引导时创建;
来自失败关闭路径的孤立文件将在 7 天后被清理。完整 schema 见于
[docs/INTEGRATIONS.txt](docs/INTEGRATIONS.txt) §5.6。
- **结构化 journald 审计事件** — 会话打开时的 `AUTHNFT_EVENT=open`
和关闭时的 `AUTHNFT_EVENT=close`,两者均处于
`SYSLOG_IDENTIFIER=pam_authnft` 之下,携带一个共享的
`AUTHNFT_CORRELATION` token,允许 SIEM 将这两个事件关联起来(并且,
按照惯例,也可以关联产生相同 token 的上游认证事件)。
上游 PAM 模块通过
`pam_putenv(pamh, "AUTHNFT_CORRELATION=")` 播种相关性。
完整字段 schema 见于 [docs/INTEGRATIONS.txt](docs/INTEGRATIONS.txt) §6.2。
这两个接收器都采用 fail-open 策略:写入失败会记录为 LOG_WARNING,但不会
拒绝会话。
### systemd 控制
由于每个会话都存在于一个命名的 `.scope` 单元中,因此可以使用完整的
systemd 资源控制和沙箱机制——`man
systemd.resource-control(5)`。`data/authnft.slice` 中的所有设置
均已被注释掉;请根据需要取消注释。
**出站网络策略** — 通过 systemd 的 cgroup-BPF 集成执行,
独立于 nftables:
```
IPAddressDeny=any
IPAddressAllow=10.0.0.0/8
SocketBindDeny=ipv4:tcp:1-1023
SocketBindDeny=ipv6:tcp:1-1023
```
**Syscall 和 capability 限制** — 在创建时应用于 scope 中的所有
进程:
```
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM
NoNewPrivileges=yes
CapabilityBoundingSet=
RestrictNamespaces=yes
```
## 测试
```
make test # unit tests, no root needed
make test-integration # pamtester + valgrind, requires root
```
容器工作流(推荐——无主机变更,仅需 `podman`):
```
make test-container # unit suite, 13 stages, CAP_NET_ADMIN
make test-integration-container # pamtester end-to-end + valgrind
make trace-container # seccomp allowlist trace
make test-musl # unit suite built against musl (Alpine)
```
有关单元 + 集成阶段的矩阵(阶段 0–13 和 10.1–10.24)
以及 CI 门禁清单,请参阅
[docs/CONTRIBUTING.txt](docs/CONTRIBUTING.txt) § Tests。
## 局限性
- 仅支持 cgroupv2 统一层级;未经测试的混合设置。
- 强依赖 systemd;不支持非 systemd init。
- Fragment 的语法错误会在加载时被捕获并记录;语义错误
由管理员负责。
- 如果在注销时清理失败(例如 nftables 不可用),set 元素将通过
插入时的安全兜底超时在 24 小时后过期。会话
JSON 孤立文件将由 systemd-tmpfiles 在 7 天后清理。
- cgroup 路径是在 `open_session` 时根据 scope 单元名称确定性地
构建的。`socket cgroupv2` 匹配适用于在会话 scope 内创建的
socket;在创建 scope 之前存在的 socket(例如
SSH 控制连接)携带其原始 cgroup,因此不参与匹配。该模块将
`ct state established,related accept` 添加到共享的 `filter` 链
以处理 scope 之前的流量。
- pam_authnft 不会对传递包含的 fragment 验证
属主或模式。管理员必须确保每个包含的文件均属主为 root 且
不对全局可写。
## 稳定性与路线图
**当前稳定** — PAM 接口(恰好两个导出符号),三个
nftables set 类型及其 schema,fragment 属主模型
(`st_uid == 0`,不可全局写入),记录在
[INTEGRATIONS.txt §6.1](docs/INTEGRATIONS.txt) 中的元素 comment 语法,
会话标识 JSON schema(`v=2`, §5.6),结构化 journald 审计字段(§6.2),
以及 Linux audit-syscall 通道
(带有原因标签 `missing | perms | content | nft-syntax` 的
`AUDIT_USER_ERR`,§6.2.7)。
**1.0 版本之前可能更改** — `claims_env` 线路格式细节、
`rhost_policy=kernel` NETLINK 内部机制、`authnft.slice` 附带的默认值。
**已规划** — OSS-Fuzz 注册(项目文件暂存于
[infra/oss-fuzz/](infra/oss-fuzz/),提交受限于项目年龄),
fragment linter(包装 libnftables 试运行),可插拔 fragment
来源,为 Arch (AUR) 和 Debian 提供打包。完整列表请参见
[docs/TODO.txt](docs/TODO.txt)。
## 项目文档
| 文档 | 内容 |
|---|---|
| [docs/ARCHITECTURE.txt](docs/ARCHITECTURE.txt) | 生命周期、信任模型、会话标识、seccomp 设计、会话标识文件、审计事件 |
| [docs/INTEGRATIONS.txt](docs/INTEGRATIONS.txt) | 针对生产者和消费者的稳定契约:PAM 栈(§1)、claims_env keyring(§2)、nft fragment 组合(§4.6)、systemd scope(§5)、会话 JSON(§5.6)、结构化 journal 事件(§6.2)、Linux audit-syscall 事件(§6.2.7) |
| [docs/CONTRIBUTING.txt](docs/CONTRIBUTING.txt) | 构建、布局、不变式、风格、测试过程、seccomp 白名单推导 |
| [docs/TODO.txt](docs/TODO.txt) | 近期、中期和推迟的工作项 |
| [docs/DOC_CHECKLIST.txt](docs/DOC_CHECKLIST.txt) | 按变更类型的文档更新矩阵 |
| [docs/THIRD_PARTY.md](docs/THIRD_PARTY.md) | 权威的依赖清单:许可证、版本下限、安全提要 |
| [docs/INCIDENT_RESPONSE.md](docs/INCIDENT_RESPONSE.md) | 处理安全报告的内部运行手册(公开政策见 [SECURITY.md](SECURITY.md)) |
| [docs/SECURITY_PRACTICES.md](docs/SECURITY_PRACTICES.md) | 项目中每个安全工具、文档、目标和里程碑的单页概览 |
| [docs/REPRODUCIBLE_BUILDS.md](docs/REPRODUCIBLE_BUILDS.md) | 我们提供的可重现性是什么,我们不提供什么,以及打包者如何验证发布产物 |
| [SECURITY.md](SECURITY.md) | 漏洞范围和报告程序 |
## 许可证
GPL-2.0-or-later。详情请参阅 [LICENSE](LICENSE)。
每个源文件都带有 `SPDX-License-Identifier: GPL-2.0-or-later`
标签。接收者可以在 GPL-2.0 的条款下重新分发和/或修改本软件,或者,
由其自行选择,Free Software Foundation 发布的任何更高版本。
Copyright (C) 2025-2026 Avinash H. Duduskar.
packets matched by socket cgroupv2 level 2 U->>P: close_session P->>A: pam_sm_close_session A->>N: delete jump rule (by handle) A->>N: flush + delete per-session chain A->>N: delete 3 per-session sets A->>F: unlink session JSON A->>J: AUTHNFT_EVENT=close (same correlation token) A-->>P: PAM_SUCCESS ``` ### 包分类 当一个包进入 kernel 时,nftables 会遍历 `filter` 链。第一条 规则接受已建立/相关的流量(涵盖诸如 SSH 控制连接本身的 scope 前期 socket)。新连接跳转到按会话的链中;该链的规则 会检查该会话自己的按会话 set。每个会话都有其独立的链和 set—— alice 和 bob 由完全不同的规则匹配。 ``` incoming packet │ ▼ ┌──────────────────────────────────┐ │ chain filter │ │ hook input, priority filter - 1 │ ├──────────────────────────────────┤ │ ct state established,related │ ──▶ pre-scope sockets, accept │ jump session_alice_1127936 │ ──▶ alice's per-session chain │ jump session_bob_4321 │ ──▶ bob's per-session chain └─────────────────┬────────────────┘ │ ▼ (alice's session chain) ┌──────────────────────────────────┐ ┌──────────────────────────────────┐ │ chain session_alice_1127936 │ │ set session_alice_1127936_v4 │ ├──────────────────────────────────┤ ├──────────────────────────────────┤ │ socket cgroupv2 level 2 │ lookup │ { "authnft.slice/ │ │ . ip saddr │ ──────▶│ authnft-alice-1127936 │ │ @session_alice_1127936_v4 │ │ .scope" . 192.0.2.1 } │ │ accept │ └──────────────────────────────────┘ │ (loaded from alice's fragment │ │ with @session_v4 placeholder │ │ substituted at open_session) │ └──────────────────────────────────┘ key the set is matched on: socket's originating cgroup (set at socket creation) . packet source IP ``` 会话之间是相互隔离的:alice 的按会话链只引用 alice 的按会话 set,而该 set 仅包含一个元素(她的 cg_path . src_ip)。 在 close_session 时移除该元素,或者完全删除按会话链,都会 立即停止其规则的触发——bob 的链和 set 完全不受影响。 在打开会话时: 1. 规范化 `PAM_RHOST`:IPv4/IPv6 字面量直接通过,去除 zone 后缀, 主机名根据 `rhost_policy` 处理(参见[模块参数](#module-arguments))。 2. 使用 seccomp-BPF 白名单(默认 `SCMP_ACT_KILL`)锁定 PAM 进程。 3. 通过 D-Bus 在 `authnft.slice` 下创建一个命名的 transient `.scope`。 4. 构建 scope 的 cgroupv2 路径(`authnft.slice/
OIDC PAM module] CM[Config management
Ansible/Salt/Puppet] OPS[Operator] end subgraph kernel["Kernel + userspace primitives"] KR[Kernel keyring
add_key/keyctl] FS["/etc/authnft/users/
fragments"] ENV[PAM env
claims_env, AUTHNFT_CORRELATION] end subgraph authnft["pam_authnft"] OPEN[pam_sm_open_session] CLOSE[pam_sm_close_session] end subgraph sinks["Consumers (read)"] NFT[nftables sets
nft list] JSON["/run/authnft/sessions/
JSON files"] JRNL[journald
AUTHNFT_EVENT] SCOPE[systemd scopes
systemctl] end subgraph audience["Who reads what"] FW[Firewall tooling] SIEM[SIEM / SOC] ORCH[Orchestrators] end IB -->|claims tag| KR IB -->|correlation token| ENV CM -->|writes| FS OPS -->|writes| FS KR --> OPEN FS --> OPEN ENV --> OPEN OPEN --> NFT OPEN --> JSON OPEN --> JRNL OPEN --> SCOPE CLOSE --> NFT CLOSE --> JRNL NFT --> FW JSON --> SIEM JRNL --> SIEM SCOPE --> ORCH ``` 生产者(左侧)独立于消费者(右侧)。pam_authnft 位于中间, 没有共享库或回调注册表——每个箭头都是记录在案的 kernel 或用户空间原语。 ## 快速开始 ### 前提条件 - Linux kernel >= 5.10,cgroupv2 统一层级 - 带有 D-Bus 的 systemd - nftables >= 1.0.6(`socket cgroupv2` 表达式需要 kernel >= 4.10) - 构建:`gcc`, `make`, `pkg-config` - 库:`libnftables`, `libseccomp`, `libsystemd`, `libcap`, `libaudit`, `pam` ### 构建并安装 ``` make # release build make debug # rebuild with -DDEBUG -g for stderr tracing make man # build pam_authnft(8) manpage (requires pandoc) sudo make install # installs pam_authnft.so, authnft.slice, tmpfiles.d sudo make install-man ``` 将模块安装到 `/usr/lib/security/pam_authnft.so`,将 `authnft.slice` 安装到 `/etc/systemd/system/`,并将用于在引导时 创建 `/run/authnft/sessions/` 的 tmpfiles.d 片段安装到 `/usr/lib/tmpfiles.d/authnft.conf`。 ### 最简工作配置 ``` # 创建 authnft 组(成员受 session firewall 规则约束) sudo groupadd authnft # 向组中添加用户 sudo usermod -aG authnft alice # 为该用户创建 root 拥有的 fragment sudo tee /etc/authnft/users/alice > /dev/null <<'EOF' add rule inet authnft @session_chain socket cgroupv2 level 2 . ip saddr @session_v4 accept EOF sudo chmod 644 /etc/authnft/users/alice ``` 添加到 `/etc/pam.d/sshd`(在 `pam_systemd.so` 之后): ``` session optional pam_authnft.so ``` 在打开会话时,没有有效 fragment 的组成员将被拒绝(并记录到 syslog)。非组成员将直接通过而不受影响。 请查看 `examples/examples_generator.sh -f` 以获取受限端口、masquerade 和限时 fragment 的变体。 ## 配置参考 ### 模块参数 | 参数 | 默认值 | 效果 | |---|---|---| | `host_policy=lax` | ✓ | 如果 PAM_RHOST 解析为 IP 则使用它,否则回退到仅 cgroup 的 set | | `rhost_policy=strict` | | 当 PAM_RHOST 不是可解析的 IP 字面量时拒绝会话(0.2 之前的行为) | | `rhost_policy=kernel` | | 通过 `NETLINK_SOCK_DIAG` 从会话进程自身的 ESTABLISHED TCP socket 推导出对端 IP(参见 `ss(8)`)。如果与 PAM_RHOST 存在差异,则记录警告。查找失败时回退到 `lax` | | `claims_env=NAME` | | 读取 PAM 环境变量 `NAME` 以获取 kernel-keyring 序列号;将经过清理的 keyed payload 嵌入到 nftables 元素的 comment 中。参见 [docs/INTEGRATIONS.txt](docs/INTEGRATIONS.txt) §2 | | `AUTHNFT_NO_SANDBOX=1` | | 禁用 seccomp sandbox。仅用于调试 | ### Kernel keyring 交接 (claims_env) 当设置了 `claims_env=NAME` 时,在同一会话中较早运行的上游 PAM 模块 可以通过 Linux kernel keyring 将会话元数据传递给 pam_authnft。 keyring 是一个由 kernel 管理的键/值存储,其作用域限定于 登录会话——参见 `keyrings(7)`。它不是文件、socket 或共享内存: kernel 分配 key,强制执行权限,并在会话结束时自动将其销毁。 ``` sequenceDiagram participant U as Upstream PAM module
(producer) participant K as Kernel keyring participant E as PAM env participant A as pam_authnft
(consumer) participant N as nftables Note over U,A: Both modules run in the same PAM session,
producer earlier in the stack than consumer U->>K: add_key("user", "
UID-locked, TTL-bounded A->>E: pam_getenv("NAME") E-->>A: "
标签:Cgroupv2, Linux, nftables, PAM, 客户端加密, 网络防火墙, 身份认证