Releases: anhtuank7c/pamsignal
v0.6.2
Patch release — no daemon code changes (the binary is identical to 0.6.1). This release is about everything around the daemon.
Highlights
- 📊 Grafana fleet dashboard — new
examples/grafana/integration: a Grafana Alloy config, a 12-panel dashboard, 4 alert rules, a one-command docker-compose try-it stack, andverify.sh. See every host's auth activity on one screen. - 📚 Documentation overhaul (EN + VI) — restructured README plus three new guides: Secure SSH & Manage a Fleet, Grafana from Zero, and Use Cases & Integrations — each mirrored in Vietnamese under
docs/vi/. - 📦 Debian packaging hygiene —
debian/watch,debian/upstream/metadata(DEP-12),Standards-Version4.7.0.
Full notes: CHANGELOG.md
Install
Signed .deb (Ubuntu 24.04 + 20.04 Focal) and .rpm (Fedora + EL9) packages are attached below once CI finishes, and the signed apt/dnf repos update automatically. Repo setup: see the README.
pamsignal 0.6.1
Patch release. One user-facing addition (--help) plus the CI hardening accumulated since 0.6.0.
What's new
--help / -h flag
```
$ pamsignal --help
Usage: pamsignal [OPTION]...
Real-time PAM authentication monitor with multi-channel alerts.
…
```
Prints a complete usage summary covering every option, the compiled-in default config path, the canonical files, and pointers to `pamsignal(8)` and `pamsignal.conf(5)`. Goes to stdout, exits 0, runs before any privilege/journal-access check — works under root, dpkg/rpm post-install scripts, or a plain operator.
GNU coding-standards baseline: pamsignal had `--version` since 0.4.1, but `--help` was the missing half. Now closed. The `pamsignal(8)` man page also documents both `-V`/`--version` and `-h`/`--help` explicitly in the OPTIONS section.
CI hardening
Both `test-deb` (Noble) and `test-deb-focal` now assert:
- `pamsignal --version` produces non-empty stdout matching `^pamsignal [0-9]+.[0-9]+.[0-9]+$`
- `pamsignal --help` produces non-empty stdout starting with `Usage: pamsignal` and containing `--foreground`
- Short forms `-V` / `-h` produce output byte-identical to the long forms
Closes the "installed cleanly but the flag prints nothing" failure mode that a `systemctl is-active` check would miss — a real operator-reported issue on 0.6.0 that turned out to be PATH-shadowing by a stale `/usr/local/bin/pamsignal` from an earlier `meson install`. A future regression in arg parsing can't reach the release page anymore.
Internal `test-deb-focal` instrumentation (`[N/9]` markers, `FAIL: ` on assertion failure, `printf %q` for perm comparison, SIGPIPE-safe man check, removal of docker minimal-image's `path-exclude=/usr/share/man/*` rule before install) — invisible to operators, benefits every future release.
Operator notes
No daemon-side changes. `pamsignal.service` behaviour is unchanged from 0.6.0. Operators on 0.6.0 don't need to upgrade urgently — this is a convenience + CI release, not a bug or security fix.
Install
| Distribution | Method |
|---|---|
| Ubuntu 24.04 / Debian 12 | `apt update && apt upgrade pamsignal` from the gh-pages repo |
| Ubuntu 20.04 LTS (Focal, ESM) | Download `pamsignal_0.6.1-1_focal_amd64.deb` below + `apt install ./.deb` |
| Fedora / CentOS / RHEL 9 / AlmaLinux 9 / Rocky 9 | `dnf upgrade pamsignal` from the gh-pages repo |
Full changelog
See CHANGELOG.md.
pamsignal 0.6.0
Highlights
Ubuntu 20.04 (Focal) is now a supported install target
A Focal-targeted .deb is built and smoke-tested in CI on every release and attached to this release page. Operators on 20.04 install with a one-line download + apt install:
```bash
VERSION=0.6.0
curl -fL -o pamsignal_focal.deb
"https://github.com/anhtuank7c/pamsignal/releases/download/v\${VERSION}/pamsignal_\${VERSION}-1_focal_amd64.deb"
sudo apt install ./pamsignal_focal.deb
```
No gh-pages apt pocket for Focal (rationale: ESM-only since 2025-04-30; per-distroseries repo infrastructure does not pay off for the audience size). Two systemd directives (`ProtectProc`, `ProcSubset`, both added in v247) are logged-and-ignored on Focal's systemd 245; everything else in the hardening unit is honored. See `docs/distros.md` for the full lifecycle picture (Focal exits ESM April 2030).
`pamsignal.conf(5)` man page
Operators now get `man pamsignal.conf` instead of grepping source comments or `docs/configuration.md`. Documents every config key, value range, mTLS and `webhook_auth_header` validation rules, reload semantics, and security notes. Installed to `/man5/` via meson.
Documentation refresh
- README quickstart "Configure Alerts" surfaces `alert_cooldown_sec` and `enable_notification_type` with operator-facing semantics on day one.
- Fail2ban integration guide (`examples/fail2ban/README.md`) rewritten from a 39-line stub into a complete walkthrough — install on Ubuntu/Debian and CentOS Stream 9 / AlmaLinux 9 / Rocky Linux 9 / Fedora, per-distro `banaction` selection, the critical whitelist-yourself-first step, verification flow, `bantime.increment` tuning, troubleshooting, and an architecture diagram.
Internal hygiene
RAII-style cleanup helpers (`include/ps_cleanup.h`) applied to the three fd-ladder-heavy security functions: `build_secrets_memfd` + `fire_curl` (memfd carrying webhook secrets to curl), `open_config_secure` + `ps_config_load` (O_NOFOLLOW config opener), and `validate_tls_path`. Defense in depth against future-edit fd leaks; zero runtime behaviour change.
Packaging compatibility
This release also fixes three pre-existing packaging-side bugs that surfaced when the new Focal job and new conf(5) man page first exercised the workflow: `debian/control` switched to the legacy `debhelper (>= 12~)` Build-Depends + `debian/compat` file (compatible with Focal's debhelper 12.10 AND Noble's 13.x); `debian/rules` uses `ninja` directly rather than the meson 0.54+ `compile`/`install` subcommands; `meson.build` uses `not fuzz_opt.disabled()` instead of meson 0.59's `.allowed()`; `pamsignal.spec` lists the new `%{_mandir}/man5/pamsignal.conf.5*` file. All four are backwards-compatible.
Install
| Distribution | Method |
|---|---|
| Ubuntu 24.04 / Debian 12 | `apt install pamsignal` from the gh-pages repo |
| Ubuntu 20.04 LTS (Focal, ESM) | Download `pamsignal_0.6.0-1_focal_amd64.deb` below + `apt install ./.deb` |
| Fedora / CentOS / RHEL 9 / AlmaLinux 9 / Rocky 9 | `dnf install pamsignal` from the gh-pages repo |
See the README install section for the full per-distro commands.
Full changelog
See CHANGELOG.md.
v0.5.0 — Selectable chat-alert categories
Minor release. Adds the first user-configurable filter over chat alerts — enable_notification_type lets operators opt in to specific event categories (login_success, login_failed, session_open, session_close, brute_force, all) rather than receiving every PAM event. Requested by @huy-lv (#22) for the common deployment shape where the host's notable signal is "someone got in" rather than "someone tried." The filter gates chat dispatch only — journalctl -t pamsignal still records every event, so the forensic trail and threat-model assumptions are unchanged. Default is all, so existing deployments behave identically without touching their config. Beneath the new filter, the existing per-event suppression for sudo/su LOGIN_FAILED (only the brute-force aggregate fires for those services) is untouched. Also ships two transitive-dependency security bumps in the bundled example receivers (idna CVE-2026-45409 in the Python receiver, qs CVE-2026-8723 in the Node.js receiver) — neither affects the C daemon, but the examples need to install cleanly into their own ecosystems. Internal: 13 new CMocka cases (9 config + 4 notify gating), zero new build warnings, ASVS L1 pre-merge gate clean.
Features
-
enable_notification_typeconfig key — select which event categories fire a chat alert (#22). Reported by @huy-lv. Comma-separated list of tokens:login_success,login_failed,session_open,session_close,brute_force, orall. Default — when the key is omitted or set toall— is every category, so existing deployments behave unchanged. Filters only the chat dispatch (Telegram / Slack / Teams / WhatsApp / Discord / custom webhook); the localjournalctl -t pamsignaltrail records every event regardless, preserving the forensic record assumed bydocs/threat-model.md. Unknown tokens, empty values, and empty list elements are hard errors at config load (matches the strictness of the existing validators). Implemented as a 5-bit mask onps_config_t.enable_notification_type(PS_NOTIFY_LOGIN_SUCCESS,PS_NOTIFY_LOGIN_FAILED,PS_NOTIFY_SESSION_OPEN,PS_NOTIFY_SESSION_CLOSE,PS_NOTIFY_BRUTE_FORCE);ps_notify_eventgates on the per-event-type bit andps_notify_brute_force/ps_notify_local_brute_forcegate on the brute-force bit. The existing per-event suppression for sudo/suLOGIN_FAILEDinsrc/journal_watch.cis unchanged and layered beneath this filter. Parser handles arbitrary whitespace around list elements and is case-insensitive on token names. Tests: 9 new CMocka cases intests/test_config.c(default = all, single category, multi-category CSV,allsentinel, whitespace + case tolerance, full 5-category list, unknown token rejected, empty value rejected, empty list element rejected, omitted-key keeps default) and 4 new cases intests/test_notify.cexercising the dispatch gate via the file-static cooldown clock (event_notify_bitmapping, gated-off doesn't dispatch, gated-on does dispatch,PS_EVENT_UNKNOWNnever dispatches). Documented indocs/configuration.md(new "Notification-type filter" section with token table, scope note, examples) andpamsignal.conf.example.
Security
-
examples/nodejs-webhook/: bumpqs6.14.2 / 6.15.1 → 6.15.2 inpnpm-lock.yaml(Dependabot alert #2, CVE-2026-8723, CVSS v4 6.3 medium).qs >= 6.11.1, <= 6.15.1throwsTypeErrorsynchronously whenqs.stringifyis called with botharrayFormat: 'comma'andencodeValuesOnly: trueon an array containingnull/undefined— the rawencoderruns beforeskipNulls/strictNullHandlingget a chance to handle the bad element. Under Express the throw is caught by the framework error boundary and returns 500; outside a handler (background jobs, startup paths) it crashes the worker. PAMSignal itself is C and unaffected; the alert fires only on the Express example receiver's transitiveqs(viaexpress,body-parser,formidable,supertest). Pinned via pnpmoverridesinpnpm-workspace.yamlso both 6.14.2 (express 4.x) and 6.15.1 (body-parser / formidable) collapse to the patched 6.15.2;package.jsondirect deps unchanged. All 64 jest tests still pass. -
examples/python-webhook/: bumpidna3.14 → 3.15 inuv.lock(Dependabot alert #1, CVE-2026-45409, CVSS v4 6.9 medium).idna < 3.15is vulnerable to a DoS whenidna.encode()is called with crafted long inputs (e.g."٠" * N):valid_contextoruns before the length-rejection check, re-opening the gap that CVE-2024-3651 was supposed to close. PAMSignal itself is C and unaffected; the alert fires only on the Flask example receiver's transitive dep viarequests. Lockfile-only refresh viauv lock --upgrade-package idna;pyproject.tomlunchanged.
v0.4.1 — --version flag, CentOS 7 docs, Node.js mTLS example
Patch release. Closes one operator-facing UX gap (pamsignal --version now works — the man page synopsis and the package post-install smoke checks have always claimed this flag existed, but it didn't), documents the rationale for one cutoff that came up in operator conversations (CentOS / RHEL 7 unsupportability + migration paths), aligns the APT install snippet with the FHS keyring-path convention (#14), and ships an HTTPS + mTLS variant of the bundled Node.js webhook receiver to match what the threat-model docs claimed it could do (#12). No daemon behaviour changes; existing configs work unchanged. Internal: integration test suite rewritten to exercise the parse → track → format pipeline on real journal log lines, formatter coverage in test_notify.c expanded 4 → 27 cases, install-test default matrix narrowed to the build-matching Tier 1 pair. ASAN + UBSAN clean across all five test suites.
Documentation
-
docs/distros.md: CentOS / RHEL 7 unsupportability fully documented. The Tier 3 table row previously said only "Won't compile; many hardening directives ignored" — operators asking whether the cutoff could be relaxed for their CentOS 7 fleet had no specific answer to point at. Rewrote the row to cite the three independent blockers (stock kernel 3.10 lacks thememfd_createsyscall added in Linux 3.17; glibc 2.17 lacks the wrapper added in 2.27; CentOS 7 itself reached EOL on 2024-06-30 — running a security daemon on an OS that no longer receives upstream security updates is counter-productive) and added a "Why CentOS / RHEL 7 isn't supportable" subsection that spells out exactly which part of the threat model breaks (attack #3, alert-credential exposure via/proc/<pid>/cmdlinewhen memfd-backed curl config isn't available) and offers three concrete migration paths: in-place migration to AlmaLinux 9 / Rocky 9 / RHEL 9 via the officialconvert2rhel/migrate2rocky.sh/almalinux-deploy.shscripts (recommended), container deployment on the existing host with a newer-glibc image (works only ifuname -rshows a backport ≥ 3.17, not stock 3.10), or sidestepping pamsignal entirely withauditd+ SIEM correlation. Linked from the Tier 3 table so the migration path is one click away from the "is my host supported" lookup.
Features
-
pamsignal --version/-Vflag. The man page (pamsignal.8.in) and the packaging smoke check intest_install_docker.shboth assumed this flag existed, butsrc/main.c::parse_argsonly ever handled--foregroundand--config— passing--versionfell through into the daemon's normal startup, hit the non-root invariant atmain.cline 77, and exited 1 with the "should not run as root" message under any package-postinstall context (dpkg, rpm, containerRUN). Added a--version/-Vbranch that prints the meson-injectedPAMSIGNAL_VERSIONand exits 0 before any privilege checks, sopamsignal --versionworks from any context including root. The version string flows frommeson.build(-DPAMSIGNAL_VERSION="<meson.project_version()>") so it stays in sync with the man page's.THline and the.deb/.rpmpackage metadata. Synopsis line added topamsignal.8.in. -
test_install_docker.shdefault matrix narrowed to Tier 1. Without arguments the script previously tested all seven distros (Ubuntu 24.04 / 22.04, Debian 12, Fedora 40, CentOS Stream 9, AlmaLinux 9, Rocky Linux 9), which produced false-negative[FAIL]results: the bundled.debpinslibc6 (>= 2.38)to ubuntu:24.04's glibc, and the.rpmpins to fedora:40's glibc 2.39+, so older targetsapt/dnf-refuse the install correctly. That's the documented Tier 1 vs Tier 2 distinction indocs/distros.md— Tier 2 needs a per-distroseries build pipeline that doesn't ship yet. Default invocation now runs onlyubuntu:24.04(the.debbuild env) andfedora:40(the.rpmbuild env). The full matrix remains opt-in via the existing distro-name positional args (./test_install_docker.sh <deb> <rpm> ubuntu debian fedora centos almalinux rockylinux), so anyone exercising a same-distroseries-build pipeline keeps the wider check available. Usage text updated to explain the rationale.
Fixed
-
APT signing key path moved to
/etc/apt/keyrings(#14). Reported by @hongquan. Our install snippets directed users to drop the repository signing key at/usr/share/keyrings/pamsignal.gpg, which is reserved for keys shipped by official distribution packages (debian-archive-keyring,ubuntu-keyring, etc.) — administrator-installed keys belong in/etc/apt/keyrings. Thesources.list(5)man page (Debian 12+ / Ubuntu 22.04+) makes this explicit: "The recommended locations for keyrings are/usr/share/keyringsfor keyrings managed by packages, and/etc/apt/keyringsfor keyrings managed by the system operator." Apt accepts either path in thesigned-by=option — the distinction is FHS-compliance and convention, not function — so this is a documentation fix only and existing installs keep working without intervention. Updated: the install snippet inREADME.md, theaptblock indocs/deployment.md, the auto-rendered install snippet on the gh-pages landing page (in.github/workflows/release-packages.yml, takes effect on the next release-packages run), and a workflow comment in.github/workflows/bootstrap-signing-key.ymlfor consistency. Each snippet now prependssudo install -d -m 0755 /etc/apt/keyringssince the directory isn't pre-created on every base image (older containers, some minimal Ubuntu/Debian installs); the command is idempotent and safe to re-run.Migrating an existing install (optional). If you installed pamsignal before this fix and want to align with the FHS convention, three idempotent commands handle the move:
sudo install -d -m 0755 /etc/apt/keyrings sudo mv /usr/share/keyrings/pamsignal.gpg /etc/apt/keyrings/pamsignal.gpg sudo sed -i 's|/usr/share/keyrings/pamsignal.gpg|/etc/apt/keyrings/pamsignal.gpg|' \ /etc/apt/sources.list.d/pamsignal.list sudo apt updateThe final
apt updateis for confirmation only — noNO_PUBKEYerror in its output means the move worked and apt still trusts the repo. You do not need to do this. Apt resolves the absolute path insigned-by=directly, so the old/usr/share/keyrings/pamsignal.gpglayout will continue to work through future package upgrades and key rotations without issue. Migrate only if you prefer your system to follow the documented convention.
Examples
- Node.js webhook example: HTTPS + mTLS server variant (#12). The bundled
examples/nodejs-webhook/reference receiver previously listened only on plain HTTP viaapp.listen(); the README's "Mutual TLS (advanced)" section (added with #8) showed anhttps.createServer({ requestCert: true, rejectUnauthorized: true }, app)snippet but the actual code that would do that didn't ship. This change closes the docs-vs-reality gap.src/server.tsnow branches on env vars: whenTLS_KEY_PATHandTLS_CERT_PATHare set, it constructs an HTTPS server (with optionalTLS_CLIENT_CA_PATHfor trust-store override andTLS_REQUIRE_CLIENT_CERT=truefor full mTLS); otherwise the existing plain-HTTP path is unchanged..env.exampledocuments the four new env vars under a comment block that names the correspondingwebhook_client_cert/webhook_client_keykeys on the pamsignal side.scripts/gen-test-certs.shproduces a CA + server cert + client cert under./certs/for local end-to-end testing — it builds an internal CA, signs server.crt withsubjectAltName=DNS:localhost,IP:127.0.0.1(so Node's HTTPS client accepts it), signs a client.crt with CN=pamsignal-test-client, and tightens private-key files to mode 0600 to match what pamsignal will accept. The README's "Mutual TLS (advanced)" subsection is rewritten as a runnable end-to-end demo (cert script + env vars + matchingpamsignal.confblock) rather than a conceptual snippet, and explicitly distinguishes the demo cert pipeline from production cert managers (cert-manager, certbot, systemd-creds).tests/mtls.test.tsadds three Jest cases that spin up the full HTTPS server with the same construction pattern, generate certs at setup-time via openssl spawn, and verify: a request with a valid client cert + Bearer succeeds (200), a request without a client cert is rejected at the TLS layer before reaching Express, and the Bearer middleware still 401s on a successful handshake when the token doesn't match. Total Jest cases 7 → 10..gitignoreaddscerts/,package-lock.json,yarn.lockso locally-generated test certs and accidentally-installed npm/yarn lockfiles don't pollute the pnpm-managed repo. No change to pamsignal itself — pamsignal v0.4.0 already speaks mTLS correctly via #8.
v0.4.0 — Custom webhook authentication (Bearer + mTLS)
Minor release. Closes the long-standing gap in the custom-webhook authentication story: the channel now ships with two additive auth mechanisms — a single arbitrary HTTP header (Bearer / API key / HMAC-style; #7) and full mutual-TLS client authentication (cert + key + optional CA bundle; #8). Both flow through the same memfd-backed curl -K config the existing alert-credential isolation already used, so the curl child's argv is byte-identical regardless of which auth modes are configured — /proc/*/cmdline reveals nothing about which channel or which credentials are in play. No breaking changes.
What's new
webhook_auth_header — single HTTP header for the custom webhook (#7)
webhook_url = https://siem.example.com/ingest/pamsignal
webhook_auth_header = Authorization: Bearer <token>Works with any single-header auth scheme: Bearer / OAuth, X-API-Key, Authorization: Splunk <token> (HEC), DD-API-KEY (Datadog), Wazuh API JWT, custom HMAC headers, etc. The header value is rendered into a memfd-backed curl -K config file — the secret never appears in argv, /proc/<pid>/cmdline, or any process listing. Validator at config load rejects CRLF / quote / backslash injection and refuses the key set without webhook_url.
Previously the only authenticated path was a reverse proxy in front of the receiver injecting the header — the bundled examples/nodejs-webhook/ reference receiver explicitly required this setup. The example README now documents direct configuration as the recommended path.
webhook_client_cert / webhook_client_key / webhook_ca_bundle — mTLS client auth (#8)
webhook_url = https://siem.internal.example.com/ingest
webhook_client_cert = /etc/pamsignal/webhook-client.crt
webhook_client_key = /etc/pamsignal/webhook-client.key
# Optional, only if the receiver's CA isn't in the system trust store
webhook_ca_bundle = /etc/pamsignal/internal-ca.pemTargets the operator profile that already runs PKI (internal CA + cert-manager / SPIFFE / service-mesh issuance). Stronger than Bearer alone — the private key never travels over the wire, replay windows don't exist, credential rotation is delegated to the cert-management pipeline. Combines additively with webhook_auth_header — the corporate-SIEM / Wazuh pattern (mTLS for transport-layer service identity, Bearer for per-request authorization).
Validator at config load: O_NOFOLLOW open (symlinks rejected), regular-file required, ownership outside {root, daemon-uid} rejected, webhook_client_key additionally rejected if group- or world-readable. Path strings constrained to printable ASCII minus " and \ for unambiguous handling in the curl -K config. Cross-field consistency: cert without key (or key without cert) is a config-load error; any TLS key without webhook_url is rejected.
Documentation refresh
docs/configuration.md— full "Custom webhook authentication" reference covering both modes, combined-pattern examples, and the validation rules.docs/architecture.md— new "Curl invocation (memfd-backed config)" subsection.docs/alerts.md— "Authentication (optional)" + "Mutual TLS (optional, advanced)" subsections under "Custom webhook (ECS JSON)".docs/deployment.md— cert/key file placement guidance.docs/threat-model.md— attack #3 extended (covers the new auth surface), NS8 rewritten (Bearer + mTLS now ship; HMAC-signed payloads remain out of scope).pamsignal.8.in— SECURITY + FILES sections updated.examples/nodejs-webhook/— adds an mTLS-enabledhttps.createServervariant.
Internal changes (no operator impact)
post_alert()andbuild_secrets_memfd()insrc/notify.cswap from positional arguments to acurl_config_tstruct that carries the optional auth header and TLS paths by name. The four legacy callers (Telegram, Slack/Teams/Discord, WhatsApp) pass struct-literal arguments and produce identical wire output to v0.3.x.build_secrets_memfdbuffer grows 2048 → 4096 to fit up to four config lines without truncation. Eachsnprintfappend is bounds-checked.
Tests
CMocka test_config suite grew 33 → 53 (20 new cases): 9 for webhook_auth_header parse + validation, 11 for mTLS parse + perm validation including symlink rejection, world/group-readable key rejection, missing-cert-or-key, no-webhook_url, path-with-quote, and combined Bearer+mTLS load. All 4 suites pass under ASAN + UBSAN. clang-tidy clean. OWASP ASVS L1 audit zero FAIL findings.
Upgrading
sudo apt upgrade pamsignal # Debian / Ubuntu
sudo dnf upgrade pamsignal # Fedora / EL9Existing v0.3.x configs continue to work unchanged. To enable the new auth options, see Configuration → Custom webhook authentication.
Closed issues
- #7 —
webhook_auth_headerfor the custom webhook channel - #8 — Optional mTLS client auth for the custom webhook channel
Full changelog: v0.3.4...v0.4.0
0.3.4 — Security: accept sshd-session (OpenSSH 9.8+ on Ubuntu 26.04 / Fedora 41+)
Security release. Fixes a real production defect: pamsignal silently dropped every sshd auth event on Ubuntu 26.04, Fedora 41+, and Debian Trixie (any host running OpenSSH 9.8+). Surfaced by an end-to-end scenario test on Ubuntu 26.04 / OpenSSH 9.10p2 / OpenSSL 3.5.5 / systemd 259 — a stack CI doesn't currently exercise. Bundles repository-hygiene additions (CONTRIBUTING.md, issue templates, PR template) that accumulated under ## Unreleased since v0.3.3, plus the tests/scenario.sh script that caught the sshd-session bug, plus forward-compat hardening of the CI mock webhook for OpenSSL 3.5+.
Security
-
_EXEallowlist now acceptssshd-session. OpenSSH 9.8 (released 2024-07) split the server into a privilege-separated listener (sshd) and a per-connection auth process (sshd-session). Failed-password / accepted-password journal entries on Ubuntu 26.04, Fedora 41+, and Debian Trixie come fromsshd-session, notsshd. Pamsignal's anti-spoofing_EXEallowlist accepted only the latter, so every sshd auth event on those distributions was dropped atsrc/journal_watch.c's pre-parse filter — the daemon ran fine, journald accumulated events fine, the daemon just didn't see them. Fix: extend the basename allowlist withsshd-session(refactored into a testableps_is_trusted_exehelper). Eight new CMocka test cases cover the canonical paths (/usr/sbin/sshd,/usr/bin/sudo, etc.), the new sshd-session paths (/usr/sbin/sshd-session,/usr/lib/openssh/sshd-session), the alternate system-prefix paths (/sbin,/lib,/lib64,/opt), and the spoofing-rejection paths (logger, /tmp/, /home/, basename-imposter binaries, NULL/empty input, paths without a slash). test_journal_watch grows from 19 → 27 tests.
Tooling
-
tests/scenario.sh— full-stack local end-to-end scenario test that mirrorsrelease-packages.yml's test-deb job and adds two pieces of coverage that don't exist in CI: explicitType=notifyactivation verification (systemctl reportsactiveonly aftersd_notify(READY=1)actually fires), and the v0.3.0 sudo brute-force scenario with testpamuser temporarily added to/etc/sudoers.d/so PAM auth is actually invoked. Twelve phases with pass/fail output, an EXIT trap that always cleans up (sshd_config restored from backup, testpamuser deleted, /etc/pamsignal removed, mock webhook stopped, test CA removed from system trust store, sudoers.d entry removed). The script'sconfirm()reads from/dev/ttydirectly so it works under wrappers that capture stdout. Caught the sshd-session bug above on its first non-CI run. - CI mock webhook hardened for OpenSSL 3.5+ peers. The Python
BaseHTTPRequestHandlerdefaults to HTTP/1.0 + noContent-Length+ no explicit TLSclose_notify, which OpenSSL 3.5+ (Ubuntu 26.04, Fedora 41+) reports aserror:0A000126:SSL routines::unexpected eof while reading, breaking curl with exit 56 even though the HTTP response was successfully received. Forward-port theconnection.unwrap()+Content-Length+Connection: close+protocol_version='HTTP/1.1'fixes fromtests/scenario.shintorelease-packages.yml's test-deb mock so the workflow keeps passing when GitHub Actions promotes the runner image from ubuntu-24.04 (OpenSSL 3.0) to ubuntu-26.04 (OpenSSL 3.5+). Also adds aREADY-poll startup check with bounded retries so a Python-side bind error fails loudly with the captured stderr instead of silently hanging.
Repository hygiene
-
CONTRIBUTING.mdat the repo root documents the contributor workflow: clone+build quickstart, branch-per-change with Conventional Commits (with thefeat:/fix:/security:/refactor:/refactor!:/docs:/chore:/test:/perf:type table the project actually uses), coding standards (cross-referenced to.clang-format/.clang-tidy), the test requirement (every new function or changed behavior gets a CMocka test), the full pre-commit checklist (clang-format, clang-tidy, meson compile, meson test, optional sanitizer build), CHANGELOG conventions, code-review focus areas (threat-model alignment, test adequacy, API stability, style), and a packaging-changes section explicitly calling out thesystemd-analyzeCI gate at threshold 20 plus the el9-rpm%changelog-parsing trap. - Issue templates rewritten as Linux-daemon reports, not generic open-source-project forms.
.github/ISSUE_TEMPLATE/bug_report.ymlis a structured capture of the system state we'd ask the operator to gather anyway — a component dropdown (parser / brute-force tracker / alert dispatch / unit / packaging / journald integration / config parsing), distribution + version + kernel + systemd version + MAC-policy enforcement state, thesystemctl status pamsignaloutput, thejournalctl -u pamsignalexcerpt, the source-of-truth auth-event excerpt with the right per-service-unit filters, thesystemd-delta+ drop-in-overrides listing (frequent root cause for "directive X isn't behaving" reports), thesystemd-analyze securityscore (CI-gated at 20; a higher score on the operator's host suggests a drop-in or repackaging loosened a directive), the redacted config, and concrete operational reproduction steps (what auth events were generated, with what config values, in what order). The template includes the exact commands to run for each capture so an operator who doesn't know the daemon internals can still file a report we can act on.feature_request.ymlrewritten around the project's operator-facing surface area (config keys, journal fields, chat-text alert shape, ECS JSON webhook fields, unit directives, CLI flags, alert-channel additions) with a mandatory compatibility-commitment dropdown (additive / default-on-overridable / default-off opt-in / deprecation-cycle / breaking) referencing the PAMSIGNAL_* retirement precedent, and a threat-model-alignment field that distinguishes "strengthens an in-scope attack" from "argues for moving NS1–NS10 into scope" (the latter being the highest bar).config.ymlunchanged — disables blank issues, points security reports at GitHub Security Advisories, routes question traffic toquestion-labeled issues. - Pull-request template (
.github/pull_request_template.md) auto-populates new PRs with a summary / linked-issue / test-plan / threat-model-alignment structure plus the eight-item pre-commit checklist that mirrorsCONTRIBUTING.md. Reviewers can see at a glance whether the contributor ran the local checks.
0.3.3 — Sandbox tightening, threat model, sanitizer CI
Hardening release. Three independent improvements: the systemd unit's exposure score drops from 22 to 13 ("OK" band, near the achievable floor for a daemon with the daemon's network and journal-access constraints) via nine new directives; a canonical docs/threat-model.md documents what pamsignal defends against and what it deliberately doesn't (cross-referenced to source-line mitigations); ASAN + UBSAN run on every push and PR via a new .github/workflows/ci.yml workflow. No source-code behavior change visible to operators — the daemon's API surface (config keys, journal fields, webhook payload) is unchanged. Upgrade is apt upgrade pamsignal / dnf upgrade pamsignal; the systemd unit is replaced atomically and daemon-reload is auto-fired by the maintainer scripts.
Security
- systemd unit hardening: exposure score 22 → 13 (displayed 2.2 → 1.3, "OK" band, near the achievable floor for a daemon that needs network egress + journal access). Nine new directives added to
pamsignal.service.in, each justified against the daemon's actual behavior (no defensive copy-paste):UMask=0077,ProtectClock=yes,ProtectHostname=yes,ProtectProc=invisible,ProcSubset=pid,SystemCallArchitectures=native,RemoveIPC=yes,RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6(allowlists exactly the four families the daemon and its curl child need — drops AF_PACKET, AF_BLUETOOTH, AF_VSOCK, etc.), andDevicePolicy=closed(overrides the implicit RTC-read grant fromPrivateDevices=yes). The CI regression-gate threshold drops from 30 to 20 to lock in the new baseline. Directives deliberately not added because they would break the daemon:PrivateNetwork=yes(curl needs outgoing HTTPS),PrivateUsers=yes(would interact badly withSupplementaryGroups=systemd-journalfor journal-read access),IPAddressDeny=any(Telegram/Slack/Discord/etc. backend IPs change too frequently to allowlist),RootDirectory=/RootImage=(would require portable-service repackaging, out of scope). Documented in the directive comments so future readers know what the trade-offs were.
CI
- AddressSanitizer + UndefinedBehaviorSanitizer on every push. New
.github/workflows/ci.ymlrunsmeson setup -Db_sanitize=address,undefined -Db_lundef=false --buildtype=debugoptimized, builds, and runs the full CMocka suite under both sanitizers on every push tomainand every PR targeting it. ASAN catches use-after-free, heap/stack OOB, double-free, and leaks; UBSAN catches signed-integer overflow, misaligned pointer access, null deref, narrowing conversions, etc. — the dynamic-runtime evidence behind the threat-model claim that memory-safety bugs insrc/are an in-scope defense (attack #8). Initial run found zero issues on the existing test surface. Stack traces in failure reports include symbolized backtraces (print_stacktrace=1);halt_on_error=1+abort_on_error=1ensure the first finding stops the run rather than continuing in an undefined state. Concurrency group set to cancel older runs when newer commits land on the same ref.
Documentation
-
docs/threat-model.mddocuments what pamsignal defends against, what it deliberately does not, and the design rationale behind the split. Sections cover: assets in priority order (alert integrity, alert credentials, thepamsignaluser's privilege envelope, journal entries pamsignal writes), adversary classes with explicit capabilities (external remote, local unprivileged, in-pamsignal-group, compromised daemon, network attacker on alert path), nine in-scope attacks each cross-referenced to the source-line of its mitigation (_EXEallowlist, brute-force tracker semantics, memfd credential isolation,clearenv()+ absolute-pathexecv, TLS-only--proto =https,sanitize_string+json_escape, the systemd hardening directives, compiler hardening + libFuzzer, per-IP cooldown), ten explicit out-of-scope non-goals (root-on-host, in-pamsignal-group, compromised journald/libsystemd/curl, compromised alert provider, durable delivery, multi-host correlation, authenticated alert delivery, admin misconfiguration, input-flood DoS), the trust-boundary table, and the deliberate design limitations.SECURITY.md's scope section now references the threat model rather than duplicating the breakdown; thepamsignal(8)man page'sSEE ALSOpoints readers there before reporting suspected vulnerabilities; the README's documentation index links it. Designed as the canonical reference for "should this contribution land?" — a feature that strengthens an in-scope mitigation is welcome; a feature that pulls work into the daemon from an out-of-scope area gets pointed at this document.
0.3.2 — Type=notify, watchdog, and SECURITY.md
Best-practices polish release. Three small, independent changes that bring the project closer to "modern systemd-native daemon" baseline: a documented vulnerability-disclosure channel, Type=notify + WatchdogSec= integration, and a CI gate that prevents the unit's hardening score from regressing in future PRs. No source-code behavior change beyond the systemd integration; the existing CMocka suite passes unchanged and systemd-analyze security still scores 2.2.
CI
-
systemd-analyze securityregression gate. Thetest-debjob now runssystemd-analyze security --threshold=30 pamsignal.serviceafter install. Current internal score is 22 (displayed as 2.2 — "OK", second-best band).systemd-analyze's--thresholdoperates on a 0–100 internal scale; the displayed "Overall exposure level" line shows the same value divided by 10. Threshold 30 leaves 8 points of headroom (equivalent to 0.8 in displayed-decimal units) for routine adjustments but fails the workflow if a future PR strips a major hardening directive (MemoryDenyWriteExecute=,SystemCallFilter=,CapabilityBoundingSet=,RestrictNamespaces=, etc., each worth 5+ score points internally). Catches the class of regression where an unrelated change accidentally weakens the daemon's sandbox.
Operations
-
Type=notify+WatchdogSec=30sinpamsignal.service. The daemon now signals readiness viasd_notify(READY=1)afterps_journal_watch_init()succeeds (and not before), so systemd holds the unit inactivatinguntil pamsignal can actually process events —Wants=/After=chains resolve correctly andsystemctl statusdoesn't briefly lie about state during the startup window. The main loop also pingssd_notify(WATCHDOG=1)every iteration; ifsd_journal_waitever wedges (kernel bug, journal corruption) systemd auto-restarts pamsignal at the 30 s threshold instead of letting the daemon silently stop processing auth events.NotifyAccess=mainconfines notification socket access to the parent process so the fork+exec curl children can't spoof readiness/watchdog messages. No-op whenNOTIFY_SOCKETis unset (manual launch, tests).
Documentation
-
SECURITY.mdat the repo root documents the responsible-disclosure channel (GitHub Security Advisories preferred, email fallback with the same PGP key fingerprint that signs the release packages), 90-day coordinated-disclosure timeline, supported version policy (latest minor only), and a scope table separating in-scope findings (parser bypasses, brute-force tracker bugs, alert-payload injection, hardening regressions) from out-of-scope dependencies (curl, libsystemd, alert delivery channels, root-already-on-host scenarios).
0.3.1 — Revert /usr/sbin path move
Patch release that reverts the /usr/bin → /usr/sbin move from v0.3.0. The reasoning that landed v0.3.0 cited FHS §4.10's letter ("system administration daemons live in sbin"), but the systemd-era convention has moved past that distinction: journalctl, systemctl, loginctl, udevadm, podman, and containerd all ship in /usr/bin despite being administrator commands. Fedora 42+ has formally retired the bin/sbin split — %{_sbindir} == %{_bindir} == /usr/bin — and Debian Trixie has the merge on its roadmap. The v0.3.0 layout was on the wrong side of that trajectory and produced two CI-test bugs we had to fix during the v0.3.0 release run; reverting now is cheaper than keeping the migration as the ecosystem unwinds in the same direction.
Breaking
- Daemon binary back to
bindir. Packaged installs land at/usr/bin/pamsignalagain (was/usr/sbin/pamsignalfor the ~30 minutes v0.3.0 was on the apt/dnf gh-pages repos). Dev installs land at/usr/local/bin/pamsignal. The systemd unit'sExecStartis updated by the sameconfigure_filesubstitution that landed in v0.3.0, so anapt upgrade/dnf upgradefrom v0.3.0 → v0.3.1 atomically removes/usr/sbin/pamsignal, installs/usr/bin/pamsignal, and rewritesExecStartin lockstep —systemctl daemon-reloadis auto-fired by the maintainer scripts. Anyone who scripted against the absolute/usr/sbin/pamsignalpath (download count for v0.3.0 was 0 at the time of v0.3.1) needs to revert their scripts to/usr/bin/pamsignal.