Renames the package and all artifacts to authforge to drop the
distro-specific prefix, since the roadmap targets Ubuntu + Debian +
KDE + eventually Fedora (option C in the design).
- deb packages: authforge, authforge-{daemon,pam,cli,gui,gnome-integration}
- binaries: authforged, authforgectl, authforge (GUI)
- D-Bus name: io.dangerousthings.AuthForge
- PAM module: pam_authforge_pending.so
- Paths: /etc/authforge/, /var/lib/authforge/, /usr/share/pam-configs/authforge
- PPA: ppa:dangerousthings/authforge
Filesystem path /home/work/VSCodeProjects/ubuntu_fido/ left as-is for
historical reference; can rename later via git mv at the dir level.
Verified: cargo build/test/clippy/fmt clean, pam builds, gui builds,
all 5 debs produced.
19 KiB
authforge — 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
pamu2fcfgover 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/authforge/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_authforge_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
┌─────────────────────────┐ ┌──────────────────────────┐ ┌──────────────┐
│ authforge GTK app │ │ authforgectl (CLI) │ │ Future KDE │
│ (libadwaita panel) │ │ (admin/fleet scripting) │ │ KCM module │
└─────────────┬───────────┘ └────────────┬─────────────┘ └──────┬───────┘
│ D-Bus (io.dangerousthings.AuthForge) │ │
└─────────────┬─────────────┴────────────────────────┘
▼
┌────────────────────────────────────┐
│ authforged (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/authforge/ ~/.config/Yubico/ /var/lib/authforge/
(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/authforge/, and /var/lib/authforge/. 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-fido2andwebauthn-rscrates 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 |
|---|---|---|
authforge |
Empty metapackage. | Depends |
authforge-daemon |
/usr/sbin/authforged, systemd unit (authforge.service), D-Bus service file, polkit rules under /usr/share/polkit-1/actions/ |
Hard dep of metapackage |
authforge-pam |
/usr/lib/$DEB_HOST_MULTIARCH/security/pam_authforge_pending.so, pam-auth-update profile at /usr/share/pam-configs/authforge |
Hard dep |
authforge-cli |
/usr/bin/authforgectl (Rust binary, talks D-Bus) |
Hard dep |
authforge-gui |
/usr/bin/authforge (GTK4/libadwaita), .desktop file with Categories=Settings;Security;, app icon |
Recommends (so headless servers can skip) |
authforge-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
sudo add-apt-repository ppa:dangerousthings/authforge
sudo apt install authforge
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
apt install authforge-daemon authforge-pam authforge-cli
Skips the GUI metadata. Configure via /etc/authforge/policy.d/90-fleet.conf or authforgectl. We additionally publish:
- An Ansible role
dangerousthings.authforgeon Galaxy wrappingapt + debconf + drop-in config. - A debconf preseed schema so
debian-installer/cloud-init can answer questions at install time.
Clean uninstall
apt purge authforge* runs pam-auth-update --remove (cleanly retracts our PAM hooks), stops and disables the systemd unit, removes /etc/authforge/ only on purge (not remove), and leaves ~/.config/Yubico/u2f_keys files alone (so reinstall is painless).
GUI design
App: authforge
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
- System login (gdm-password):
- "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 authforge-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 authforge --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 authforge-gnome-integration is installed, the gnome-control-center Users → Add User dialog gains a new section: Configure security, with three radio options:
- Enroll security credential now — opens enrollment modal, admin must have the user's key/implant present. Sets a real password (admin-supplied).
- 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/authforge/pending/<user>. - 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)
- User opens
authforgefrom Activities. - Clicks "+ Add Security Key".
- Modal: "Plug in your key now or tap your NFC implant to a reader." Daemon polls
libfido2for new device. - Device detected → optional PIN/UV prompt for FIDO2 → daemon emits
pamu2fcfg-equivalent registration line. - User names the credential ("Yellow Yubikey", "xSIID implant").
- Daemon writes line to
~/.config/Yubico/u2f_keys(or central file if policy says so). - 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
- Admin opens Users panel, clicks Add User.
- Fills in account, picks "Enroll security credential now".
- 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.
- 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):
- Admin picks "Require user to set up at first login", enters/accepts temp password.
- Daemon runs
chage -d 0 <user>(built-in Linux mechanism: forces password change at next login). - Daemon writes flag file
/var/lib/authforge/pending/<user>containing JSON:{"required_methods": ["fido2"], "created": "...", "deadline": null}.
First login (user side):
- User logs into GDM with temp password.
- PAM
passwdmodule forces password change (existing Ubuntu behavior, no work for us). - GNOME session starts.
- Our autostart entry
/etc/xdg/autostart/authforge-firstrun.desktoprunsauthforge --first-run. - Detects pending flag, takes over the screen with a fullscreen modal: "Welcome. Before you continue, please enroll a security key."
- User enrolls (Flow A inside the modal).
- On success, daemon clears pending flag.
- Modal closes, user lands on a normal desktop.
Backstop (the PAM module):
- If user kills
authforge --first-runand tries to do anything sudo/login-related,pam_authforge_pending.sois 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
authforge --first-runcallsgnome-session-quit --logout --no-promptafter 60 seconds of inactivity in the modal.
Flow D: Lost key recovery
- User can't authenticate.
- User contacts admin.
- Admin runs
authforgectl recovery generate <user>(or clicks button in GUI). Outputs an 8-digit one-time code valid 24 h. - User enters code at GDM password prompt —
pam_authforge_pending.sorecognizes recovery code prefix, lets them through with a forced re-enrollment flag set. - 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/authforge:
Name: Dangerous Things authforge 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_authforge_pending.so
Default no means the package install does not enable enforcement — admins / GUI explicitly turn it on per stack.
Policy file format (/etc/authforge/policy.d/*.conf)
TOML, parsed in lexical order, last-key-wins:
[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/authforge/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, mockinglibfido2via trait abstraction. - PAM module: small C test harness using
pam_test_authenticatefrom 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+umockdevfor USB simulation, or thelibfido2software 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_authforge). 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_fprintdintegration. - 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-authforge-implementation-plan.md.