-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
Sentinel is two cooperating processes glued by a Unix pipe:
┌──────────────┐ fork+exec ┌──────────────────┐
│ pam_sentinel │ ─────────────────▶│ sentinel-helper │
│ (.so in │ pipe(stdout) │ (Wayland GUI) │
│ caller PAM) │ ◀─────────────────│ │
└──────────────┘ "ALLOW\n" / └──────────────────┘
"DENY\n" / "TIMEOUT\n"
Loaded by libpam into whatever process is authenticating (sudo,
pkexec, etc). Implements pam_sm_authenticate and returns one of:
| PAM result | When |
|---|---|
PAM_SUCCESS |
Helper printed ALLOW
|
PAM_AUTH_ERR |
Helper printed DENY, or TIMEOUT with default policy |
PAM_IGNORE |
Headless and headless_action = "password"; or enabled = false; or config parse error (fail-open) |
PAM_AUTHINFO_UNAVAIL |
Helper failed to spawn or crashed |
-
Read config —
/etc/security/sentinel.conf. Parse failure logs and returnsPAM_IGNORE. -
Pick service config — apply
[services.<svc>]overrides on top of[general]for the current PAM service. -
Detect display — if
XDG_RUNTIME_DIRandWAYLAND_DISPLAYof the target user aren't set, take theheadless_actionpath. -
Resolve requesting process — read
/proc/<requesting_pid>/exeand/proc/<requesting_pid>/commto populate%psubstitutions. -
Fork + exec helper — drop privileges to the target user
(
setgid+setuid+initgroups),dup2a pipe write end onto stdout,execvsentinel-helper. The PAM module never holds root on the helper side. -
Wait —
poll(POLLIN, timeout). Helper has its own internal timeout; the PAM-side poll useshelper_timeout + 5sas a safety margin. If poll times out the module sendsSIGKILLand reaps. - Read — read up to 32 bytes from the pipe; first newline-terminated token is the verdict.
-
Map and return — translate
ALLOW/DENY/TIMEOUT→ PAM result.
pam_sentinel.so runs in the auth process's privilege context (often
root for sudo, root for pkexec). The helper does not — the fork
sets uid/gid to the target user (the user being authenticated
to; for sudo -u root that's root, but for sudo -u alice it's
alice) and the helper runs entirely as that user. The helper never sees
or writes anything to disk that requires elevated privilege; the only
thing it returns is three bytes of stdout.
A libcosmic + iced application that creates a single
zwlr-layer-shell-v1 overlay surface:
| Layer-shell setting | Value |
|---|---|
Layer |
Overlay |
Anchor |
TOP | BOTTOM | LEFT | RIGHT (full-screen) |
KeyboardInteractivity |
Exclusive |
exclusive_zone |
-1 (ignore other layer surfaces) |
namespace |
"sentinel" |
The view is a translucent black backdrop (55% alpha) filling the
output, with the cosmic-themed dialog card centered on top. Allow is
disabled for min_display_time_ms after surface creation (defaults to
500 ms) to block instant scripted clicks.
After the user clicks (or timeout fires), the helper prints exactly one
of ALLOW\n, DENY\n, TIMEOUT\n to stdout and exits with code
0 (allow) or 1 (deny / timeout).
Auto-deny on Escape keypress and on window close (compositor
forcibly destroyed the surface).
An earlier draft used ext-session-lock-v1 to take over the entire
session. Two problems:
- Hard-fail risk: a buggy lock implementation locks the user out irrecoverably.
- The session-lock protocol is intended for "everything-blocking" screensavers — semantically wrong for a one-shot confirmation.
Layer-shell + exclusive keyboard achieves the same UX (full-screen
overlay, blocks input to everything beneath) without the lock-out
risk. The cost is that a malicious compositor or process running as
the same user could in theory inject events via
wlr_virtual_pointer/virtual_keyboard. For the realistic threat
model (a clickjacking app, a runaway script) this is plenty.
install.sh writes one line to /var/lib/sentinel/install.state per
file it touches:
CREATED \t /usr/lib/sentinel-helper \t
CREATED \t /usr/lib/security/pam_sentinel.so \t
REPLACED\t /etc/pam.d/polkit-1 \t /etc/pam.d/polkit-1.pre-sentinel.bak
A trap on ERR INT TERM walks the same in-memory log in reverse and
restores pre-install state if anything fails. uninstall.sh reads the
on-disk state file and does the same restoration logic.
Rationale: an install/uninstall script that touches PAM config has
high blast radius (a botched run can lock the user out of sudo).
Make every change reversible, atomic, and idempotent.
.
├── Cargo.toml # workspace root
├── crates/
│ ├── sentinel-config/ # shared since v0.5 — schema, parser,
│ │ │ # format_message, log_kv quote helper
│ │ ├── build.rs # bakes SENTINEL_CONFIG_PATH
│ │ └── src/lib.rs
│ ├── pam-sentinel/
│ │ ├── build.rs # bakes SENTINEL_HELPER_PATH
│ │ └── src/
│ │ ├── lib.rs # PAM hook entrypoints + structured outcome logs
│ │ ├── helper.rs # fork/exec + pipe communication
│ │ ├── agent_bypass.rs # bypass socket client (since v0.4)
│ │ ├── locale.rs # since v0.5: pull LANG/LC_* from
│ │ │ # /proc/<requesting_pid>/environ,
│ │ │ # validate, forward to helper child
│ │ ├── proc_info.rs # /proc/<pid>/exe lookups
│ │ └── display.rs # WAYLAND_DISPLAY / runtime dir detection
│ ├── sentinel-helper/
│ │ ├── locales/ # since v0.5 — 12 embedded fluent bundles
│ │ │ ├── en-US/sentinel-helper.ftl
│ │ │ ├── de-DE/sentinel-helper.ftl
│ │ │ └── …
│ │ └── src/
│ │ ├── main.rs # cosmic boot + layer-shell auto-downgrade heuristic
│ │ ├── app.rs # ConfirmApp Application impl + layer-shell init,
│ │ │ # process-icon resolution, default-string
│ │ │ # detection for i18n
│ │ ├── i18n.rs # since v0.5 — fluent-bundle wrapper
│ │ ├── cli.rs # clap arg parser (incl. --layer-shell flag)
│ │ └── result.rs # Outcome enum
│ └── sentinel-polkit-agent/ # since v0.4
│ └── src/
│ ├── main.rs # zbus + tokio boot, agent registration
│ ├── agent.rs # AuthenticationAgent impl + inflight mutex
│ ├── session.rs # one-auth orchestration
│ ├── helper_ui.rs # spawn sentinel-helper, parse verdict
│ ├── helper1.rs # talk to /run/polkit/agent-helper.socket
│ ├── socket_server.rs# bypass socket the PAM module connects to
│ ├── approval_queue.rs
│ ├── identity.rs
│ ├── subject.rs
│ └── authority.rs
├── config/ # config files installed under /etc
├── install.sh / uninstall.sh # transactional installer + in-place agent restart
├── packaging/ # Arch PKGBUILDs, systemd drop-in, XDG autostart
├── nix/module.nix # NixOS module
├── flake.nix
├── scripts/build-release.sh
└── .github/workflows/
├── ci.yml # since v0.5 — fmt/clippy/test/build on PRs
└── release.yml # tag v* → builds .deb/.rpm/tarballs + GH release
GPL-3.0-or-later. Provided as-is, without warranty of any kind (GPL §15-16). Use at your own risk on production systems.