Initial commit: license, gitignore, readme, design docs
This commit is contained in:
310
docs/plans/2026-04-26-ubuntu-fido-design.md
Normal file
310
docs/plans/2026-04-26-ubuntu-fido-design.md
Normal file
@@ -0,0 +1,310 @@
|
||||
# ubuntu-fido — Design Document
|
||||
|
||||
**Date:** 2026-04-26
|
||||
**Status:** Draft (brainstorm complete, validated through Section 2 with stakeholder)
|
||||
**Owner:** Dangerous Things (ops@dangerousthings.com)
|
||||
|
||||
## Problem
|
||||
|
||||
Enabling U2F / FIDO2 passkey / TOTP on Ubuntu desktops today is a manual, error-prone process. `libpam-u2f` exists and works, but:
|
||||
- New-account enrollment requires the admin to either physically have the user's key at provisioning time, or talk the user through `pamu2fcfg` over chat.
|
||||
- There is no GUI. Admins hand-edit `/etc/pam.d/*` files, with no safety net against locking themselves out.
|
||||
- There is no first-login enrollment pattern equivalent to Windows OOBE / macOS Setup Assistant.
|
||||
- TOTP and U2F live in separate, unrelated tools.
|
||||
|
||||
This project ships a single deb package that gives end users and admins a turnkey path to MFA — install, click, done — while staying compatible with fleet-management workflows.
|
||||
|
||||
## Scope decisions (locked)
|
||||
|
||||
| # | Decision | Reasoning |
|
||||
|---|---|---|
|
||||
| 1 | **Audience: end-user-first, admin-extensible.** PPA + apt + GUI is the headline path; drop-in `/etc/` config files cover fleet/Ansible. | Maximizes reach and matches Dangerous Things' customer mix (hobbyists + small business IT). |
|
||||
| 2 | **v1 target: Ubuntu LTS + GNOME.** Ultimate target: Ubuntu + Debian × GNOME + KDE Plasma. | ~25–30% of Linux desktop on day one, ~50–60% at target state. v1 ships in months not years. |
|
||||
| 3 | **GUI: standalone GTK4/libadwaita app + optional gnome-control-center Users-panel shortcut deb.** | gnome-control-center has no stable plugin API. Standalone app is forward-compatible; shortcut deb adds discoverability without coupling to GNOME release cycle. |
|
||||
| 4 | **Authenticators: U2F + FIDO2 passkey + TOTP.** TOTP behind a build-time feature flag. | Full scope as written; flag lets us ship a beta without TOTP if the QR/recovery-code UX slips schedule. |
|
||||
| 5 | **Policy contexts: 3 in main GUI (gdm-password, sudo, sshd) with Disabled / Optional / Required modes.** Full list (polkit, su, login, plus auto-detected stacks) under pkexec admin gate or via `/etc/ubuntu-fido/policy.d/*.conf`. | 95% case stays simple; advanced/fleet path unrestricted. Mandatory pre-commit lockout simulation. |
|
||||
| 6 | **First-login enrollment: `chage -d 0` + autostart enrollment app + small `pam_ubuntu_fido_pending.so` blocking module.** | Belt-and-suspenders: leverages existing forced-password-change, adds enrollment via autostart, PAM module ensures user can't bypass by killing the modal. |
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────┐ ┌──────────────────────────┐ ┌──────────────┐
|
||||
│ ubuntu-fido GTK app │ │ ubuntu-fidoctl (CLI) │ │ Future KDE │
|
||||
│ (libadwaita panel) │ │ (admin/fleet scripting) │ │ KCM module │
|
||||
└─────────────┬───────────┘ └────────────┬─────────────┘ └──────┬───────┘
|
||||
│ D-Bus (org.dt.UbuntuFido) │ │
|
||||
└─────────────┬─────────────┴────────────────────────┘
|
||||
▼
|
||||
┌────────────────────────────────────┐
|
||||
│ ubuntu-fidod (system daemon) │ ← polkit-mediated
|
||||
│ - enrollment orchestration │
|
||||
│ - policy read/write + lockout │
|
||||
│ safety simulation │
|
||||
│ - libfido2 bindings (Rust) │
|
||||
│ - PAM stack edits via │
|
||||
│ pam-auth-update │
|
||||
└────────────────┬───────────────────┘
|
||||
▼
|
||||
┌─────────────────┬───────┴────────┬──────────────────────┐
|
||||
▼ ▼ ▼ ▼
|
||||
/etc/pam.d/* /etc/ubuntu-fido/ ~/.config/Yubico/ /var/lib/ubuntu-fido/
|
||||
(stack edits policy.d/*.conf u2f_keys (per-user) pending/<user>
|
||||
via pam-auth- (admin/fleet /etc/u2f_mappings (first-login flag)
|
||||
update) drop-ins) (central override)
|
||||
```
|
||||
|
||||
### Trust boundary
|
||||
|
||||
The daemon owns all writes to `/etc/pam.d/`, `/etc/ubuntu-fido/`, and `/var/lib/ubuntu-fido/`. GUIs never write these directly. polkit policies decide who can call which method:
|
||||
|
||||
| D-Bus method | Default polkit action |
|
||||
|---|---|
|
||||
| `EnrollOwn(user)`, `RemoveOwn(user)` | `auth_self_keep` (user authenticates with password once per 5 min) |
|
||||
| `EnrollOther(user)`, `SetPolicy(...)`, `SetPendingFlag(user)` | `auth_admin_keep` (admin password) |
|
||||
| `GetState()`, `GetPolicy()` | `yes` (read is free) |
|
||||
|
||||
### Why Rust for the daemon
|
||||
|
||||
- Memory safety in code that handles credential material.
|
||||
- `ctap-hid-fido2` and `webauthn-rs` crates are mature and actively maintained.
|
||||
- Cross-compiles cleanly to other platforms if the project ever expands beyond Linux.
|
||||
- Slightly larger build footprint than C, immaterial for a deb package.
|
||||
|
||||
## Package layout
|
||||
|
||||
| Package | Contents | Type |
|
||||
|---|---|---|
|
||||
| `ubuntu-fido` | Empty metapackage. | Depends |
|
||||
| `ubuntu-fido-daemon` | `/usr/sbin/ubuntu-fidod`, systemd unit (`ubuntu-fido.service`), D-Bus service file, polkit rules under `/usr/share/polkit-1/actions/` | Hard dep of metapackage |
|
||||
| `ubuntu-fido-pam` | `/usr/lib/$DEB_HOST_MULTIARCH/security/pam_ubuntu_fido_pending.so`, pam-auth-update profile at `/usr/share/pam-configs/ubuntu-fido` | Hard dep |
|
||||
| `ubuntu-fido-cli` | `/usr/bin/ubuntu-fidoctl` (Rust binary, talks D-Bus) | Hard dep |
|
||||
| `ubuntu-fido-gui` | `/usr/bin/ubuntu-fido` (GTK4/libadwaita), `.desktop` file with `Categories=Settings;Security;`, app icon | **Recommends** (so headless servers can skip) |
|
||||
| `ubuntu-fido-gnome-integration` | Tiny shim that adds a "Configure security…" launcher button to gnome-control-center's Users panel via overlay `.desktop` extension | **Suggests** (not auto-installed) |
|
||||
|
||||
### Headline install
|
||||
|
||||
```bash
|
||||
sudo add-apt-repository ppa:dangerousthings/ubuntu-fido
|
||||
sudo apt install ubuntu-fido
|
||||
```
|
||||
|
||||
Two commands. After install:
|
||||
- Daemon is running.
|
||||
- All PAM policy is **Disabled** by default. Installation never enables MFA enforcement on its own.
|
||||
- GUI is available from Activities (search "Authentication" or "Security Keys").
|
||||
- Empty state: "No security keys configured. Plug in or scan a key to enroll."
|
||||
|
||||
### Headless / fleet install
|
||||
|
||||
```bash
|
||||
apt install ubuntu-fido-daemon ubuntu-fido-pam ubuntu-fido-cli
|
||||
```
|
||||
Skips the GUI metadata. Configure via `/etc/ubuntu-fido/policy.d/90-fleet.conf` or `ubuntu-fidoctl`. We additionally publish:
|
||||
|
||||
- An **Ansible role** `dangerousthings.ubuntu_fido` on Galaxy wrapping `apt + debconf + drop-in config`.
|
||||
- A **debconf preseed schema** so `debian-installer`/cloud-init can answer questions at install time.
|
||||
|
||||
### Clean uninstall
|
||||
|
||||
`apt purge ubuntu-fido*` runs `pam-auth-update --remove` (cleanly retracts our PAM hooks), stops and disables the systemd unit, removes `/etc/ubuntu-fido/` only on `purge` (not `remove`), and leaves `~/.config/Yubico/u2f_keys` files alone (so reinstall is painless).
|
||||
|
||||
## GUI design
|
||||
|
||||
### App: `ubuntu-fido`
|
||||
|
||||
GTK4 + libadwaita. Single-window adaptive layout, looks like a stock Settings panel. Top-level navigation via libadwaita `AdwViewStack`:
|
||||
|
||||
#### 1. **Security Keys** tab (default)
|
||||
- List of currently enrolled credentials per the active user. Each row: nickname, type icon (USB-A / NFC / implant / passkey), date added, last used, "Test" button, "Remove" button.
|
||||
- "+ Add Security Key" primary action button.
|
||||
- Empty state: large illustration + "Plug in your security key, or tap an NFC implant to begin."
|
||||
|
||||
#### 2. **TOTP** tab (visible only if TOTP build-flag enabled)
|
||||
- List of TOTP enrollments (typically 0 or 1 per user — Linux PAM TOTP is one-secret-per-user by convention).
|
||||
- "Generate TOTP secret" → modal with QR code + secret string + 8 recovery codes (one-time use).
|
||||
|
||||
#### 3. **Authentication Policy** tab
|
||||
- Three master toggles for the 95% case:
|
||||
- **System login** (gdm-password): `Disabled / Optional / Required`
|
||||
- **Privileged commands** (sudo): same
|
||||
- **Remote SSH** (sshd): same
|
||||
- "Advanced…" disclosure → pkexec prompt → full list including polkit, su, login, and any auto-detected PAM stacks. Each with same three modes plus a free-text condition (e.g., "only outside corporate network" via pam_access — v2 feature, scoped out of v1).
|
||||
- Big yellow callout above the toggles when local user has no MFA credentials enrolled: **"You have no security keys. Enabling 'Required' will lock you out at next login."** Apply button disabled until user enrolls or explicitly confirms with second password prompt.
|
||||
|
||||
#### 4. **Recovery** tab
|
||||
- Manage recovery codes (regenerate, view remaining count).
|
||||
- Print recovery sheet (PDF, optionally including a QR-encoded backup of the user's TOTP secret for users who consent).
|
||||
- "Emergency unlock" — generates a single-use recovery code an admin can hand to a user who lost their key, valid for 24 h.
|
||||
|
||||
### App: gnome-control-center Users panel shortcut
|
||||
|
||||
Optional `ubuntu-fido-gnome-integration` package ships an overlay file under `/usr/share/gnome-control-center/users/` (or whatever the current GNOME version uses) that adds an additional row labeled **"Authentication & Security Keys…"** to each user's detail view. Clicking launches `ubuntu-fido --user <username>` with elevated D-Bus permissions if the launching user is in `sudo`/`admin` group.
|
||||
|
||||
The overlay is shipped separately precisely because gnome-control-center has no stable plugin API — this package may need re-tuning every GNOME release. If it ever breaks, the standalone app keeps working; only the discoverability shortcut is lost.
|
||||
|
||||
### Add User flow extension
|
||||
|
||||
When `ubuntu-fido-gnome-integration` is installed, the **gnome-control-center Users → Add User** dialog gains a new section: **Configure security**, with three radio options:
|
||||
|
||||
1. **Enroll security credential now** — opens enrollment modal, admin must have the user's key/implant present. Sets a real password (admin-supplied).
|
||||
2. **Require user to set up at first login** — admin sets a temp password (or accepts a generated one). We run `chage -d 0 <user>` and create `/var/lib/ubuntu-fido/pending/<user>`.
|
||||
3. **No MFA** — only available if local policy permits ("Required" stacks would block this option with a tooltip).
|
||||
|
||||
## Enrollment flows
|
||||
|
||||
### Flow A: User self-enrollment (the common case)
|
||||
|
||||
1. User opens `ubuntu-fido` from Activities.
|
||||
2. Clicks "+ Add Security Key".
|
||||
3. Modal: "Plug in your key now or tap your NFC implant to a reader." Daemon polls `libfido2` for new device.
|
||||
4. Device detected → optional PIN/UV prompt for FIDO2 → daemon emits `pamu2fcfg`-equivalent registration line.
|
||||
5. User names the credential ("Yellow Yubikey", "xSIID implant").
|
||||
6. Daemon writes line to `~/.config/Yubico/u2f_keys` (or central file if policy says so).
|
||||
7. Modal closes, list refreshes.
|
||||
|
||||
polkit asks for user password if it's been > 5 minutes since last auth (auth_self_keep).
|
||||
|
||||
### Flow B: Admin-enrolls-now at account creation
|
||||
|
||||
1. Admin opens Users panel, clicks Add User.
|
||||
2. Fills in account, picks "Enroll security credential now".
|
||||
3. Same modal as Flow A but for the new account; daemon writes to that user's home (creates dir with correct ownership) or to central file.
|
||||
4. Account ready for normal login from minute one.
|
||||
|
||||
polkit asks admin password.
|
||||
|
||||
### Flow C: First-login enrollment (the novel piece)
|
||||
|
||||
**At account creation (admin side):**
|
||||
1. Admin picks "Require user to set up at first login", enters/accepts temp password.
|
||||
2. Daemon runs `chage -d 0 <user>` (built-in Linux mechanism: forces password change at next login).
|
||||
3. Daemon writes flag file `/var/lib/ubuntu-fido/pending/<user>` containing JSON: `{"required_methods": ["fido2"], "created": "...", "deadline": null}`.
|
||||
|
||||
**First login (user side):**
|
||||
1. User logs into GDM with temp password.
|
||||
2. PAM `passwd` module forces password change (existing Ubuntu behavior, no work for us).
|
||||
3. GNOME session starts.
|
||||
4. Our autostart entry `/etc/xdg/autostart/ubuntu-fido-firstrun.desktop` runs `ubuntu-fido --first-run`.
|
||||
5. Detects pending flag, takes over the screen with a fullscreen modal: "Welcome. Before you continue, please enroll a security key."
|
||||
6. User enrolls (Flow A inside the modal).
|
||||
7. On success, daemon clears pending flag.
|
||||
8. Modal closes, user lands on a normal desktop.
|
||||
|
||||
**Backstop (the PAM module):**
|
||||
- If user kills `ubuntu-fido --first-run` and tries to do anything sudo/login-related, `pam_ubuntu_fido_pending.so` is in their auth stack; it sees the pending flag and denies auth with a friendly message: "Account setup incomplete. Please complete enrollment in the Authentication app."
|
||||
- A watchdog inside `ubuntu-fido --first-run` calls `gnome-session-quit --logout --no-prompt` after 60 seconds of inactivity in the modal.
|
||||
|
||||
### Flow D: Lost key recovery
|
||||
|
||||
1. User can't authenticate.
|
||||
2. User contacts admin.
|
||||
3. Admin runs `ubuntu-fidoctl recovery generate <user>` (or clicks button in GUI). Outputs an 8-digit one-time code valid 24 h.
|
||||
4. User enters code at GDM password prompt — `pam_ubuntu_fido_pending.so` recognizes recovery code prefix, lets them through with a forced re-enrollment flag set.
|
||||
5. User logs in, immediately sees first-login-style modal forcing them to enroll a new key before doing anything else.
|
||||
|
||||
## PAM and policy details
|
||||
|
||||
### pam-auth-update profile
|
||||
|
||||
`/usr/share/pam-configs/ubuntu-fido`:
|
||||
|
||||
```
|
||||
Name: Dangerous Things ubuntu-fido MFA
|
||||
Default: no
|
||||
Priority: 192
|
||||
Auth-Type: Additional
|
||||
Auth:
|
||||
[success=ok default=1 ignore=ignore] pam_u2f.so cue authfile=/etc/u2f_mappings
|
||||
[success=ok default=die] pam_ubuntu_fido_pending.so
|
||||
```
|
||||
|
||||
Default `no` means the package install does not enable enforcement — admins / GUI explicitly turn it on per stack.
|
||||
|
||||
### Policy file format (`/etc/ubuntu-fido/policy.d/*.conf`)
|
||||
|
||||
TOML, parsed in lexical order, last-key-wins:
|
||||
|
||||
```toml
|
||||
[stacks.gdm-password]
|
||||
mode = "required" # disabled | optional | required
|
||||
methods = ["fido2", "totp"] # any-of
|
||||
|
||||
[stacks.sudo]
|
||||
mode = "optional"
|
||||
methods = ["fido2"]
|
||||
|
||||
[stacks.sshd]
|
||||
mode = "required"
|
||||
methods = ["fido2"]
|
||||
|
||||
[storage]
|
||||
backend = "central" # per-user | central
|
||||
central_path = "/etc/u2f_mappings"
|
||||
|
||||
[firstrun]
|
||||
default_required_methods = ["fido2"]
|
||||
deadline_hours = 0 # 0 = no auto-expire
|
||||
```
|
||||
|
||||
Daemon reloads on `SIGHUP` or when a watched file changes (uses `inotify`).
|
||||
|
||||
### Lockout safety simulation
|
||||
|
||||
Before applying any policy change, daemon simulates the new policy against:
|
||||
- The currently logged-in interactive user (resolved via `loginctl`).
|
||||
- Every user listed in `/var/lib/ubuntu-fido/users.db` (cached enrollment registry).
|
||||
|
||||
For each user, it runs the new policy through a dry-run PAM check. If the result would deny login *without* a way to recover (no enrolled credentials of any required method), the daemon refuses the change and returns an actionable error: "Applying this policy would lock out user `alice` from `gdm-password` (no fido2 credentials enrolled). Enroll a credential for alice or set this stack to 'optional'."
|
||||
|
||||
The GUI surfaces this as a red banner with a one-click "fix it" suggestion. The CLI returns non-zero with the same message. Override flag `--force-i-know-what-im-doing` exists for the determined.
|
||||
|
||||
## Testing strategy
|
||||
|
||||
### Unit
|
||||
- Daemon: Rust `cargo test`, mocking `libfido2` via trait abstraction.
|
||||
- PAM module: small C test harness using `pam_test_authenticate` from libpam-tests-dev.
|
||||
|
||||
### Integration
|
||||
- Disposable Multipass / LXD VM that boots a minimal Ubuntu 24.04 GNOME image, installs the deb from a local apt repo, and runs scripted enrollment via virtual FIDO2 device (`vhci-hcd` + `umockdev` for USB simulation, or the `libfido2` software backend).
|
||||
- One scripted test per flow A/B/C/D above. Each asserts both successful path and lockout-prevention failure mode.
|
||||
|
||||
### Manual smoke (release gate)
|
||||
- Real Yubikey (USB-A and NFC), Token2, and a Dangerous Things xSIID/Apex implant via NFC reader.
|
||||
- Test on Ubuntu 22.04 LTS, 24.04 LTS, and the current latest LTS at release time.
|
||||
|
||||
### Continuous fleet test
|
||||
- Vagrant-based fleet rehearsal: 5 VMs preseeded with our debconf answers, joined to a fake LDAP, Ansible role applied, simulated user creations, simulated lost-key recoveries.
|
||||
|
||||
## Roadmap to ultimate target (C)
|
||||
|
||||
The architecture is designed so each step here is additive — no refactor of v1.
|
||||
|
||||
| Phase | Deliverable | Effort |
|
||||
|---|---|---|
|
||||
| **v1.0** | Ubuntu LTS + GNOME, U2F + FIDO2 passkey + TOTP. | Reference. |
|
||||
| **v1.1** | Debian stable packaging (same source, second build target). | ~2 weeks. |
|
||||
| **v1.2** | KDE Plasma front-end as a KCM module (`kcm_ubuntu_fido`). Reuses the same daemon over D-Bus. Built with KF6/Qt6. | ~6–8 weeks (mostly Qt port of GUI). |
|
||||
| **v1.3** | Fedora/RHEL spec file. PAM module and daemon already work; needs `.rpm` packaging and SELinux policy. GNOME GUI works as-is. | ~3–4 weeks. |
|
||||
| **v2.0** | Optional: organization-wide credential sync via FreeIPA / LDAP integration. Pure backend feature. | Out of scope for now. |
|
||||
|
||||
## Risks and open questions
|
||||
|
||||
| Risk | Mitigation |
|
||||
|---|---|
|
||||
| GNOME panel shortcut deb breaks on GNOME version bumps. | Standalone app remains functional; shortcut deb is `Suggests:` only, version-pinned, and can be skipped without losing core feature. |
|
||||
| `gdm-password` and screen unlock share a PAM stack — making MFA "required" affects both. Some users want different policies for "log in" vs "unlock screensaver". | Document the limitation. v2 could split via custom stacks with `pam_succeed_if user_unlock_session`. |
|
||||
| User loses last security key with no TOTP and no recovery codes. | Recovery flow (Flow D) requires admin involvement — accepted trade-off. Document prominently in onboarding. Single-user-laptop edge case: instruct user to keep recovery codes printed. |
|
||||
| Implants need NFC readers; not all desktop systems have them. | Out of scope for the package. Document supported readers. Standard libfido2 NFC stack works once a reader is plugged in. |
|
||||
| Pre-existing libpam-u2f config is already in use on the system. | postinst checks for existing `pam_u2f.so` lines in `/etc/pam.d/`; if found, prompts admin (or refuses to install non-interactively) until they migrate or accept import. |
|
||||
|
||||
## Out of scope for v1
|
||||
|
||||
- Fingerprint readers / FIDO2 platform authenticators (laptop biometric sensors). Future phase via `pam_fprintd` integration.
|
||||
- WebAuthn for browser-based logins to web apps. Different layer entirely.
|
||||
- Smart-card / PIV authentication. Adjacent space, future phase via `pam_pkcs11`.
|
||||
- Per-network policy ("require MFA only off corp VPN"). Defer to v2.
|
||||
- macOS / Windows clients. This is Linux-specific.
|
||||
|
||||
---
|
||||
|
||||
*This design was developed via guided brainstorming on 2026-04-26. The next artifact is a detailed implementation plan in `2026-04-26-ubuntu-fido-implementation-plan.md`.*
|
||||
Reference in New Issue
Block a user