mirror of
https://github.com/ryan4yin/nix-config.git
synced 2026-11-13 12:13:46 +01:00
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:
1 parent
0376697cef
commit
7dc4e7a39f
28 files changed
+578
-29
No files matched your search
@@ -0,0 +1,2 @@
|
||||
{ lib, outputs }:
|
||||
import ../../../x86_64-linux/tests/security-audit/expected.nix { inherit lib outputs; }
|
||||
@@ -0,0 +1,2 @@
|
||||
{ lib, outputs }:
|
||||
import ../../../x86_64-linux/tests/security-audit/expr.nix { inherit lib outputs; }
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
lib,
|
||||
outputs,
|
||||
myvars,
|
||||
}:
|
||||
lib.genAttrs (builtins.attrNames outputs.nixosConfigurations) (
|
||||
name:
|
||||
{
|
||||
# Neither password nor PAM keyboard-interactive login is offered.
|
||||
PasswordAuthentication = false;
|
||||
KbdInteractiveAuthentication = false;
|
||||
# Undeclared keys in ~/.ssh/authorized_keys are not honoured.
|
||||
authorizedKeysInHomedir = false;
|
||||
# Only the interactive admin keys open a root shell; remote-builder host keys
|
||||
# are pinned to the nix-daemon protocol, served by the system daemon.
|
||||
unrestrictedRootKeys = builtins.length (
|
||||
myvars.mainSshAuthorizedKeys ++ myvars.secondaryAuthorizedKeys
|
||||
);
|
||||
}
|
||||
// lib.optionalAttrs (name == "aquamarine") {
|
||||
builderKeys = map (
|
||||
key: ''restrict,command="nix-daemon --stdio --store daemon" ${key}''
|
||||
) myvars.remoteBuilderClientKeys;
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
lib,
|
||||
outputs,
|
||||
}:
|
||||
lib.genAttrs (builtins.attrNames outputs.nixosConfigurations) (
|
||||
name:
|
||||
let
|
||||
inherit (outputs.nixosConfigurations.${name}.config) services users;
|
||||
in
|
||||
{
|
||||
inherit (services.openssh.settings) PasswordAuthentication KbdInteractiveAuthentication;
|
||||
inherit (services.openssh) authorizedKeysInHomedir;
|
||||
# Keys that may open a root session without a forced command.
|
||||
unrestrictedRootKeys = builtins.length (
|
||||
lib.filter (key: !lib.hasPrefix "restrict," key) users.users.root.openssh.authorizedKeys.keys
|
||||
);
|
||||
}
|
||||
// lib.optionalAttrs (name == "aquamarine") {
|
||||
builderKeys = lib.filter (lib.hasPrefix "restrict,") users.users.root.openssh.authorizedKeys.keys;
|
||||
}
|
||||
)
|
||||
@@ -214,6 +214,12 @@ in
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
networking = myvars.networking;
|
||||
};
|
||||
# Load the audit rules in a VM: one rule that fails to load leaves
|
||||
# auditing off, which evaluation cannot catch.
|
||||
security-auditd = import ../tests/security-auditd.nix {
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
inherit (inputs) agenix;
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
lib,
|
||||
outputs,
|
||||
}:
|
||||
lib.genAttrs (builtins.attrNames outputs.nixosConfigurations) (
|
||||
name:
|
||||
let
|
||||
# Homelab servers reachable with the root deployment keys; not desktops.
|
||||
audited = builtins.elem name [
|
||||
"youko"
|
||||
"shoryu"
|
||||
"shushou"
|
||||
"aquamarine"
|
||||
];
|
||||
in
|
||||
{
|
||||
auditd = audited;
|
||||
rootSession = audited;
|
||||
sessionsOnly = audited;
|
||||
# Only emitted where agenix has secrets: a watch on a missing path
|
||||
# aborts loading the remaining rules.
|
||||
agenixWatch = name == "youko";
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
lib,
|
||||
outputs,
|
||||
}:
|
||||
lib.genAttrs (builtins.attrNames outputs.nixosConfigurations) (
|
||||
name:
|
||||
let
|
||||
inherit (outputs.nixosConfigurations.${name}) config;
|
||||
in
|
||||
{
|
||||
auditd = config.security.auditd.enable;
|
||||
rootSession = lib.any (lib.hasSuffix "-k root-session") config.security.audit.rules;
|
||||
sessionsOnly = lib.elem "-a never,task -F auid=unset" config.security.audit.rules;
|
||||
agenixWatch = lib.elem "-a always,exit -F dir=/run/agenix.d -F perm=rwa -k credential-access" config.security.audit.rules;
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
let
|
||||
sandbox = {
|
||||
CapabilityBoundingSet = "";
|
||||
NoNewPrivileges = true;
|
||||
PrivateDevices = true;
|
||||
ProtectSystem = "strict";
|
||||
SystemCallFilter = [
|
||||
"@system-service"
|
||||
"~@clock @cpu-emulation @debug @module @mount @obsolete @privileged @raw-io @reboot @swap"
|
||||
];
|
||||
};
|
||||
in
|
||||
{
|
||||
# Only 80/443.
|
||||
caddy = sandbox // {
|
||||
CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
|
||||
};
|
||||
rustfs = sandbox;
|
||||
vmalert-homelab = sandbox;
|
||||
# The NPU is allowed through DeviceAllow instead.
|
||||
whisper-asr = sandbox // {
|
||||
PrivateDevices = false;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{ lib, outputs }:
|
||||
let
|
||||
keys = [
|
||||
"CapabilityBoundingSet"
|
||||
"NoNewPrivileges"
|
||||
"PrivateDevices"
|
||||
"ProtectSystem"
|
||||
"SystemCallFilter"
|
||||
];
|
||||
units =
|
||||
host: names:
|
||||
lib.genAttrs names (
|
||||
n: lib.getAttrs keys outputs.nixosConfigurations.${host}.config.systemd.services.${n}.serviceConfig
|
||||
);
|
||||
in
|
||||
units "youko" [
|
||||
"caddy"
|
||||
"rustfs"
|
||||
"vmalert-homelab"
|
||||
]
|
||||
// units "aquamarine" [ "whisper-asr" ]
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
lib,
|
||||
outputs,
|
||||
myvars,
|
||||
}:
|
||||
lib.genAttrs (builtins.attrNames outputs.nixosConfigurations) (
|
||||
name:
|
||||
{
|
||||
# Neither password nor PAM keyboard-interactive login is offered.
|
||||
PasswordAuthentication = false;
|
||||
KbdInteractiveAuthentication = false;
|
||||
# Undeclared keys in ~/.ssh/authorized_keys are not honoured.
|
||||
authorizedKeysInHomedir = false;
|
||||
# Only the interactive admin keys open a root shell; remote-builder host keys
|
||||
# are pinned to the nix-daemon protocol, served by the system daemon.
|
||||
unrestrictedRootKeys = builtins.length (
|
||||
myvars.mainSshAuthorizedKeys ++ myvars.secondaryAuthorizedKeys
|
||||
);
|
||||
}
|
||||
// lib.optionalAttrs (name == "aquamarine") {
|
||||
builderKeys = map (
|
||||
key: ''restrict,command="nix-daemon --stdio --store daemon" ${key}''
|
||||
) myvars.remoteBuilderClientKeys;
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
lib,
|
||||
outputs,
|
||||
}:
|
||||
lib.genAttrs (builtins.attrNames outputs.nixosConfigurations) (
|
||||
name:
|
||||
let
|
||||
inherit (outputs.nixosConfigurations.${name}.config) services users;
|
||||
in
|
||||
{
|
||||
inherit (services.openssh.settings) PasswordAuthentication KbdInteractiveAuthentication;
|
||||
inherit (services.openssh) authorizedKeysInHomedir;
|
||||
# Keys that may open a root session without a forced command.
|
||||
unrestrictedRootKeys = builtins.length (
|
||||
lib.filter (key: !lib.hasPrefix "restrict," key) users.users.root.openssh.authorizedKeys.keys
|
||||
);
|
||||
}
|
||||
// lib.optionalAttrs (name == "aquamarine") {
|
||||
builderKeys = lib.filter (lib.hasPrefix "restrict,") users.users.root.openssh.authorizedKeys.keys;
|
||||
}
|
||||
)
|
||||
Reference in new issue
Block a user