All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
This release groups three independent opt-in features (service accounts, session-containment hardening, syscall-level audit trace) and a security-analysis update. None of them changes existing behaviour: a v0.1.5 deployment upgrades to v0.2.0 with no flag set and runs identically. (Note: v0.1.6 was prepared internally but never published; its contents are folded into v0.2.0.)
-
Service accounts (machine accounts) — local Unix accounts declared in
/etc/open-bastion/service-accounts.conf(0600 root:root) that LemonLDAP::NG never sees, for CI agents and headless tooling.pam_openbastionmaterialises the Unix user on first login (create_user = true), with forced uid/gid and auto-created primary group.libnss_openbastionresolves service accounts sosshd's pre-authgetpwnam()succeeds; path configurable viaservice_accounts_file =innss_openbastion.conf.- Mode E support via
scripts/ob-service-account-keys(AuthorizedKeysCommandhelper) so plain (non-SSO-signed) keys can authenticate registered service accounts without breaking theAuthorizedKeysFile noneguarantee. - New
docker-demo-token-svc/variant (coexists withdocker-demo-token) and integration tests (tests/test_integration_token_svc.sh+ Phase 7 intests/test_integration_maxsec.sh).
-
Session-containment hardening (
ob-bastion-setup --enable-hardening, opt-in, off by default) — closes the known SSH evasion channels (setsid+nohuporphans, deferredat/cronjobs,systemd-run --usertimers) without any new setuid binary.KillUserProcesses=yesdeployed via/etc/systemd/logind.conf.d/open-bastion.conf(SIGHUP-applied, non-disruptive — does not kill the admin's own session)./etc/at.allowempty +systemctl mask atddisableat(1)for non-root users;/etc/cron.allowroot-only disablescrontab(1)for non-root users (cron itself stays up because Mode E uses/etc/cron.d/open-bastion-krl).- Pre-flight refusal if any non-root user has
Linger=yes, which would let them schedule jobs viasystemd-run --user --on-active=…(operator mustloginctl disable-linger <user>before re-running). nproccap (256,@ob-servicegroup exempt) as defense in depth against fork-bomb-style runaway processes.- Templates ship under
/usr/share/open-bastion/hardening/(read only; deployment artefacts in/etc/are written byob-bastion-setup, not by dpkg/rpm). - New
doc/hardening.mdandtests/test_ob_bastion_setup_hardening.sh(20 tests).
-
Primary audit trace via auditd (
ob-bastion-setup --enable-audit-trace, opt-in, off by default) — syscall-level, tamper-evident audit independent of the pty session recording./etc/audit/rules.d/open-bastion.rules:-S execve -S execveat(both —execveatalone bypasses anexecve-only rule),-S connect, watches on/etc/passwd,/etc/shadow,/etc/group,/etc/sudoers(and.d),/etc/ssh/sshd_config(and.d),/var/lib/open-bastion/sessions/,/etc/open-bastion/./etc/cron.daily/open-bastion-audit-rotate— daily SIGUSR1 to auditd; combined withnum_logs=7gives ~1 week local retention./etc/audit/auditd.confis intentionally not modified (it is a single admin-tunable file owned by theauditpackage; we use the drop-in mechanismrules.d/and document the recommended retention values for the admin to apply manually).- Warns and skips (does not refuse) if
auditdis not installed so the rest ofob-bastion-setupcontinues normally. auditdis declared asRecommends:(Debian) /Recommends:(RPM) — never installed silently.- New
doc/audit.mdandtests/test_ob_bastion_setup_audit.sh(11 tests).
- Security analysis updated (
doc/security/02-ssh-connection.md,doc/security/99-risk-reduce.md):- R-S18 corrected — the previous claim that the setgid wrapper
- sticky bit prevented users from deleting their own recordings
was inaccurate: the per-user subdirectory is
2770 user:ob-sessions, so the user is owner and canrmtheir own files. Score revised from(P=1, I=1)to(P=2, I=1)— syslogauth.info(start/end) and the new auditd watch on/var/lib/open-bastion/sessions/preserve the timeline and record any unlink even if the file is deleted. The wrapper still provides cross-user isolation (which is what it was always really doing).
- sticky bit prevented users from deleting their own recordings
was inaccurate: the per-user subdirectory is
- R-S19 (new) — session-containment evasion via
setsid/nohup. Initial(P=3, I=3); residual(P=1, I=1)with hardening + audit trace activated. - R-S20 (new) — deferred action via
at/cron/systemd-run --user --on-active=…. Initial(P=2, I=3); residual(P=1, I=2)with hardening (limit: pre-existing crontabs in/var/spool/cron/crontabs/are not purged on activation). - R-S21 (new) — action not captured by the pty (
execveat, UDPsendto,io_uring, TIOCSTI, ptrace, intra-sessionLD_PRELOAD). Initial(P=2, I=3); residual(P=1, I=2)with audit trace (limit: UDPsendto/sendmsgnot traced by default — opt-in extension documented). - New section "Pistes d'amélioration — Containment et Traçabilité"
in
99-risk-reduce.mdwith concrete next-steps (privileged session collector,audisp-syslogforwarding, MAC profiles, cryptographic recording signatures, etc.).
- R-S18 corrected — the previous claim that the setgid wrapper
libnss_openbastion: enforce strict0600 root:rootonservice-accounts.conf(mirrorspam_openbastion).- Service-account entries are not persisted to the on-disk NSS cache to avoid exposing local-only metadata.
- Hardening pre-flight is a security gate: the linger check
fails the step (
return 1, no/etc/writes) even under--yes, so an operator cannot accidentally bypass it in batch mode.
-
All three new features are opt-in:
- Service accounts: leave
service_accounts_fileunset inopenbastion.conf/nss_openbastion.conf. - Hardening: do not pass
--enable-hardeningtoob-bastion-setup. - Audit trace: do not pass
--enable-audit-trace.
A v0.1.5 deployment upgraded to v0.2.0 with no flag set behaves exactly like v0.1.5.
- Service accounts: leave
-
On a dedicated bastion host, the recommended invocation is now
ob-bastion-setup --portal … --enable-hardening --enable-audit-trace. Both flags can be combined with--max-security(Mode E). -
The hardening step refuses to run if any non-root user has
Linger=yes. If you have legitimate lingering services, disable linger (loginctl disable-linger <user>) before re-running, or leave--enable-hardeningoff. -
auditdis aRecommends:notDepends:— it is not pulled in automatically byapt install --no-install-recommends. Operators who want the audit trace mustapt install auditdexplicitly. -
v0.1.6was prepared internally (CHANGELOG entry + commitc591109) but never tagged or published. Its contents are folded into v0.2.0; no v0.1.6 → v0.2.0 upgrade path exists.
- SSH key fingerprint binding on
/pam/authorizeand/pam/verify(requires LemonLDAP::NG PamAccess ≥ 0.1.16 and SSHCA ≥ 0.1.16).pam_openbastionnow forwards the SHA256 fingerprint of the SSH key used to open the session in the JSON body of both endpoints. LLNG cross-checks it against the user's persistent session (_sshCerts) and rejects the call if the certificate is unknown, revoked, or expired — independently of the localsshdKRL. This closes a gap where a certificate revoked on the portal could still open a session (or escalate via sudo) until the KRL propagated, or at all ifRevokedKeyswas missing fromsshd_config. - Out-of-band fingerprint channel for modern OpenSSH (≥ 9.x), which
does not propagate
SSH_USER_AUTHto the PAM environment duringpam_acct_mgmt:- New helper
/usr/local/sbin/ob-ssh-principals, wired asAuthorizedPrincipalsCommand %u %fbyob-bastion-setupandob-backend-setup. It drops the fingerprint to/run/open-bastion/ssh-fp/<sshd-session-pid>.fpatomically (mktemp+mv). pam_openbastionwalks/procup to thesshd-sessionancestor and reads the matching file, with strict validation: directory not group/world-writable, file regular, owner == spool directory owner, mode0600,nlink == 1, content matchesSHA256:<base64>, size ≤ 512 B. Fall back to parsingSSH_USER_AUTHif a custom-patched sshd does expose it.- Spool directory deployed as
0700 nobody:nogroup(theAuthorizedPrincipalsCommandUser); hardenedsystemd-tmpfilesdrop-in (/etc/tmpfiles.d/open-bastion-ssh-fp.conf) recreates it at boot so/runwipes do not silently disable the binding.
- New helper
- Strict SHA256 filter.
pam_openbastionrefuses to forward a fingerprint that is not in theSHA256:<base64>form expected by LLNG (so ansshdconfigured withFingerprintHash md5cannot trigger systematic HTTP 400 from the portal). Non-SHA256 values are discarded and the call falls back to the pre-binding behaviour.
ob_client: new top-levelfingerprintfield in/pam/authorizeand/pam/verifyrequest bodies when available.ob_verify_token()grows an optionalfingerprintparameter.ob_ssh_cert_info_t: newkey_fingerprintfield populated from the spool or, as a fallback, fromSSH_USER_AUTH.- Integration tests (
tests/test_integration_{docker,maxsec}.sh): three new cases — fingerprint accepted/unknown/malformed on/pam/authorize, rejection of a certificate revoked via/ssh/myrevokewithout KRL refresh, and an end-to-end SSH attempt with that revoked certificate that must be refused at the PAMaccountphase.
- Re-run
ob-bastion-setuporob-backend-setupon every bastion / backend: they now install/usr/local/sbin/ob-ssh-principals, the/run/open-bastion/ssh-fpspool, and thesystemd-tmpfilesdrop-in. TheAuthorizedPrincipalsCommandline insshd_config.d/50-llng-bastion.confis updated to pass%u %f. - Bastions running against a LemonLDAP::NG portal without PamAccess
0.1.16 remain fully functional: the portal ignores the
fingerprintfield (backward-compatible). The extra security layer activates as soon as the portal is upgraded. ExposeAuthInfo yesis no longer required for the fingerprint binding (the helper + spool are self-sufficient); it remains useful for session auditing.
- Session recorder wrapper (
ob-session-recorder-wrapper): full rewrite with defense-in-depth against privilege escalation- Explicitly drop elevated gid via
setregid()beforeexec(fixes a vector where theob-sessionsgid would leak into the recorder script's saved gid and child processes) - Switch to directory-based privilege separation: the wrapper creates
$SESSIONS_DIR/$USERwith mode2770(setgid) so files inside inherit theob-sessionsgroup without the script needing elevated gid - Sanitize environment before
exec: stripLD_PRELOAD,LD_LIBRARY_PATH,LD_AUDIT,BASH_ENV,ENV,SHELLOPTS,BASHOPTS,CDPATH,GCONV_PATH,HOSTALIASES,LOCALDOMAIN,LOCPATH,MALLOC_TRACE,NIS_PATH,NLSPATH,RESOLV_HOST_CONF,RES_OPTIONS,TMPDIR; forcePATH=/usr/sbin:/usr/bin:/sbin:/bin - Validate username against
^[a-z_][a-z0-9_.-]*$before use in path construction (prevents path traversal) - Resolve username from the real uid via
getpwuid()instead of a user-controllable env variable - Fix TOCTOU races in session directory creation by using
fstat/fchown/fchmodon an opened fd (CodeQL)
- Explicitly drop elevated gid via
scripts/ob-session-recorder: deriveSESSION_USERfromid -uninstead of$USER; validate with the same regex- NSS module (
libnss_openbastion):- Config file and token file now opened with
O_NOFOLLOWand verified viafstat: must be owned by root, must be a regular file, must not be group/world-writable; token file must not be group/world-readable - Add integer overflow check and 256 KB response cap in
write_callback - Emit syslog diagnostics for every previously-silent rejection path
- Config file and token file now opened with
- Defense-in-depth sudo:
- New system group
open-bastion-sudocreated automatically bydebian/postinstand the RPM pre-install scriptlet pam_openbastionsession hook syncs membership on every login:sudo_allowed=true→ add user to the group,false→ removenscdgroup cache invalidated after a membership change sosudosees the update immediatelyob-bastion-setupwrites/etc/sudoers.d/open-bastionas%open-bastion-sudo ALL=(ALL) ALLfor new installs (does not overwrite an existing file)
- New system group
- NSS configuration path:
libnss_openbastionnow reads its config from/etc/open-bastion/nss_openbastion.conf— where CMake installs it and whereob-bastion-setup/ob-backend-setuphave always written. The module was hard-coded to/etc/nss_llng.conf(leftover from thellng-pam-module→open-bastionrename), so NSS never found its config and silently refused to resolve users. Docker demos masked this with auseradd -mfallback that created local accounts whenever NSS failed; the fallback is removed and demos now fail fast on real NSS breakage ob-bastion-setup/ob-backend-setup: update internal variable and write config to the new NSS path
quick-start/directory: minimal 2-container demo (LLNG portal + single SSH server) usingyadd/lemonldap-ng-portaldirectly, relying on the plugin autoloader (nocustomPluginsedit needed). README documents installing the four Open-Bastion plugins (pam-access,ssh-ca,oidc-device-authorization,oidc-device-organization) on an existing LemonLDAP::NG vialemonldap-ng-storeor Debian packagesdocker-demo-cert/README.md: hands-on enrollment walkthrough fully refreshed (container names, config paths, script names) after thellng-*→open-bastion/ob-*rename
- NSS config path: if you deployed v0.1.3 and ran
ob-bastion-setuporob-backend-setup, you have an orphaned/etc/nss_llng.conf. Re-running the setup script after upgrade writes the config to the new path and fixes user resolution. You can thenrm /etc/nss_llng.confto clean up. The Debian/RPM postinst does not migrate it automatically. - Session recorder usernames: usernames with characters outside
[a-z_][a-z0-9_.-]*(e.g. AD-styleDOMAIN\useroruser@realm) are now rejected by the wrapper. Open-Bastion's NSS module generates POSIX-safe usernames, so this only affects custom integrations. - NSS token file permissions: the module now requires
/etc/open-bastion/tokento be mode0600 root:root.ob-enrollwrites it with these permissions by default; custom deployments using0640with a group read will need to tighten.
- Session recording privilege separation via setgid wrapper
(
ob-session-recorder-wrapper, groupob-sessions, directory mode1770) - New risk R-S18: session recording tampering (mitigated P=1/I=1)
- PAM module name:
pam_llng.so→pam_openbastion.soacross scripts and setup tooling - NSS module symbols:
_nss_llng_*→_nss_openbastion_* - NSS
nsswitch.confsource name usesopenbastion;server_token_fileconfig key aligned ob-enroll: sendclient_secretto the device endpoint (optional but accepted by RFC 8628)ob-bastion-setup: addIncludedirective,AuthorizedPrincipalsCommand,PermitRootLogin no, NSS configuration,pam_mkhomedir.so- Session recorder paths:
ob-session-recorder,/etc/open-bastion/ - Sudo Mode E: remove
pam_unix.sofromaccountstack (NSS-only users), create/etc/sudoers.d/open-bastion
- Pre-hardening bootstrap:
securetty ttyS0,PermitRootLogin no, emergency-access service account
- Mode E: Maximum Security (#100): New security configuration enforcing the
strictest SSH posture
- SSH authentication via SSO-signed certificates only (
AuthorizedKeysFile none) - sudo only via fresh LLNG temporary token (PAM-access re-authentication)
- Mandatory KRL (Key Revocation List) with automatic refresh via
/ssh/revoked --max-securityoption inob-backend-setupandob-bastion-setupscripts- KRL refresh script with proper SSL/timeout option inheritance
- SSH authentication via SSO-signed certificates only (
- docker-demo-maxsec: Full Docker Compose demo for Mode E architecture
- CI integration tests for Mode E (
test_integration_maxsec.sh): Validates certificate-only auth, unsigned key rejection, KRL configuration, sudo PAM hardening, and password authentication is disabled - EBIOS security study refactored for maximum security target:
doc/security/00-architecture.mdtranslated to French with Mode E introductiondoc/security/02-ssh-connection.mdsimplified to single architecture (Mode E)doc/security/03-offboarding.mdsimplified to Mode E offboarding procedure- New risks R-S15 (stale KRL) and R-S16 (sudo escalation) documented
OB_BASTION_JWT→LLNG_BASTION_JWT: Aligned environment variable name across all files to match the actual PAM module code andob-ssh-proxyAllowAgentForwarding noon bastion: Agent forwarding is not needed (ob-ssh-proxy handles JWT injection, not ProxyJump)- ProxyJump references replaced with
ob-ssh-proxyin security documentation: native ProxyJump is incompatible with bastion JWT injection - KRL format validation: Verify SSH KRL magic bytes (
SSHKRL) before replacing the revocation file, preventing HTML error pages from breaking sshd
- Supplementary groups synchronization (#95): LLNG can now manage Unix supplementary
groups on target servers via the
managed_groupsconfiguration- Local whitelist for managed groups (
allowed_managed_groups): Defense-in-depth option to restrict which groups LLNG can modify on each server
- Local whitelist for managed groups (
- CrowdSec IP/CIDR whitelist (#96): New
crowdsec_whitelistoption to bypass CrowdSec checks for trusted IPs/networks (VPN exit nodes, corporate NAT)- Supports IPv4, IPv6, and CIDR notation
- Prevents self-inflicted DoS on shared IPs
- TOCTOU race condition in cache_key.c (#97): Use
open()withO_CREAT|O_EXCL|O_NOFOLLOWinstead offopen()to prevent symlink attacks - Check
fclose()return value to detect flush errors before rename
Initial release.