Initial commit: license, gitignore, readme, design docs

This commit is contained in:
michael
2026-04-26 14:12:17 -07:00
commit d59fbf4e4e
6 changed files with 1807 additions and 0 deletions

View 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. | ~2530% of Linux desktop on day one, ~5060% 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. | ~68 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. | ~34 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`.*

File diff suppressed because it is too large Load Diff