diff --git a/caesium.home.nix b/caesium.home.nix index 3258e69..14a5226 100644 --- a/caesium.home.nix +++ b/caesium.home.nix @@ -123,6 +123,76 @@ let echo "Review the diff in $MAP_FILE, then run nix-update to apply it." ''; }; + + # Programs OTP slot 2 for KeePassXC HMAC-SHA1 challenge-response (see + # programs.keepassxc.nix). Unlike yubikey-enroll/-unenroll, this never + # touches the repo: the shared secret must stay off disk entirely, so it's + # only ever printed to the terminal for the user to record offline. + yubikeyChalresp = pkgs.writeShellApplication { + name = "yubikey-chalresp"; + runtimeInputs = [ pkgs.yubikey-manager pkgs.openssl ]; + text = '' + usage() { + cat >&2 <<'EOF' + usage: + yubikey-chalresp generate + Print one random 40-char hex secret. Record it offline (e.g. in + the KeePassXC database itself once it exists) -- it is never + written to disk by this script. + + yubikey-chalresp apply [--force] + Program the inserted YubiKey's OTP slot 2 with the given secret, + requiring touch to respond. Run once per key with the SAME + secret so any enrolled key can unlock the database. Refuses to + overwrite an already-programmed slot 2 unless --force is given. + + yubikey-chalresp verify + Run a fixed test challenge against the inserted key's slot 2 and + print the response. Run this on every enrolled key and compare + the printed responses by eye -- they must all match before any + key is trusted with a real database. + EOF + } + + require_hex_secret() { + local secret="$1" + if ! [[ "$secret" =~ ^[0-9a-fA-F]{40}$ ]]; then + echo "error: secret must be exactly 40 hex characters (20 bytes)" >&2 + exit 1 + fi + } + + case "''${1:-}" in + generate) + openssl rand -hex 20 + echo "Record this secret offline now -- it is the only credential" >&2 + echo "for a hardware-key-only KeePassXC database. It was not saved" >&2 + echo "to disk anywhere by this script." >&2 + ;; + apply) + SECRET="''${2:-}" + FORCE="''${3:-}" + [[ -n "$SECRET" ]] || { usage; exit 1; } + require_hex_secret "$SECRET" + if ykman otp info | grep -q "Slot 2: programmed" && [[ "$FORCE" != "--force" ]]; then + echo "error: slot 2 is already programmed; pass --force to overwrite" >&2 + exit 1 + fi + ykman otp chalresp --touch --force 2 "$SECRET" + echo "Slot 2 programmed. Repeat on every other YubiKey with the same secret." + ;; + verify) + echo "Touch the key when it blinks..." >&2 + ykman otp calculate 2 "deadbeef" + echo "Compare this response against the other enrolled keys." >&2 + ;; + *) + usage + exit 1 + ;; + esac + ''; + }; in { @@ -131,6 +201,7 @@ in ./programs.thunderbird.nix ./module.accounts.nix ./module.evolution.nix + ./programs.keepassxc.nix ]; programs.firefox.enable = true; @@ -140,6 +211,7 @@ in faugus-launcher yubikeyEnroll yubikeyUnenroll + yubikeyChalresp ]; services.flatpak = { diff --git a/caesium.nixos.nix b/caesium.nixos.nix index a49a486..80f4d95 100644 --- a/caesium.nixos.nix +++ b/caesium.nixos.nix @@ -24,6 +24,7 @@ networking.networkmanager.enable = true; networking.interfaces.enp13s0.wakeOnLan.enable = true; + networking.nftables.enable = true; networking.firewall.enable = true; # Declaratively restrict ports to CIDRs! @@ -34,6 +35,31 @@ "192.168.1.0/16" ]; }; + + tarkov = { + port = 6969; + + allowedCIDRs = [ + "192.168.1.0/16" + ]; + }; + + fika = { + # 25565 UDP is correct -- this is Fika's in-game networking port per + # the official docs (wiki.project-fika.com/hosting-a-fika-server/ + # host-using-port-forwarding: "6969 TCP (SPT backend server)" + + # "25565 UDP (Fika in-game networking)"), and matches what the SPT + # launcher itself reports on start ("starting fika server 25565"). + # The 6790 seen in fika.jsonc/the client log is natPunchServer.port, + # a separate, currently-disabled (enable: false) NAT-punch relay + # feature -- unrelated to normal LAN raid traffic. Don't change this + # to 6790 again; that was a wrong deduction, reverted. + port = 25565; + protocol = "udp"; + allowedCIDRs = [ + "192.168.1.0/16" + ]; + }; }; # --- STORAGE & SSD OPTIMIZATION --- @@ -200,6 +226,7 @@ }; # --- PROGRAMS --- + programs.gamescope.enable = true; services.printing = { enable = true; drivers = [ @@ -209,6 +236,22 @@ programs.zsh.enable = true; + # SPT's launcher (SPT.Launcher.Linux) is a .NET single-file apphost that + # embeds WebKitGTK via Photino. It can't run under an explicit ld.so + # (apphosts resolve their bundle through /proc/self/exe), so it needs a + # real /lib64/ld-linux — that's nix-ld. Libraries below are exactly what + # ldd reports missing for Photino.Native.so. + programs.nix-ld = { + enable = true; + libraries = with pkgs; [ + webkitgtk_4_1 # also provides libjavascriptcoregtk-4.1 + gtk3 + glib + libnotify + stdenv.cc.cc.lib + ]; + }; + programs.steam = { enable = true; remotePlay.openFirewall = true; @@ -235,6 +278,12 @@ curl python3 nodejs_26 + # umu's bundled Steam Runtime container (pressure-vessel/bwrap) needs a + # real FHS to find binaries like `true` during its sandbox setup -- see + # umu-steam-run.sh in ~/spt-nix. System-wide so any launcher (SPT, Faugus, + # Lutris, Heroic, ...) that shells out to umu-run/proton directly can + # find it on PATH without a nix-shell wrapper. + steam-run home-manager glib ]; diff --git a/module.firewall.nix b/module.firewall.nix index ad91e3a..9de8774 100644 --- a/module.firewall.nix +++ b/module.firewall.nix @@ -34,15 +34,17 @@ let formatCidrRule = rule: cidr: let family = if hasInfix ":" cidr then "ip6" else "ip"; - # toString handles both integers (2222) and strings ("8000-8010") cleanly - portStr = toString rule.port; + # toString handles both integers (2222) and strings ("8000-8010") cleanly -- + # nftables accepts dash-range syntax directly in a rule statement. + portStr = toString rule.port; in - "${family} saddr ${cidr} ${rule.protocol} dport ${portStr} accept comment \"${rule.name}\""; + "${family} saddr ${cidr} ${rule.protocol} dport ${portStr} accept comment \"${rule.name}\""; - formatRule = ruleName: rule: + formatRule = _ruleName: rule: concatMapStringsSep "\n" (formatCidrRule rule) rule.allowedCIDRs; -in { +in +{ options.networking.firewall.restrictedPorts = mkOption { type = types.attrsOf (types.submodule ruleOpts); default = { }; @@ -50,7 +52,7 @@ in { }; config = mkIf (cfg != { }) { - networking.firewall.extraInputRules = + networking.firewall.extraInputRules = concatMapStringsSep "\n" (ruleName: formatRule ruleName cfg.${ruleName}) (attrNames cfg); }; } diff --git a/programs.keepassxc.nix b/programs.keepassxc.nix new file mode 100644 index 0000000..29d5388 --- /dev/null +++ b/programs.keepassxc.nix @@ -0,0 +1,61 @@ +# KeePassXC replaces KWallet as the org.freedesktop.secrets provider so the +# vault can be unlocked with a YubiKey (HMAC-SHA1 challenge-response on OTP +# slot 2) instead of the login password. KWallet's only unattended-unlock +# path is pam_kwallet inheriting the login password, which passwordless +# YubiKey login (module.yubikey.nix) never supplies -- KeePassXC is the only +# one of the two with native hardware-key support. +# +# Database creation is manual/interactive (GUI wizard), not declared here: +# the .kdbx is mutable state, and the Secret Service "exposed group" setting +# lives inside the database file itself with no Nix-level equivalent. See +# yubikey-chalresp (caesium.home.nix) for programming the shared +# challenge-response secret onto each key first. +{ + lib, + ... +}: + +{ + xdg.autostart.enable = true; + + programs.keepassxc = { + enable = true; + autostart = true; + settings = { + FdoSecrets.Enabled = true; + FdoSecrets.ConfirmDeleteItem = true; + + # Required, not cosmetic: this home-manager module installs its own + # browser native-messaging manifest. Leaving KeePassXC's own updater on + # collides with it and throws an error dialog on every launch. + Browser.UpdateBinaryPath = false; + + GUI.ShowTrayIcon = true; + GUI.MinimizeToTray = true; + + General.MinimizeAfterUnlock = true; + General.BackupBeforeSave = true; + + Security.LockDatabaseIdle = true; + Security.LockDatabaseIdleSeconds = 900; + }; + }; + + # Non-empty `programs.keepassxc.settings` links keepassxc.ini read-only into + # the Nix store; KeePassXC will report a config-write error at startup + # because it also persists runtime state (last-opened DB, window geometry) + # there. Expected and harmless -- see nix-community/home-manager#8257. If + # the dialog proves intolerable, drop `settings` above, install the package + # via home.packages instead, and seed a mutable keepassxc.ini with a + # home.activation script (module.evolution.nix uses the same technique for + # EDS *.source files). + + # Disable KWallet outright so nothing fights KeePassXC for the + # org.freedesktop.secrets D-Bus name. Linked read-only on purpose: it stops + # Plasma silently re-enabling the wallet from System Settings. + xdg.configFile."kwalletrc".text = lib.generators.toINI { } { + Wallet = { + Enabled = false; + }; + }; +}