feat(canokey): sudo, SSH, LUKS and OpenPGP setup for daily use (#516)

Use the CanoKey for daily authentication on idols-ai and shoukei, with
the existing passwords, passphrases and per-host SSH keys as fallback.

- sudo: pam_u2f with a shared origin (pam://ryan4yin) and a Nix-store
  authfile built from myvars.canokeyU2fKeys; the primary key is
  registered and touch-to-sudo works on idols-ai.
- SSH: one resident ed25519-sk key (application=ssh:ryan4yin) exported
  to ~/.ssh/id_ed25519_sk; GitHub tries it before the per-host key.
- shoukei LUKS: fido2-device=auto with token-timeout=10s. Update
  nixos-apple-silicon (linux-asahi 7.2.14), whose initrd adds
  thunderbolt_apple and reset_apple_cio so USB-C comes up before unlock.
- GPG: scdaemon disable-ccid so it uses pcscd.
- docs/security-keys.md: setup, PINs, enrollment, backup key and
  recovery; deduplicate the FIDO notes left by #515.

Assisted-by: Claude Code (claude-opus-5-5)
This commit is contained in:
ryan4yin | 二花 authored and GitHub committed 2026-11-04 01:50:21 -06:00
1 parent 2cf3a58028
commit 71fa928254
14 files changed
+309 -47

No files matched your search

+1
View File
@@ -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.
+7 -11
View File
@@ -34,7 +34,7 @@ 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. |
@@ -42,6 +42,7 @@ small, reversible controls and narrow exceptions; benchmark before adopting expe
| 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 |
@@ -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
+235
View File
@@ -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 '<subkey-fingerprint>!'`) 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=<n>`; 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
Generated
+3 -3
View File
@@ -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": {
+11 -14
View File
@@ -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`
+1 -2
View File
@@ -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
+2 -2
View File
@@ -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
+3
View File
@@ -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;
};
}
+4
View File
@@ -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;
+5 -1
View File
@@ -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;
+5 -1
View File
@@ -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;
@@ -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"
];
};
};
+19 -13
View File
@@ -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
];
};
}
+6
View File
@@ -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"