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 + + + + + + + + + + + + +
+ + +
+
+ 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 +
+
+ + +
+
+ Linagora + 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
+
+ + +
+
+ Linagora +
+
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.
+
+
+
Linagora
01 / who
+
+ + +
+
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
  • +
+
+
+root@prod-db-07 ~/.ssh +# 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 +
+
+
Open-Bastion
03 / sudo
+
+ + +
+
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. +
+
Open-Bastion
06 / bet
+
+ + +
+
Introducing
+

+ PAM & NSS modules for Linux, wired into LemonLDAP::NG. +

+
+
+
TWO AUTH MODES
+
Token
or
SSH key
+
+
+
TWO DECISIONS
+
SSH access
and
sudo privilege
+
+
+
EVERYWHERE
+
Bastion
and
backends
+
+
+
+ C & shell · ~68 % / 25 % + Debian · Ubuntu · Rocky + AGPL-3.0 +
+
Open-Bastion
07 / meet
+
+ + +
+
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.
  • +
+
+alice@laptop ~ +$ 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) +
+
+
+
Open-Bastion
12 / sudo
+
+ + +
+
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 + + + + +
+
+
Open-Bastion
13 / offline
+
+ + +
+
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
  • +
+
+
+/var/log/openbastion/sessions/ +$ 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.
+
+
+
Open-Bastion
16 / depth
+
+ + +
+
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.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ProjectLanguageLicenceIdentity modelEnforces on backends
Open-BastionC + shellAGPL-3.0Your SSO (LLNG / OIDC)Yes — PAM everywhere
TeleportGoEnt. / BSLOwn user model (+ SSO)Yes — its own agent
The Bastion (OVH)PerlApache-2.0Own user DBBastion-side only
BoundaryGoMPL / BSLOwn 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!)
+
+
+
+admin@prod-bastion-01 +# 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 +
+
Open-Bastion
19 / deploy
+
+ + +
+
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. +
+
+
+
Open-Bastion
20 / config
+
+ + +
+
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 +
+
+
Open-Bastion
21 / next
+
+ + +
+
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
+
+
+
+
Open-Bastion
22 / try it
+
+ + +
+
+ Linagora +
+
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

+
+ + + +
+
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 + + + + + + + + + + + + + + + +
+ + +
+
+ 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 +
+
+ + +
+
+ Linagora + 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
+
+ + +
+
+ Linagora +
+
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.
+
+
+
Linagora
01 / who
+
+ + +
+
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
  • +
+
+
+root@prod-db-07 ~/.ssh +# 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 +
+
+
Open-Bastion
03 / sudo
+
+ + +
+
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. +
+
Open-Bastion
06 / bet
+
+ + +
+
Introducing
+

+ PAM & NSS modules for Linux, wired into LemonLDAP::NG. +

+
+
+
TWO AUTH MODES
+
Token
or
SSH key
+
+
+
TWO DECISIONS
+
SSH access
and
sudo privilege
+
+
+
EVERYWHERE
+
Bastion
and
backends
+
+
+
+ C & shell · ~68 % / 25 % + Debian · Ubuntu · Rocky + AGPL-3.0 +
+
Open-Bastion
07 / meet
+
+ + +
+
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.
  • +
+
+alice@laptop ~ +$ 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) +
+
+
+
Open-Bastion
12 / sudo
+
+ + +
+
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 + + + + +
+
+
Open-Bastion
13 / offline
+
+ + +
+
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
  • +
+
+
+/var/log/openbastion/sessions/ +$ 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.
+
+
+
Open-Bastion
16 / depth
+
+ + +
+
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.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ProjectLanguageLicenceIdentity modelEnforces on backends
Open-BastionC + shellAGPL-3.0Your SSO (LLNG / OIDC)Yes — PAM everywhere
TeleportGoEnt. / BSLOwn user model (+ SSO)Yes — its own agent
The Bastion (OVH)PerlApache-2.0Own user DBBastion-side only
BoundaryGoMPL / BSLOwn 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!)
+
+
+
+admin@prod-bastion-01 +# 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 +
+
Open-Bastion
19 / deploy
+
+ + +
+
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. +
+
+
+
Open-Bastion
20 / config
+
+ + +
+
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 +
+
+
Open-Bastion
21 / next
+
+ + +
+
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
+
+
+
+
Open-Bastion
22 / try it
+
+ + +
+
+ Linagora +
+
Questions?
+

Thank you.

+
+
+ github.com/linagora/open-bastion + xguimard@linagora.com + AGPL-3.0 +
+
+
+ +
+ + +
+

Title variant

+
+ + + +
+
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