diff --git a/doc/README.md b/doc/README.md
index 09dc0c65..3566e86c 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -62,13 +62,13 @@ Who can do what, where — and which knob lives on the SSO vs the server.
## Reference
-| Document | Description |
-| ----------------------------------------------- | ------------------------------------------------------- |
-| [Canonical names and paths](reference-paths.md) | Authoritative paths, unit names and package names |
-| [Configuration reference](configuration.md) | Every `openbastion.conf` key |
-| [Troubleshooting](troubleshooting.md) | Logs, debug mode, endpoint tests, common issues |
-| [Desktop SSO](desktop-sso.md) | LightDM greeter + LLNG login **(experimental / alpha)** |
-| [Competitors](competitors.md) | Comparison with other solutions |
+| Document | Description |
+| ------------------------------------------- | ------------------------------------------------------- |
+| [Configuration reference](configuration.md) | Every `openbastion.conf` key |
+| [Troubleshooting](troubleshooting.md) | Logs, debug mode, endpoint tests, common issues |
+| [Desktop SSO](desktop-sso.md) | LightDM greeter + LLNG login **(experimental / alpha)** |
+| [Competitors](competitors.md) | Comparison with other solutions |
+| [Slide deck](slides/README.md) | HTML meetup presentation (intro talk) |
## Security analysis (EBIOS Risk Manager)
diff --git a/doc/slides/Open-Bastion-print.html b/doc/slides/Open-Bastion-print.html
new file mode 100644
index 00000000..796c47a7
--- /dev/null
+++ b/doc/slides/Open-Bastion-print.html
@@ -0,0 +1,958 @@
+
+
+
+
+Open-Bastion — Linagora
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Meetup · 2026
+
+
+
Centralized access control for Linux
+
Open- Bastion
+
+ SSH & sudo, governed by the SSO you already run.
+
+
+
+ github.com/linagora/open-bastion
+ AGPL-3.0
+
+
+
+
+
+
+
+
AGPL-3.0 · v0.5.1
+
+
+
$ ssh user@bastion
+
Open-Bastion
+
+ > One SSO. · Every server. · SSH and sudo.
+
+
+
Linagora meetup · 2026 · github.com/linagora/open-bastion
+
+
+
+
+
+
+
+
A question.
+
Who, exactly, has a shell on that server?
+
+
meetup · 2026
+
+
+
A proposition.
+
+
Open- Bastion
+
SSH & sudo, governed by your SSO.
+
+
github.com/linagora/open-bastion
+
+
+
+
+
+
+ About us
+ Linagora. French. Open source. Pragmatic.
+
+
+
IDENTITY
+
LemonLDAP::NG
+
SSO & WebSSO, 18+ years, LTS.
+
+
+
COLLAB
+
Twake Workplace
+
Mail, chat, drive, meet.
+
+
+
SHELL
+
Open-Bastion
+
Today's talk. AGPL-3.0.
+
+
+
+
+
+
+
+ Problem I
+ SSH access, unsupervised, ages like milk.
+
+
+
+ Keys get pushed. People leave. Contractors finish.
+ Nothing removes them.
+
+
+ authorized_keys files that nobody audits
+ Ansible roles that only add, never remove
+ An LDAP sync nobody fully trusts
+ Revocation measured in days , not seconds
+
+
+
+
+# cat authorized_keys | wc -l
+47
+
+# who is still at the company?
+¯\_(ツ)_/¯
+
+# last login by sophie (left 2023-04):
+last month
+
+
+ Open-Bastion
02 / ssh problem
+
+
+
+
+ Problem II
+ And then there's sudo.
+
+
+
+ SSH is who's in the building.
+ Sudo is who's got the keys to the safe.
+
+
+ Same drift, harder consequences. sudoers spread across machines, Ansible roles and a wiki page from 2019.
+ NOPASSWD ALL that everyone swears is temporary.
+
+
+
+# /etc/sudoers.d/legacy — last edit: 2021
+%devs ALL=(ALL:ALL) NOPASSWD: ALL
+alice ALL=(ALL:ALL) ALL
+bob ALL=(ALL:ALL) ALL # left 2022-07
+ci-bot ALL=(ALL:ALL) NOPASSWD: ALL
+#tempo ALL=(ALL) NOPASSWD: /bin/systemctl
+
+
+
+
+
+
+
+ What we do about it
+ The status quo, honestly.
+
+
+
01
+
Push keys with Ansible
+
Works. Until it doesn't. Rollback? See you in 20 minutes.
+
+
+
02
+
LDAP + sssd
+
Great for users. Sudo? Group schema. Revocation? Propagation.
+
+
+
03
+
A script from Jérôme
+
Jérôme left in 2022. The script remains.
+
+
+
+ Audit trail: whatever grep on a log file happens to tell you.
+
+ Open-Bastion
04 / status quo
+
+
+
+
+ Meanwhile
+ For web apps, we solved this a decade ago.
+
+
+
+ One identity, one place to grant & revoke
+ MFA, session lifetime, device posture — included
+ Audit trail is a table, not a grep
+ Off-board in seconds, not days
+
+
+
+
SSO
+
…but not your shell.
+
+
+ Open-Bastion
05 / web solved this
+
+
+
+
+ Our bet
+
+ The SSO you already run
+ can drive ssh and sudo .
+
+
+ Not as a side-car. Not as a custom PAM hack.
+ As the actual authorization decision — for every shell, on every host.
+
+
+
+
+
+
+ Introducing
+
+ PAM & NSS modules for Linux, wired into LemonLDAP::NG.
+
+
+
+
TWO AUTH MODES
+
Tokenor SSH key
+
+
+
TWO DECISIONS
+
SSH accessand sudo privilege
+
+
+
EVERYWHERE
+
Bastionand backends
+
+
+
+ C & shell · ~68 % / 25 %
+ Debian · Ubuntu · Rocky
+ AGPL-3.0
+
+
+
+
+
+
+ Architecture
+ One brain. Many shells.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ user
+ SSH client
+
+
+
+
+
+ Bastion
+ sshd + PAM + NSS
+
+ pam_ob
+
+ nss_ob
+
+
+
+
+
+ backend db-01
+ sshd + PAM
+ group: db-prod
+
+
+
+ backend app-03
+ sshd + PAM
+ group: app-prod
+
+
+
+ backend ci-09
+ sshd + PAM
+ group: ci
+
+
+
+
+
+ LemonLDAP::NG
+ the brain
+
+ /oauth2/introspect
+
+ /pam/authorize
+
+
+
+
+ ssh
+
+
+
+
+
+ Cert-vouched SSH
+
+
+
+
+
+
+
+ may I?
+
+ Open-Bastion
08 / architecture
+
+
+
+
+ Linux plumbing
+ Two modules, zero magic.
+
+
+
PAM
+
pam_openbastion.so
+
+ The decision . auth + account stack asks LLNG: is this person real, is this server in-scope, is sudo allowed?
+
+
+
+
NSS
+
libnss_openbastion.so.2
+
+ The resolution . Accounts don't need to pre-exist in /etc/passwd . Auto-provision on first login.
+
+
+
+
+# /etc/pam.d/sshd — recommended mode
+auth sufficient pam_openbastion.so
+auth required pam_deny.so
+account required pam_openbastion.so
+account required pam_unix.so
+session required pam_unix.so
+
+ Open-Bastion
09 / pam & nss
+
+
+
+
+ Auth mode · 1 / 2
+ Token. Copy-paste. Done.
+
+
+ User opens the LLNG portal
+ Portal issues a short-lived token
+ User pastes it as the SSH password
+ Bastion introspects via OIDC
+ No password. No key. No rotation.
+
+
+
+$ ssh alice@bastion.example.com
+Password: ••••••••••••••••••••••
+ (paste LLNG token)
+
+Welcome to prod-bastion-01.
+alice@bastion:~$ _
+
+
+ Open-Bastion
10 / token auth
+
+
+
+
+ Auth mode · 2 / 2
+ SSH key. Plus a check.
+
+
+
+ Users keep the flow they already know.
+
+
+ The twist: possession of the key is necessary , not sufficient.
+ After the key matches, the module asks LLNG:
+
+
+
+ Is alice allowed on db-07 ,
+ in group db-prod , right now?
+
+
+
+
+# POST /pam/authorize
+Authorization : Bearer <server-token>
+Content-Type : application/json
+
+{
+ "user" : "alice" ,
+ "host" : "db-07" ,
+ "server_group" : "db-prod" ,
+ "action" : "ssh"
+}
+
+# 200 OK
+{ "allowed" : true , "ttl" : 300 }
+
+
+ Open-Bastion
11 / ssh key auth
+
+
+
+
+ Sudo
+
+ Same PAM. Same endpoint. Different question.
+
+
+
+
+ sudoers on disk is the static whitelist
+ PAM asks LLNG: may this user sudo, here, now?
+ One place to grant. One place to revoke.
+ Short cache TTL — because sudo is the blast radius.
+
+
+
+
SUDO DECISION
+
+ { "action" : "sudo" ,
+ "command" : "/usr/bin/systemctl" }
+
+
+ TTL: 60s (default)
+
+
+
+
+
+
+
+
+ Resilience
+ LLNG down? Ops carries on.
+
+
+
+ Encrypted authorization cache.
+ SSH keys still authenticate.
+
+
+ AES-256-GCM encrypted cache on disk
+ Configurable TTL per decision type
+ Brute-force protection with rate limits
+ Sudo TTL shorter than SSH TTL, by policy
+
+
+
+
+
+
+
+ LemonLDAP::NG
+ unreachable
+
+
+
+ Bastion
+ still authenticating
+
+
+
+
+ Auth cache
+ AES-256-GCM · on disk
+
+ alice @ db-07 → ok
+ alice @ app-03 → ok
+ bob @ * → denied
+ ttl: ssh 300s · sudo 60s
+
+
+
+
+
+
+
+
+
+
+
+ Chain of custody
+
+ Backends trust bastions cryptographically — not by IP.
+
+
+
+
+ Bastion mints a short-lived, LLNG-signed SSH certificate per hop
+ Backend trusts the LLNG SSH CA — verifies the cert offline
+ Pinned to the bastion: source-address + allowed_bastions
+ No user key on the bastion. Direct SSH to a backend? Refused.
+
+
+
+# ob-ssh: seamless bastion → backend hop
+ob-ssh user@backend # also ob-scp / ob-sftp
+
+# On backend: trust the LLNG SSH CA
+TrustedUserCAKeys /etc/ssh/open-bastion_ca.pub
+AuthorizedPrincipalsCommand ob-ssh-principals
+
+
+ Open-Bastion
14 / bastion→backend
+
+
+
+
+ For the auditors
+ Every keystroke, receipts included.
+
+
+
+ Optional. Opt-in per server group.
+
+
+ Formats: script , asciinema , ttyrec
+ Unique session ID + correlation tag
+ One SSH session → one audit record
+ Plugs into your existing log pipeline
+
+
+
+
+$ ls -la
+-rw------- alice_db-07_2026-04-17T13:42.cast
+-rw------- alice_db-07_2026-04-17T13:42.json
+-rw------- bob_app-03_2026-04-17T14:08.cast
+
+$ asciinema play alice_db-07_*.cast
+▶ replaying 00:04:12 — correlation:a4f1…
+
+
+ Open-Bastion
15 / recording
+
+
+
+
+ Defense in depth
+ The paranoid's toolbox.
+
+
+
Audit
+
Structured JSON logs with correlation IDs. Grep still works; so does your SIEM.
+
+
+
Rate limits
+
Exponential backoff on failure. Per-user, per-IP, per-token.
+
+
+
Secrets
+
AES-256-GCM encrypted secret storage. Webhook notifications for security events.
+
+
+
Token binding
+
Tokens bound to source IP and client fingerprint. Stolen tokens don't travel.
+
+
+
Key policy
+
Refuse DSA. Refuse 1024-bit RSA. Configurable per group.
+
+
+
CrowdSec
+
Optional pre-auth IP blocking, post-auth failure reporting. Compatible with Crowdsieve.
+
+
+
+
+
+
+
+ Robots
+ Ansible, backup, CI — the robot lane.
+
+
+
+ SSH key auth, no OIDC dance.
+
+
+ Per-server config file
+ Fine-grained sudo permissions
+ Automatic account provisioning
+ Humans go through SSO. Robots don't need to.
+
+
+
+# /etc/open-bastion/service-accounts.d/ansible.conf
+[ansible]
+public_key = ssh-ed25519 AAAA… ansible@ci
+home = /var/lib/ansible
+shell = /bin/bash
+sudo = /usr/bin/apt update
+sudo = /usr/bin/systemctl restart *
+sudo_nopasswd = true
+
+
+ Open-Bastion
17 / service accounts
+
+
+
+
+ The neighbours
+ Briefly, who's on this street.
+
+
+
+
+ Project
+ Language
+ Licence
+ Identity model
+ Enforces on backends
+
+
+
+
+ Open-Bastion
+ C + shell
+ AGPL-3.0
+ Your SSO (LLNG / OIDC)
+ Yes — PAM everywhere
+
+
+ Teleport
+ Go
+ Ent. / BSL
+ Own user model (+ SSO)
+ Yes — its own agent
+
+
+ The Bastion (OVH)
+ Perl
+ Apache-2.0
+ Own user DB
+ Bastion-side only
+
+
+ Boundary
+ Go
+ MPL / BSL
+ Own user model (+ SSO)
+ Yes — broker model
+
+
+
+
+
+ We're the one where your existing SSO is the brain , the code fits in less than a weekend to read , and backends enforce too.
+
+ Open-Bastion
18 / neighbours
+
+
+
+
+ Install
+ Five steps. No YAML DSL.
+
+
+
01
+
Install package
+
apt · dnf · from source
+
+
+
02
+
Write config
+
portal_url, client, group
+
+
+
03
+
ob-enroll
+
code → admin approves
+
+
+
04
+
Edit pam.d/sshd
+
4 lines
+
+
+
05
+
Test — 2nd terminal
+
(keep the first open!)
+
+
+
+
+# apt install open-bastion
+# vi /etc/open-bastion/openbastion.conf
+# ob-enroll
+User code: GXPR-7K2M ← admin approves in LLNG portal
+✓ enrolled
+# systemctl restart sshd
+
+
+
+
+
+
+ The whole shape
+ Config, front to back.
+
+
+
/etc/open-bastion/openbastion.conf
+
+portal_url = https://auth.example.com
+client_id = pam-access
+client_secret = <redacted>
+server_group = db-prod
+log_level = info
+cache_ttl_ssh = 300
+cache_ttl_sudo = 60
+
+
+
+
/etc/pam.d/sudo
+
+# sudo through LLNG
+auth sufficient pam_openbastion.so
+auth required pam_deny.so
+account required pam_openbastion.so
+session required pam_unix.so
+
+
+ Same module, same endpoint. Different action .
+
+
+
+
+
+
+
+
+ Roadmap · early alpha today
+ Next: the desktop too.
+
+
+
+ Workstations login with the same LLNG identity.
+
+
+ LightDM greeter with embedded portal
+ MFA — TOTP, WebAuthn, SMS
+ Offline mode: Argon2id + AES-256-GCM
+ One SSO. Shell, sudo, desktop.
+
+
+
+ + desktop
+
+
+
+
+
+
+
+ Your move
+ Try it. Read it. Break it.
+
+
+
+
DOCKER DEMO
+
docker-demo-token/
+
docker-demo-cert/
+
docker-demo-maxsec/
+
Three flavours. Pick one. 10 minutes end-to-end.
+
+
+
REPO
+
github.com/linagora/open-bastion
+
⭐ if it's useful · 🐛 if it isn't
+
+
+
+
+
CONTRIBUTE
+
Issues, PRs, docs, packaging, translations.
+
AGPL-3.0. Upstream-first.
+
+
+
NEED HELP DEPLOYING?
+
Linagora offers support, training, custom integrations.
+
linagora.com
+
+
+
+
+
+
+
+
+
+
+
+
Questions?
+
Thank you.
+
+ If the first question is "why not Teleport" ,
+ I have a slide for that — I just didn't show it.
+
+
+
+ github.com/linagora/open-bastion
+ xguimard@linagora.com
+ AGPL-3.0
+
+
+
+
+
+
+
+
+
Title variant
+
+ A · Editorial
+ B · Terminal
+ C · Question
+
+
Navigate to slide 1 to see the change.
+
+
+
+
+
+
+
diff --git a/doc/slides/Open-Bastion.html b/doc/slides/Open-Bastion.html
new file mode 100644
index 00000000..7bc8ef38
--- /dev/null
+++ b/doc/slides/Open-Bastion.html
@@ -0,0 +1,950 @@
+
+
+
+
+Open-Bastion — Linagora
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Meetup · 2026
+
+
+
Centralized access control for Linux
+
Open- Bastion
+
+ SSH & sudo, governed by the SSO you already run.
+
+
+
+ github.com/linagora/open-bastion
+ AGPL-3.0
+
+
+
+
+
+
+
+
AGPL-3.0 · v0.5.1
+
+
+
$ ssh user@bastion
+
Open-Bastion
+
+ > One SSO. · Every server. · SSH and sudo.
+
+
+
Linagora meetup · 2026 · github.com/linagora/open-bastion
+
+
+
+
+
+
+
+
A question.
+
Who, exactly, has a shell on that server?
+
+
meetup · 2026
+
+
+
A proposition.
+
+
Open- Bastion
+
SSH & sudo, governed by your SSO.
+
+
github.com/linagora/open-bastion
+
+
+
+
+
+
+ About us
+ Linagora. French. Open source. Pragmatic.
+
+
+
IDENTITY
+
LemonLDAP::NG
+
SSO & WebSSO, 18+ years, LTS.
+
+
+
COLLAB
+
Twake Workplace
+
Mail, chat, drive, meet.
+
+
+
SHELL
+
Open-Bastion
+
Today's talk. AGPL-3.0.
+
+
+
+
+
+
+
+ Problem I
+ SSH access, unsupervised, ages like milk.
+
+
+
+ Keys get pushed. People leave. Contractors finish.
+ Nothing removes them.
+
+
+ authorized_keys files that nobody audits
+ Ansible roles that only add, never remove
+ An LDAP sync nobody fully trusts
+ Revocation measured in days , not seconds
+
+
+
+
+# cat authorized_keys | wc -l
+47
+
+# who is still at the company?
+¯\_(ツ)_/¯
+
+# last login by sophie (left 2023-04):
+last month
+
+
+ Open-Bastion
02 / ssh problem
+
+
+
+
+ Problem II
+ And then there's sudo.
+
+
+
+ SSH is who's in the building.
+ Sudo is who's got the keys to the safe.
+
+
+ Same drift, harder consequences. sudoers spread across machines, Ansible roles and a wiki page from 2019.
+ NOPASSWD ALL that everyone swears is temporary.
+
+
+
+# /etc/sudoers.d/legacy — last edit: 2021
+%devs ALL=(ALL:ALL) NOPASSWD: ALL
+alice ALL=(ALL:ALL) ALL
+bob ALL=(ALL:ALL) ALL # left 2022-07
+ci-bot ALL=(ALL:ALL) NOPASSWD: ALL
+#tempo ALL=(ALL) NOPASSWD: /bin/systemctl
+
+
+
+
+
+
+
+ What we do about it
+ The status quo, honestly.
+
+
+
01
+
Push keys with Ansible
+
Works. Until it doesn't. Rollback? See you in 20 minutes.
+
+
+
02
+
LDAP + sssd
+
Great for users. Sudo? Group schema. Revocation? Propagation.
+
+
+
03
+
A script from Jérôme
+
Jérôme left in 2022. The script remains.
+
+
+
+ Audit trail: whatever grep on a log file happens to tell you.
+
+ Open-Bastion
04 / status quo
+
+
+
+
+ Meanwhile
+ For web apps, we solved this a decade ago.
+
+
+
+ One identity, one place to grant & revoke
+ MFA, session lifetime, device posture — included
+ Audit trail is a table, not a grep
+ Off-board in seconds, not days
+
+
+
+
SSO
+
…but not your shell.
+
+
+ Open-Bastion
05 / web solved this
+
+
+
+
+ Our bet
+
+ The SSO you already run
+ can drive ssh and sudo .
+
+
+ Not as a side-car. Not as a custom PAM hack.
+ As the actual authorization decision — for every shell, on every host.
+
+
+
+
+
+
+ Introducing
+
+ PAM & NSS modules for Linux, wired into LemonLDAP::NG.
+
+
+
+
TWO AUTH MODES
+
Tokenor SSH key
+
+
+
TWO DECISIONS
+
SSH accessand sudo privilege
+
+
+
EVERYWHERE
+
Bastionand backends
+
+
+
+ C & shell · ~68 % / 25 %
+ Debian · Ubuntu · Rocky
+ AGPL-3.0
+
+
+
+
+
+
+ Architecture
+ One brain. Many shells.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ user
+ SSH client
+
+
+
+
+
+ Bastion
+ sshd + PAM + NSS
+
+ pam_ob
+
+ nss_ob
+
+
+
+
+
+ backend db-01
+ sshd + PAM
+ group: db-prod
+
+
+
+ backend app-03
+ sshd + PAM
+ group: app-prod
+
+
+
+ backend ci-09
+ sshd + PAM
+ group: ci
+
+
+
+
+
+ LemonLDAP::NG
+ the brain
+
+ /oauth2/introspect
+
+ /pam/authorize
+
+
+
+
+ ssh
+
+
+
+
+
+ Cert-vouched SSH
+
+
+
+
+
+
+
+ may I?
+
+ Open-Bastion
08 / architecture
+
+
+
+
+ Linux plumbing
+ Two modules, zero magic.
+
+
+
PAM
+
pam_openbastion.so
+
+ The decision . auth + account stack asks LLNG: is this person real, is this server in-scope, is sudo allowed?
+
+
+
+
NSS
+
libnss_openbastion.so.2
+
+ The resolution . Accounts don't need to pre-exist in /etc/passwd . Auto-provision on first login.
+
+
+
+
+# /etc/pam.d/sshd — recommended mode
+auth sufficient pam_openbastion.so
+auth required pam_deny.so
+account required pam_openbastion.so
+account required pam_unix.so
+session required pam_unix.so
+
+ Open-Bastion
09 / pam & nss
+
+
+
+
+ Auth mode · 1 / 2
+ Token. Copy-paste. Done.
+
+
+ User opens the LLNG portal
+ Portal issues a short-lived token
+ User pastes it as the SSH password
+ Bastion introspects via OIDC
+ No password. No key. No rotation.
+
+
+
+$ ssh alice@bastion.example.com
+Password: ••••••••••••••••••••••
+ (paste LLNG token)
+
+Welcome to prod-bastion-01.
+alice@bastion:~$ _
+
+
+ Open-Bastion
10 / token auth
+
+
+
+
+ Auth mode · 2 / 2
+ SSH key. Plus a check.
+
+
+
+ Users keep the flow they already know.
+
+
+ The twist: possession of the key is necessary , not sufficient.
+ After the key matches, the module asks LLNG:
+
+
+
+ Is alice allowed on db-07 ,
+ in group db-prod , right now?
+
+
+
+
+# POST /pam/authorize
+Authorization : Bearer <server-token>
+Content-Type : application/json
+
+{
+ "user" : "alice" ,
+ "host" : "db-07" ,
+ "server_group" : "db-prod" ,
+ "action" : "ssh"
+}
+
+# 200 OK
+{ "allowed" : true , "ttl" : 300 }
+
+
+ Open-Bastion
11 / ssh key auth
+
+
+
+
+ Sudo
+
+ Same PAM. Same endpoint. Different question.
+
+
+
+
+ sudoers on disk is the static whitelist
+ PAM asks LLNG: may this user sudo, here, now?
+ One place to grant. One place to revoke.
+ Short cache TTL — because sudo is the blast radius.
+
+
+
+
SUDO DECISION
+
+ { "action" : "sudo" ,
+ "command" : "/usr/bin/systemctl" }
+
+
+ TTL: 60s (default)
+
+
+
+
+
+
+
+
+ Resilience
+ LLNG down? Ops carries on.
+
+
+
+ Encrypted authorization cache.
+ SSH keys still authenticate.
+
+
+ AES-256-GCM encrypted cache on disk
+ Configurable TTL per decision type
+ Brute-force protection with rate limits
+ Sudo TTL shorter than SSH TTL, by policy
+
+
+
+
+
+
+
+ LemonLDAP::NG
+ unreachable
+
+
+
+ Bastion
+ still authenticating
+
+
+
+
+ Auth cache
+ AES-256-GCM · on disk
+
+ alice @ db-07 → ok
+ alice @ app-03 → ok
+ bob @ * → denied
+ ttl: ssh 300s · sudo 60s
+
+
+
+
+
+
+
+
+
+
+
+ Chain of custody
+
+ Backends trust bastions cryptographically — not by IP.
+
+
+
+
+ Bastion mints a short-lived, LLNG-signed SSH certificate per hop
+ Backend trusts the LLNG SSH CA — verifies the cert offline
+ Pinned to the bastion: source-address + allowed_bastions
+ No user key on the bastion. Direct SSH to a backend? Refused.
+
+
+
+# ob-ssh: seamless bastion → backend hop
+ob-ssh user@backend # also ob-scp / ob-sftp
+
+# On backend: trust the LLNG SSH CA
+TrustedUserCAKeys /etc/ssh/open-bastion_ca.pub
+AuthorizedPrincipalsCommand ob-ssh-principals
+
+
+ Open-Bastion
14 / bastion→backend
+
+
+
+
+ For the auditors
+ Every keystroke, receipts included.
+
+
+
+ Optional. Opt-in per server group.
+
+
+ Formats: script , asciinema , ttyrec
+ Unique session ID + correlation tag
+ One SSH session → one audit record
+ Plugs into your existing log pipeline
+
+
+
+
+$ ls -la
+-rw------- alice_db-07_2026-04-17T13:42.cast
+-rw------- alice_db-07_2026-04-17T13:42.json
+-rw------- bob_app-03_2026-04-17T14:08.cast
+
+$ asciinema play alice_db-07_*.cast
+▶ replaying 00:04:12 — correlation:a4f1…
+
+
+ Open-Bastion
15 / recording
+
+
+
+
+ Defense in depth
+ The paranoid's toolbox.
+
+
+
Audit
+
Structured JSON logs with correlation IDs. Grep still works; so does your SIEM.
+
+
+
Rate limits
+
Exponential backoff on failure. Per-user, per-IP, per-token.
+
+
+
Secrets
+
AES-256-GCM encrypted secret storage. Webhook notifications for security events.
+
+
+
Token binding
+
Tokens bound to source IP and client fingerprint. Stolen tokens don't travel.
+
+
+
Key policy
+
Refuse DSA. Refuse 1024-bit RSA. Configurable per group.
+
+
+
CrowdSec
+
Optional pre-auth IP blocking, post-auth failure reporting. Compatible with Crowdsieve.
+
+
+
+
+
+
+
+ Robots
+ Ansible, backup, CI — the robot lane.
+
+
+
+ SSH key auth, no OIDC dance.
+
+
+ Per-server config file
+ Fine-grained sudo permissions
+ Automatic account provisioning
+ Humans go through SSO. Robots don't need to.
+
+
+
+# /etc/open-bastion/service-accounts.d/ansible.conf
+[ansible]
+public_key = ssh-ed25519 AAAA… ansible@ci
+home = /var/lib/ansible
+shell = /bin/bash
+sudo = /usr/bin/apt update
+sudo = /usr/bin/systemctl restart *
+sudo_nopasswd = true
+
+
+ Open-Bastion
17 / service accounts
+
+
+
+
+ The neighbours
+ Briefly, who's on this street.
+
+
+
+
+ Project
+ Language
+ Licence
+ Identity model
+ Enforces on backends
+
+
+
+
+ Open-Bastion
+ C + shell
+ AGPL-3.0
+ Your SSO (LLNG / OIDC)
+ Yes — PAM everywhere
+
+
+ Teleport
+ Go
+ Ent. / BSL
+ Own user model (+ SSO)
+ Yes — its own agent
+
+
+ The Bastion (OVH)
+ Perl
+ Apache-2.0
+ Own user DB
+ Bastion-side only
+
+
+ Boundary
+ Go
+ MPL / BSL
+ Own user model (+ SSO)
+ Yes — broker model
+
+
+
+
+
+ We're the one where your existing SSO is the brain , the code fits in less than a weekend to read , and backends enforce too.
+
+ Open-Bastion
18 / neighbours
+
+
+
+
+ Install
+ Five steps. No YAML DSL.
+
+
+
01
+
Install package
+
apt · dnf · from source
+
+
+
02
+
Write config
+
portal_url, client, group
+
+
+
03
+
ob-enroll
+
code → admin approves
+
+
+
04
+
Edit pam.d/sshd
+
4 lines
+
+
+
05
+
Test — 2nd terminal
+
(keep the first open!)
+
+
+
+
+# apt install open-bastion
+# vi /etc/open-bastion/openbastion.conf
+# ob-enroll
+User code: GXPR-7K2M ← admin approves in LLNG portal
+✓ enrolled
+# systemctl restart sshd
+
+
+
+
+
+
+ The whole shape
+ Config, front to back.
+
+
+
/etc/open-bastion/openbastion.conf
+
+portal_url = https://auth.example.com
+client_id = pam-access
+client_secret = <redacted>
+server_group = db-prod
+log_level = info
+cache_ttl_ssh = 300
+cache_ttl_sudo = 60
+
+
+
+
/etc/pam.d/sudo
+
+# sudo through LLNG
+auth sufficient pam_openbastion.so
+auth required pam_deny.so
+account required pam_openbastion.so
+session required pam_unix.so
+
+
+ Same module, same endpoint. Different action .
+
+
+
+
+
+
+
+
+ Roadmap · early alpha today
+ Next: the desktop too.
+
+
+
+ Workstations login with the same LLNG identity.
+
+
+ LightDM greeter with embedded portal
+ MFA — TOTP, WebAuthn, SMS
+ Offline mode: Argon2id + AES-256-GCM
+ One SSO. Shell, sudo, desktop.
+
+
+
+ + desktop
+
+
+
+
+
+
+
+ Your move
+ Try it. Read it. Break it.
+
+
+
+
DOCKER DEMO
+
docker-demo-token/
+
docker-demo-cert/
+
docker-demo-maxsec/
+
Three flavours. Pick one. 10 minutes end-to-end.
+
+
+
REPO
+
github.com/linagora/open-bastion
+
⭐ if it's useful · 🐛 if it isn't
+
+
+
+
+
CONTRIBUTE
+
Issues, PRs, docs, packaging, translations.
+
AGPL-3.0. Upstream-first.
+
+
+
NEED HELP DEPLOYING?
+
Linagora offers support, training, custom integrations.
+
linagora.com
+
+
+
+
+
+
+
+
+
+
+
+
Questions?
+
Thank you.
+
+
+ github.com/linagora/open-bastion
+ xguimard@linagora.com
+ AGPL-3.0
+
+
+
+
+
+
+
+
+
Title variant
+
+ A · Editorial
+ B · Terminal
+ C · Question
+
+
Navigate to slide 1 to see the change.
+
+
+
+
+
+
diff --git a/doc/slides/README.md b/doc/slides/README.md
new file mode 100644
index 00000000..3ef24e6a
--- /dev/null
+++ b/doc/slides/README.md
@@ -0,0 +1,16 @@
+# Open Bastion — slide deck
+
+A self-contained HTML presentation (Linagora meetup talk introducing Open
+Bastion: SSO-driven SSH & sudo).
+
+- **[`Open-Bastion.html`](Open-Bastion.html)** — the deck; open it in a browser
+ (navigate with arrow keys; speaker notes are embedded).
+- **[`Open-Bastion-print.html`](Open-Bastion-print.html)** — print / export-to-PDF
+ layout (all slides stacked).
+- `brand.css`, `deck-stage.js`, `linagora.png` — the deck's assets (keep
+ alongside the HTML).
+
+> This is a **talk snapshot**, not a spec. For the authoritative, up-to-date
+> details, see the documentation in [`../`](../README.md) — e.g. the
+> bastion→backend hop uses [certificate vouching](../bastion-architecture.md)
+> (the deck's older "JWT" wording has been corrected here).
diff --git a/doc/slides/brand.css b/doc/slides/brand.css
new file mode 100644
index 00000000..aff74f65
--- /dev/null
+++ b/doc/slides/brand.css
@@ -0,0 +1,337 @@
+/* Open-Bastion deck — Linagora-aligned palette + typography */
+
+:root {
+ /* Linagora brand — corporate blue + warm accent */
+ --ob-ink: #0a1628;
+ --ob-ink-2: #132240;
+ --ob-paper: #ffffff;
+ --ob-paper-warm: #f5f2ec;
+ --ob-paper-cool: #eef2f7;
+ --ob-rule: #d9dfe7;
+ --ob-muted: #5a6b82;
+ --ob-blue: #b8243c; /* Linagora primary red (was blue) */
+ --ob-blue-deep: #7f1525; /* deeper red */
+ --ob-accent: #c8304a; /* bright red accent */
+ --ob-accent-soft: #e87a8a;
+ --ob-green: #2d7a4f;
+ --ob-red: #b23a3a;
+ --ob-code-bg: #0d1b2e;
+ --ob-code-fg: #dbe4f0;
+ --ob-code-dim: #8599b3;
+ --ob-code-accent: #f1a0ac;
+
+ --font-sans: "Inter", "Helvetica Neue", Helvetica, Arial, sans-serif;
+ --font-serif: "Source Serif 4", Georgia, "Times New Roman", serif;
+ --font-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
+}
+
+/* Fonts — loaded via in HTML to avoid blocking @import */
+
+html, body {
+ margin: 0;
+ padding: 0;
+ background: #000;
+ font-family: var(--font-sans);
+ color: var(--ob-ink);
+ font-feature-settings: "ss01", "cv11";
+}
+
+/* Slide base */
+.slide {
+ background: var(--ob-paper);
+ color: var(--ob-ink);
+ font-family: var(--font-sans);
+ font-size: 34px;
+ line-height: 1.35;
+ display: flex;
+ flex-direction: column;
+ padding: 100px 120px 80px;
+ box-sizing: border-box;
+ position: relative;
+}
+
+.slide--dark {
+ background: var(--ob-ink);
+ color: var(--ob-paper);
+}
+.slide--blue {
+ background: var(--ob-blue-deep);
+ color: var(--ob-paper);
+}
+.slide--warm {
+ background: var(--ob-paper-warm);
+ color: var(--ob-ink);
+}
+.slide--cool {
+ background: var(--ob-paper-cool);
+ color: var(--ob-ink);
+}
+
+/* Chrome: tiny footer bar with Linagora / slide label */
+.chrome {
+ position: absolute;
+ left: 120px;
+ right: 120px;
+ bottom: 36px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ font-family: var(--font-mono);
+ font-size: 16px;
+ color: var(--ob-muted);
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+}
+.slide--dark .chrome, .slide--blue .chrome { color: rgba(255,255,255,0.5); }
+
+.chrome__left { display: flex; gap: 18px; align-items: center; }
+.chrome__dot {
+ width: 8px; height: 8px; border-radius: 999px;
+ background: var(--ob-accent);
+}
+
+/* Kicker (eyebrow) */
+.kicker {
+ font-family: var(--font-mono);
+ font-size: 26px;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+ color: var(--ob-accent);
+ margin: 0 0 28px 0;
+ font-weight: 500;
+}
+.slide--dark .kicker, .slide--blue .kicker { color: var(--ob-accent-soft); }
+
+/* Titles */
+h1.title {
+ font-family: var(--font-sans);
+ font-weight: 700;
+ font-size: 96px;
+ line-height: 1.02;
+ letter-spacing: -0.02em;
+ margin: 0 0 32px 0;
+}
+h2.title {
+ font-family: var(--font-sans);
+ font-weight: 700;
+ font-size: 72px;
+ line-height: 1.05;
+ letter-spacing: -0.015em;
+ margin: 0 0 48px 0;
+}
+h3.subtitle {
+ font-family: var(--font-sans);
+ font-weight: 500;
+ font-size: 40px;
+ line-height: 1.3;
+ color: var(--ob-muted);
+ margin: 0;
+ letter-spacing: -0.005em;
+}
+.slide--dark h3.subtitle, .slide--blue h3.subtitle { color: rgba(255,255,255,0.7); }
+
+.lead {
+ font-family: var(--font-serif);
+ font-weight: 400;
+ font-size: 44px;
+ line-height: 1.28;
+ letter-spacing: -0.005em;
+}
+
+.body { font-size: 32px; line-height: 1.4; }
+.small { font-size: 26px; line-height: 1.4; }
+
+/* Accents */
+.accent { color: var(--ob-accent); }
+.muted { color: var(--ob-muted); }
+.slide--dark .muted, .slide--blue .muted { color: rgba(255,255,255,0.6); }
+
+/* Code blocks */
+.code {
+ font-family: var(--font-mono);
+ background: var(--ob-code-bg);
+ color: var(--ob-code-fg);
+ border-radius: 14px;
+ padding: 36px 44px;
+ font-size: 26px;
+ line-height: 1.55;
+ white-space: pre;
+ overflow: hidden;
+ box-shadow: 0 20px 60px rgba(10,22,40,0.15);
+}
+.code .tok-c { color: var(--ob-code-dim); font-style: italic; } /* comment */
+.code .tok-k { color: #8fb8ff; } /* keyword */
+.code .tok-s { color: #b8e0b4; } /* string */
+.code .tok-n { color: var(--ob-code-accent); } /* num / param */
+.code .tok-p { color: #ffbd7a; font-weight: 600; } /* highlight */
+.code .tok-d { color: var(--ob-code-fg); } /* default */
+
+.terminal {
+ font-family: var(--font-mono);
+ background: #0d1b2e;
+ color: #dbe4f0;
+ border-radius: 14px;
+ padding: 32px 40px;
+ font-size: 24px;
+ line-height: 1.55;
+ white-space: pre;
+ box-shadow: 0 30px 80px rgba(10,22,40,0.25);
+ position: relative;
+}
+.terminal::before {
+ content: "";
+ position: absolute;
+ top: 16px; left: 20px;
+ width: 12px; height: 12px; border-radius: 999px;
+ background: #ff5f57;
+ box-shadow: 20px 0 0 #ffbd2e, 40px 0 0 #28c840;
+}
+.terminal__header {
+ display: block;
+ padding-bottom: 20px;
+ margin-bottom: 20px;
+ border-bottom: 1px solid rgba(255,255,255,0.08);
+ color: #8599b3;
+ font-size: 20px;
+ text-align: center;
+ letter-spacing: 0.02em;
+}
+.term-prompt { color: #8fd3a3; }
+.term-user { color: #8fb8ff; }
+.term-path { color: #e8a76a; }
+.term-ok { color: #8fd3a3; }
+.term-fail { color: #e78b8b; }
+.term-dim { color: #8599b3; }
+.term-key { color: #ffbd7a; }
+
+/* Utility */
+.grid-2 {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 56px;
+ align-items: start;
+}
+.grid-3 {
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr;
+ gap: 40px;
+}
+.row { display: flex; gap: 40px; }
+.col { display: flex; flex-direction: column; gap: 24px; }
+
+/* Callout card (no left-border trope — subtle paper-on-paper) */
+.card {
+ background: var(--ob-paper);
+ border: 1px solid var(--ob-rule);
+ border-radius: 16px;
+ padding: 36px 40px;
+}
+.slide--dark .card, .slide--blue .card {
+ background: rgba(255,255,255,0.05);
+ border-color: rgba(255,255,255,0.12);
+}
+
+.pill {
+ display: inline-flex;
+ align-items: center;
+ gap: 12px;
+ padding: 8px 20px 8px 16px;
+ border-radius: 999px;
+ background: var(--ob-paper-warm);
+ border: 1px solid var(--ob-rule);
+ font-family: var(--font-mono);
+ font-size: 22px;
+ letter-spacing: 0.02em;
+}
+.pill__dot {
+ width: 10px; height: 10px; border-radius: 999px;
+ background: var(--ob-accent);
+}
+
+/* Section divider slide */
+.section-number {
+ font-family: var(--font-mono);
+ font-size: 200px;
+ font-weight: 300;
+ line-height: 1;
+ color: rgba(255,255,255,0.12);
+ letter-spacing: -0.04em;
+}
+
+/* Huge figure */
+.figure {
+ font-family: var(--font-sans);
+ font-weight: 700;
+ font-size: 280px;
+ line-height: 0.9;
+ letter-spacing: -0.04em;
+}
+
+/* Quote */
+blockquote.quote {
+ font-family: var(--font-serif);
+ font-style: italic;
+ font-weight: 400;
+ font-size: 64px;
+ line-height: 1.22;
+ letter-spacing: -0.01em;
+ margin: 0;
+ max-width: 1400px;
+}
+blockquote.quote::before { content: "\201C"; margin-right: 4px; }
+blockquote.quote::after { content: "\201D"; }
+
+/* Lists (no bullet tropes) */
+ul.clean {
+ list-style: none;
+ padding: 0; margin: 0;
+ display: flex; flex-direction: column;
+ gap: 24px;
+}
+ul.clean li {
+ display: grid;
+ grid-template-columns: 44px 1fr;
+ align-items: baseline;
+ font-size: 32px;
+ line-height: 1.4;
+}
+ul.clean li::before {
+ content: attr(data-mark);
+ font-family: var(--font-mono);
+ font-size: 22px;
+ color: var(--ob-accent);
+ font-weight: 500;
+}
+
+/* Diagram primitives */
+.node {
+ background: var(--ob-paper);
+ border: 1.5px solid var(--ob-ink);
+ border-radius: 12px;
+ padding: 18px 26px;
+ font-family: var(--font-mono);
+ font-size: 22px;
+ color: var(--ob-ink);
+ display: inline-flex;
+ align-items: center;
+ gap: 12px;
+ white-space: nowrap;
+}
+.node--accent { background: var(--ob-accent); color: var(--ob-paper); border-color: var(--ob-accent); }
+.node--blue { background: var(--ob-blue); color: var(--ob-paper); border-color: var(--ob-blue); }
+.node--ghost { background: transparent; border-style: dashed; color: var(--ob-muted); border-color: var(--ob-muted); }
+
+/* Linagora logotype (text-based, brand-accurate proportion) */
+.linagora-mark {
+ font-family: var(--font-sans);
+ font-weight: 700;
+ letter-spacing: -0.02em;
+ display: inline-flex;
+ align-items: center;
+ gap: 10px;
+}
+.linagora-mark__dot {
+ width: 12px; height: 12px; border-radius: 999px;
+ background: var(--ob-accent);
+ display: inline-block;
+}
diff --git a/doc/slides/deck-stage.js b/doc/slides/deck-stage.js
new file mode 100644
index 00000000..9caa508c
--- /dev/null
+++ b/doc/slides/deck-stage.js
@@ -0,0 +1,621 @@
+/**
+ * — reusable web component for HTML decks.
+ *
+ * Handles:
+ * (a) speaker notes — reads