chore: update phantom to niri

This commit is contained in:
Bogdan Buduroiu 2026-09-14 11:43:15 +08:00
commit 30398e5d78
Signed by: bogdan
GPG key ID: F71EF75A4C09A095
17 changed files with 447 additions and 148 deletions

View file

@ -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;
}

View file

@ -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;
};
};
};
}

View file

@ -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 = [

View file

@ -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

View file

@ -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
View 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
'';
}

View file

@ -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

View file

@ -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

View file

@ -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=";

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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"
'';
};
}

View 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";
};
};
})
];
}

View file

@ -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";
}

View file

@ -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.
'';
};

View file

@ -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
];
}