Interoperability with pam_authnft — per-session kernel-enforced network policy from OIDC claims#14
Conversation
e1fda0f to
266dd0e
Compare
6922054 to
70616ca
Compare
|
@Strykar, thanks for this. The direction is right. ScopeWe'll take the generic primitive: keyring entry plus What needs to change before we can merge
Unrelated but also blocking
If any of the above reads as unreasonable, say so. Alternative path: we can take the primitive over as a fresh PR crediting you, which may be easier than rewriting against internal assumptions that haven't landed in OSS yet. Either way works. |
Signed-off-by: Strykar <2946372+Strykar@users.noreply.github.com>
…lation token Signed-off-by: Strykar <2946372+Strykar@users.noreply.github.com> Co-Authored-By: Claude Opus
1. Remove AUTHNFT_CORRELATION export — consumer-agnostic 2. Detect shared @us keyring, refuse to publish 3. Close TOCTOU: publish to @p, lock down, then KEYCTL_LINK 4. Versioned payload (v=1;) with percent-encoded delimiters 5. #[cfg(target_os = "linux")] on keyring module and call site Co-Authored-By: Claude Opus
70616ca to
4fa6b12
Compare
Three maintainer fixes on top of @Strykar's review-response commit: 1. format_claims: skip oversized pairs whole rather than truncate mid- escape. The previous out.truncate(MAX_PAYLOAD) could land between '%' and '3D', handing a corrupt percent-escape to decoders. Now the function pre-computes each encoded pair length and skips ones that won't fit whole. Later pairs still get a chance to fit, so payloads with one oversized claim still include shorter subsequent claims. 2. Add negative test for the @us refusal path. publish() is refactored to delegate to publish_with_detector() which takes a pluggable is_shared detector. The public API is unchanged; the test injects || true and asserts KeyringError::SharedSessionKeyring is returned before any kernel state is created. Corresponds to invariant prodnull#1 in ADR-026. 3. Drop CString::new("user").unwrap() in favour of the c-string literal c"user". No runtime allocation, no unwrap path. Fixes a clippy unwrap_used violation the crate's #![deny(clippy::unwrap_used)] would otherwise catch. New tests added: - format_claims_skips_oversized_pair_whole - format_claims_never_truncates_mid_escape - format_claims_fits_typical_payload - publish_refuses_shared_session_keyring - publish_proceeds_when_session_is_isolated Verified locally: cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings (macOS + Linux), and the full pam-prmana keyring test suite (9/9 pass on Linux). Refs: prodnull#14, ADR-026. Co-Authored-By: Avinash H. Duduskar <Strykar@users.noreply.github.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Fast turnaround, thanks. All five requirements are satisfied. I pushed one maintainer commit (
Verified locally: ADR-026 (kernel-keyring session-claims publication) and the new Nothing blocking from my end now. Once CI goes green on this branch I can merge. |
ADR-026 captures the architectural decision behind PR #14 (kernel-keyring session-claims publication by pam-prmana): adopt the generic primitive, decline consumer-specific env var exports, with five binding invariants covering keyring anchoring, atomic ACL, versioned/ escaped payload format, Linux-only build, and the PRMANA_KEY ABI commitment. TB-8 in docs/threat-model.md is the trust-boundary analysis consumed by the ADR: POSSESSOR semantics (inherited across fork/execve including setuid, contrary to "current process only" mental models), nine threats with STRIDE categories, normative mitigations tied 1:1 to the ADR invariants, four residual risks, and a positive/negative/ adversarial/fuzz test matrix. Supporting doc updates: - docs/adr/README.md: index entry for 026. - docs/pam-integration.md: recommend pam_keyinit.so force revoke in the SSH example; new section documenting the v=1 wire contract, keyring semantics, non-fatal failure mode, and a C example for consumers reading the payload. - docs/security-guide.md: operator bullet about pam_keyinit in the Production Requirements list. The capability was threat-modeled adversarially by two independent AI reviewers (Codex and Gemini-3-pro) before acceptance. Convergence on Option 2 scope and on the five merge preconditions. Refs: #14. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three files disagreed on what license prmana ships under and what commit-provenance record contributors owe: - LICENSE: Apache-2.0. - CONTRIBUTING.md: CC BY-NC-SA 4.0 (a content license that forbids commercial use — completely wrong for OSS code). - .github/PULL_REQUEST_TEMPLATE.md: "dual license (Apache-2.0 OR MIT)" plus "I have signed off my commits (DCO)". LICENSE is authoritative. This commit aligns the other two on Apache-2.0 and drops the phantom MIT half of the "dual license" line. Signing: CONTRIBUTING.md required Gitsign; the PR template already asked for DCO. External contributors to Linux-ecosystem projects almost universally use DCO. Rather than turn away quality contributions over a signing-format mismatch, the signing section now accepts either Gitsign OR DCO (`git commit -s`). Gitsign remains preferred for maintainer commits; that can be enforced via branch protection on main rather than via CONTRIBUTING.md text. @Strykar's PR #14 was DCO-signed. This change reconciles the project's stated expectation with the commit-provenance form external contributors actually use. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The license triad fix you flagged is up as #18 (CONTRIBUTING.md → Apache-2.0, PR template drops "OR MIT", signing section now accepts Gitsign or DCO). Your DCO-signed commits on this PR are fine as-is — no action needed from you. |
Three files disagreed on what license prmana ships under and what commit-provenance record contributors owe: - LICENSE: Apache-2.0. - CONTRIBUTING.md: CC BY-NC-SA 4.0 (a content license that forbids commercial use — completely wrong for OSS code). - .github/PULL_REQUEST_TEMPLATE.md: "dual license (Apache-2.0 OR MIT)" plus "I have signed off my commits (DCO)". LICENSE is authoritative. This commit aligns the other two on Apache-2.0 and drops the phantom MIT half of the "dual license" line. Signing: CONTRIBUTING.md required Gitsign; the PR template already asked for DCO. External contributors to Linux-ecosystem projects almost universally use DCO. Rather than turn away quality contributions over a signing-format mismatch, the signing section now accepts either Gitsign OR DCO (`git commit -s`). Gitsign remains preferred for maintainer commits; that can be enforced via branch protection on main rather than via CONTRIBUTING.md text. @Strykar's PR #14 was DCO-signed. This change reconciles the project's stated expectation with the commit-provenance form external contributors actually use. Co-authored-by: prmana Developers <prodnull@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ADR-026 captures the architectural decision behind PR #14 (kernel-keyring session-claims publication by pam-prmana): adopt the generic primitive, decline consumer-specific env var exports, with five binding invariants covering keyring anchoring, atomic ACL, versioned/ escaped payload format, Linux-only build, and the PRMANA_KEY ABI commitment. TB-8 in docs/threat-model.md is the trust-boundary analysis consumed by the ADR: POSSESSOR semantics (inherited across fork/execve including setuid, contrary to "current process only" mental models), nine threats with STRIDE categories, normative mitigations tied 1:1 to the ADR invariants, four residual risks, and a positive/negative/ adversarial/fuzz test matrix. Supporting doc updates: - docs/adr/README.md: index entry for 026. - docs/pam-integration.md: recommend pam_keyinit.so force revoke in the SSH example; new section documenting the v=1 wire contract, keyring semantics, non-fatal failure mode, and a C example for consumers reading the payload. - docs/security-guide.md: operator bullet about pam_keyinit in the Production Requirements list. The capability was threat-modeled adversarially by two independent AI reviewers (Codex and Gemini-3-pro) before acceptance. Convergence on Option 2 scope and on the five merge preconditions. Refs: #14. Co-authored-by: prmana Developers <prodnull@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Export authenticated session metadata through the Linux kernel keyring so
that a downstream PAM session module (pam_authnft) can enforce per-session
network policy at the kernel level. Each SSH login gets nftables rules
scoped to its cgroup, tagged with the OIDC identity that authorized it,
and correlated in journald with the prmana auth event. A containerized
interop test builds both projects from source, runs a real PAM session with
systemd and nftables, and produces an audit report with packet-level
enforcement proof.
No new dependencies. Non-fatal on failure. 31 lines added, 3 removed.
Type of Change
Related Issues
N/A — new interoperability feature.
Changes Made
user,uid, andoptionally
acr(authentication strength) anddpop(DPoP JWKthumbprint) alongside the existing
jti,exp,iss,sidfields.v=1;prefix) with percent-encoded delimitersto prevent injection via claim values.
permissions (SETPERM + SET_TIMEOUT), then linked into the session
keyring — closing the TOCTOU window between
add_keyandSETPERM.@ussession keyring and refuses to publish (non-fatal).Operator must run
pam_keyinit.so force revokebefore prmana.#[cfg(target_os = "linux")]on keyring module and call site.Problem
prmana authenticates. But once the session is open, there is no
kernel-level enforcement tying that session's network traffic to the
identity that was just verified. An authenticated user's packets are
indistinguishable from any other process on the host. Session isolation
is left to application-layer controls.
What this enables
When paired with pam_authnft
(GPL-2.0, independent project, no shared codebase):
Per-session microsegmentation. Each login gets its own nftables
chain and three per-session sets bound to its cgroupv2 scope. Firewall
rules match on the session's cgroup + source IP. Other sessions on the
same host are unaffected — per-session isolation is verified by
integration test 10.13.
Policy derived from identity claims. The per-user nftables fragment
can encode allowed ports, denied networks, compliance zones — driven
by what the IdP puts in the token. The kernel enforces it.
Auditable chain of custody. The nftables set element carries
prmana's claims as its comment. A single
journalctlquery on theshared correlation token returns both the auth event and the session
event. No log correlation infrastructure required.
Zero daemons, zero sidecars. The bridge is a kernel keyring entry
and a PAM environment variable. Cleanup is automatic — the kernel
tears down the keyring when the session ends, pam_authnft deletes the
per-session chain, sets, and jump rule on close.
Mechanism
After successful authentication, prmana publishes session claims to the
kernel's process keyring via
add_key(2), applies UID-only ACL andtoken-aligned TTL while the key is still in
@p, then links into thesession keyring and exposes the serial through
pam_putenv. pam_authnftreads the serial, fetches the payload via
keyctl(2), sanitizes it, andembeds it in the nftables element. No IPC, no files, no sockets — the
kernel manages the lifecycle.
The sequence:
add_key("user", "prmana_<sid>", payload, PROCESS_KEYRING)— publish claims to@pkeyctl(SET_TIMEOUT)— align lifetime with token expirykeyctl(SETPERM)— lock to POSSESSOR-only view/read/searchkeyctl(LINK, serial, SESSION_KEYRING)— move to session keyringkeyctl(UNLINK, serial, PROCESS_KEYRING)— remove from@ppam_putenv("PRMANA_KEY=<serial>")— expose to downstream modulesWhat gets shared at authentication time
v1jtif47ac10bexp1745053200issidp.example.comsiddemouseraliceuid1001acrurn:example:mfadpopSHA256:x4Ei9K2pValues containing
;,=, or%are percent-encoded. Additional fieldscan be added by pushing more pairs — the format is extensible and
pam_authnft treats the payload as opaque.
Note: nftables limits element comments to 128 characters. The full
claim set is always available in the journal (
AUTHNFT_CLAIMS_TAG) andthe session JSON at
/run/authnft/sessions/<scope>.json.What the operator sees
Live nftables state during a session (from the containerized interop test):
Deployment
Two independent projects. Apache-2.0 + GPL-2.0, composed through PAM and
the kernel keyring. No license entanglement, no shared trust boundary.
Security Checklist
Testing
cargo test)make test-integration)Test Environment
Manual Test Steps
A containerized end-to-end interop test is available in the pam_authnft
project. Requires only podman — no host mutation:
Builds both modules inside a Fedora container with systemd, runs a real
PAM session, and produces an audit report covering: kernel keyring state,
live nftables table, three packet tests (port 443 accepted, port 80 to
10.0.0.0/8 dropped, port 9090 accepted) with tcpdump, correlated journal
events, and session metadata.
Results at
/tmp/authnft-interop-result/audit_report.txt.Sample report: https://gist.github.com/Strykar/08b2ae0b9113a0974219a48cd00ca881
Documentation
Reviewer Notes
This patch addresses all five blockers from the initial review:
can build
prmana-<sid>fromPRMANA_SESSION_ID.to the shared user-session keyring.
@p, locked down, then linkedto session keyring.
v=1;prefix, percent-encoded delimiters.#[cfg(target_os = "linux")]on module and call site.The keyring payload format is extensible — additional token claims can be
included by pushing more key=value pairs. pam_authnft sanitizes the
payload to
[A-Za-z0-9_=,.:;/-]before embedding it in nftablescomments, so injection is not possible.
License clarification needed: I noticed that
CONTRIBUTING.mdstatescontributions are licensed under CC BY-NC-SA 4.0, while the
LICENSEfileand
Cargo.tomlspecify Apache-2.0, and the PR template references"Apache-2.0 OR MIT." I have left the contributing/license checkboxes
unchecked until the intended license is confirmed. Happy to agree once
clarified.
Commit signing: The contributing guidelines require Gitsign (Sigstore
keyless signing). These commits have DCO sign-off but are not yet
Gitsign-signed. I can re-sign with Gitsign if that is a hard requirement
for merge.
By submitting this PR, I confirm that: