feat(security): harden homelab SSH, services and audit trail

The homelab hosts accept root SSH with long-lived file keys, run several
exposed services with almost no systemd sandboxing, and keep no audit trail of
privileged actions. Close those three gaps, weighing each measure against its
cost instead of adopting a policy wholesale.

SSH
- Turn off KbdInteractiveAuthentication: nixpkgs defaults it to true and PAM
  then still accepts the Unix password despite PasswordAuthentication = false.
- authorizedKeysInHomedir = false: only keys declared in Nix take effect;
  youko keeps Gitea's forced-command file through `Match User gitea`.
- Pin aquamarine's passphrase-free remote-builder keys to
  `restrict,command="nix-daemon --stdio --store daemon"`, so builds run in the
  system daemon instead of an audited root login session.

systemd sandboxing
- Add the mylib.systemdHardening preset and apply it to the exposed, weakly
  sandboxed services: caddy 7.8 -> 1.5, vmalert 8.2 -> 1.4, rustfs 6.6 -> 1.4,
  whisper-asr 7.7 -> 1.6. An eval test pins the key restrictions.
- Rejected: a global all-keys policy (<=0.2 score gains on services nixpkgs
  already sandboxes) and per-service confinement (bind paths to maintain).

auditd on all four hosts, reusing nixpkgs' loader and unit
- Audit login sessions only: a first `-a never,task -F auid=unset` rule keeps
  services out of the per-syscall audit path. A compromised service is
  therefore unrecorded until it opens a session.
- Record root commands, credential and host-key access, writes to SSH, account,
  PAM, Nix-trust and audit configuration, module loads and session denials.
- Rejected: global denial logging, service-UID watches and
  --loginuid-immutable (the kernel already records loginuid changes).

Verified on all four hosts after deployment: zero failed units, k3s 6/6 Ready,
and the exposed services healthy.

Assisted-by: Claude Code (claude-opus-5-5)
This commit is contained in:
Ryan Yin committed 2026-10-31 01:09:15 +08:00
1 parent 0376697cef
commit 7dc4e7a39f
28 files changed
+578 -29

No files matched your search

+6 -2
View File
@@ -21,10 +21,14 @@ reduce code duplication and make it easier to add new machines.
7. **`genMicrovmGuestModule.nix`** - NixOS microVM guest configuration generator
8. **`genLibvirtDomainXml.nix`** - libvirt domain XML generator (imported directly by host configs)
### Shared Presets
9. **`systemdHardening.nix`** - systemd sandboxing preset for exposed services
### Entry Point
9. **`default.nix`** - Main entry point that imports the generators and exports them as a single
attribute set, plus the `relativeToRoot` and `scanPaths` helpers
10. **`default.nix`** - Main entry point that imports the generators and exports them as a single
attribute set, plus the `relativeToRoot` and `scanPaths` helpers
## Usage
+2
View File
@@ -9,6 +9,8 @@
genVmHostModule = import ./genVmHostModule.nix;
genMicrovmGuestModule = import ./genMicrovmGuestModule.nix;
systemdHardening = import ./systemdHardening.nix { inherit lib; };
# use path relative to the root of the project
relativeToRoot = lib.path.append ../.;
scanPaths =
+47
View File
@@ -0,0 +1,47 @@
# Shared systemd sandboxing preset, adapted from xddxdd's serviceHardenArgs
# (github.com/xddxdd/nixos-config, nixos/minimal-modules/harden.nix).
{ lib }:
rec {
# For a network daemon that runs as an unprivileged user and only needs its
# own state; override keys where a service needs more.
service = {
AmbientCapabilities = "";
CapabilityBoundingSet = "";
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateMounts = true;
PrivateTmp = true;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
RemoveIPC = true;
RestrictAddressFamilies = [
"AF_UNIX"
"AF_INET"
"AF_INET6"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
# Fail a denied syscall rather than kill the service while it is new to
# the preset; switch to "kill" once it has run stably.
SystemCallErrorNumber = "EPERM";
SystemCallFilter = [
"@system-service"
"~@clock @cpu-emulation @debug @module @mount @obsolete @privileged @raw-io @reboot @swap"
];
};
# Apply a preset at mkOptionDefault priority, so nixpkgs module values win.
mkDefaults = lib.mapAttrs (_: lib.mkOptionDefault);
}