chore: update phantom to niri
This commit is contained in:
parent
7957deff45
commit
30398e5d78
17 changed files with 447 additions and 148 deletions
|
|
@ -6,10 +6,8 @@
|
|||
# Inherit the full framework13 experience
|
||||
../framework13
|
||||
|
||||
# Override monitor config for unknown displays
|
||||
./hyprland.nix
|
||||
];
|
||||
|
||||
# Phantom-specific home settings can go here
|
||||
# Most config comes from framework13 profile
|
||||
# Keep the shared bindings/layout without Framework's monitor placement.
|
||||
desktop.niri.portable = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
# Phantom USB - Hyprland monitor configuration
|
||||
# Auto-detect displays since we don't know what machine we'll boot on
|
||||
{
|
||||
wayland.windowManager.hyprland.settings = {
|
||||
# Auto-configure any connected monitor
|
||||
# This overrides the framework13 specific monitor config
|
||||
monitor = [
|
||||
# Prefer high resolution, auto-position, with scaling
|
||||
",preferred,auto,1"
|
||||
];
|
||||
|
||||
# Generic touchpad settings (works on most laptops)
|
||||
device = {
|
||||
name = "generic-touchpad";
|
||||
natural_scroll = true;
|
||||
};
|
||||
|
||||
# Input configuration for unknown hardware
|
||||
input = {
|
||||
# Enable tap-to-click for any touchpad
|
||||
touchpad = {
|
||||
tap-to-click = true;
|
||||
natural_scroll = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -112,13 +112,14 @@
|
|||
};
|
||||
|
||||
# Phantom USB - ephemeral NixOS that leaves no trace
|
||||
# LUKS-encrypted, impermanent root, Tailscale auto-connect
|
||||
# LUKS-encrypted, impermanent root, WireGuard home overlay
|
||||
phantom = nixosSystem {
|
||||
inherit specialArgs;
|
||||
modules = phantom ++ [
|
||||
./phantom
|
||||
|
||||
"${mod}/programs/hyprland"
|
||||
"${mod}/programs/niri"
|
||||
inputs.stylix.nixosModules.stylix
|
||||
|
||||
# Declarative disk partitioning
|
||||
inputs.disko.nixosModules.disko
|
||||
|
|
@ -134,7 +135,9 @@
|
|||
|
||||
{
|
||||
home-manager = {
|
||||
users.bogdan.imports = homeImports."bogdan@phantom";
|
||||
users.bogdan.imports = homeImports."bogdan@phantom" ++ [
|
||||
../home/programs/wayland/noctalia-stylix.nix
|
||||
];
|
||||
extraSpecialArgs = specialArgs;
|
||||
backupFileExtension = ".hm-backup";
|
||||
sharedModules = [
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
# Phantom USB - Ephemeral NixOS
|
||||
|
||||
A portable, encrypted NixOS that boots from USB, connects to your infrastructure, and leaves no trace when removed.
|
||||
A portable, encrypted NixOS that boots from USB into Niri + Noctalia, with an
|
||||
ephemeral root and selective state on the encrypted USB. The host machine's
|
||||
disks and EFI variables are not used for installation.
|
||||
|
||||
## Architecture
|
||||
|
||||
|
|
@ -31,9 +33,10 @@ A portable, encrypted NixOS that boots from USB, connects to your infrastructure
|
|||
## Features
|
||||
|
||||
- **LUKS2 + YubiKey FIDO2**: One touch to unlock at boot
|
||||
- **Ephemeral root**: tmpfs `/` - everything in RAM, zero disk trace
|
||||
- **Ephemeral root**: tmpfs `/`; only explicitly persisted state survives reboot
|
||||
- **Impermanence**: Only `/persist` survives reboot
|
||||
- **Tailscale auto-connect**: Joins your tailnet immediately on boot
|
||||
- **Niri + Noctalia**: Framework bindings and applications, automatic display placement
|
||||
- **WireGuard auto-connect once provisioned**: Dedicated home-overlay peer, key on encrypted `/persist`
|
||||
- **Portable EFI**: Works on any UEFI machine without modifying host
|
||||
- **Memory scrub**: Clears caches on shutdown
|
||||
- **Kernel hardening**: lockdown=confidentiality, init_on_alloc, etc.
|
||||
|
|
@ -76,35 +79,90 @@ sudo nixos-install --flake .#phantom --root /mnt --no-root-password
|
|||
|
||||
**Note:** The installation writes to a temporary BTRFS root at `/mnt`. After the first boot, this will be overlayed with tmpfs (ephemeral root).
|
||||
|
||||
### 3. Embed the Tailscale Auth Key
|
||||
### 3. Provision the WireGuard Peer
|
||||
|
||||
The Tailscale key is stored in plaintext on `/persist` (protected by LUKS encryption):
|
||||
The currently pinned private flake has no Phantom WireGuard identity. Until it
|
||||
does, evaluation emits a warning and the overlay is disabled. Do not reuse the
|
||||
Framework or Steam Deck private key/address: each running peer needs its own.
|
||||
|
||||
In the private flake, add `vars.hosts.phantom.wireguard.overlayIP`, using a free
|
||||
address from the existing overlay. Register the corresponding public key and
|
||||
that address's `/32` on the home hub. Update the secrets input through the normal
|
||||
reviewed lockfile workflow before building the USB.
|
||||
|
||||
Install the dedicated private key from a root-readable file on the provisioning
|
||||
machine (replace the source path below). It remains plaintext inside encrypted
|
||||
`/persist`, never in the Nix store or this repository:
|
||||
|
||||
```bash
|
||||
# Create secrets directory on the encrypted persist partition
|
||||
sudo mkdir -p /mnt/persist/secrets
|
||||
|
||||
# Decrypt the Tailscale auth key from SOPS and write to persist
|
||||
# NOTE: Adjust the SOPS path to match your secrets location
|
||||
sops -d --extract '["tailscale_auth_key"]' secrets/iso/tailscale.yaml | \
|
||||
sudo tee /mnt/persist/secrets/tailscale-authkey > /dev/null
|
||||
|
||||
# Lock down permissions (root-only read)
|
||||
sudo chmod 600 /mnt/persist/secrets/tailscale-authkey
|
||||
sudo install -d -m 0700 /mnt/persist/secrets
|
||||
sudo install -o root -g root -m 0400 /path/to/phantom-private-key \
|
||||
/mnt/persist/secrets/wg-private-key
|
||||
```
|
||||
|
||||
### 4. Enroll YubiKey for FIDO2 Unlock
|
||||
### 4. Enroll Credentials for FIDO2 Unlock
|
||||
|
||||
Order matters. Each step is authorised by a credential enrolled in the step
|
||||
before it, so running these out of order leaves you with nothing to authorise
|
||||
the next one.
|
||||
|
||||
```bash
|
||||
# Remove the temporary password and enroll YubiKey instead
|
||||
# You'll be prompted to touch your YubiKey during enrollment
|
||||
sudo systemd-cryptenroll /dev/disk/by-partlabel/disk-phantom-cryptroot --fido2-device=auto --wipe-slot=password
|
||||
DEV=/dev/disk/by-partlabel/disk-phantom-cryptroot
|
||||
|
||||
# 4a. Enroll the YubiKey, authorised by the temporary password from step 1.
|
||||
# Touch the key when it flashes.
|
||||
sudo systemd-cryptenroll "$DEV" --fido2-device=auto
|
||||
|
||||
# 4b. Enroll a recovery key. NOT OPTIONAL — see below.
|
||||
# Prints a high-entropy passphrase exactly once; store it offline now.
|
||||
sudo systemd-cryptenroll "$DEV" --unlock-fido2-device=auto --recovery-key
|
||||
|
||||
# 4c. Only now remove the temporary password slot.
|
||||
sudo systemd-cryptenroll "$DEV" --unlock-fido2-device=auto --wipe-slot=password
|
||||
|
||||
# 4d. Verify before you walk away. Non-zero exit means a lockout risk remains.
|
||||
sudo phantom-unlock check
|
||||
```
|
||||
|
||||
**Important:**
|
||||
- The `--wipe-slot=password` removes the temporary password
|
||||
- After this, ONLY your YubiKey can unlock the USB
|
||||
- Keep a recovery key if you want a backup: add `--recovery-key` before wiping
|
||||
**The recovery key is not optional.** Without step 4b the volume ends up with
|
||||
exactly one credential bound to one piece of hardware. A lost, broken, or
|
||||
factory-reset YubiKey then destroys 230GB permanently — there is no second
|
||||
path in.
|
||||
|
||||
If you own a backup YubiKey, enroll it too — but note this needs **both keys
|
||||
plugged in simultaneously**, and `auto` does not work on either side: systemd
|
||||
cannot tell which token is authorising and which is being enrolled, and
|
||||
refuses rather than guessing. Give explicit paths:
|
||||
|
||||
```bash
|
||||
# With both keys inserted, list them and note which hidraw is which:
|
||||
nix shell nixpkgs#libfido2 -c fido2-token -L
|
||||
|
||||
# Existing (authorises) → --unlock-fido2-device
|
||||
# New (gets enrolled) → --fido2-device
|
||||
sudo systemd-cryptenroll "$DEV" \
|
||||
--unlock-fido2-device=/dev/hidrawX \
|
||||
--fido2-device=/dev/hidrawY
|
||||
```
|
||||
|
||||
You touch the existing key first, then the new one.
|
||||
|
||||
**The backup key must actually support FIDO2.** `systemd-cryptenroll` needs
|
||||
CTAP2 with the `hmac-secret` extension; U2F-only hardware cannot hold a LUKS
|
||||
keyslot at all. In practice that rules out the YubiKey NEO (`0x0116`) and
|
||||
other pre-2018 keys, which fail with `Specified device ... is not a FIDO2
|
||||
device` no matter how the flags are arranged. A NEO appearing under
|
||||
`fido2-token -L` is not evidence it will work — that listing enumerates HID
|
||||
devices, not CTAP2 capability.
|
||||
|
||||
If you only have one FIDO2-capable key, the recovery key from 4b *is* your
|
||||
backup — do not skip it.
|
||||
|
||||
**Do not skip 4d.** If step 4a silently failed, the volume still unlocks
|
||||
during setup (the temporary password is live until 4c) and the gap only
|
||||
surfaces at the next boot, long after `/tmp/disk-password` was shredded.
|
||||
`phantom-unlock check` counts keyslots and FIDO2 tokens in the header and
|
||||
fails loudly while the situation is still repairable.
|
||||
|
||||
### 5. Cleanup and Unmount
|
||||
|
||||
|
|
@ -119,7 +177,7 @@ sudo umount -R /mnt
|
|||
sudo cryptsetup luksClose phantom-crypt
|
||||
```
|
||||
|
||||
The USB is now ready! Remove it and test booting on any UEFI machine.
|
||||
The USB is now ready for a boot test on a compatible x86_64 UEFI machine.
|
||||
|
||||
## Usage
|
||||
|
||||
|
|
@ -128,8 +186,37 @@ The USB is now ready! Remove it and test booting on any UEFI machine.
|
|||
1. Plug USB into any UEFI machine
|
||||
2. Boot from USB (may need to select boot device in BIOS)
|
||||
3. Touch YubiKey when prompted to unlock LUKS
|
||||
4. System boots, Tailscale auto-connects
|
||||
5. Login with YubiKey touch (or password if configured)
|
||||
4. System boots into Niri; select Wi-Fi in Noctalia with Mod+Ctrl+N if needed
|
||||
5. Once provisioned, `wg-quick-wg0` connects automatically and retries failed starts
|
||||
6. Unlock Noctalia using the configured login credentials
|
||||
|
||||
The shared WireGuard module provides the same hub routes and host aliases as
|
||||
the other machines. Phantom enables only the home overlay; Mullvad internet
|
||||
egress is not configured. Internet traffic uses the physical uplink.
|
||||
|
||||
### Mounting on another running machine
|
||||
|
||||
Booting phantom and *mounting* phantom are different code paths, and only the
|
||||
first one is automatic. Use the helper (available on any host importing
|
||||
`system/services/yubikey.nix`):
|
||||
|
||||
```bash
|
||||
sudo phantom-unlock # unlock via YubiKey, mount @persist
|
||||
sudo phantom-unlock --with-nix # also mount @nix
|
||||
sudo phantom-unlock status
|
||||
sudo phantom-unlock lock # unmount and close
|
||||
```
|
||||
|
||||
Mounts land under `/run/media/phantom/`, deliberately away from `/mnt` so this
|
||||
never collides with an in-progress install.
|
||||
|
||||
**Do not reach for `cryptsetup open` here.** The YubiKey enrollment writes a
|
||||
*systemd* FIDO2 token into the LUKS2 header, and `cryptsetup` has no handler
|
||||
for it — no plugin is installed on these hosts. It does not report the token
|
||||
as unusable; it ignores it, finds no other handler, and prompts for a
|
||||
passphrase. A completely healthy volume therefore presents as one you have
|
||||
lost the credentials for. `phantom-unlock` goes through `systemd-cryptsetup`,
|
||||
which reads the token properly.
|
||||
|
||||
### What Persists
|
||||
|
||||
|
|
@ -138,21 +225,31 @@ Only these survive reboot (configured in `system/phantom/impermanence.nix`):
|
|||
| Path | Purpose |
|
||||
|------|---------|
|
||||
| `/var/lib/nixos` | User/group UID/GID mappings |
|
||||
| `/var/lib/tailscale` | Tailscale node identity |
|
||||
| `/var/lib/bluetooth` | Paired Bluetooth devices |
|
||||
| `/var/lib/systemd/coredump` | Existing persistence entry; core dumps are disabled by kernel configuration |
|
||||
| `/etc/NetworkManager/system-connections` | Saved WiFi networks |
|
||||
| `/etc/machine-id` | Stable machine identity |
|
||||
| `~/.gnupg` | GPG keyring |
|
||||
| `~/.ssh` | SSH keys |
|
||||
| `~/.local/share/keyrings` | GNOME keyring |
|
||||
| `~/.bash_history` | Existing shell history persistence |
|
||||
| `/persist/secrets/wg-private-key` | Dedicated WireGuard identity, provisioned directly |
|
||||
|
||||
Noctalia's `~/.local/state/noctalia` is deliberately not persisted, so settings
|
||||
overrides and clipboard history reset on reboot. Generated themes are recreated
|
||||
at login. Existing data under `/persist/var/lib/tailscale` is not deleted by this
|
||||
migration, but is no longer mounted or used.
|
||||
|
||||
### Shutdown Behavior
|
||||
|
||||
On shutdown:
|
||||
1. Tailscale logs out (node disappears from tailnet)
|
||||
1. WireGuard interfaces stop; the hub retains the provisioned peer registration
|
||||
2. Memory caches dropped
|
||||
3. LUKS volume locked
|
||||
4. tmpfs cleared - no trace remains
|
||||
4. tmpfs state is lost; explicitly persisted state remains on the USB
|
||||
|
||||
Dropping caches is not a guarantee of physical RAM erasure. Network peers and
|
||||
remote services can retain logs of the session.
|
||||
|
||||
## Updating
|
||||
|
||||
|
|
@ -172,26 +269,56 @@ sudo nixos-rebuild switch --flake /path/to/dotfiles#phantom
|
|||
|
||||
Ensure the YubiKey is plugged in before GRUB loads. The initrd needs to detect it early.
|
||||
|
||||
### Tailscale not connecting
|
||||
### "It's asking for a passphrase I never set"
|
||||
|
||||
Almost always the wrong unlock command rather than a lost volume. Check the
|
||||
header before concluding anything is unrecoverable:
|
||||
|
||||
Check if the auth key exists:
|
||||
```bash
|
||||
cat /persist/secrets/tailscale-authkey
|
||||
sudo phantom-unlock check
|
||||
```
|
||||
|
||||
If that reports a FIDO2 token, the volume is healthy and you were using
|
||||
`cryptsetup open`, which cannot read systemd tokens — use `sudo phantom-unlock`
|
||||
instead (see *Mounting on another running machine*).
|
||||
|
||||
If it reports **no** FIDO2 token, enrollment never completed and the only
|
||||
credential is whatever passphrase was set at step 1. That is the genuinely
|
||||
unrecoverable case, and it is what step 4d exists to prevent.
|
||||
|
||||
Never run `disko --mode disko` to "reset" a volume you cannot open. It
|
||||
repartitions and destroys `/persist` — including `~/.ssh`, `~/.gnupg` and
|
||||
`~/.local/share/keyrings`, which may exist nowhere else.
|
||||
|
||||
### WireGuard not connecting
|
||||
|
||||
Check provisioning without printing private key material:
|
||||
```bash
|
||||
sudo test -s /persist/secrets/wg-private-key
|
||||
sudo stat -c '%a %U %G' /persist/secrets/wg-private-key
|
||||
```
|
||||
|
||||
Check the service status:
|
||||
```bash
|
||||
systemctl status tailscale-autoconnect
|
||||
journalctl -u tailscale-autoconnect
|
||||
systemctl status wg-quick-wg0
|
||||
journalctl -b -u wg-quick-wg0
|
||||
sudo wg show wg0 latest-handshakes
|
||||
```
|
||||
|
||||
If the service does not exist, provision the private-flake address first. If it
|
||||
starts without a handshake, check the hub's peer registration, endpoint reachability
|
||||
and Wi-Fi connectivity. Noctalia's network icon is not a WireGuard health check.
|
||||
|
||||
### Adding more persistence
|
||||
|
||||
Edit `system/phantom/impermanence.nix` to add directories or files you want to persist.
|
||||
|
||||
## Security Notes
|
||||
|
||||
- The Tailscale auth key is stored unencrypted on `/persist`, but `/persist` is on the LUKS-encrypted partition
|
||||
- The WireGuard private key is root-only on LUKS-encrypted `/persist`
|
||||
- YubiKey FIDO2 provides strong authentication without needing to remember a password
|
||||
- The empty user password means YubiKey is required for sudo (configured as `sufficient`)
|
||||
- Kernel lockdown prevents memory dumps even with physical access
|
||||
- The existing empty initial password and `sufficient` U2F PAM configuration are
|
||||
retained; they do not establish YubiKey-only authentication. Provision a usable
|
||||
login/unlock credential and verify the Noctalia lock screen before deployment.
|
||||
- Kernel lockdown limits some memory access paths; it does not guarantee protection
|
||||
from every physical attack
|
||||
|
|
|
|||
|
|
@ -10,6 +10,20 @@
|
|||
# System identity
|
||||
networking.hostName = "phantom";
|
||||
|
||||
# Same static fallback as Framework; Noctalia owns live application colors.
|
||||
stylix = {
|
||||
enable = true;
|
||||
image = ../../home/wallpapers/taipei-toner-4k-negative.png;
|
||||
base16Scheme = "${pkgs.base16-schemes}/share/themes/rose-pine.yaml";
|
||||
polarity = "dark";
|
||||
fonts.monospace = {
|
||||
package = pkgs.nerd-fonts.hack;
|
||||
name = "Hack Nerd Font Mono";
|
||||
};
|
||||
fonts.sizes.terminal = 13;
|
||||
opacity.terminal = 0.9;
|
||||
};
|
||||
|
||||
# System architecture
|
||||
nixpkgs.hostPlatform = "x86_64-linux";
|
||||
|
||||
|
|
|
|||
203
pkgs/phantom-unlock.nix
Normal file
203
pkgs/phantom-unlock.nix
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
{
|
||||
writeShellApplication,
|
||||
cryptsetup,
|
||||
systemd,
|
||||
util-linux,
|
||||
coreutils,
|
||||
gnugrep,
|
||||
}:
|
||||
|
||||
# Attach phantom's LUKS volume from an ALREADY-RUNNING machine.
|
||||
#
|
||||
# WHY THIS EXISTS IN THIS SHAPE
|
||||
#
|
||||
# Phantom boots on any UEFI box without help: system/phantom/boot.nix enables a
|
||||
# systemd initrd and passes `fido2-device=auto` through crypttab, so
|
||||
# systemd-cryptsetup reads the FIDO2 token out of the LUKS2 header and one
|
||||
# YubiKey touch unlocks the root device. That path is fine and this script has
|
||||
# nothing to do with it.
|
||||
#
|
||||
# NONE of that machinery exists when you merely plug the USB into a running
|
||||
# system. There is no crypttab entry and no initrd, so the obvious command is
|
||||
# `cryptsetup open` — and cryptsetup CANNOT READ systemd's FIDO2 token. It is a
|
||||
# systemd-specific JSON token serviced by a libcryptsetup token plugin that is
|
||||
# not installed here. cryptsetup does not report this: it ignores the token,
|
||||
# finds no usable handler, and falls through to a passphrase prompt. A perfectly
|
||||
# healthy volume therefore presents as "I've forgotten the passphrase and lost
|
||||
# the disk" — which is precisely how a wipe gets contemplated for no reason.
|
||||
#
|
||||
# So the out-of-band unlock has to go through systemd-cryptsetup explicitly.
|
||||
# This wraps that, and makes the passphrase path an ANNOUNCED fallback rather
|
||||
# than a silent degradation, so "the YubiKey didn't work" is always visible as
|
||||
# its own event.
|
||||
#
|
||||
# `check` exists for the other half of that afternoon: the steady state after
|
||||
# README step 4 is a volume with exactly ONE credential bound to ONE physical
|
||||
# key. That is a backup problem disguised as a security property, and it is
|
||||
# invisible unless something counts the keyslots for you.
|
||||
|
||||
writeShellApplication {
|
||||
name = "phantom-unlock";
|
||||
|
||||
# EVERY external command must be listed; writeShellApplication PREPENDS these
|
||||
# to PATH and runs shellcheck at build time. systemd-cryptsetup is NOT in
|
||||
# bin/ (it lives in lib/systemd), so it is referenced by absolute path below
|
||||
# rather than through runtimeInputs.
|
||||
runtimeInputs = [
|
||||
cryptsetup
|
||||
util-linux
|
||||
coreutils
|
||||
gnugrep
|
||||
];
|
||||
|
||||
text = ''
|
||||
DEVICE=/dev/disk/by-partlabel/disk-phantom-cryptroot
|
||||
MAPPER=phantom-crypt
|
||||
BASE=/run/media/phantom
|
||||
SYSTEMD_CRYPTSETUP=${systemd}/lib/systemd/systemd-cryptsetup
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
phantom-unlock — attach the phantom USB's encrypted volume on a running system
|
||||
|
||||
phantom-unlock unlock with the YubiKey and mount @persist
|
||||
phantom-unlock --with-nix also mount @nix (for chroot / rebuild work)
|
||||
phantom-unlock status show device, mapper and mount state
|
||||
phantom-unlock check audit the header for a lockout risk
|
||||
phantom-unlock lock unmount and close the volume
|
||||
|
||||
Booting phantom needs none of this — see system/phantom/boot.nix.
|
||||
EOF
|
||||
}
|
||||
|
||||
need_root() {
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "phantom-unlock: must run as root — re-run the same command with sudo" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
need_device() {
|
||||
if [ ! -e "$DEVICE" ]; then
|
||||
echo "phantom-unlock: $DEVICE not found — is the USB plugged in?" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
mount_subvol() {
|
||||
sub=$1
|
||||
target=$2
|
||||
mkdir -p "$target"
|
||||
if findmnt -n "$target" >/dev/null 2>&1; then
|
||||
echo "already mounted: $target"
|
||||
else
|
||||
mount -o "subvol=$sub,compress=zstd:3,noatime" "/dev/mapper/$MAPPER" "$target"
|
||||
echo "mounted $sub -> $target"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_unlock() {
|
||||
with_nix=$1
|
||||
need_root
|
||||
need_device
|
||||
|
||||
if [ -e "/dev/mapper/$MAPPER" ]; then
|
||||
echo "volume already open at /dev/mapper/$MAPPER"
|
||||
else
|
||||
echo "unlocking via FIDO2 — touch your YubiKey when it flashes..."
|
||||
# The `-` is the key-file argument: none, because the FIDO2 token in
|
||||
# the LUKS2 header supplies the key material.
|
||||
if ! "$SYSTEMD_CRYPTSETUP" attach "$MAPPER" "$DEVICE" - fido2-device=auto; then
|
||||
echo "" >&2
|
||||
echo "FIDO2 unlock FAILED. This is the fallback path, not the normal one." >&2
|
||||
echo "Enter the recovery key you enrolled (not a passphrase you invented):" >&2
|
||||
cryptsetup open "$DEVICE" "$MAPPER"
|
||||
fi
|
||||
fi
|
||||
|
||||
mount_subvol @persist "$BASE/persist"
|
||||
if [ "$with_nix" = "1" ]; then
|
||||
mount_subvol @nix "$BASE/nix"
|
||||
fi
|
||||
echo ""
|
||||
echo "done. lock again with: sudo phantom-unlock lock"
|
||||
}
|
||||
|
||||
cmd_lock() {
|
||||
need_root
|
||||
for m in "$BASE/nix" "$BASE/persist"; do
|
||||
if findmnt -n "$m" >/dev/null 2>&1; then
|
||||
umount "$m"
|
||||
echo "unmounted $m"
|
||||
fi
|
||||
done
|
||||
if [ -e "/dev/mapper/$MAPPER" ]; then
|
||||
cryptsetup close "$MAPPER"
|
||||
echo "closed $MAPPER"
|
||||
else
|
||||
echo "mapper already closed"
|
||||
fi
|
||||
rmdir "$BASE/nix" "$BASE/persist" "$BASE" 2>/dev/null || true
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
if [ -e "$DEVICE" ]; then
|
||||
echo "device: present"
|
||||
else
|
||||
echo "device: ABSENT (USB not plugged in?)"
|
||||
fi
|
||||
if [ -e "/dev/mapper/$MAPPER" ]; then
|
||||
echo "mapper: OPEN (/dev/mapper/$MAPPER)"
|
||||
else
|
||||
echo "mapper: closed"
|
||||
fi
|
||||
for m in "$BASE/persist" "$BASE/nix"; do
|
||||
if findmnt -n "$m" >/dev/null 2>&1; then
|
||||
echo "mount: $m"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Counts credentials rather than trusting that README step 4 was completed.
|
||||
# A single keyslot is the lockout condition: one dead YubiKey and the
|
||||
# volume is unrecoverable, with no warning until the day it matters.
|
||||
cmd_check() {
|
||||
need_root
|
||||
need_device
|
||||
dump=$(cryptsetup luksDump "$DEVICE")
|
||||
slots=$(echo "$dump" | grep -cE '^[[:space:]]+[0-9]+: luks2' || true)
|
||||
fido=$(echo "$dump" | grep -c 'systemd-fido2' || true)
|
||||
|
||||
echo "keyslots: $slots"
|
||||
echo "fido2 tokens: $fido"
|
||||
|
||||
if [ "$fido" -eq 0 ]; then
|
||||
echo "" >&2
|
||||
echo "WARNING: no FIDO2 token in the header. Boot-time YubiKey unlock" >&2
|
||||
echo "cannot work; this volume is passphrase-only." >&2
|
||||
echo " Fix: sudo systemd-cryptenroll $DEVICE --fido2-device=auto" >&2
|
||||
fi
|
||||
|
||||
if [ "$slots" -le 1 ]; then
|
||||
echo "" >&2
|
||||
echo "WARNING: only $slots keyslot. Losing or resetting that YubiKey" >&2
|
||||
echo "destroys access to this volume permanently." >&2
|
||||
echo " Fix: sudo systemd-cryptenroll $DEVICE \\" >&2
|
||||
echo " --unlock-fido2-device=auto --recovery-key" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "ok: $slots credentials enrolled, no single point of failure"
|
||||
}
|
||||
|
||||
case "''${1:-unlock}" in
|
||||
unlock) cmd_unlock 0 ;;
|
||||
--with-nix) cmd_unlock 1 ;;
|
||||
status) cmd_status ;;
|
||||
check) cmd_check ;;
|
||||
lock) cmd_lock ;;
|
||||
-h|--help) usage ;;
|
||||
*) usage; exit 1 ;;
|
||||
esac
|
||||
'';
|
||||
}
|
||||
|
|
@ -15,8 +15,7 @@ let
|
|||
./network
|
||||
];
|
||||
|
||||
# Desktop environment (Hyprland-based systems)
|
||||
# Extends base with graphics, audio, and Hyprland
|
||||
# Desktop services; each host selects its compositor.
|
||||
desktop = base ++ [
|
||||
./hardware/graphics.nix
|
||||
./hardware/bluetooth.nix
|
||||
|
|
@ -74,18 +73,17 @@ let
|
|||
./phantom/boot.nix # Portable EFI boot with LUKS/FIDO2
|
||||
./phantom/hardware.nix # Universal hardware support
|
||||
./phantom/impermanence.nix # Ephemeral root with selective persistence
|
||||
./phantom/paranoid.nix # Memory scrub, tailscale logout on shutdown
|
||||
./phantom/tailscale.nix # Boot-time auto-connect (key on /persist)
|
||||
./phantom/paranoid.nix # Memory cleanup on shutdown
|
||||
./phantom/wireguard.nix # Boot-time home overlay (key on /persist)
|
||||
./phantom/user.nix # bogdan user with YubiKey auth
|
||||
./phantom/packages.nix # Portable hacking station tools
|
||||
|
||||
./network/tailscale.nix # Base Tailscale service
|
||||
./services/yubikey.nix # For runtime sops decryption
|
||||
];
|
||||
|
||||
# Steam Deck - minimal system modules
|
||||
# Hardware and Gaming Mode handled by Jovian-NixOS
|
||||
# Does not extend desktop (uses Jovian's Plasma/Gaming Mode)
|
||||
# Does not extend desktop (Jovian owns Gaming Mode and Niri session switching)
|
||||
steamdeck = base ++ [
|
||||
./core/boot.nix
|
||||
|
||||
|
|
|
|||
|
|
@ -105,6 +105,11 @@ let
|
|||
resolvectl = "${config.systemd.package}/bin/resolvectl";
|
||||
in {
|
||||
options.network.wireguard = {
|
||||
privateKeyFile = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
description = "Runtime home-overlay key path; null uses SOPS, a custom path skips its key declaration.";
|
||||
};
|
||||
overlayIP = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "10.99.0.2";
|
||||
|
|
@ -162,7 +167,7 @@ in {
|
|||
# ── Secrets ──────────────────────────────────────────────────────────────
|
||||
# Declared per enabled interface, never unconditionally: see the header
|
||||
# note on sops-install-secrets being all-or-nothing.
|
||||
sops.secrets = {
|
||||
sops.secrets = lib.optionalAttrs (cfg.privateKeyFile == null) {
|
||||
${wgSecret} = {
|
||||
inherit sopsFile;
|
||||
mode = "0400";
|
||||
|
|
@ -203,7 +208,8 @@ in {
|
|||
# Home hub overlay
|
||||
wg0 = {
|
||||
address = [ "${cfg.overlayIP}/32" ];
|
||||
privateKeyFile = config.sops.secrets.${wgSecret}.path;
|
||||
privateKeyFile = if cfg.privateKeyFile == null
|
||||
then config.sops.secrets.${wgSecret}.path else cfg.privateKeyFile;
|
||||
mtu = 1360;
|
||||
# Routes are added manually with a high metric so that, at home, the
|
||||
# directly-connected LAN route (NM metric ~100-600) and the DHCP
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@
|
|||
hister = prev.callPackage "${self}/pkgs/hister.nix" {};
|
||||
md2pdf = prev.callPackage "${self}/pkgs/md2pdf" {};
|
||||
mullvad-jump = prev.callPackage "${self}/pkgs/mullvad-jump.nix" {};
|
||||
phantom-unlock = prev.callPackage "${self}/pkgs/phantom-unlock.nix" {};
|
||||
invoice = prev.callPackage "${self}/pkgs/invoice.nix" {
|
||||
srcHash = "sha256-Qa7lTpRBB3m3GiK5mBkDMyg0InUqziUANf5Ds5XDnGg=";
|
||||
vendorHash = "sha256-mLn9hN7hd3MPYx0STiwCL8pTTYtDlycVkSLUEq8NZOE=";
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@
|
|||
|
||||
directories = [
|
||||
"/var/lib/nixos" # User/group uid/gid mappings
|
||||
"/var/lib/tailscale"
|
||||
"/var/lib/bluetooth"
|
||||
"/var/lib/systemd/coredump"
|
||||
"/etc/NetworkManager/system-connections"
|
||||
|
|
|
|||
|
|
@ -24,8 +24,7 @@
|
|||
pciutils
|
||||
lshw
|
||||
|
||||
# Screenshot/clipboard (Hyprland)
|
||||
inputs.hyprland-contrib.packages.${pkgs.stdenv.hostPlatform.system}.grimblast
|
||||
# Screenshot/clipboard (Niri's screenshot helper is in the compositor module)
|
||||
wl-clipboard
|
||||
slurp
|
||||
grim
|
||||
|
|
|
|||
|
|
@ -22,21 +22,6 @@
|
|||
script = "true";
|
||||
};
|
||||
|
||||
# Tailscale logout on shutdown - immediately removes node from tailnet
|
||||
systemd.services.tailscale-logout = {
|
||||
description = "Logout from Tailscale on shutdown";
|
||||
before = [ "shutdown.target" "reboot.target" "halt.target" ];
|
||||
after = [ "tailscaled.service" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
ExecStop = "${pkgs.tailscale}/bin/tailscale logout";
|
||||
};
|
||||
script = "true";
|
||||
};
|
||||
|
||||
# Lock LUKS volumes on suspend
|
||||
powerManagement.powerDownCommands = ''
|
||||
${pkgs.cryptsetup}/bin/cryptsetup luksClose phantom-crypt || true
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
# Phantom USB - System-level Tailscale auto-connect
|
||||
# Uses auth key embedded on /persist during USB creation
|
||||
# No runtime YubiKey needed - LUKS encryption protects the key at rest
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
{
|
||||
# Override the base tailscale-autoconnect from system/network/tailscale.nix
|
||||
# Use mkForce to replace the empty stub with actual auto-connect logic
|
||||
systemd.services.tailscale-autoconnect = {
|
||||
description = lib.mkForce "Automatic Tailscale connection on boot";
|
||||
after = lib.mkForce [ "network-online.target" "tailscaled.service" ];
|
||||
wants = lib.mkForce [ "network-online.target" "tailscaled.service" ];
|
||||
|
||||
serviceConfig = {
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
|
||||
script = lib.mkForce ''
|
||||
sleep 2
|
||||
|
||||
status=$(${pkgs.tailscale}/bin/tailscale status --json 2>/dev/null | \
|
||||
${pkgs.jq}/bin/jq -r '.BackendState' 2>/dev/null || echo "Unknown")
|
||||
|
||||
if [ "$status" = "Running" ]; then
|
||||
echo "Tailscale already connected"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
AUTH_KEY_FILE="/persist/secrets/tailscale-authkey"
|
||||
|
||||
if [ ! -f "$AUTH_KEY_FILE" ]; then
|
||||
echo "ERROR: Tailscale auth key not found at $AUTH_KEY_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
AUTH_KEY=$(cat "$AUTH_KEY_FILE")
|
||||
HOSTNAME="phantom-$(date +%m%d-%H%M)"
|
||||
|
||||
echo "Connecting to Tailscale as $HOSTNAME..."
|
||||
${pkgs.tailscale}/bin/tailscale up \
|
||||
--authkey="$AUTH_KEY" \
|
||||
--hostname="$HOSTNAME" \
|
||||
--accept-routes \
|
||||
--reset
|
||||
|
||||
echo "Tailscale connected as $HOSTNAME"
|
||||
'';
|
||||
};
|
||||
}
|
||||
36
system/phantom/wireguard.nix
Normal file
36
system/phantom/wireguard.nix
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# The LUKS volume protects the boot-time key; no runtime token is needed for wg0.
|
||||
{ inputs, lib, ... }:
|
||||
let
|
||||
peer = inputs.secrets.vars.hosts.phantom.wireguard or null;
|
||||
in {
|
||||
# Do not borrow another host's identity or invent an address. Once the
|
||||
# dedicated peer is provisioned in the private flake, use the shared routing.
|
||||
imports = lib.optional (peer != null) ../network/wireguard.nix;
|
||||
|
||||
config = lib.mkMerge [
|
||||
{
|
||||
warnings = lib.optional (peer == null) ''
|
||||
Phantom's WireGuard peer is not provisioned. Add
|
||||
vars.hosts.phantom.wireguard.overlayIP in the private flake, register
|
||||
its public key on the home hub, and install its root-only private key
|
||||
at /persist/secrets/wg-private-key. The home overlay is disabled until then.
|
||||
'';
|
||||
}
|
||||
(lib.optionalAttrs (peer != null) {
|
||||
network.wireguard = {
|
||||
inherit (peer) overlayIP;
|
||||
privateKeyFile = "/persist/secrets/wg-private-key";
|
||||
mullvad.enable = false;
|
||||
};
|
||||
systemd.services.wg-quick-wg0 = {
|
||||
# The removable system may boot before Wi-Fi is available. Retry
|
||||
# endpoint DNS resolution until the uplink comes up.
|
||||
unitConfig.RequiresMountsFor = [ "/persist" ];
|
||||
serviceConfig = {
|
||||
Restart = "on-failure";
|
||||
RestartSec = "10s";
|
||||
};
|
||||
};
|
||||
})
|
||||
];
|
||||
}
|
||||
|
|
@ -104,6 +104,4 @@ in {
|
|||
# One Settings portal publishes Noctalia's GTK dark/light preference.
|
||||
# Multiple implementations cause duplicate appearance notifications.
|
||||
xdg.portal.config.niri."org.freedesktop.impl.portal.Settings" = [ "gtk" ];
|
||||
|
||||
desktop.greetd.sessionCommand = "${pkgs.niri}/bin/niri-session";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,10 @@ in {
|
|||
options.desktop.greetd = {
|
||||
sessionCommand = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "${pkgs.niri}/bin/niri-session";
|
||||
description = ''
|
||||
Command greetd runs to start the Wayland session. Set by the active
|
||||
compositor module (system/programs/hyprland or system/programs/niri).
|
||||
Command greetd runs to start the Wayland session. Defaults to Niri;
|
||||
the Hyprland module overrides it for hosts using that compositor.
|
||||
'';
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -10,5 +10,12 @@
|
|||
# YubiKey management tools
|
||||
environment.systemPackages = with pkgs; [
|
||||
yubikey-manager # ykman CLI tool
|
||||
|
||||
# Attach phantom's LUKS volume from THIS running machine. Lives here
|
||||
# rather than in system/phantom/ on purpose: it is needed on the host
|
||||
# doing the provisioning, not on phantom itself, which unlocks from its
|
||||
# own initrd. `cryptsetup open` cannot read the FIDO2 token and degrades
|
||||
# silently to a passphrase prompt — see pkgs/phantom-unlock.nix.
|
||||
phantom-unlock
|
||||
];
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue