claude'd better security things

This commit is contained in:
Simon Gardling 2025-10-17 19:35:58 -04:00
parent 9e35448f04
commit f9515dd160
Signed by: titaniumtown
GPG Key ID: 9AB28AC10ECE533D
34 changed files with 327 additions and 144 deletions

2
.gitattributes vendored
View File

@ -1 +1,3 @@
secrets/** filter=git-crypt diff=git-crypt
usb-secrets/usb-secrets/usb-secrets-key filter=git-crypt diff=git-crypt

64
age-secrets.nix Normal file
View File

@ -0,0 +1,64 @@
{
config,
lib,
pkgs,
...
}:
{
# Configure all agenix secrets
age.secrets = {
# ZFS encryption key
zfs-key = {
file = ./secrets/zfs-key.age;
mode = "0400";
owner = "root";
group = "root";
};
# Secureboot keys archive
secureboot-tar = {
file = ./secrets/secureboot.tar.age;
mode = "0400";
owner = "root";
group = "root";
};
# System passwords
hashedPass = {
file = ./secrets/hashedPass.age;
mode = "0400";
owner = "root";
group = "root";
};
# Service authentication
caddy_auth = {
file = ./secrets/caddy_auth.age;
mode = "0400";
owner = "root";
group = "root";
};
jellyfin-api-key = {
file = ./secrets/jellyfin-api-key.age;
mode = "0400";
owner = "root";
group = "root";
};
slskd_env = {
file = ./secrets/slskd_env.age;
mode = "0400";
owner = "root";
group = "root";
};
# Network configuration
wg0-conf = {
file = ./secrets/wg0.conf.age;
mode = "0400";
owner = "root";
group = "root";
};
};
}

View File

@ -14,6 +14,8 @@
./hardware.nix
./zfs.nix
./impermanence.nix
./usb-secrets.nix
./age-secrets.nix
./services/postgresql.nix
./services/jellyfin.nix
@ -26,8 +28,6 @@
./services/qbittorrent.nix
./services/bitmagnet.nix
# ./services/matrix.nix
# ./services/owntracks.nix
./services/soulseek.nix
./services/llama-cpp.nix
@ -111,16 +111,18 @@
};
system.activationScripts = {
# extract all my secureboot keys
# TODO! awful secrets management, it's globally readable in /nix/store
"secureboot-keys".text = ''
#!/bin/sh
rm -fr ${config.boot.lanzaboote.pkiBundle} || true
mkdir -p ${config.boot.lanzaboote.pkiBundle}
${pkgs.gnutar}/bin/tar xf ${./secrets/secureboot.tar} -C ${config.boot.lanzaboote.pkiBundle}
chown -R root:wheel ${config.boot.lanzaboote.pkiBundle}
chmod -R 500 ${config.boot.lanzaboote.pkiBundle}
'';
# extract secureboot keys from agenix-decrypted tar
"secureboot-keys" = {
deps = [ "agenix" ];
text = ''
#!/bin/sh
rm -fr ${config.boot.lanzaboote.pkiBundle} || true
mkdir -p ${config.boot.lanzaboote.pkiBundle}
${pkgs.gnutar}/bin/tar xf ${config.age.secrets.secureboot-tar.path} -C ${config.boot.lanzaboote.pkiBundle}
chown -R root:wheel ${config.boot.lanzaboote.pkiBundle}
chmod -R 500 ${config.boot.lanzaboote.pkiBundle}
'';
};
};
environment.etc = {
@ -286,7 +288,7 @@
];
# TODO! use proper secrets management
hashedPassword = lib.strings.trim (builtins.readFile ./secrets/hashedPass);
hashedPasswordFile = config.age.secrets.hashedPass.path;
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO4jL6gYOunUlUtPvGdML0cpbKSsPNqQ1jit4E7U1RyH" # laptop

88
flake.lock generated
View File

@ -1,5 +1,28 @@
{
"nodes": {
"agenix": {
"inputs": {
"darwin": "darwin",
"home-manager": "home-manager",
"nixpkgs": [
"nixpkgs"
],
"systems": "systems"
},
"locked": {
"lastModified": 1754433428,
"narHash": "sha256-NA/FT2hVhKDftbHSwVnoRTFhes62+7dxZbxj5Gxvghs=",
"owner": "ryantm",
"repo": "agenix",
"rev": "9edb1787864c4f59ae5074ad498b6272b3ec308d",
"type": "github"
},
"original": {
"owner": "ryantm",
"repo": "agenix",
"type": "github"
}
},
"crane": {
"locked": {
"lastModified": 1754269165,
@ -15,6 +38,28 @@
"type": "github"
}
},
"darwin": {
"inputs": {
"nixpkgs": [
"agenix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1744478979,
"narHash": "sha256-dyN+teG9G82G+m+PX/aSAagkC+vUv0SgUw3XkPhQodQ=",
"owner": "lnl7",
"repo": "nix-darwin",
"rev": "43975d782b418ebf4969e9ccba82466728c2851b",
"type": "github"
},
"original": {
"owner": "lnl7",
"ref": "master",
"repo": "nix-darwin",
"type": "github"
}
},
"deploy-rs": {
"inputs": {
"flake-compat": "flake-compat",
@ -146,7 +191,7 @@
},
"flake-utils": {
"inputs": {
"systems": "systems_2"
"systems": "systems_3"
},
"locked": {
"lastModified": 1731533236,
@ -185,6 +230,27 @@
}
},
"home-manager": {
"inputs": {
"nixpkgs": [
"agenix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1745494811,
"narHash": "sha256-YZCh2o9Ua1n9uCvrvi5pRxtuVNml8X2a03qIFfRKpFs=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "abfad3d2958c9e6300a883bd443512c55dfeb1be",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "home-manager",
"type": "github"
}
},
"home-manager_2": {
"inputs": {
"nixpkgs": [
"nixpkgs"
@ -360,9 +426,10 @@
},
"root": {
"inputs": {
"agenix": "agenix",
"deploy-rs": "deploy-rs",
"disko": "disko",
"home-manager": "home-manager",
"home-manager": "home-manager_2",
"impermanence": "impermanence",
"lanzaboote": "lanzaboote",
"llamacpp": "llamacpp",
@ -463,6 +530,21 @@
"type": "github"
}
},
"systems_3": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"trackerlist": {
"flake": false,
"locked": {
@ -481,7 +563,7 @@
},
"utils": {
"inputs": {
"systems": "systems"
"systems": "systems_2"
},
"locked": {
"lastModified": 1731533236,

View File

@ -47,6 +47,11 @@
url = "github:nix-community/impermanence";
};
agenix = {
url = "github:ryantm/agenix";
inputs.nixpkgs.follows = "nixpkgs";
};
senior_project-website = {
url = "github:Titaniumtown/senior-project-website";
flake = false;
@ -76,6 +81,7 @@
srvos,
deploy-rs,
impermanence,
agenix,
...
}@inputs:
let
@ -97,7 +103,6 @@
jellyfin = 8096; # no services.jellyfin option for this
torrent = 6011;
bitmagnet = 3333;
owntracks = 3825;
gitea = 2283;
immich = 2284;
soulseek_web = 5030;
@ -110,7 +115,6 @@
certs = services_dir + "/http_certs";
domain = "gardling.com";
wg_ip = "192.168.15.1";
matrix_hostname = "matrix.${service_configs.https.domain}";
};
gitea = {
@ -142,10 +146,6 @@
cacheDir = services_dir + "/jellyfin_cache";
};
owntracks = {
data_dir = services_dir + "/owntracks";
};
slskd = rec {
base = "/var/lib/slskd";
downloads = base + "/downloads";
@ -221,6 +221,8 @@
lanzaboote.nixosModules.lanzaboote
agenix.nixosModules.default
home-manager.nixosModules.home-manager
(
{

22
secrets.nix Normal file
View File

@ -0,0 +1,22 @@
let
# USB secrets key - for encrypting/decrypting all secrets
usbSecretsKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN8+eSX2LH5wEHVG9sSv97ceD5zdTarV0lRvoUso4A7p USB secrets decryption key";
in
{
# ZFS encryption key
"zfs-key.age".publicKeys = [ usbSecretsKey ];
# Secureboot keys archive
"secureboot.tar.age".publicKeys = [ usbSecretsKey ];
# System passwords and auth
"hashedPass.age".publicKeys = [ usbSecretsKey ];
# Service authentication
"caddy_auth.age".publicKeys = [ usbSecretsKey ];
"jellyfin-api-key.age".publicKeys = [ usbSecretsKey ];
"slskd_env.age".publicKeys = [ usbSecretsKey ];
# Network configuration
"wg0.conf.age".publicKeys = [ usbSecretsKey ];
}

Binary file not shown.

BIN
secrets/caddy_auth.age Normal file

Binary file not shown.

Binary file not shown.

BIN
secrets/hashedPass.age Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
secrets/secureboot.tar.age Normal file

Binary file not shown.

BIN
secrets/slskd_env.age Normal file

Binary file not shown.

Binary file not shown.

BIN
secrets/wg0.conf.age Normal file

Binary file not shown.

Binary file not shown.

BIN
secrets/zfs-key.age Normal file

Binary file not shown.

View File

@ -25,7 +25,7 @@
};
services.caddy.virtualHosts."bitmagnet.${service_configs.https.domain}".extraConfig = ''
${builtins.readFile ../secrets/caddy_auth}
import ${config.age.secrets.caddy_auth.path}
reverse_proxy ${service_configs.https.wg_ip}:${builtins.toString service_configs.ports.bitmagnet}
'';
}

View File

@ -66,6 +66,12 @@ in
};
};
# Add agenix dependency for caddy service
systemd.services.caddy = {
after = [ "agenix.service" ];
requires = [ "agenix.service" ];
};
systemd.tmpfiles.rules = [
"d ${config.services.caddy.dataDir} 700 ${config.services.caddy.user} ${config.services.caddy.group}"
];

View File

@ -37,7 +37,7 @@
systemd.services.llama-cpp.serviceConfig.DynamicUser = lib.mkForce false;
services.caddy.virtualHosts."llm.${service_configs.https.domain}".extraConfig = ''
${builtins.readFile ../secrets/caddy_auth}
import ${config.age.secrets.caddy_auth.path}
reverse_proxy :${builtins.toString config.services.llama-cpp.port}
'';
}

View File

@ -1,65 +0,0 @@
{
pkgs,
config,
service_configs,
lib,
...
}:
{
services.matrix-conduit.settings.global.registration_token =
builtins.readFile ../secrets/matrix_reg_token;
services.caddy.virtualHosts.${service_configs.https.domain}.extraConfig = lib.mkBefore ''
header /.well-known/matrix/* Content-Type application/json
header /.well-known/matrix/* Access-Control-Allow-Origin *
respond /.well-known/matrix/server `{"m.server": "${service_configs.https.matrix_hostname}:${service_configs.ports.https}"}`
respond /.well-known/matrix/client `{"m.server":{"base_url":"https://${service_configs.https.matrix_hostname}"},"m.homeserver":{"base_url":"https://${service_configs.https.matrix_hostname}"},"org.matrix.msc3575.proxy":{"base_url":"https://${config.services.matrix-conduit.settings.global.server_name}"}}`
'';
services.caddy.virtualHosts."${service_configs.https.matrix_hostname}".extraConfig = ''
reverse_proxy :${builtins.toString config.services.matrix-conduit.settings.global.port}
'';
# Exact duplicate
services.caddy.virtualHosts."${service_configs.https.matrix_hostname}:8448".extraConfig =
config.services.caddy.virtualHosts."${config.services.matrix-conduit.settings.global.server_name
}".extraConfig;
services.matrix-conduit = {
enable = true;
package = pkgs.conduwuit;
settings.global = {
port = 6167;
server_name = service_configs.https.domain;
database_backend = "rocksdb";
allow_registration = true;
new_user_displayname_suffix = "";
trusted_servers = [
"matrix.org"
"constellatory.net"
"tchncs.de"
"envs.net"
];
# without this, conduit fails to start
address = "0.0.0.0";
};
};
systemd.tmpfiles.rules = [
"Z /var/lib/private/matrix-conduit 0770 conduit conduit"
];
# for federation
networking.firewall.allowedTCPPorts = [
8448
];
# for federation
networking.firewall.allowedUDPPorts = [
8448
];
}

View File

@ -1,46 +0,0 @@
{
pkgs,
service_configs,
username,
...
}:
let
owntracks_pkg = pkgs.owntracks-recorder.overrideAttrs (old: {
installPhase = old.installPhase + ''
mkdir -p $out/usr/share/ot-recorder
cp -R docroot/* $out/usr/share/ot-recorder'';
});
in
{
users.groups.owntracks = { };
users.users.owntracks = {
isNormalUser = true;
group = "owntracks";
};
systemd.services.owntracks = {
enable = true;
description = "Store and access data published by OwnTracks apps";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
User = "owntracks";
Group = "owntracks";
WorkingDirectory = "${owntracks_pkg}";
ExecStart = "${owntracks_pkg}/bin/ot-recorder -S ${service_configs.owntracks.data_dir} --doc-root usr/share/ot-recorder --http-port ${builtins.toString service_configs.ports.owntracks} --port 0";
};
};
systemd.tmpfiles.rules = [
"Z ${service_configs.owntracks.data_dir} 0770 owntracks owntracks"
];
services.caddy.virtualHosts."owntracks.${service_configs.https.domain}".extraConfig = ''
${builtins.readFile ../secrets/owntracks_caddy_auth}
reverse_proxy :${builtins.toString service_configs.ports.owntracks}
'';
users.users.${username}.extraGroups = [
"owntracks"
];
}

View File

@ -102,7 +102,7 @@
];
services.caddy.virtualHosts."torrent.${service_configs.https.domain}".extraConfig = ''
${builtins.readFile ../secrets/caddy_auth}
import ${config.age.secrets.caddy_auth.path}
reverse_proxy ${service_configs.https.wg_ip}:${builtins.toString config.services.qbittorrent.webuiPort}
'';

View File

@ -26,7 +26,7 @@ in
"skskd_env".text = ''
#!/bin/sh
rm -fr ${slskd_env} || true
cp ${../secrets/slskd_env} ${slskd_env}
cp ${config.age.secrets.slskd_env.path} ${slskd_env}
chmod 0500 ${slskd_env}
chown ${config.services.slskd.user}:${config.services.slskd.group} ${slskd_env}
'';
@ -67,6 +67,12 @@ in
users.users.${config.services.jellyfin.user}.extraGroups = [ "music" ];
users.users.${username}.extraGroups = [ "music" ];
# Add agenix dependencies for slskd service
systemd.services.slskd = {
after = [ "agenix.service" ];
requires = [ "agenix.service" ];
};
systemd.tmpfiles.rules = [
"Z ${service_configs.music_dir} 0750 ${username} music"
"Z ${service_configs.slskd.base} 0750 ${config.services.slskd.user} ${config.services.slskd.group}"

View File

@ -2,13 +2,14 @@
pkgs,
service_configs,
eth_interface,
config,
...
}:
{
# network namespace that is proxied through mullvad
vpnNamespaces.wg = {
enable = true;
wireguardConfigFile = ../secrets/wg0.conf;
wireguardConfigFile = config.age.secrets.wg0-conf.path;
accessibleFrom = [
# "192.168.0.0/24"
];
@ -20,13 +21,15 @@
"network.target"
"jellyfin.service"
"qbittorrent.service"
"agenix.service"
];
requires = [ "agenix.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "simple";
ExecStart = pkgs.writeShellScript "jellyfin-monitor-start" ''
export JELLYFIN_API_KEY=$(cat ${../secrets/jellyfin-api-key})
export JELLYFIN_API_KEY=$(cat ${config.age.secrets.jellyfin-api-key.path})
exec ${
pkgs.python3.withPackages (ps: with ps; [ requests ])
}/bin/python ${./jellyfin-qbittorrent-monitor.py}

58
usb-secrets.nix Normal file
View File

@ -0,0 +1,58 @@
{
config,
lib,
pkgs,
...
}:
{
# Extract USB secrets key in main system before agenix
systemd.services.usb-secrets = {
description = "Extract USB secrets key";
wantedBy = [ "sysinit.target" ];
before = [ "agenix.service" ];
wants = [ "local-fs.target" ];
after = [ "local-fs.target" ];
unitConfig.DefaultDependencies = false;
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
mkdir -p /run/secrets /mnt/usb
# Check if key already exists
if [ -f /run/secrets/usb-secrets-key ]; then
echo "USB secrets key already loaded"
exit 0
fi
# Wait for USB devices
for i in {1..30}; do
[ -e /dev/disk/by-label/SECRETS ] && break
sleep 1
done
# Mount USB and copy key
if mount /dev/disk/by-label/SECRETS /mnt/usb 2>/dev/null; then
if [ -f /mnt/usb/usb-secrets-key ]; then
install -m 600 /mnt/usb/usb-secrets-key /run/secrets/usb-secrets-key
umount /mnt/usb
echo "USB secrets key loaded"
else
umount /mnt/usb
echo "Key file not found"
exit 1
fi
else
echo "USB not found"
exit 1
fi
'';
};
age.identityPaths = [ "/run/secrets/usb-secrets-key" ];
systemd.tmpfiles.rules = [
"d /run/secrets 0700 root root -"
];
}

44
usb-secrets/setup-usb.sh Executable file
View File

@ -0,0 +1,44 @@
#!/usr/bin/env nix-shell
#! nix-shell -i bash -p parted dosfstools
set -euo pipefail
SCRIPT_DIR="$(dirname "$(realpath "$0")")"
USB_DEVICE="$1"
if [[ -z "${USB_DEVICE:-}" ]]; then
echo "Usage: $0 <usb_device>"
echo "Example: $0 /dev/sdb"
exit 1
fi
if [[ ! -b "$USB_DEVICE" ]]; then
echo "Error: $USB_DEVICE is not a block device"
exit 1
fi
if [[ ! -f "$SCRIPT_DIR/usb-secrets/usb-secrets-key" ]]; then
echo "Error: usb-secrets-key not found at $SCRIPT_DIR/usb-secrets/usb-secrets-key"
exit 1
fi
echo "WARNING: This will completely wipe $USB_DEVICE"
echo "Press Ctrl+C to abort, or Enter to continue..."
read
echo "Creating partition and formatting as FAT32..."
parted -s "$USB_DEVICE" mklabel msdos
parted -s "$USB_DEVICE" mkpart primary fat32 0% 100%
parted -s "$USB_DEVICE" set 1 boot on
USB_PARTITION="${USB_DEVICE}1"
mkfs.fat -F 32 -n "SECRETS" "$USB_PARTITION"
echo "Copying key to USB..."
MOUNT_POINT=$(mktemp -d)
trap "umount $MOUNT_POINT 2>/dev/null || true; rmdir $MOUNT_POINT" EXIT
mount "$USB_PARTITION" "$MOUNT_POINT"
cp "$SCRIPT_DIR/usb-secrets/usb-secrets-key" "$MOUNT_POINT/"
umount "$MOUNT_POINT"
echo "USB setup complete! Label: SECRETS"
echo "Create multiple backup USB keys for redundancy."

Binary file not shown.

View File

@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN8+eSX2LH5wEHVG9sSv97ceD5zdTarV0lRvoUso4A7p USB secrets decryption key

12
zfs.nix
View File

@ -1,4 +1,5 @@
{
config,
service_configs,
pkgs,
...
@ -10,13 +11,14 @@ let
in
{
system.activationScripts = {
# TODO! replace with proper secrets management
# Copy decrypted ZFS key from agenix to expected location
# /etc is on tmpfs due to impermanence, so no persistent storage risk
"zfs-key".text = ''
#!/bin/sh
rm -fr ${zfs-key} || true
cp ${./secrets/zfs-key} ${zfs-key}
chmod 0500 ${zfs-key}
chown root:wheel ${zfs-key}
rm -f ${zfs-key} || true
cp ${config.age.secrets.zfs-key.path} ${zfs-key}
chmod 0400 ${zfs-key}
chown root:root ${zfs-key}
'';
};