diff --git a/docs/README.md b/docs/README.md index 6ec32076..aacba8d0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -17,6 +17,7 @@ those entry points. - [Security](SECURITY.md) — threat model, trust boundaries, implemented controls and audits. - [Backups](BACKUP.md) — backup layers, password model, retention and restore procedure. +- [Hardware security keys](security-keys.md) — CanoKey/YubiKey setup, backup keys and recovery. - [Workarounds and known gaps](WORKAROUNDS.md) — pins, temporary workarounds and removal conditions. - [BIOS settings](bios.md) — firmware settings for the physical hosts. - [Wake-on-LAN](wake-on-lan.md) — firmware prerequisites, NixOS setup and wake commands. diff --git a/docs/SECURITY.md b/docs/SECURITY.md index aeac6a92..82efe44f 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -33,18 +33,19 @@ small, reversible controls and narrow exceptions; benchmark before adopting expe ## Architecture and trust boundaries -| Boundary | Current design | Limitation | -| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| Administration | Key-based SSH; root key login retained for Colmena/MicroVM deployment; only keys declared in Nix are honoured, and no password or keyboard-interactive login; remote-builder host keys only reach `nix-daemon` | Deployment credentials are root-equivalent file keys, usable by any same-user process through the desktop SSH agent; one stolen key reaches every host | -| Host network | Shared nftables firewall trusts the home LAN, tailnet interface and local Podman bridge; IPv6 link-local is accepted | These are broad trust zones, not service-level least privilege; router exposure and tailnet ACLs require separate verification | -| Monitoring | node/smartctl exporters bind the host LAN IPv4 (9100/9633); firewall rules restrict exporter ports 9100, 9835 and 9633 to the monitoring host over IPv4. RAPL counters are exposed only on x86 server hosts, to `node-exporter` through the `rapl` group. | nvidia_gpu_exporter (9835) still binds wildcard; the IPv6 denial for those three ports is in the shared firewall; current running hosts require separate verification. The node_exporter account can measure package energy for local side channels; interactive users, desktops, and guests keep the kernel-default root-only permission. | -| VM hosts | shoryu, shushou and youko run guests behind `br0`; ruby/kana are libvirt guests | A bridge is not network isolation; host compromise affects its guests | -| k3s | Three control-plane and three worker MicroVMs; Cilium, API VIP and NFS CSI | Cluster-admin, privileged/host-mounted pods and writable NFS data are powerful trust boundaries | -| MicroVM storage | Host store shared read-only through virtiofs; guest `/etc`, `/var` and `/home` persisted in separate images | Read-only store does not isolate the host-side virtiofs implementation or protect writable guest state | -| Desktop apps | Selected apps use nixpak/bubblewrap; Podman is rootless | Rules and shared directories vary per app; user namespaces are needed for these sandboxes | -| Secrets | agenix declarations here, encrypted data in the private nix-secrets repository | Recipients and plaintext file permissions must be reviewed; root on a recipient can read its decrypted secrets | -| Supply chain | Locked flake inputs; explicit binary-cache trust keys on NixOS; normal user is not a Nix trusted-user | A trusted cache or imported Nix module can supply code executed as root; a lock file is not a security audit | -| Recovery | NixOS generations, btrbk/restic, hourly read-only snapshots of the REST store, and documented backup procedures | A rollback does not undo compromised data or secrets; backup configuration alone does not prove restore works; the offsite immutable copy is still planned | +| Boundary | Current design | Limitation | +| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Administration | Key-based SSH; root key login retained for Colmena/MicroVM deployment; only keys declared in Nix are honoured, and no password or keyboard-interactive login; remote-builder host keys only reach `nix-daemon` | Deployment credentials are root-equivalent file keys, usable by any same-user process through the desktop SSH agent; one stolen key reaches every host | +| Host network | Shared nftables firewall trusts the home LAN, tailnet interface and local Podman bridge; IPv6 link-local is accepted | These are broad trust zones, not service-level least privilege; router exposure and tailnet ACLs require separate verification | +| Monitoring | node/smartctl exporters bind the host LAN IPv4 (9100/9633); firewall rules restrict exporter ports 9100, 9835 and 9633 to the monitoring host over IPv4. RAPL counters are exposed only on x86 server hosts, to `node-exporter` through the `rapl` group. | nvidia_gpu_exporter (9835) still binds wildcard; the IPv6 denial for those three ports is in the shared firewall; current running hosts require separate verification. The node_exporter account can measure package energy for local side channels; interactive users, desktops, and guests keep the kernel-default root-only permission. | +| VM hosts | shoryu, shushou and youko run guests behind `br0`; ruby/kana are libvirt guests | A bridge is not network isolation; host compromise affects its guests | +| k3s | Three control-plane and three worker MicroVMs; Cilium, API VIP and NFS CSI | Cluster-admin, privileged/host-mounted pods and writable NFS data are powerful trust boundaries | +| MicroVM storage | Host store shared read-only through virtiofs; guest `/etc`, `/var` and `/home` persisted in separate images | Read-only store does not isolate the host-side virtiofs implementation or protect writable guest state | +| Desktop apps | Selected apps use nixpak/bubblewrap; Podman is rootless | Rules and shared directories vary per app; user namespaces are needed for these sandboxes | +| Security keys | idols-ai/shoukei use a CanoKey for touch-based sudo (pam_u2f, password fallback), one resident `ed25519-sk` SSH key for GitHub and all hosts, FIDO2 LUKS unlock on shoukei and on-card OpenPGP subkeys; key management happens only in the phone app ([setup](./security-keys.md)) | Touch-only sudo can be triggered by any same-user process; per-host file SSH keys and passphrases stay valid as fallback, so their weaknesses remain until retired | +| Secrets | agenix declarations here, encrypted data in the private nix-secrets repository | Recipients and plaintext file permissions must be reviewed; root on a recipient can read its decrypted secrets | +| Supply chain | Locked flake inputs; explicit binary-cache trust keys on NixOS; normal user is not a Nix trusted-user | A trusted cache or imported Nix module can supply code executed as root; a lock file is not a security audit | +| Recovery | NixOS generations, btrbk/restic, hourly read-only snapshots of the REST store, and documented backup procedures | A rollback does not undo compromised data or secrets; backup configuration alone does not prove restore works; the offsite immutable copy is still planned | Sources: [host layout](../hosts/README.md), [k3s deployment](../hosts/k8s/README.md), [firewall](../modules/nixos/base/networking/firewall.nix), [SSH](../modules/nixos/base/ssh.nix), @@ -332,13 +333,9 @@ isolation. Source review and socket metadata—not reads of private credentials Browser media and browserpass are enabled only for Firefox/Chromium/Chrome. Brave Origin and Edge are backup browsers with no media grants. browserpass exposes password-store/GPG data and agent operations. -- Firefox/Chromium/Chrome receive FIDO2 security-key passthrough for WebAuthn/passkeys. Host udev - rules hard-link only FIDO-tagged hidraw nodes into `/dev/fido-hidraw` and remove them on unplug; - the sandbox binds that directory plus read-only `/sys/class/hidraw` and the udev database. The - CanoKey KBD and CCID interfaces, other HID devices and raw USB stay outside the sandbox; Brave - Origin and Edge receive no FIDO2 passthrough. A compromised browser can use the key's FIDO - interface; the key's PIN and touch policy remain the authentication boundary. Hotplug works for - host-network Firefox/Chromium; pasta-isolated Chrome needs the key present at launch. +- Firefox/Chromium/Chrome receive only a security key's FIDO interface (mechanism and limits in the + [hardening README](../hardening/README.md#limits)). A compromised browser can use that interface; + the key's PIN and touch policy remain the authentication boundary. - Nixpak supplements Chromium's renderer sandbox, which remains enabled. Zoom/WeChat retain their FHS runtimes inside the outer boundary; inner mounts cannot recover hidden host resources. - Desktop access has limits: GPU drivers remain exposed, PulseAudio permits recording, read-only @@ -355,9 +352,8 @@ isolation. Source review and socket metadata—not reads of private credentials - Policy/build passes for all nine wrappers. Interactive validation passed for Firefox, Chrome, Chromium, Brave Origin, Edge, QQ, Telegram and Zoom; the Chromium smoke test also validates renderer sandboxing, environment clearing, bind scope and profile persistence. WeChat's - interactive Wayland check remains pending. FIDO2 browser passthrough passed WebAuthn with the - CanoKey in Firefox and Chrome; Chromium and plugging the key in after launch still need - validation. See the [runtime validation](../hardening/tests/runtime-validation.md). + interactive Wayland check remains pending. See the + [runtime validation](../hardening/tests/runtime-validation.md). #### Agent-first diagnostics by host role diff --git a/docs/security-keys.md b/docs/security-keys.md new file mode 100644 index 00000000..2b4820a0 --- /dev/null +++ b/docs/security-keys.md @@ -0,0 +1,235 @@ +# Hardware security keys + +How the CanoKey (or a YubiKey) is set up and used on the desktops: initial setup, adding a backup +key, and recovery. Commands here are the single source; the Nix files only point to this page. + +Scope: **idols-ai** and **shoukei** (`modules.desktop.canokey.enable`). frieren only shares the +GnuPG `scdaemon` setting; servers never use a key. + +## Principle + +Management and use happen on different devices. + +- **PCs only use the key:** WebAuthn/passkeys, sudo, SSH, LUKS unlock and OpenPGP (pass). +- **Management happens only in the phone app** (CanoKey Console on iOS/Android): every PIN change, + touch policies, application and factory reset, device settings. The CanoKey Admin PIN is never + typed on a PC; the OpenPGP Admin PIN is typed once, for `keytocard`. +- Sandboxed browsers see only the FIDO HID interface, never raw USB or CCID, so the WebUSB console + is unreachable from them ([hardening](../hardening/README.md)). Unsandboxed software is trusted; + if it is malicious, the PC is compromised regardless. + +| Use | Config | Daily use | Fallback without the key | +| ------------ | ------------------------------------------------------------------------------------------------------------------ | ---------------------------------------- | ----------------------------------------- | +| Browsers | [nixpak FIDO2 passthrough](../hardening/nixpaks/default.nix) | FIDO2 PIN and/or touch, as the site asks | site recovery codes, other registered key | +| sudo | [canokey.nix](../modules/nixos/desktop/canokey.nix), registrations in [vars](../vars/default.nix) `canokeyU2fKeys` | touch | password | +| SSH | [ssh.nix](../home/base/tui/ssh.nix), public keys in [vars](../vars/default.nix) `mainSshAuthorizedKeys` | touch per connection | per-host file keys | +| shoukei LUKS | [hardware-configuration.nix](../hosts/12kingdoms-shoukei/hardware-configuration.nix) `crypttabExtraOpts` | FIDO2 PIN + touch | passphrase (prompted after 10 s) | +| pass / GPG | [gpg](../home/base/tui/gpg/default.nix) `scdaemonSettings.disable-ccid` | OpenPGP PIN once per plug-in, then touch | off-card subkey backup | + +Login (greetd) stays password-based: the password also unlocks GNOME Keyring. + +## 1. Device setup + +### 1.1 Replace every default secret first + +Each application has its own secrets: separate values, separate retry counters (3 on released +firmware), and blocking one does not affect the others. The defaults are public (from the +[CanoKey user guide](https://docs.canokeys.org/userguide/)), so change all of them in the phone app +(CanoKey Console) **before using the key for anything**, including applications you do not use: with +a default PIN or management key, anyone holding the key can use or rewrite that application. + +| Application | Secret | Default | Protects | Set it to | Typed | +| ----------- | ----------------- | ---------------------- | ---------------------------------------------------------- | --------------------------------------------------------------------- | --------------- | +| Device | CanoKey Admin PIN | `123456` | settings, per-application and factory reset | random, 12+ characters (6–64); save offline | phone only | +| WebAuthn | FIDO2 PIN | unset | passkeys, LUKS unlock, SSH key creation and export | memorable, 8+ characters (4–63); memorize | daily, browsers | +| OpenPGP | PIN | `123456` | decrypt (pass), sign, authenticate | memorable, 8+ characters (6–64), not the FIDO2 PIN; memorize | daily, pinentry | +| OpenPGP | Admin PIN | `12345678` | key import (`keytocard`), touch policy, unblocking the PIN | random, 12+ characters (8–64); save offline | setup only | +| OpenPGP | Reset Code | unset | unblocking the PIN without the Admin PIN | leave unset (the Admin PIN also unblocks), or random and save offline | recovery only | +| PIV | PIN / PUK | `123456` / `12345678` | PIV keys / unblocking the PIV PIN | 8 random digits each (maximum 8); do not save | never | +| PIV | management key | `0102030405060708` × 3 | writing PIV keys and certificates | the app's random management key button; do not save | never | +| OATH | none: no password | — | anyone holding the key can read its TOTP/HOTP codes | — | — | + +Every PIN locks after 3 wrong attempts, so length matters less than not reusing a value. Generate +the random values with the phone's password manager; "save offline" means that password manager or +paper, never `pass`: `pass` itself is decrypted by this key. The FIDO2 PIN is typed into browsers, +the least trusted place, so it must not also unlock pass. + +PIV is unused, so its secrets are random and discarded: PIV is then locked rather than open with +public defaults. To use PIV later, reset just the PIV application in the phone app (needs only the +CanoKey Admin PIN), which restores the defaults. + +### 1.2 Firmware and settings (phone app) + +Touch policies are enforced **only over USB, not NFC**. OpenPGP and PIV stay reachable over NFC on +released firmware, but each NFC tap is a new card session, so their PINs are still required. + +1. Check the firmware version. U2F (sudo) does not work on 3.0.0; 3.0.2 and later are fine. The + newest public release is canokey-core [3.0.3](https://github.com/canokeys/canokey-core/tags). +2. Settings on released firmware (the user guide lists LED, NDEF and WebUSB login page): + + | Setting | Choice | Why | + | ----------------- | -------------------------------------- | ------------------------------------------------------------------------- | + | LED | on | an unexpected blink means something requested a touch | + | WebUSB login page | off | the console is not used on PCs | + | NDEF | off, or read-only if the app offers it | default is enabled; anyone nearby can read it, and rewrite it if writable | + +3. The user guide also documents options for **firmware 3.1.0**, which has no public release yet + (only the docs mention it; canokey-core's `dev` branch is unversioned). If the app shows them: + + | Option | Choice | Why | + | ------------------------------------------- | --------------------------------------------- | ------------------------------------------ | + | OpenPGP / PIV over NFC | off, unless the phone needs OpenPGP | touch is not enforced over NFC | + | Password/HOTP keyboard output, PIV switches | off unless used | smaller attack surface | + | WebAuthn `alwaysUv` | off | on breaks U2F, which sudo uses | + | WebAuthn minimum PIN length | 6–8 | the PIN locks after a few failures | + | Long-press reset | off, unless the app documents it as PIN-gated | keeps reset a phone-side management action | + +## 2. Enroll a key + +Only after section 1.1. Do these in order; each step can be rolled back on its own. Only the OpenPGP +step is irreversible. + +### Browsers (WebAuthn / passkeys) + +Nothing to enroll on the PC. Register the key on each site, preferably as a passkey. After a first +deploy via `switch` without reboot, see the FIDO2 notes in the +[hardening README](../hardening/README.md#limits). + +### sudo + +```bash +# pamu2fcfg is installed once canokey.nix is deployed; before that: +# Long options: some nix versions expand short flags such as -i after --command. +nix shell nixpkgs#pam_u2f --command \ + pamu2fcfg --nouser --origin=pam://ryan4yin # touch the key +``` + +Add the printed `KeyHandle,PublicKey,es256,+presence` string to `canokeyU2fKeys` in +[vars/default.nix](../vars/default.nix) (public data; a leading `:` is accepted) and deploy. sudo +then shows "Please touch the device"; without the key it asks for the password. While the list is +empty, sudo is unchanged. + +Every host shares the origin `pam://ryan4yin`, so one registration covers idols-ai and shoukei. + +### SSH + +One SSH key, used for GitHub and every host, lives on the key as a resident credential: + +```bash +ssh-keygen -t ed25519-sk -O resident -O application=ssh:ryan4yin -C ryan@canokey +``` + +This writes `~/.ssh/id_ed25519_sk` on the machine that runs it. On each other desktop, export the +key handle to ssh's default identity: + +```bash +cd ~/.ssh && ssh-keygen -K \ + && mv id_ed25519_sk_rk_ryan4yin id_ed25519_sk && mv id_ed25519_sk_rk_ryan4yin.pub id_ed25519_sk.pub +``` + +How it works, and what the prompts mean: + +- The private key never leaves the CanoKey. `~/.ssh/id_ed25519_sk` holds only the credential ID, + application and public key. Every authentication sends the server's challenge to the key, which + signs it after a touch; with the key unplugged, ssh skips this identity and tries the next one. +- **FIDO2 PIN:** asked only when creating the credential and when exporting it with `-K`, not for + each login. Adding `-O verify-required` at creation would require the PIN for every login too. +- **Touch:** required for every new connection, including each `git push`/`fetch`. Existing sessions + and reused connections need none: LAN hosts multiplex for 30 minutes (`ControlMaster` in + [ssh.nix](../home/base/tui/ssh.nix)). A deploy that opens one connection per host needs one touch + per host. +- **File passphrase:** leave it empty. Reading the file already takes your user session, and + software running as you is trusted (see [Principle](#principle)); it could also capture the + passphrase the next time you type it. A passphrase would cost one prompt per login, not per + connection: `AddKeysToAgent yes` keeps the unlocked handle in the agent, which still asks the key + for each signature. + +Add the public key to `mainSshAuthorizedKeys` in [vars/default.nix](../vars/default.nix) and to +GitHub. GitHub tries `id_ed25519_sk` before the per-host key; other hosts try it as a default +identity. Keys already loaded in `ssh-agent` are offered first, so the per-host keys can still win +until they are retired. + +### shoukei LUKS + +Enroll before deploying the crypttab change: + +```bash +systemd-cryptenroll --fido2-device=list # as the user: shows the key's hidraw node +sudo systemd-cryptenroll --fido2-device=/dev/hidrawN /dev/disk/by-uuid/1c37820e-2501-46e4-bec4-27c28691a5b4 +sudo systemd-cryptenroll /dev/disk/by-uuid/1c37820e-2501-46e4-bec4-27c28691a5b4 # list slots +``` + +Name the hidraw node instead of `--fido2-device=auto`: libfido2 is built with PC/SC support, so root +also finds the key's FIDO applet through pcscd (`pcsc://slot0`) and `auto` stops with "More than one +FIDO device found". Unlocking at boot is unaffected: pcscd does not run in the initrd. + +The default enrollment requires the FIDO2 PIN and a touch; keep it that way, since the key often +travels with the laptop. The passphrase slot stays. Reboot to verify. + +### pass (OpenPGP) + +The password store is encrypted to the `[E]` subkey `0xEF824EB73CFD6CC7`; the primary key is +offline. + +1. Verify the **off-card backup** of all three subkeys first: `keytocard` replaces the on-disk copy + with a stub. +2. Move the subkeys: `gpg --edit-key 0xCC88591C033F1924`, then `key 1` → `keytocard` (encryption), + and likewise for the signing and authentication subkeys; `save`. +3. Require touch for SIG, DEC and AUT in the phone app's OpenPGP page (or on the PC: + `gpg --card-edit` → `admin` → `uif 1 on`, `uif 2 on`, `uif 3 on`). Decrypt touch matters most: + browserpass gives browsers the GPG agent socket, and without touch a compromised browser could + decrypt the whole store silently after the first PIN. +4. On other machines that still hold on-disk copies, delete them + (`gpg --delete-secret-keys '!'`) and run `gpg --card-status` so gpg records + stubs for the card. + +`keytocard` is the only step that types the OpenPGP Admin PIN on a PC; change it again in the phone +app afterwards. + +## 3. Add a backup key + +A second CanoKey or a YubiKey works the same way; the module name is CanoKey-specific but nothing in +it is. On a YubiKey, manage settings with Yubico Authenticator or `ykman`; OpenPGP touch is +`ykman openpgp keys set-touch dec on` (and `sig`, `aut`). + +| Use | Backup key step | +| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Device | Section 1 again | +| Browsers | Register it on every site as an additional security key | +| sudo | `pamu2fcfg --nouser --origin=pam://ryan4yin` with only the backup plugged in; append to `canokeyU2fKeys` | +| SSH | Create a resident key on it with the same command; add its public key to `mainSshAuthorizedKeys` and GitHub. Use it with `ssh-add -K` (loads resident keys into the agent) instead of exporting a file | +| shoukei LUKS | Run the same `systemd-cryptenroll --fido2-device=auto` with only the backup plugged in; it takes the next slot | +| OpenPGP | Import the subkeys from the backup into a fresh, temporary `GNUPGHOME`, `keytocard` them onto the backup key, then delete that `GNUPGHOME`. The stubs remember the card serial; after swapping cards run `gpg-connect-agent "scd serialno" "learn --force" /bye` | + +## 4. Lost key or reset + +| Situation | Action | Lost | +| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | +| Key lost | Remove its `canokeyU2fKeys` entry and SSH public key; `systemd-cryptenroll --wipe-slot=`; remove it on each site | nothing, if a backup key or the passwords remain | +| FIDO2 PIN forgotten or locked | Phone app: reset the WebAuthn application (needs the Admin PIN) | all passkeys, the SSH resident key, LUKS and sudo enrollments; redo section 2 | +| OpenPGP PIN locked | `gpg --card-edit` → `admin` → `passwd`, unblock with the OpenPGP Admin PIN or Reset Code | nothing | +| All OpenPGP PINs and Reset Code lost | Phone app: reset the OpenPGP application | on-card subkeys; re-import from the backup | +| Admin PIN forgotten | Exhaust the Admin PIN, then in the app tap Reset and touch the key while the LED blinks, repeating until it stops | all user data; serial and attestation keys stay | + +After any reset, rerun sections 1 and 2 for the affected applications. + +## Enrollment status + +| Key | Firmware | sudo | SSH | shoukei LUKS | OpenPGP | Browsers | +| ------------------------ | -------- | ----------------- | ---------------------------------------------------------- | ----------------------------- | -------- | -------------------------------- | +| CanoKey Canary (primary) | 3.0.3 | works on idols-ai | key created; public key not yet in `mainSshAuthorizedKeys` | enrolled; unlock not verified | deferred | WebAuthn works in Firefox/Chrome | + +shoukei's first boots with the FIDO2 slot asked for the PIN, then fell back to the passphrase: USB +only came up after switch-root, because on linux-asahi 7.2 its dwc3/PHY depend on the Thunderbolt +(ACIO) nodes, whose `thunderbolt_apple` and `reset_apple_cio` drivers were missing from the initrd. +nixos-apple-silicon added both upstream (`275e570ea8`), and the input now includes it; unlocking +with the key is not yet verified. Also not verified: the CanoKey Admin PIN retry count, and how +touch is performed when resetting over NFC. + +## References + +- [CanoKey user guide](https://docs.canokeys.org/userguide/): settings, WebAuthn, OpenPGP, PIV +- [NixOS wiki: YubiKey](https://wiki.nixos.org/wiki/Yubikey): pam_u2f, `disable-ccid` +- [pam-u2f](https://developers.yubico.com/pam-u2f/), `systemd-cryptenroll(1)`, `crypttab(5)` +- Comparable NixOS configs: cloud-gouv/securix (`modules/pam/u2f.nix`), kclejeune/system, + joshsymonds/nix-config, wimpysworld/nix-config diff --git a/flake.lock b/flake.lock index 18e362a4..b1c732b3 100644 --- a/flake.lock +++ b/flake.lock @@ -598,11 +598,11 @@ ] }, "locked": { - "lastModified": 1792772387, - "narHash": "sha256-MK6/UsOv1Ii71c+2U28GpmKn+Fm4MZG2TmIscuvUERw=", + "lastModified": 1793626980, + "narHash": "sha256-U98lLEsMqZUE9cp0gQTVlZdWSUBi+59OmwS7aB5txdo=", "owner": "nix-community", "repo": "nixos-apple-silicon", - "rev": "46e80bc641f5aaee6ed7ef90a12314a81492f48f", + "rev": "481cf53add16849ecd02c4419a1794ce70bfc0eb", "type": "github" }, "original": { diff --git a/hardening/README.md b/hardening/README.md index d2a9164e..ba1f480c 100644 --- a/hardening/README.md +++ b/hardening/README.md @@ -18,12 +18,10 @@ workflow. browsers share an app-specific `/tmp` so external URLs reuse one browser instance. - Read-only `/nix/store`; GPU and display; fonts and themes; LAN and Internet. - Portals for file chooser, URI opening, notifications, input methods and document exports. -- No whole host home, system D-Bus, SSH agent, raw USB/serial or raw camera. For - Firefox/Chromium/Chrome, the only device passthrough is the FIDO HID interface of a security key: - host udev rules hard-link FIDO-tagged hidraw nodes into `/dev/fido-hidraw`, which the sandbox - binds together with read-only `/sys/class/hidraw` and the udev database, so keys can be plugged in - after launch. Other HID interfaces, CCID and raw USB stay hidden. Raw PipeWire is granted only to - MuseScore for its ALSA-via-PipeWire audio path; see [Limits](#limits). +- No whole host home, system D-Bus, SSH agent, raw USB/serial or raw camera. The only device + passthrough is a security key's FIDO interface for Firefox/Chromium/Chrome; see [Limits](#limits). + Raw PipeWire is granted only to MuseScore for its ALSA-via-PipeWire audio path; see + [Limits](#limits). - Host loopback only for Firefox/Chromium; all other apps use pasta-isolated loopback. Chromium is an intentional development exception. Brave Origin and Edge are backup browsers and receive no media grants. @@ -77,16 +75,15 @@ All browsers get read-only NSS certificates, printing, native notifications and - PulseAudio grants recording authority. Read-only GPG sockets still permit signing and decryption. - pasta isolates loopback, not LAN access. Chromecast discovery is unverified. - Brave Origin and Edge intentionally have no PulseAudio, Camera, ScreenCast or MPRIS grants. They - are backup browsers, not media browsers. They also receive no FIDO2 passthrough. -- FIDO2 passthrough is limited to the hidraw interface whose report descriptor declares the FIDO - usage page. On the CanoKey that is the FIDO interface only: its KBD and CCID interfaces, other HID - devices and raw USB stay outside the sandbox. A compromised browser process can send raw CTAP to - the key; the key's PIN and touch policy remain the authentication boundary. The read-only udev + are backup browsers, not media browsers. +- FIDO2 passthrough: host udev rules hard-link only hidraw nodes whose report descriptor declares + the FIDO usage page into `/dev/fido-hidraw`; the sandbox binds that directory plus read-only + `/sys/class/hidraw` and the udev database. The CanoKey's KBD and CCID interfaces, other HID + devices and raw USB stay outside, so the WebUSB console is unreachable by design + ([key policy](../docs/security-keys.md)). A compromised browser process can send raw CTAP to the + key; the key's PIN and touch policy remain the authentication boundary. The read-only udev database exposes properties of all host devices, much of which is already readable via the sysfs device binds. -- CanoKey management (CanoKey Console over WebUSB) is intentionally unavailable: Firefox has no - WebUSB and the sandbox exposes no raw USB. PCs only use the key; management happens in the phone - app. See [canokey.nix](../modules/nixos/desktop/canokey.nix) for the policy and its limits. - FIDO2 hotplug relies on udev events, which only reach the host network namespace. Firefox and Chromium therefore detect keys plugged in after launch; pasta-isolated Chrome needs the key present before launch. `/dev/fido-hidraw` is created at boot. When first deploying with `switch` diff --git a/hardening/nixpaks/default.nix b/hardening/nixpaks/default.nix index 6b8a5539..0fd95a83 100644 --- a/hardening/nixpaks/default.nix +++ b/hardening/nixpaks/default.nix @@ -19,8 +19,7 @@ in # FIDO2 hotplug for sandboxed browsers (see modules/fido-bwrap.nix). The # directory is created at boot, before udevd starts, so the browser bind # succeeds even with no key present. NixOS applies /dev tmpfiles entries only - # at boot; after a switch without reboot, run - # `sudo systemd-tmpfiles --create --prefix=/dev/fido-hidraw` once. + # at boot; for a first deploy via switch, see hardening/README.md (Limits). # Hard links share the device inode, so logind's uaccess ACLs stay # authoritative, and removal on unplug leaves no stale node for a reused # hidraw minor. 60-fido-id.rules sets ID_FIDO_TOKEN from the report diff --git a/home/base/tui/gpg/README.md b/home/base/tui/gpg/README.md index 0081f23c..dca8ccdd 100644 --- a/home/base/tui/gpg/README.md +++ b/home/base/tui/gpg/README.md @@ -32,8 +32,8 @@ Currently I use both age & GnuPG: 1. Age for secrets encryption(ssh key & other secret files), it's simple and easy to use. 2. GnuPG for password-store and email encryption. -> At present, the safe and efficient use of GPG is probably combined with hardware keys such as -> yubikey. but I don't have one, so I won't talk about it here. +> Moving the subkeys onto a hardware key (CanoKey/YubiKey) is covered in +> [hardware security keys](../../../../docs/security-keys.md#pass-openpgp). ## Practical Cryptography for Developers diff --git a/home/base/tui/gpg/default.nix b/home/base/tui/gpg/default.nix index 3e2a2f25..1fed9d59 100644 --- a/home/base/tui/gpg/default.nix +++ b/home/base/tui/gpg/default.nix @@ -78,5 +78,8 @@ # Specify how many times the passphrases mangling for symmetric encryption is repeated s2k-count = "65011712"; }; + # Reach the CanoKey through pcscd instead of scdaemon's own CCID driver, + # which would grab the USB interface exclusively (CanoKey OpenPGP FAQ). + scdaemonSettings.disable-ccid = true; }; } diff --git a/home/base/tui/ssh.nix b/home/base/tui/ssh.nix index bc07981c..c3a1dcb2 100644 --- a/home/base/tui/ssh.nix +++ b/home/base/tui/ssh.nix @@ -26,6 +26,10 @@ in { home.file.".ssh/romantic.pub".source = "${mysecrets}/public/romantic.pub"; + # One SSH key for GitHub and every host lives on the CanoKey as a resident + # FIDO2 credential, exported on each desktop to ~/.ssh/id_ed25519_sk (ssh's + # default identity); every use needs a touch. The per-host keys stay + # authorized as the backup. Setup: docs/security-keys.md. programs.ssh = { enable = true; diff --git a/home/hosts/linux/12kingdoms-shoukei.nix b/home/hosts/linux/12kingdoms-shoukei.nix index d5260b6a..ef5ab73b 100644 --- a/home/hosts/linux/12kingdoms-shoukei.nix +++ b/home/hosts/linux/12kingdoms-shoukei.nix @@ -11,7 +11,11 @@ in modules.desktop.vinput.enable = true; - programs.ssh.settings."github.com".IdentityFile = "${config.home.homeDirectory}/.ssh/${hostName}"; + # CanoKey key first (docs/security-keys.md), this host's key as backup. + programs.ssh.settings."github.com".IdentityFile = [ + "${config.home.homeDirectory}/.ssh/id_ed25519_sk" + "${config.home.homeDirectory}/.ssh/${hostName}" + ]; modules.desktop.gaming.enable = false; modules.desktop.niri.enable = true; diff --git a/home/hosts/linux/idols-ai.nix b/home/hosts/linux/idols-ai.nix index fae0ba86..0adb3d65 100644 --- a/home/hosts/linux/idols-ai.nix +++ b/home/hosts/linux/idols-ai.nix @@ -12,7 +12,11 @@ in modules.desktop.vinput.enable = true; - programs.ssh.settings."github.com".IdentityFile = "${config.home.homeDirectory}/.ssh/idols-ai"; + # CanoKey key first (docs/security-keys.md), this host's key as backup. + programs.ssh.settings."github.com".IdentityFile = [ + "${config.home.homeDirectory}/.ssh/id_ed25519_sk" + "${config.home.homeDirectory}/.ssh/idols-ai" + ]; modules.desktop.gaming.enable = true; modules.desktop.niri.enable = true; diff --git a/hosts/12kingdoms-shoukei/hardware-configuration.nix b/hosts/12kingdoms-shoukei/hardware-configuration.nix index 3562f543..001c415e 100644 --- a/hosts/12kingdoms-shoukei/hardware-configuration.nix +++ b/hosts/12kingdoms-shoukei/hardware-configuration.nix @@ -53,6 +53,13 @@ in # Enabling this should improve performance on SSDs; # https://wiki.archlinux.org/index.php/Dm-crypt/Specialties#Disable_workqueue_for_increased_solid_state_drive_(SSD)_performance bypassWorkqueues = true; + # Unlock with the CanoKey (FIDO2 PIN + touch); the passphrase slot stays. + # Without the key, the passphrase prompt appears after token-timeout. + # Enroll before deploying: docs/security-keys.md. + crypttabExtraOpts = [ + "fido2-device=auto" + "token-timeout=10s" + ]; }; }; diff --git a/modules/nixos/desktop/canokey.nix b/modules/nixos/desktop/canokey.nix index 2c8c3117..b956730f 100644 --- a/modules/nixos/desktop/canokey.nix +++ b/modules/nixos/desktop/canokey.nix @@ -2,6 +2,7 @@ pkgs, config, lib, + myvars, ... }: let @@ -12,26 +13,31 @@ in enable = lib.mkEnableOption "CanoKey hardware security key support"; }; - # CanoKey docs: https://docs.canokeys.org/userguide/ - # - # Principle: management and use happen on different devices. Linux PCs only - # use the key (WebAuthn/passkeys, OpenPGP, SSH). CanoKey Console management - # (Admin PIN, application/factory reset, device settings) happens only in the - # phone app (https://docs.canokeys.org/userguide/admin/), so the Admin PIN is - # never typed on a PC. - # - # Sandboxed browsers back this up: they see only the FIDO HID interface - # (hardening/nixpaks), not raw USB or CCID, so the WebUSB console cannot reach - # the key, and the vendor's WebUSB udev rule (MODE 0666 on the raw USB node) - # is deliberately not installed. Unsandboxed software is trusted; if it is - # malicious, the PC is compromised regardless. + # Usage policy, setup, backup keys and recovery: docs/security-keys.md. + # PCs only use the key and the phone app manages it, so the vendor's WebUSB + # udev rule (MODE 0666 on the raw USB node) is deliberately not installed. config = lib.mkIf cfg.enable { # systemd tags FIDO/CCID devices uaccess (60-fido-id.rules); pcscd covers OpenPGP. services.pcscd.enable = true; + # sudo accepts a key touch instead of the password; without the key it asks + # for the password as before. Login keeps the password, which also unlocks + # GNOME Keyring. sudo is unchanged until myvars.canokeyU2fKeys has an entry + # (registration: docs/security-keys.md). + security.pam.u2f.settings = { + authfile = pkgs.writeText "u2f-keys" '' + ${myvars.username}:${lib.concatMapStringsSep ":" (lib.removePrefix ":") myvars.canokeyU2fKeys} + ''; + # Shared by all hosts, so one registration works on idols-ai and shoukei. + origin = "pam://ryan4yin"; + cue = true; + }; + security.pam.services.sudo.u2f.enable = myvars.canokeyU2fKeys != [ ]; + environment.systemPackages = with pkgs; [ ccid pcsc-tools + pam_u2f # pamu2fcfg ]; }; } diff --git a/vars/default.nix b/vars/default.nix index 91542e0b..1d28ef01 100644 --- a/vars/default.nix +++ b/vars/default.nix @@ -29,6 +29,12 @@ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJwoI5MAogEa726jwwHL5EgM1X/i2A5d2pgV7i7t8fzB ryan@shoukei" "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDc1PNTXzzvd93E+e9LXvnEzqgUI5gMTEF/IitvzgmL+ ryan@frieren" ]; + # CanoKey U2F registrations for sudo (modules/nixos/desktop/canokey.nix). + # Key handles and public keys only, so they are safe to commit. + canokeyU2fKeys = [ + # CanoKey Canary (primary) + "DwXDxVU+NDTUZs/d2EjpsPAh4mrg7dqdg/mAozkw/mUAAdEYDZIbm8kTBUGA49wXW0ZvSpF9VNYh2w84Xz9nAIAW////+Q==,6hqEjWBJ+ueBKBJaNijNkUsmlgEHMgv0KdpprqdJjHKoU/FKpeqo12SWlvdgiIVWvqTaGfe4fNu/ckcGY5mlsA==,es256,+presence" + ]; secondaryAuthorizedKeys = [ # the backup ssh keys for disaster recovery "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMzYT0Fpcp681eHY5FJV2G8Mve53iX3hMOLGbVvfL+TF ryan@romantic"