From a7381a83bfa836b0c7b1171b5c9b97868610db89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Geyslan=20Greg=C3=B3rio?= Date: Sun, 21 Jun 2026 11:42:10 -0300 Subject: [PATCH 01/16] refactor(netmode)!: rename si-netmode to safe-netmode Unify the live image's network commands under one prefix. The core si-netmode command becomes safe-netmode, alongside the existing safe-network-on/off wrappers and the safe-status dashboard. Renames the binary, the boot service (safe-netmode-default-offline), the polkit policy and action (org.local.safe-netmode), the desktop launcher items, and the /run state dir, plus all references in the host test script and the docs. Also corrects the DESIGN.md keybinding diagram, which showed si-netmode where the Sway bindings actually call the safe-network-* wrappers. BREAKING CHANGE: the in-image command `si-netmode` is renamed to `safe-netmode`; the systemd unit `si-netmode-default-offline.service` and polkit action `org.local.si-netmode` are renamed to match. --- DESIGN.md | 17 ++++++++------- README.md | 8 +++---- image/docs/README.md | 10 ++++----- image/docs/gpg-yubikey.md | 2 +- image/docs/network-modes.md | 12 +++++------ image/docs/runtime-install.md | 2 +- image/scripts/README.md | 2 +- modules/netmode.nix | 40 +++++++++++++++++------------------ si-test.sh | 2 +- 9 files changed, 48 insertions(+), 47 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index ab3e764..d06a894 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -157,7 +157,7 @@ flake.nix ├── modules/builder.nix podman + make + git so the live image can │ rebuild safe-image (self-hosting) ├── modules/sway.nix Sway WM, keybindings, fonts, dark mode - ├── modules/netmode.nix si-netmode, nftables, polkit, systemd + ├── modules/netmode.nix safe-netmode, nftables, polkit, systemd ├── modules/yubikey-gpg.nix GPG agent, YubiKey udev rules, scdaemon, │ pcscd, safe-yubikey-fetch-pubkey command └── modules/docs.nix safe-docs (glow TUI + ghostwriter), in-image docs @@ -183,7 +183,7 @@ BIOS/UEFI │ └── re-apply udev rules to USB devices │ (ensures MODE="0666" on YubiKey in QEMU) │ - ├── si-netmode-default-offline.service (oneshot) + ├── safe-netmode-default-offline.service (oneshot) │ runs before network-pre.target and NetworkManager │ ├── nftables lockdown table active │ ├── rfkill block all @@ -211,7 +211,7 @@ which is before NetworkManager, DHCP, or any network service can start. ┌─────────────────────────┐ │ BOOT (offline) │ └────────────┬────────────┘ - │ si-netmode-default-offline.service + │ safe-netmode-default-offline.service ▼ ┌──────────────────────────┐ ┌──────▶│ OFFLINE |◀───────┐ @@ -221,7 +221,7 @@ which is before NetworkManager, DHCP, or any network service can start. │ │ - routes flushed │ │ │ │ - NetworkManager off │ │ │ └────────────┬─────────────┘ │ - │ │ si-netmode online │ + │ │ safe-netmode online │ │ │ (sudo / polkit) │ │ ▼ │ │ ┌──────────────────────────┐ │ @@ -231,12 +231,13 @@ which is before NetworkManager, DHCP, or any network service can start. │ │ - interfaces up │ │ │ │ - NetworkManager on │ │ │ └────────────┬─────────────┘ │ - │ │ si-netmode offline │ + │ │ safe-netmode offline │ └─────────────────────┘ (sudo / polkit) │ │ - Sway keybindings: │ - Mod+Shift+i → si-netmode online ───┤ - Mod+Shift+o → si-netmode offline ──┘ + Sway keybindings (wrappers add a desktop │ + notification, then call safe-netmode): │ + Mod+Shift+i → safe-network-on ────────┤ + Mod+Shift+o → safe-network-off ────────┘ ``` Mode transitions are atomic: the old nftables table is deleted before the new diff --git a/README.md b/README.md index c3e9867..6fa6871 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,7 @@ Replace `/dev/sdX` with your USB device. For full write-back verification see │ ├── base.nix # Base packages, user, autologin, serial console │ ├── builder.nix # Podman + make + git so the image can rebuild itself │ ├── sway.nix # Sway WM, keybindings, fonts, dark mode -│ ├── netmode.nix # si-netmode, nftables, polkit, systemd service +│ ├── netmode.nix # safe-netmode, nftables, polkit, systemd service │ ├── yubikey-gpg.nix # GPG, YubiKey udev rules, scdaemon PC/SC config │ └── docs.nix # safe-docs command, in-image documentation ├── image/ @@ -202,9 +202,9 @@ Toggle from Sway (Mod = Win/Super key): Or from the terminal: ```bash -sudo si-netmode offline -sudo si-netmode online -si-netmode status +sudo safe-netmode offline +sudo safe-netmode online +safe-netmode status ``` ## YubiKey & GPG guides diff --git a/image/docs/README.md b/image/docs/README.md index b5a007a..7e2e054 100644 --- a/image/docs/README.md +++ b/image/docs/README.md @@ -36,9 +36,9 @@ Mod = Win/Super key ## Network ```bash -sudo si-netmode offline # full lockdown -sudo si-netmode online # allow outbound -si-netmode status # links, routes, firewall state +sudo safe-netmode offline # full lockdown +sudo safe-netmode online # allow outbound +safe-netmode status # links, routes, firewall state safe-status # full system overview ``` @@ -67,7 +67,7 @@ trust dependency on the host's OS or container daemon. ```bash # 1. Boot the verified live ISO. # 2. Bring up the network and clone the source. -sudo si-netmode online +sudo safe-netmode online git clone && cd safe-image # 3. Build — same Makefile targets as on a normal host. make build-no-sign # or `make build` to sign with your YubiKey @@ -87,7 +87,7 @@ ykman openpgp info # OpenPGP applet status gpg --card-status # GPG card info # Fresh boot — pull your public key from the URL stored on the card. -# Needs network: run `sudo si-netmode online` first. +# Needs network: run `sudo safe-netmode online` first. safe-yubikey-fetch-pubkey ``` diff --git a/image/docs/gpg-yubikey.md b/image/docs/gpg-yubikey.md index 8fea82d..3eee2f9 100644 --- a/image/docs/gpg-yubikey.md +++ b/image/docs/gpg-yubikey.md @@ -31,7 +31,7 @@ The live image's home is tmpfs, so the GPG keyring starts empty every boot. Re-import the public key: ```bash -sudo si-netmode online # the fetch needs network +sudo safe-netmode online # the fetch needs network safe-yubikey-fetch-pubkey # one-shot wrapper around gpg card fetch ``` diff --git a/image/docs/network-modes.md b/image/docs/network-modes.md index b77544b..10237a0 100644 --- a/image/docs/network-modes.md +++ b/image/docs/network-modes.md @@ -1,11 +1,11 @@ # Network modes -This image has two explicit modes managed by `si-netmode`. +This image has two explicit modes managed by `safe-netmode`. ## Offline mode ```bash -si-netmode offline +safe-netmode offline ``` Expected behavior: @@ -19,7 +19,7 @@ Expected behavior: ## Online mode ```bash -si-netmode online +safe-netmode online ``` Expected behavior: @@ -36,7 +36,7 @@ Use online mode only when you intentionally need connectivity. ```bash safe-status -si-netmode status +safe-netmode status ``` ## Connecting to Wi-Fi @@ -45,7 +45,7 @@ The image ships NetworkManager with `wpa_supplicant` as its Wi-Fi backend (the default). Go online first; then use `nmcli` or `nmtui`. ```bash -si-netmode online # unblocks radios, starts NM +safe-netmode online # unblocks radios, starts NM nmcli device wifi list # scan nmcli device wifi connect password # join ``` @@ -59,7 +59,7 @@ nmtui A connection saved by `nmcli` lives in `/etc/NetworkManager/system-connections/` and is wiped at reboot — by design for a live image. Re-enter the credentials after each boot, or copy a `.nmconnection` file from a USB stick before -running `si-netmode online`. +running `safe-netmode online`. If `nmcli device wifi list` shows no devices: diff --git a/image/docs/runtime-install.md b/image/docs/runtime-install.md index 78f2b27..505e947 100644 --- a/image/docs/runtime-install.md +++ b/image/docs/runtime-install.md @@ -16,7 +16,7 @@ ISO ships with `nix-command` and `flakes` already enabled, and `nixpkgs` resolves through the default flake registry. ```bash -sudo si-netmode online +sudo safe-netmode online ``` ## Ephemeral — for "I need this once" diff --git a/image/scripts/README.md b/image/scripts/README.md index b062c54..5cd6be9 100644 --- a/image/scripts/README.md +++ b/image/scripts/README.md @@ -8,5 +8,5 @@ This directory is copied into the live image at: /etc/safe-live/scripts/ ``` -Core scripts such as `si-netmode`, `safe-status`, and `safe-docs` are +Core scripts such as `safe-netmode`, `safe-status`, and `safe-docs` are generated declaratively by Nix in `modules/`. diff --git a/modules/netmode.nix b/modules/netmode.nix index 44e2d03..880f5c7 100644 --- a/modules/netmode.nix +++ b/modules/netmode.nix @@ -38,8 +38,8 @@ let } ''; - siNetmode = pkgs.writeShellApplication { - name = "si-netmode"; + safeNetmode = pkgs.writeShellApplication { + name = "safe-netmode"; runtimeInputs = with pkgs; [ coreutils gawk @@ -55,7 +55,7 @@ let set -euo pipefail MODE="''${1:-}" - STATE_DIR="/run/si-netmode" + STATE_DIR="/run/safe-netmode" STATE_FILE="$STATE_DIR/state" list_ifaces() { @@ -84,7 +84,7 @@ NFT } go_offline() { - echo "[si-netmode] Going OFFLINE" + echo "[safe-netmode] Going OFFLINE" apply_offline_firewall @@ -109,7 +109,7 @@ NFT } go_online() { - echo "[si-netmode] Going ONLINE" + echo "[safe-netmode] Going ONLINE" apply_online_firewall @@ -149,7 +149,7 @@ NFT nft list ruleset || true ;; *) - echo "Usage: si-netmode {offline|online|status}" >&2 + echo "Usage: safe-netmode {offline|online|status}" >&2 exit 1 ;; esac @@ -175,7 +175,7 @@ NFT echo "== safe live status ==" echo "hostname: $(hostname)" - echo "netmode: $(cat /run/si-netmode/state 2>/dev/null || echo unknown)" + echo "netmode: $(cat /run/safe-netmode/state 2>/dev/null || echo unknown)" echo echo "== network services ==" @@ -222,9 +222,9 @@ NFT safeNetworkOff = pkgs.writeShellApplication { name = "safe-network-off"; - runtimeInputs = with pkgs; [ siNetmode libnotify ]; + runtimeInputs = with pkgs; [ safeNetmode libnotify ]; text = '' - if sudo -n /run/current-system/sw/bin/si-netmode offline; then + if sudo -n /run/current-system/sw/bin/safe-netmode offline; then ${_notify "Offline" "All interfaces disabled" "network-offline" "normal"} else ${_notify "Netmode failed" "Could not go offline" "dialog-error" "critical"} @@ -234,9 +234,9 @@ NFT safeNetworkOn = pkgs.writeShellApplication { name = "safe-network-on"; - runtimeInputs = with pkgs; [ siNetmode libnotify ]; + runtimeInputs = with pkgs; [ safeNetmode libnotify ]; text = '' - if sudo -n /run/current-system/sw/bin/si-netmode online; then + if sudo -n /run/current-system/sw/bin/safe-netmode online; then ${_notify "Online" "Network enabled" "network-idle" "normal"} else ${_notify "Netmode failed" "Could not go online" "dialog-error" "critical"} @@ -245,7 +245,7 @@ NFT }; offlineDesktop = pkgs.makeDesktopItem { - name = "si-netmode-offline"; + name = "safe-netmode-offline"; desktopName = "Go Offline (Disable Network)"; genericName = "Safe Live Offline Mode"; comment = "Disable all network interfaces and block traffic"; @@ -256,7 +256,7 @@ NFT }; onlineDesktop = pkgs.makeDesktopItem { - name = "si-netmode-online"; + name = "safe-netmode-online"; desktopName = "Go Online (Enable Network)"; genericName = "Safe Live Online Mode"; comment = "Re-enable network interfaces and allow outbound traffic"; @@ -267,17 +267,17 @@ NFT }; polkitPolicy = pkgs.stdenvNoCC.mkDerivation { - name = "si-netmode-polkit-policy"; + name = "safe-netmode-polkit-policy"; dontUnpack = true; installPhase = '' mkdir -p $out/share/polkit-1/actions - cat > $out/share/polkit-1/actions/org.local.si-netmode.policy <<'POLICY' + cat > $out/share/polkit-1/actions/org.local.safe-netmode.policy <<'POLICY' - + Toggle network mode Authentication is required to change network mode @@ -285,7 +285,7 @@ NFT auth_admin yes - /run/current-system/sw/bin/si-netmode + /run/current-system/sw/bin/safe-netmode true @@ -306,7 +306,7 @@ in networking.nftables.enable = true; networking.nftables.ruleset = lockdownRules; - systemd.services.si-netmode-default-offline = { + systemd.services.safe-netmode-default-offline = { description = "Set network mode to offline at boot"; wantedBy = [ "multi-user.target" ]; wants = [ "network-pre.target" ]; @@ -315,13 +315,13 @@ in serviceConfig = { Type = "oneshot"; - ExecStart = "${siNetmode}/bin/si-netmode offline"; + ExecStart = "${safeNetmode}/bin/safe-netmode offline"; RemainAfterExit = true; }; }; environment.systemPackages = [ - siNetmode + safeNetmode safeNetworkOff safeNetworkOn safeStatus diff --git a/si-test.sh b/si-test.sh index d6fc33f..f90fb33 100755 --- a/si-test.sh +++ b/si-test.sh @@ -124,7 +124,7 @@ echo "==> Waiting up to ${TIMEOUT}s for boot and offline confirmation..." deadline=$(( $(date +%s) + TIMEOUT )) while (( $(date +%s) < deadline )); do # The login prompt or shell prompt reaching ttyS0 means multi-user.target - # completed, which includes si-netmode-default-offline (boots offline). + # completed, which includes safe-netmode-default-offline (boots offline). if grep -q "safe-live login:\|nixos@safe-live" "$SERIAL_LOG" 2>/dev/null; then echo "PASS: safe-live booted and reached login on ttyS0." exit 0 From ff3671727458a078bae0cf0e0244898889136525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Geyslan=20Greg=C3=B3rio?= Date: Sun, 21 Jun 2026 11:42:42 -0300 Subject: [PATCH 02/16] feat(image): sync time over NTP and strengthen key-generation guide Enable systemd-timesyncd so the clock can synchronize over NTP when the image is online. Correct dates matter for GPG key creation and expiry timestamps. Offline-first is preserved: nftables drops outbound traffic in offline mode, so the clock only syncs during an explicit online window. The key-generation and expiry-extension guides now sync time in a brief online window before any key work, and the generate-keys guide adds a strong random certify passphrase step (gpg --gen-random --armor 1 36). --- image/docs/guides/01-generate-keys.md | 34 ++++++++++++++++++++++++--- image/docs/guides/04-extend-expiry.md | 12 ++++++++++ modules/base.nix | 6 +++++ 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/image/docs/guides/01-generate-keys.md b/image/docs/guides/01-generate-keys.md index 56cf6cf..70d4dcc 100644 --- a/image/docs/guides/01-generate-keys.md +++ b/image/docs/guides/01-generate-keys.md @@ -17,13 +17,28 @@ encryption, and authentication. ## Before you begin -> **STOP -- Verify you are offline.** +### Set the correct system time (briefly online, then offline) + +GPG records creation and expiry timestamps **inside** the key material, so the +system clock must be correct *before* you generate anything. The live image is +amnesic and may boot with a wrong clock. Sync it over NTP in a short online +window, then return offline to do all key work: + +```bash +sudo safe-netmode online # brief online window for NTP only +sudo timedatectl set-ntp true # enable NTP synchronization +timedatectl status # wait for: System clock synchronized: yes +date -u # sanity-check the date/time (UTC) +sudo safe-netmode offline # disable the network again +``` + +> **STOP -- Verify you are offline before continuing.** > > ```bash > ip link | grep 'state UP' > ``` > -> No interfaces should be UP. If any are, run `sudo si-netmode offline`. +> No interfaces should be UP. If any are, run `sudo safe-netmode offline`. ## 1. Create the master key (certify only) @@ -31,6 +46,18 @@ The master key only certifies subkeys -- it does not sign, encrypt, or authenticate. It will be backed up offline and never stored on the YubiKey. +First, generate a strong random passphrase for the master (certify) key. This +emits 36 bytes of randomness, base64-armored (~48 characters) -- far stronger +than anything you would invent: + +```bash +gpg --gen-random --armor 1 36 +``` + +Record the output in your offline backup (write it down / store it on the +encrypted backup USB). It is the only thing protecting your certify key, and +you will paste it when GPG prompts for a passphrase below. + ```bash gpg --expert --full-gen-key ``` @@ -44,7 +71,8 @@ When prompted: 3. Select **Curve 25519** (option 1) 4. Set expiry to **0 (does not expire)** -- subkeys will expire instead 5. Enter your real name and email -6. Set a strong passphrase (you will only need it when certifying) +6. Paste the random passphrase you generated above (you will only need it + when certifying) Note your key ID: diff --git a/image/docs/guides/04-extend-expiry.md b/image/docs/guides/04-extend-expiry.md index 1f173a5..77b3d41 100644 --- a/image/docs/guides/04-extend-expiry.md +++ b/image/docs/guides/04-extend-expiry.md @@ -17,6 +17,18 @@ so it must be done on the air-gapped safe-image. ## Before you begin +The new expiry you set is recorded against the current system clock, so make +sure the time is correct first. Sync it over NTP in a brief online window, +then return offline: + +```bash +sudo safe-netmode online # brief online window for NTP only +sudo timedatectl set-ntp true +timedatectl status # wait for: System clock synchronized: yes +date -u # sanity-check the date/time (UTC) +sudo safe-netmode offline # disable the network again +``` + > **STOP -- Verify you are offline.** > > ```bash diff --git a/modules/base.nix b/modules/base.nix index 3a1b111..043e3f6 100644 --- a/modules/base.nix +++ b/modules/base.nix @@ -31,6 +31,12 @@ time.timeZone = lib.mkDefault "UTC"; i18n.defaultLocale = "en_US.UTF-8"; + # NTP time sync. Offline-first: the clock only syncs while online (nftables + # drops outbound traffic in offline mode). Correct dates matter for GPG key + # creation and expiry timestamps — the in-image key guides sync time in a + # brief online window before generating or renewing keys. + services.timesyncd.enable = lib.mkDefault true; + nix.settings.experimental-features = [ "nix-command" "flakes" ]; environment.systemPackages = with pkgs; [ From 9798368010c27e695b1276fa13aa9e72357715be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Geyslan=20Greg=C3=B3rio?= Date: Sun, 21 Jun 2026 11:43:14 -0300 Subject: [PATCH 03/16] docs(gpg): switch keyserver to keyserver.ubuntu.com Replace keys.openpgp.org with keyserver.ubuntu.com across the isolated signing environment (lib/gpg-env.sh), the pubkey-fetch recovery hints (lib/yubikey-fetch-pubkey.sh), and the in-image key guides. This matches the keyserver the live image already ships in dirmngr.conf and returns the full key with its UIDs intact (no email-verification step), which is what signature verification needs. The daily-use guide's publish note is rewritten to describe keyserver.ubuntu.com's behavior and trade-offs. --- image/docs/guides/03-daily-use.md | 13 ++++++++----- image/docs/guides/04-extend-expiry.md | 2 +- image/docs/guides/05-revoke-keys.md | 4 ++-- image/docs/guides/06-recovery.md | 4 ++-- image/docs/guides/README.md | 2 +- lib/gpg-env.sh | 11 ++++++----- lib/yubikey-fetch-pubkey.sh | 2 +- 7 files changed, 21 insertions(+), 17 deletions(-) diff --git a/image/docs/guides/03-daily-use.md b/image/docs/guides/03-daily-use.md index 95bb794..f19cd21 100644 --- a/image/docs/guides/03-daily-use.md +++ b/image/docs/guides/03-daily-use.md @@ -31,7 +31,7 @@ gpg --import /run/media/$USER//KEYID-public.key **From a keyserver** (if you already published it): ```bash -gpg --keyserver hkps://keys.openpgp.org --recv-keys "${KEYID}" +gpg --keyserver hkps://keyserver.ubuntu.com --recv-keys "${KEYID}" ``` **From another machine** (copy the file over any way you like): @@ -76,11 +76,14 @@ redirect operations to the YubiKey. > etc.). ```bash -gpg --keyserver hkps://keys.openpgp.org --send-keys "${KEYID}" +gpg --keyserver hkps://keyserver.ubuntu.com --send-keys "${KEYID}" ``` -`keys.openpgp.org` will send a verification email to the UID address -on the key. Confirm it to make the key discoverable by email search. +`keyserver.ubuntu.com` accepts the upload directly and serves the key with +its UIDs intact -- no email verification step. Anyone can then fetch it by +fingerprint or by searching the UID. (Note the flip side: this keyserver does +not verify ownership and does not honor deletion requests, so only publish a +key you intend to be permanently public.) ### Configure gpg-agent @@ -232,7 +235,7 @@ gpg --verify document.pdf.asc document.pdf If you don't have the signer's key: ```bash -gpg --keyserver hkps://keys.openpgp.org --recv-keys +gpg --keyserver hkps://keyserver.ubuntu.com --recv-keys gpg --verify document.pdf.asc document.pdf ``` diff --git a/image/docs/guides/04-extend-expiry.md b/image/docs/guides/04-extend-expiry.md index 77b3d41..ef32c45 100644 --- a/image/docs/guides/04-extend-expiry.md +++ b/image/docs/guides/04-extend-expiry.md @@ -183,7 +183,7 @@ Publish to keyservers so anyone who verifies your signatures will see the new expiry dates: ```bash -gpg --keyserver hkps://keys.openpgp.org --send-keys "${KEYID}" +gpg --keyserver hkps://keyserver.ubuntu.com --send-keys "${KEYID}" ``` > **You must publish to keyservers every time you extend expiry.** diff --git a/image/docs/guides/05-revoke-keys.md b/image/docs/guides/05-revoke-keys.md index 326a2ae..a18229c 100644 --- a/image/docs/guides/05-revoke-keys.md +++ b/image/docs/guides/05-revoke-keys.md @@ -165,7 +165,7 @@ gpg --import /run/media/$USER//KEYID-public.key > the revoked subkeys. ```bash -gpg --keyserver hkps://keys.openpgp.org --send-keys "${KEYID}" +gpg --keyserver hkps://keyserver.ubuntu.com --send-keys "${KEYID}" ``` Anyone who refreshes your key will see the subkey is revoked. @@ -241,7 +241,7 @@ gpg --import /run/media/$USER//KEYID-public.key > others may still encrypt to your compromised key. ```bash -gpg --keyserver hkps://keys.openpgp.org --send-keys "${KEYID}" +gpg --keyserver hkps://keyserver.ubuntu.com --send-keys "${KEYID}" ``` After this, anyone who fetches your key will see it is revoked. diff --git a/image/docs/guides/06-recovery.md b/image/docs/guides/06-recovery.md index 9cb0f3d..25eef8f 100644 --- a/image/docs/guides/06-recovery.md +++ b/image/docs/guides/06-recovery.md @@ -178,7 +178,7 @@ gpg --card-status Publish the updated key so others reference the new card serial: ```bash -gpg --keyserver hkps://keys.openpgp.org --send-keys "${KEYID}" +gpg --keyserver hkps://keyserver.ubuntu.com --send-keys "${KEYID}" ``` ## Factory-reset a YubiKey @@ -238,7 +238,7 @@ Then, carry the updated public key to your daily machine: 11. **Publish to keyservers immediately:** ```bash -gpg --keyserver hkps://keys.openpgp.org --send-keys "${KEYID}" +gpg --keyserver hkps://keyserver.ubuntu.com --send-keys "${KEYID}" ``` 12. Update `~/.ssh/authorized_keys` on any remote hosts diff --git a/image/docs/guides/README.md b/image/docs/guides/README.md index 129106c..fc5e4ab 100644 --- a/image/docs/guides/README.md +++ b/image/docs/guides/README.md @@ -79,7 +79,7 @@ You **must** publish your public key to keyservers after: - Restoring to a new YubiKey ([06-recovery.md](06-recovery.md)) ```bash -gpg --keyserver hkps://keys.openpgp.org --send-keys "${KEYID}" +gpg --keyserver hkps://keyserver.ubuntu.com --send-keys "${KEYID}" ``` ## Recommended reading order diff --git a/lib/gpg-env.sh b/lib/gpg-env.sh index e7ff6d2..82c4ec9 100644 --- a/lib/gpg-env.sh +++ b/lib/gpg-env.sh @@ -31,10 +31,11 @@ _GPG_ENV_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=lib/yubikey-fetch-pubkey.sh source "$_GPG_ENV_DIR/yubikey-fetch-pubkey.sh" -# Keyserver used by the isolated temp home. keys.openpgp.org is reachable over -# 443 (survives networks that block hkp/11371) and serves key material fine for -# verification even when it strips unverified UIDs. -GPG_ENV_TEMP_KEYSERVER="${GPG_ENV_TEMP_KEYSERVER:-hkps://keys.openpgp.org}" +# Keyserver used by the isolated temp home. keyserver.ubuntu.com is reachable +# over hkps/443 (survives networks that block hkp/11371) and serves the full key +# with its UIDs intact, which is what verification needs. It also matches the +# keyserver the live image ships in dirmngr.conf (modules/yubikey-gpg.nix). +GPG_ENV_TEMP_KEYSERVER="${GPG_ENV_TEMP_KEYSERVER:-hkps://keyserver.ubuntu.com}" # Set by gpg_env_prepare; consumed by gpg_env_cleanup. GPG_ENV_TEMP_HOME="" @@ -149,7 +150,7 @@ To make the YubiKey usable for signing, on the host: 4. Get the PUBLIC key into the keyring (the card holds only the private half): ./lib/yubikey-fetch-pubkey.sh # card URL, then keyserver gpg --import /path/to/your-pubkey.asc # or from a file - gpg --keyserver hkps://keys.openpgp.org --recv-keys + gpg --keyserver hkps://keyserver.ubuntu.com --recv-keys Tip: set a durable URL once so fetch always works: gpg --card-edit -> admin -> url diff --git a/lib/yubikey-fetch-pubkey.sh b/lib/yubikey-fetch-pubkey.sh index 217a73b..01cf4dc 100755 --- a/lib/yubikey-fetch-pubkey.sh +++ b/lib/yubikey-fetch-pubkey.sh @@ -84,7 +84,7 @@ yubikey_fetch_pubkey() { echo " Recovery options:" >&2 echo " 1. Import from file: gpg --import /path/to/pubkey.asc" >&2 echo " 2. Set URL on card: gpg --card-edit > admin > url" >&2 - echo " 3. Try another server: gpg --keyserver hkps://keys.openpgp.org --recv-keys $key" >&2 + echo " 3. Try another server: gpg --keyserver hkps://keyserver.ubuntu.com --recv-keys $key" >&2 return 1 } From b9d15b6aa70afd8c91542384f273481e31f96120 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Geyslan=20Greg=C3=B3rio?= Date: Sun, 21 Jun 2026 11:43:39 -0300 Subject: [PATCH 04/16] docs(yubikey): document multi-card provisioning and on-card pubkey URL Add a guide section for provisioning the same subkeys onto more than one YubiKey: reset GnuPG and re-import the private keys from backup for each card (keytocard leaves only card stubs), with a warning never to import the revocation certificate. Add a section for storing the public-key URL on the card via `gpg --card-edit > admin > url`, noting that ykman cannot set this field (verified against ykman 5.9.1 and Yubico's official OpenPGP command reference). The README first-time-signing section gains a matching note and link. --- README.md | 10 ++- image/docs/guides/02-yubikey-setup.md | 91 +++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6fa6871..492acdf 100644 --- a/README.md +++ b/README.md @@ -274,20 +274,24 @@ URL-first is the right order: a URL you control (your own server, a GitHub raw URL, etc.) is more durable than a public keyserver that may hold stale data, go down, or strip identity packets. Set one with `gpg --card-edit > admin > url ` once and the -keyserver becomes a fallback rather than the primary path. +keyserver becomes a fallback rather than the primary path. Note that +`ykman` cannot set this field — `gpg --card-edit > admin > url` is the +only supported way. See +[guides/02-yubikey-setup.md](image/docs/guides/02-yubikey-setup.md) for +the full step. If your key was ever uploaded to the configured keyserver, fetch typically succeeds even without a URL on the card. If both attempts fail: ```bash gpg --import /path/to/your-pubkey.asc # from a file -gpg --keyserver hkps://keys.openpgp.org --recv-keys # try another server +gpg --keyserver hkps://keyserver.ubuntu.com --recv-keys # try another server ``` ### Verifying from another machine ```bash -gpg --keyserver hkps://keys.openpgp.org --recv-keys +gpg --keyserver hkps://keyserver.ubuntu.com --recv-keys gpg --verify out/safe-live-nixos-sway-*.iso.asc gpg --verify out/SHA256SUMS.asc ``` diff --git a/image/docs/guides/02-yubikey-setup.md b/image/docs/guides/02-yubikey-setup.md index 2f7b7c8..261f202 100644 --- a/image/docs/guides/02-yubikey-setup.md +++ b/image/docs/guides/02-yubikey-setup.md @@ -76,6 +76,44 @@ gpg/card> login gpg/card> quit ``` +## Set the public-key URL on the card (recommended) + +The card holds only your *private* keys. Tools that need the public half -- +this project's `make sign` / `make verify` and the in-image +`safe-yubikey-fetch-pubkey` -- run `gpg --card-edit > fetch`, which downloads +the public key from a **URL stored on the card**. Setting that URL once makes +the public key self-bootstrapping on any fresh machine, instead of relying on a +keyserver lookup. + +> Host your ASCII-armored public key somewhere durable and fetchable over HTTPS +> first -- your own web server, or a raw file URL such as +> `https://raw.githubusercontent.com///main/pubkey.asc`. + +```bash +gpg --card-edit +``` + +``` +gpg/card> admin +gpg/card> url + https://example.com/path/to/your-pubkey.asc +gpg/card> quit +``` + +Verify it was stored: + +```bash +gpg --card-status | grep -i 'URL of public key' +``` + +> **`ykman` cannot set this field.** The OpenPGP "URL of public key" data +> object is not exposed by any `ykman openpgp` command (verified against ykman +> 5.9.1 and Yubico's official OpenPGP command reference, +> ). +> `gpg --card-edit > admin > url` is the only supported way to set it. If you +> have nowhere to host the key, leave the URL unset and rely on the keyserver +> fallback (`keyserver.ubuntu.com`) or import the public key from a file. + ## 3. Move subkeys to YubiKey ```bash @@ -165,6 +203,59 @@ ssb> cv25519/ZZZZZZZZZZZZZZZZ ... [E] ssb> ed25519/WWWWWWWWWWWWWWWW ... [A] ``` +## Provision additional YubiKeys (optional -- same keys on several cards) + +To keep the **same** subkeys on more than one YubiKey (e.g. a daily card and a +spare in a safe), you cannot simply repeat section 3: `keytocard` *moved* the +subkey private material onto the first card and left only **stubs** (pointers to +that card's serial) in this keyring. A stub cannot be moved, so for **each** +additional YubiKey you must reset GnuPG and re-import the real private keys from +your backup. + +> **Have your encrypted backup USB ready, and insert the next YubiKey.** + +Repeat these steps for every additional card: + +1. **Reset the GnuPG environment** so the stubs pointing at the previous card + are cleared (otherwise `keytocard` has only stubs to work with): + + ```bash + gpgconf --kill all # release the previous card from gpg-agent/scdaemon + rm -rf ~/.gnupg # this live image wipes it on reboot anyway + ``` + +2. **Re-import the private keys from your backup**, mounting the encrypted + backup read-only as in [06-recovery.md](06-recovery.md): + + ```bash + sudo cryptsetup open /dev/sdX1 backup + sudo mount -o ro /dev/mapper/backup /mnt + + gpg --import /mnt/"${KEYID}-master.key" # master + subkeys; needed for keytocard + + sudo umount /mnt + sudo cryptsetup close backup + ``` + + > **Do NOT import `${KEYID}-revoke.asc`.** That file is the *revocation + > certificate*. Importing it marks the key as **revoked** in this keyring, + > and every later `keytocard`, export, or publish would then carry the + > revocation -- permanently disabling the key for everyone. Leave it + > untouched in your backup; it is only ever imported when you deliberately + > retire the key (see [05-revoke-keys.md](05-revoke-keys.md)). + +3. **Change the new card's PINs** (section 1) -- every fresh YubiKey ships with + the well-known default PINs. + +4. **Move the subkeys onto this card** exactly as in section 3 above + (`key 1` → `keytocard` → 1, `key 2` → `keytocard` → 2, + `key 3` → `keytocard` → 3, then `save`). + +5. **Verify** as in section 4 (`gpg --card-status` shows all three slots). + +Once every YubiKey is provisioned, continue with the public-key export and the +cleanup below -- they remove the re-imported private material from this session. + ## 5. Export your public key for daily use Save the public key so you can carry it to other machines: From 9c05df5ce192bb38aa8833c0be9cf44ff3f7c6ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Geyslan=20Greg=C3=B3rio?= Date: Sun, 21 Jun 2026 15:14:03 -0300 Subject: [PATCH 05/16] fix(gpg): pin keyserver to :443 to skip dirmngr SRV lookup Without an explicit port, dirmngr does an SRV lookup (_pgpkey-https._tcp.keyserver.ubuntu.com) before connecting. On networks whose resolver fails or times out SRV queries, dirmngr treats that as fatal and aborts every keyserver operation ("Server indicated a failure" / "Try again later"), even though a plain HTTPS fetch works fine. Append :443 to every hkps://keyserver.ubuntu.com reference (the isolated signing env, fetch-pubkey help, README, and the in-image key guides) so dirmngr skips SRV and connects directly. The in-image dirmngr.conf already pins :80 and is unaffected. --- README.md | 4 ++-- image/docs/guides/03-daily-use.md | 6 +++--- image/docs/guides/04-extend-expiry.md | 2 +- image/docs/guides/05-revoke-keys.md | 4 ++-- image/docs/guides/06-recovery.md | 4 ++-- image/docs/guides/README.md | 2 +- lib/gpg-env.sh | 12 ++++++++---- lib/yubikey-fetch-pubkey.sh | 2 +- 8 files changed, 20 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 492acdf..71a52b8 100644 --- a/README.md +++ b/README.md @@ -285,13 +285,13 @@ typically succeeds even without a URL on the card. If both attempts fail: ```bash gpg --import /path/to/your-pubkey.asc # from a file -gpg --keyserver hkps://keyserver.ubuntu.com --recv-keys # try another server +gpg --keyserver hkps://keyserver.ubuntu.com:443 --recv-keys # try another server ``` ### Verifying from another machine ```bash -gpg --keyserver hkps://keyserver.ubuntu.com --recv-keys +gpg --keyserver hkps://keyserver.ubuntu.com:443 --recv-keys gpg --verify out/safe-live-nixos-sway-*.iso.asc gpg --verify out/SHA256SUMS.asc ``` diff --git a/image/docs/guides/03-daily-use.md b/image/docs/guides/03-daily-use.md index f19cd21..90acc2b 100644 --- a/image/docs/guides/03-daily-use.md +++ b/image/docs/guides/03-daily-use.md @@ -31,7 +31,7 @@ gpg --import /run/media/$USER//KEYID-public.key **From a keyserver** (if you already published it): ```bash -gpg --keyserver hkps://keyserver.ubuntu.com --recv-keys "${KEYID}" +gpg --keyserver hkps://keyserver.ubuntu.com:443 --recv-keys "${KEYID}" ``` **From another machine** (copy the file over any way you like): @@ -76,7 +76,7 @@ redirect operations to the YubiKey. > etc.). ```bash -gpg --keyserver hkps://keyserver.ubuntu.com --send-keys "${KEYID}" +gpg --keyserver hkps://keyserver.ubuntu.com:443 --send-keys "${KEYID}" ``` `keyserver.ubuntu.com` accepts the upload directly and serves the key with @@ -235,7 +235,7 @@ gpg --verify document.pdf.asc document.pdf If you don't have the signer's key: ```bash -gpg --keyserver hkps://keyserver.ubuntu.com --recv-keys +gpg --keyserver hkps://keyserver.ubuntu.com:443 --recv-keys gpg --verify document.pdf.asc document.pdf ``` diff --git a/image/docs/guides/04-extend-expiry.md b/image/docs/guides/04-extend-expiry.md index ef32c45..547ad54 100644 --- a/image/docs/guides/04-extend-expiry.md +++ b/image/docs/guides/04-extend-expiry.md @@ -183,7 +183,7 @@ Publish to keyservers so anyone who verifies your signatures will see the new expiry dates: ```bash -gpg --keyserver hkps://keyserver.ubuntu.com --send-keys "${KEYID}" +gpg --keyserver hkps://keyserver.ubuntu.com:443 --send-keys "${KEYID}" ``` > **You must publish to keyservers every time you extend expiry.** diff --git a/image/docs/guides/05-revoke-keys.md b/image/docs/guides/05-revoke-keys.md index a18229c..fc262d8 100644 --- a/image/docs/guides/05-revoke-keys.md +++ b/image/docs/guides/05-revoke-keys.md @@ -165,7 +165,7 @@ gpg --import /run/media/$USER//KEYID-public.key > the revoked subkeys. ```bash -gpg --keyserver hkps://keyserver.ubuntu.com --send-keys "${KEYID}" +gpg --keyserver hkps://keyserver.ubuntu.com:443 --send-keys "${KEYID}" ``` Anyone who refreshes your key will see the subkey is revoked. @@ -241,7 +241,7 @@ gpg --import /run/media/$USER//KEYID-public.key > others may still encrypt to your compromised key. ```bash -gpg --keyserver hkps://keyserver.ubuntu.com --send-keys "${KEYID}" +gpg --keyserver hkps://keyserver.ubuntu.com:443 --send-keys "${KEYID}" ``` After this, anyone who fetches your key will see it is revoked. diff --git a/image/docs/guides/06-recovery.md b/image/docs/guides/06-recovery.md index 25eef8f..2d9bf51 100644 --- a/image/docs/guides/06-recovery.md +++ b/image/docs/guides/06-recovery.md @@ -178,7 +178,7 @@ gpg --card-status Publish the updated key so others reference the new card serial: ```bash -gpg --keyserver hkps://keyserver.ubuntu.com --send-keys "${KEYID}" +gpg --keyserver hkps://keyserver.ubuntu.com:443 --send-keys "${KEYID}" ``` ## Factory-reset a YubiKey @@ -238,7 +238,7 @@ Then, carry the updated public key to your daily machine: 11. **Publish to keyservers immediately:** ```bash -gpg --keyserver hkps://keyserver.ubuntu.com --send-keys "${KEYID}" +gpg --keyserver hkps://keyserver.ubuntu.com:443 --send-keys "${KEYID}" ``` 12. Update `~/.ssh/authorized_keys` on any remote hosts diff --git a/image/docs/guides/README.md b/image/docs/guides/README.md index fc5e4ab..74cdca2 100644 --- a/image/docs/guides/README.md +++ b/image/docs/guides/README.md @@ -79,7 +79,7 @@ You **must** publish your public key to keyservers after: - Restoring to a new YubiKey ([06-recovery.md](06-recovery.md)) ```bash -gpg --keyserver hkps://keyserver.ubuntu.com --send-keys "${KEYID}" +gpg --keyserver hkps://keyserver.ubuntu.com:443 --send-keys "${KEYID}" ``` ## Recommended reading order diff --git a/lib/gpg-env.sh b/lib/gpg-env.sh index 82c4ec9..4792bf5 100644 --- a/lib/gpg-env.sh +++ b/lib/gpg-env.sh @@ -33,9 +33,13 @@ source "$_GPG_ENV_DIR/yubikey-fetch-pubkey.sh" # Keyserver used by the isolated temp home. keyserver.ubuntu.com is reachable # over hkps/443 (survives networks that block hkp/11371) and serves the full key -# with its UIDs intact, which is what verification needs. It also matches the -# keyserver the live image ships in dirmngr.conf (modules/yubikey-gpg.nix). -GPG_ENV_TEMP_KEYSERVER="${GPG_ENV_TEMP_KEYSERVER:-hkps://keyserver.ubuntu.com}" +# with its UIDs intact, which is what verification needs. +# +# The explicit :443 is required, not cosmetic: without a port, dirmngr does an +# SRV lookup (_pgpkey-https._tcp.) first, and on networks whose resolver +# fails SRV queries that aborts every keyserver op ("Server indicated a +# failure"). An explicit port makes dirmngr skip SRV and connect directly. +GPG_ENV_TEMP_KEYSERVER="${GPG_ENV_TEMP_KEYSERVER:-hkps://keyserver.ubuntu.com:443}" # Set by gpg_env_prepare; consumed by gpg_env_cleanup. GPG_ENV_TEMP_HOME="" @@ -150,7 +154,7 @@ To make the YubiKey usable for signing, on the host: 4. Get the PUBLIC key into the keyring (the card holds only the private half): ./lib/yubikey-fetch-pubkey.sh # card URL, then keyserver gpg --import /path/to/your-pubkey.asc # or from a file - gpg --keyserver hkps://keyserver.ubuntu.com --recv-keys + gpg --keyserver hkps://keyserver.ubuntu.com:443 --recv-keys Tip: set a durable URL once so fetch always works: gpg --card-edit -> admin -> url diff --git a/lib/yubikey-fetch-pubkey.sh b/lib/yubikey-fetch-pubkey.sh index 01cf4dc..a39aada 100755 --- a/lib/yubikey-fetch-pubkey.sh +++ b/lib/yubikey-fetch-pubkey.sh @@ -84,7 +84,7 @@ yubikey_fetch_pubkey() { echo " Recovery options:" >&2 echo " 1. Import from file: gpg --import /path/to/pubkey.asc" >&2 echo " 2. Set URL on card: gpg --card-edit > admin > url" >&2 - echo " 3. Try another server: gpg --keyserver hkps://keyserver.ubuntu.com --recv-keys $key" >&2 + echo " 3. Try another server: gpg --keyserver hkps://keyserver.ubuntu.com:443 --recv-keys $key" >&2 return 1 } From 30947d0d1043f81aa9214a9c1c9abd03396d2903 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Geyslan=20Greg=C3=B3rio?= Date: Wed, 24 Jun 2026 19:57:37 -0300 Subject: [PATCH 06/16] docs: improve daily use --- image/docs/guides/03-daily-use.md | 46 +++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/image/docs/guides/03-daily-use.md b/image/docs/guides/03-daily-use.md index 90acc2b..4e05aab 100644 --- a/image/docs/guides/03-daily-use.md +++ b/image/docs/guides/03-daily-use.md @@ -206,6 +206,52 @@ ssh -T git@github.com The YubiKey will prompt for your User PIN on the first SSH connection in a session. +#### Restrict which keys gpg-agent offers over SSH (optional) + +By default `gpg-agent` offers *every* authentication-capable key it can +reach. If you keep extra on-disk keys (for example an old SSH key or a +second identity), you may want SSH to use only your YubiKey's `[A]` +subkey. + +First find the keygrip of the authentication subkey: + +```bash +gpg --list-secret-keys --with-keygrip "${KEYID}" +``` + +Each key/subkey is followed by a `Keygrip = ...` line. Copy the one +under the `[A]` (authenticate) subkey: + +``` +ssb ed25519/WWWWWWWWWWWWWWWW ... [A] + Keygrip = 1234ABCD....................0000 +``` + +List **only** that keygrip in `sshcontrol`: + +```bash +echo "1234ABCD....................0000" > ~/.gnupg/sshcontrol +gpgconf --kill gpg-agent +ssh-add -L # should now list just the one ssh-ed25519 key +``` + +> **Caveat -- plugged-in cards are always offered.** A YubiKey +> (or any active card/token) is presented to SSH *regardless* of +> `sshcontrol`, ordered by serial number. So `sshcontrol` restricts +> on-disk keys, not the card itself. If you carry more than one +> YubiKey, unplug the ones you don't want offered. +> +> **Note -- `sshcontrol` is deprecated.** Recent GnuPG (2.4+) prefers +> a per-key `Use-for-ssh` attribute over the `sshcontrol` file. Set it +> with: +> +> ```bash +> gpg-connect-agent 'KEYATTR Use-for-ssh: yes' /bye +> ``` +> +> Both mechanisms work; `sshcontrol` remains the simplest if you only +> have a single auth key. + ## Signing ### Sign a file From d2900d13d6b5fe69921c1f3c9b1bacd2e2ed1472 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Geyslan=20Greg=C3=B3rio?= Date: Wed, 24 Jun 2026 20:50:30 -0300 Subject: [PATCH 07/16] chore: mind KDF --- image/docs/gpg-yubikey.md | 14 ++++++ image/docs/guides/02-yubikey-setup.md | 69 +++++++++++++++++++++++++-- image/docs/guides/06-recovery.md | 3 +- 3 files changed, 81 insertions(+), 5 deletions(-) diff --git a/image/docs/gpg-yubikey.md b/image/docs/gpg-yubikey.md index 3eee2f9..833839f 100644 --- a/image/docs/gpg-yubikey.md +++ b/image/docs/gpg-yubikey.md @@ -25,6 +25,20 @@ ykman openpgp info gpg --card-status ``` +## KDF (PIN hashing) + +KDF (Key Derived Function) makes `gpg` hash the PIN on the host before +sending it to the card, so the PIN is never transmitted over PC/SC or stored +on the card in clear text. It is enabled per card with +`gpg --card-edit > admin > kdf-setup` and shows up in `gpg --card-status` as +`KDF setting ......: on`. + +It **must be turned on while the card is empty** — once subkeys are loaded +(`keytocard`) the setting is locked and changing it returns +`Conditions of use not satisfied`, recoverable only by a full +`ykman openpgp reset`. Provisioning enables it as the first step; see +[guides/02-yubikey-setup.md](guides/02-yubikey-setup.md#1-enable-kdf-then-change-the-yubikey-pins). + ## Bootstrap the keyring on a fresh boot The live image's home is tmpfs, so the GPG keyring starts empty every diff --git a/image/docs/guides/02-yubikey-setup.md b/image/docs/guides/02-yubikey-setup.md index 261f202..1d10f16 100644 --- a/image/docs/guides/02-yubikey-setup.md +++ b/image/docs/guides/02-yubikey-setup.md @@ -29,9 +29,38 @@ Move GPG subkeys onto a YubiKey and configure it for use. > your subkey private material will be on the YubiKey. If the card > breaks, you lose those keys permanently. -## 1. Change YubiKey PINs +## 1. Enable KDF, then change the YubiKey PINs -The default PINs are well-known and must be changed immediately. +Two things happen here, **in this order**, while the card is still empty: +turn on KDF, then change the default PINs. Both must be done before any +subkey is moved onto the card in section 3. + +### Enable KDF first (do this before anything else) + +**KDF (Key Derived Function)** makes GnuPG hash your PIN on the host +*before* sending it to the card, so the PIN never travels over the +USB/PC-SC link or gets stored on the card in clear text. Only the hash +is transmitted and stored. Without KDF, anyone able to observe the link +to the reader (or a malicious reader) sees your PIN in plain text. + +> **KDF can only be enabled on an empty card, and it must be turned on +> first.** +> +> The KDF setting can only be changed while the OpenPGP applet holds no +> keys. Once you have run `keytocard` (section 3), any attempt to change +> it fails with: +> +> ``` +> gpg: error for setup KDF: Conditions of use not satisfied +> ``` +> +> Recovering from that means factory-resetting the OpenPGP applet +> (`ykman openpgp reset`), which **wipes the keys and the PINs** and +> forces you to redo this whole guide. So enable KDF now, before the +> PIN change and before `keytocard`. + +Requires YubiKey firmware 5.2.3+ (OpenPGP 3.4) and GnuPG 2.2.1+ -- both +are satisfied by the prerequisites above. > **Insert your YubiKey now.** @@ -39,6 +68,36 @@ The default PINs are well-known and must be changed immediately. gpg --card-edit ``` +``` +gpg/card> admin +gpg/card> kdf-setup + (enter the Admin PIN -- default 12345678) +``` + +Confirm it took: + +```bash +gpg --card-status | grep -i 'KDF setting' +``` + +``` +KDF setting ......: on +``` + +> Enabling KDF re-hashes the PINs the card currently holds, so do it +> *before* you set your own PINs -- otherwise the change below has to be +> the first time the new hash format is written anyway. Setting KDF first +> and changing PINs second (next step) is the clean order. + +### Change the PINs + +The default PINs are well-known and must be changed immediately. Stay in +(or re-enter) `gpg --card-edit`: + +```bash +gpg --card-edit +``` + ``` gpg/card> admin gpg/card> passwd @@ -244,8 +303,10 @@ Repeat these steps for every additional card: > untouched in your backup; it is only ever imported when you deliberately > retire the key (see [05-revoke-keys.md](05-revoke-keys.md)). -3. **Change the new card's PINs** (section 1) -- every fresh YubiKey ships with - the well-known default PINs. +3. **Enable KDF and change the new card's PINs** (section 1) -- every fresh + YubiKey ships with KDF off and the well-known default PINs. KDF must be + enabled now, before step 4, because it cannot be changed once subkeys are on + the card. 4. **Move the subkeys onto this card** exactly as in section 3 above (`key 1` → `keytocard` → 1, `key 2` → `keytocard` → 2, diff --git a/image/docs/guides/06-recovery.md b/image/docs/guides/06-recovery.md index 2d9bf51..7ad2af8 100644 --- a/image/docs/guides/06-recovery.md +++ b/image/docs/guides/06-recovery.md @@ -111,7 +111,8 @@ gpg> quit Follow [02-yubikey-setup.md](02-yubikey-setup.md): -1. Change PINs (step 1) +1. Enable KDF and change PINs (step 1) -- KDF must be turned on while the + card is still empty, before `keytocard` 2. Move subkeys to card with `keytocard` (step 3) 3. Remove master key from the keyring (step 5) From 79450686845078691d90690ed97ff93d6bc061be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Geyslan=20Greg=C3=B3rio?= Date: Thu, 25 Jun 2026 16:01:55 -0300 Subject: [PATCH 08/16] docs: remove wrong/incomplete section --- image/docs/host-setup.md | 121 +-------------------------------------- 1 file changed, 1 insertion(+), 120 deletions(-) diff --git a/image/docs/host-setup.md b/image/docs/host-setup.md index 7dca6ae..ca44471 100644 --- a/image/docs/host-setup.md +++ b/image/docs/host-setup.md @@ -153,7 +153,7 @@ gpg-agent.conf including SSH support, cache TTL, and pinentry selection. ## 4. Shell profile The GPG environment and YubiKey touch notifications are set up together in -`~/.bashrc.d/gpg-yubikey.bash` (see section 8a-ii). If you prefer a single +`~/.bashrc.d/gpg-yubikey.bash`. If you prefer a single `~/.bashrc`, add the following directly to it instead: ```bash @@ -393,125 +393,6 @@ ls "$XDG_RUNTIME_DIR/yubikey-touch-detector.socket" From this point on, any time an operation is waiting for a touch (LED blinking), a desktop notification appears: **"YubiKey is waiting for touch"**. -### 8a-ii. CLI notifications (same terminal) - -Desktop notifications appear regardless of which terminal triggered the -operation. To also print a message in the triggering terminal, place the -following in `~/.bashrc.d/gpg-yubikey.bash` (sourced automatically if -`~/.bashrc` contains the standard `~/.bashrc.d/` loader). It requires `socat` -(`sudo dnf install socat`). - -The file should also contain the GPG environment setup so the whole YubiKey -configuration lives in one place: - -```bash -mkdir -p ~/.bashrc.d -cat > ~/.bashrc.d/gpg-yubikey.bash << 'EOF' -# GPG agent and YubiKey touch notifications - -# Restore gpg-agent + YubiKey to a working state for the current terminal. -# Called automatically at shell startup; safe to call manually any time things -# break — after killing the agent, a KVM switch, card removal, or pcscd hiccup. -gpg-restore() { - gpgconf --kill scdaemon 2>/dev/null - - if ! gpg-connect-agent updatestartuptty /bye >/dev/null 2>&1; then - gpgconf --kill gpg-agent 2>/dev/null - gpg-connect-agent updatestartuptty /bye >/dev/null 2>&1 - fi - - export SSH_AUTH_SOCK="$(gpgconf --list-dirs agent-ssh-socket)" - - # Update TTY record for pinentry-notify (gpg-agent has no controlling - # terminal so it cannot read $GPG_TTY directly). - printf '%s' "$GPG_TTY" > "${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/ytd-active-tty" - - if gpg --card-status >/dev/null 2>&1; then - echo "🟢 GPG + YubiKey: OK" - else - echo "❌ Card not accessible — if the above didn't help, try:" - echo " sudo systemctl restart pcscd.service && gpg-restore" - fi -} - -export GPG_TTY="$(tty)" -gpg-restore >/dev/null # silent on normal startup; errors still propagate - -# yubikey-touch-detector: notify current terminal when touch is needed -# -# GPG_1 is emitted only when the card is blocked on PKSIGN waiting for touch -# (the detector sends LEARN via assuan; LEARN blocks on a busy card and the -# 400 ms timer fires → GPG_1). The pinentry wrapper re-opens the key stubs -# after PIN entry so the detector re-runs at the right moment. By the time -# GPG_1 arrives here, pinentry has already exited — no pinentry handling needed. -# if tty -s && command -v socat >/dev/null 2>&1; then -# _ytd_socket="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/yubikey-touch-detector.socket" -# _ytd_warn=$(tput setaf 3 2>/dev/null) -# _ytd_ok=$(tput bold 2>/dev/null) -# _ytd_rst=$(tput sgr0 2>/dev/null) -# _ytd_cmds='gpg2?|gpgsm|gpgtar|ssh|scp|sftp|rsync|git|pass|age|sops' -# (socat -u UNIX-CONNECT:"$_ytd_socket",retry=15,interval=1 - 2>/dev/null | \ -# while IFS= read -r -d '' -n5 msg; do -# case "$msg" in -# *_1) -# _ytd_mine=0 -# _ytd_i=0 -# while [ "$_ytd_i" -lt 10 ]; do -# if ps -t "${GPG_TTY#/dev/}" -o comm= 2>/dev/null \ -# | grep -qE "^($_ytd_cmds)$"; then -# _ytd_mine=1 -# break -# fi -# sleep 0.1 -# _ytd_i=$((_ytd_i + 1)) -# done -# [ "$_ytd_mine" = 1 ] || continue -# printf '\r%s🛡️ Touch YubiKey now%s\n' \ -# "$_ytd_warn" "$_ytd_rst" >/dev/tty 2>/dev/null ;; -# *_0) -# if [ "${_ytd_mine:-0}" = 1 ]; then -# _ytd_mine=0 -# printf '\r%s🟢 YubiKey: done%s\n' \ -# "$_ytd_ok" "$_ytd_rst" >/dev/tty 2>/dev/null -# fi ;; -# esac -# done) & -# disown %% 2>/dev/null -# unset _ytd_socket _ytd_warn _ytd_ok _ytd_rst _ytd_cmds -# fi -EOF -``` - -If your `~/.bashrc` does not already source `~/.bashrc.d/`, add: - -```bash -if [ -d ~/.bashrc.d ]; then - for rc in ~/.bashrc.d/*; do - [ -f "$rc" ] && . "$rc" - done - unset rc -fi -``` - -Key points: -- `socat -u` — unidirectional (socket → stdout only); without `-u`, socat also reads - stdin and gets **SIGTTIN** in the background, silently stopping the process. -- `_ytd_cmds` — regex of all commands that route through gpg-agent. Extend as needed. -- `GPG_TTY` — set at shell startup via `export GPG_TTY="$(tty)"`. The ownership check - uses `ps -t "${GPG_TTY#/dev/}"` to confirm a triggering command is running on *this* - terminal. All other terminals skip the event silently. -- Ownership poll — waits up to 1 s for the triggering process to appear (there is a - brief window between when the key stub is opened and when the process is visible in - `ps`). -- No pinentry handling — `GPG_1` is emitted only when the card is actually blocking on - touch, never during PIN entry. The pinentry wrapper (section 8b) ensures a fresh - `GPG_1` fires after pinentry exits, so by the time this code runs, pinentry is gone. -- `_ytd_mine` — tracks whether this terminal showed a start notification so the done - message pairs correctly after the triggering process exits. -- `tput setaf`/`sgr0` — reads terminal capabilities at runtime; no hardcoded escapes. -- Socket messages are fixed 5 bytes: `GPG_1`/`GPG_0` (gpg), `U2F_1`/`U2F_0` - (FIDO2), `MAC_1`/`MAC_0` (HMAC-secret). - ### 8b. Pinentry wrapper (re-triggering touch detection after PIN entry) yubikey-touch-detector fires its `LEARN` probe 200 ms after a shadowed key From e963dfbd822e44cbeb4d03528e473b4738682373 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Geyslan=20Greg=C3=B3rio?= Date: Thu, 25 Jun 2026 22:37:56 -0300 Subject: [PATCH 09/16] style: align inline comments in code blocks across docs --- DESIGN.md | 48 +++++++++++++-------------- image/docs/README.md | 2 +- image/docs/guides/01-generate-keys.md | 6 ++-- image/docs/network-modes.md | 2 +- image/docs/runtime-install.md | 2 +- 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index d06a894..895317e 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -99,7 +99,7 @@ non-Fedora host. │ │ cp ISO → /out/ │ │ │ │ │ sha256sum → /out/SHA256SUMS │ │ │ │ └───────────────────────────────────────────────────┘ │ │ -│ └──────────────────────────────────────────────────┘ │ │ +│ └───────────────────────────────────────────────────────┘ │ │ │ │ ├─[signing]─ si-sign-outputs.sh --no-smoke │ │ │ └── gpg --detach-sign ISO + SHA256SUMS │ @@ -208,32 +208,32 @@ which is before NetworkManager, DHCP, or any network service can start. ## Network mode state machine ``` - ┌─────────────────────────┐ - │ BOOT (offline) │ - └────────────┬────────────┘ + ┌──────────────────────────┐ + │ BOOT (offline) │ + └────────────┬─────────────┘ │ safe-netmode-default-offline.service ▼ ┌──────────────────────────┐ - ┌──────▶│ OFFLINE |◀───────┐ - │ │ - nftables lockdown │ │ - │ │ - rfkill block all │ │ - │ │ - interfaces down │ │ - │ │ - routes flushed │ │ - │ │ - NetworkManager off │ │ - │ └────────────┬─────────────┘ │ - │ │ safe-netmode online │ - │ │ (sudo / polkit) │ - │ ▼ │ - │ ┌──────────────────────────┐ │ - │ │ ONLINE │ │ - │ │ - nftables: outbound ok │ │ - │ │ - rfkill unblock all │ │ - │ │ - interfaces up │ │ - │ │ - NetworkManager on │ │ - │ └────────────┬─────────────┘ │ - │ │ safe-netmode offline │ - └─────────────────────┘ (sudo / polkit) │ - │ + ┌───────▶│ OFFLINE │◀────────────┐ + │ │ - nftables lockdown │ │ + │ │ - rfkill block all │ │ + │ │ - interfaces down │ │ + │ │ - routes flushed │ │ + │ │ - NetworkManager off │ │ + │ └────────────┬─────────────┘ │ + │ │ safe-netmode online │ + │ │ (sudo / polkit) │ + │ ▼ │ + │ ┌──────────────────────────┐ │ + │ │ ONLINE │ │ + │ │ - nftables: outbound ok │ │ + │ │ - rfkill unblock all │ │ + │ │ - interfaces up │ │ + │ │ - NetworkManager on │ │ + │ └────────────┬─────────────┘ │ + │ │ safe-netmode offline │ + └─────────────────────┘ (sudo / polkit) │ + │ Sway keybindings (wrappers add a desktop │ notification, then call safe-netmode): │ Mod+Shift+i → safe-network-on ────────┤ diff --git a/image/docs/README.md b/image/docs/README.md index 7e2e054..b06a9cb 100644 --- a/image/docs/README.md +++ b/image/docs/README.md @@ -39,7 +39,7 @@ Mod = Win/Super key sudo safe-netmode offline # full lockdown sudo safe-netmode online # allow outbound safe-netmode status # links, routes, firewall state -safe-status # full system overview +safe-status # full system overview ``` Boots offline. Online mode allows outbound traffic and starts NetworkManager. diff --git a/image/docs/guides/01-generate-keys.md b/image/docs/guides/01-generate-keys.md index 70d4dcc..886179d 100644 --- a/image/docs/guides/01-generate-keys.md +++ b/image/docs/guides/01-generate-keys.md @@ -26,9 +26,9 @@ window, then return offline to do all key work: ```bash sudo safe-netmode online # brief online window for NTP only -sudo timedatectl set-ntp true # enable NTP synchronization -timedatectl status # wait for: System clock synchronized: yes -date -u # sanity-check the date/time (UTC) +sudo timedatectl set-ntp true # enable NTP synchronization +timedatectl status # wait for: System clock synchronized: yes +date -u # sanity-check the date/time (UTC) sudo safe-netmode offline # disable the network again ``` diff --git a/image/docs/network-modes.md b/image/docs/network-modes.md index 10237a0..0da1f39 100644 --- a/image/docs/network-modes.md +++ b/image/docs/network-modes.md @@ -46,7 +46,7 @@ The image ships NetworkManager with `wpa_supplicant` as its Wi-Fi backend ```bash safe-netmode online # unblocks radios, starts NM -nmcli device wifi list # scan +nmcli device wifi list # scan nmcli device wifi connect password # join ``` diff --git a/image/docs/runtime-install.md b/image/docs/runtime-install.md index 505e947..750c1f3 100644 --- a/image/docs/runtime-install.md +++ b/image/docs/runtime-install.md @@ -26,7 +26,7 @@ is registered in any profile; exit the shell and the package is unused (though its store paths linger until the next `nix-collect-garbage`). ```bash -nix shell nixpkgs#htop # interactive +nix shell nixpkgs#htop # interactive nix shell nixpkgs#imagemagick -c convert in.png out.jpg # one-shot ``` From 65a38235dff9dcc1893302f9334875dde927b23f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Geyslan=20Greg=C3=B3rio?= Date: Thu, 25 Jun 2026 22:38:01 -0300 Subject: [PATCH 10/16] docs: fix -L flag description in debugging guide -L is --print-build-logs, not keep-going + verbose. --- guides/debugging-build-failures.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/debugging-build-failures.md b/guides/debugging-build-failures.md index 86d5e49..c204d7e 100644 --- a/guides/debugging-build-failures.md +++ b/guides/debugging-build-failures.md @@ -45,7 +45,7 @@ nix build .#nixosConfigurations.safe-live.config.system.build.isoImage \ --out-link /tmp/result --no-warn-dirty -L ``` -`-L` (keep-going + verbose) prints each builder's output live, which is useful +`-L` (`--print-build-logs`) prints each builder's output live, which is useful when you want to watch the failure in real time rather than retrieve it after the fact. From e875657c1833795cc2a55eaee1c1b554c8f9c916 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Geyslan=20Greg=C3=B3rio?= Date: Thu, 25 Jun 2026 22:38:05 -0300 Subject: [PATCH 11/16] fix(gpg): update keyserver references to hkps:443 in docs and module Follows the earlier dirmngr fix; keeps prose consistent with the deployed configuration. --- README.md | 6 +++--- image/docs/gpg-yubikey.md | 4 ++-- modules/yubikey-gpg.nix | 5 ++++- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 71a52b8..83cb570 100644 --- a/README.md +++ b/README.md @@ -112,8 +112,8 @@ SPICE=1 make test-gui # use spice-app + remote-viewer (clipboard on Fedora out/ ├── safe-live-nixos-sway-