From f24a14920ecb9799a7dfdd8ae1d615d215e0f289 Mon Sep 17 00:00:00 2001 From: Richie Cahill Date: Tue, 25 Aug 2026 09:50:49 -0400 Subject: [PATCH 01/16] feat(flake): add disko to flake.nix for nixos-anywhere --- flake.lock | 21 +++++++++++++++++++++ flake.nix | 5 +++++ 2 files changed, 26 insertions(+) diff --git a/flake.lock b/flake.lock index 0c8bef6..dc9924e 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,25 @@ { "nodes": { + "disko": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1781152676, + "narHash": "sha256-RxWs5ND31KzTG7wvMM+PMfUjyNpmIEr999lqNARaM5o=", + "owner": "nix-community", + "repo": "disko", + "rev": "ff8702b4de27f72b4c78573dfb89ec74e36abdf1", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "disko", + "type": "github" + } + }, "firefox-addons": { "inputs": { "nixpkgs": [ @@ -124,6 +144,7 @@ }, "root": { "inputs": { + "disko": "disko", "firefox-addons": "firefox-addons", "home-manager": "home-manager", "nixos-hardware": "nixos-hardware", diff --git a/flake.nix b/flake.nix index 21ac018..e0fa2a1 100644 --- a/flake.nix +++ b/flake.nix @@ -35,6 +35,11 @@ url = "github:Mic92/sops-nix"; inputs.nixpkgs.follows = "nixpkgs"; }; + + disko = { + url = "github:nix-community/disko"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; outputs = -- 2.55.0 From a23c91c4ac49cf9206f8cb7296b8a6aaa664184c Mon Sep 17 00:00:00 2001 From: Richie Cahill Date: Tue, 25 Aug 2026 10:24:12 -0400 Subject: [PATCH 02/16] feat(global): refactor configuration by moving settings to minimal.nix --- users/richie/home/global.nix | 17 +---------------- users/richie/home/minimal.nix | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 16 deletions(-) create mode 100644 users/richie/home/minimal.nix diff --git a/users/richie/home/global.nix b/users/richie/home/global.nix index 5c760e4..a38dcd4 100644 --- a/users/richie/home/global.nix +++ b/users/richie/home/global.nix @@ -1,22 +1,7 @@ -{ config, ... }: { imports = [ - ./cli + ./minimal.nix ./programs.nix ./ssh_config.nix ]; - - programs = { - home-manager.enable = true; - git.enable = true; - }; - - home = { - username = "richie"; - homeDirectory = "/home/${config.home.username}"; - stateVersion = "24.05"; - sessionVariables = { - FLAKE = "$HOME/dotfiles"; - }; - }; } diff --git a/users/richie/home/minimal.nix b/users/richie/home/minimal.nix new file mode 100644 index 0000000..30928fa --- /dev/null +++ b/users/richie/home/minimal.nix @@ -0,0 +1,17 @@ +{ config, ... }: +{ + imports = [ + ./cli + ]; + + programs = { + home-manager.enable = true; + }; + + home = { + username = "richie"; + homeDirectory = "/home/${config.home.username}"; + stateVersion = "24.05"; + sessionVariables.FLAKE = "$HOME/dotfiles"; + }; +} -- 2.55.0 From 03537310cbe0e7aaa684e1be66c924aca6c6feaf Mon Sep 17 00:00:00 2001 From: Richie Cahill Date: Tue, 25 Aug 2026 11:43:05 -0400 Subject: [PATCH 03/16] feat(tailscale): add Tailscale service configuration and update secrets --- .sops.yaml | 5 +++ common/optional/tailscale.nix | 33 +++++++++++++++ users/secrets.yaml | 76 ++++++++++++++++++----------------- 3 files changed, 77 insertions(+), 37 deletions(-) create mode 100644 common/optional/tailscale.nix diff --git a/.sops.yaml b/.sops.yaml index b23668b..e9ba820 100644 --- a/.sops.yaml +++ b/.sops.yaml @@ -1,6 +1,9 @@ # Generate AGE keys from SSH keys with: # ssh-keygen -A # nix-shell -p ssh-to-age --run 'cat /etc/ssh/ssh_host_ed25519_key.pub | ssh-to-age' +# update keys after addin/removing a key +# nix-shell -p sops --run "sops updatekeys users/secrets.yaml" users/secrets.yaml + keys: - &admin_richie age1u8zj599elqqvcmhxn8zuwrufsz8w8w366d3ayrljjejljt2q45kq8mxw9c # cspell:disable-line @@ -8,6 +11,7 @@ keys: - &system_brain age1jhf7vm0005j60mjq63696frrmjhpy8kpc2d66mw044lqap5mjv4snmwvwm # cspell:disable-line - &system_jeeves age13lmqgc3jvkyah5e3vcwmj4s5wsc2akctcga0lpc0x8v8du3fxprqp4ldkv # cspell:disable-line - &system_rhapsody age1ufnewppysaq2wwcl4ugngjz8pfzc5a35yg7luq0qmuqvctajcycs5lf6k4 # cspell:disable-line + - &system_portal_1 age1vyav6kxtvt3z4vtnkkjj38eu8hlts5m7ygyckhskvalg2gpjk52su53d0a # cspell:disable-line creation_rules: - path_regex: users/secrets\.yaml$ @@ -18,3 +22,4 @@ creation_rules: - *system_brain - *system_jeeves - *system_rhapsody + - *system_portal_1 diff --git a/common/optional/tailscale.nix b/common/optional/tailscale.nix new file mode 100644 index 0000000..31528ec --- /dev/null +++ b/common/optional/tailscale.nix @@ -0,0 +1,33 @@ +{ + config, + inputs, + ... +}: +{ + networking.firewall.trustedInterfaces = [ "tailscale0" ]; + + services.tailscale = { + enable = true; + openFirewall = true; + authKeyFile = config.sops.secrets.tailscale_auth_key.path; + + # OAuth client secrets create ephemeral nodes by default. NixOS machines + # are persistent and should enroll without interactive device approval. + authKeyParameters = { + ephemeral = false; + preauthorized = true; + }; + + extraUpFlags = [ "--advertise-tags=tag:nixos" ]; + }; + + sops = { + age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ]; + + secrets.tailscale_auth_key = { + sopsFile = "${inputs.self}/users/secrets.yaml"; + owner = "root"; + mode = "0400"; + }; + }; +} diff --git a/users/secrets.yaml b/users/secrets.yaml index 68bc49c..c3e3c70 100644 --- a/users/secrets.yaml +++ b/users/secrets.yaml @@ -1,62 +1,64 @@ richie_password: ENC[AES256_GCM,data:DMi3M8aqrQ60APIofr8wJMh+VZ14hLRxz6jWZgzswr0pV/QVSX53ShBFr90ruO3mucOLYv0l+bI31covfqMAhXWBJp9wUgtC2Q==,iv:qgtn30hZfIL4dBnQSLkjbo7zPJA4m9TR0f52sTFc0v4=,tag:ydLbcGyXjv0fE+4b5ECX5w==,type:str] math_password: ENC[AES256_GCM,data:ykiSr3iBHrShJarEQSJ/zuXbCPcbW2oUpaAjblu1V15ufFKVSMZM94LlpMiCYtN9cYBLs98hcMeajJbvgbwT5emPHthy9+TJDw==,iv:1TJEUo0ishqFAZiUE1473yR3RT6Gbtqt4zM+C1a1KEk=,tag:pR6jyIj+bu3XaSx5yIHSmA==,type:str] +tailscale_auth_key: ENC[AES256_GCM,data:SZEfpVnqA84AQLA/geGsPNZfsWBktlM3Zrm7OpuSS/hCWWEKtSIbRYyovH68h8tQJBQFpv18n4oXJe9eAr6Mog==,iv:x1m6XPS/h818qNRcO+uqYjgTfd/FkxHHaSHHJrLyrp4=,tag:eOpcJIhzUWFzXTuRdS9S6w==,type:str] +tailscale_oauth_client_id: ENC[AES256_GCM,data:+cWxKlLIcJtsLhqmZUNLVDs=,iv:qrTjhLfu82pzbZnU6BrK1n+Ncbm2Cl3MMCSTKCJ6H8U=,tag:s/N5XpLwNH1gZ+LzbqRr2g==,type:str] sops: age: - enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBObHhkaFlnaG4zaTZtbkIw - TkxSQnMxbDNwUVo4R1VYRDNKVFRDUE9kb0ZjCnpWWElJUVNuNFBsMzZod1ZQY0Fa - VXgxNjZ0TUJ3cEt2OTFWL2dGY0txVHMKLS0tIHNQU3Rsanh4dkc3ZElsai9YMFdH - cXUzVmFxTUVIOWZVR2Fpa2crdWsrdlkKwdGLfbKWc25qfBKyd/cawiUWv9iepKHN - EOp/LdH2GbCfnQSVbxi28ukLHxWqOLdqMm8xSni/Of2PXvMnpdyCyQ== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBEMjQvSzZSY08vRlI0dzBP + YytpVlpPSGFHaTI5b2xzUkFFdEY0bVhQdGgwClM3UmM1OElpcEZTcTB0NHpSZ2d6 + aVBpcjVIaDk3Tko1cm4xYjdkYVQ1L1EKLS0tIE1aQW95U0JIVGc4MGloSXRXc3ZS + a3EvU1dJdmxOeVBIS28yT3dHd2ZEYWcKv5TIqRRQs3AXER1WHEx8wotyXr2Ktxsu + dwldwEHyDNqwf7UgGOZu7JueeZ7y2KC8KZ7ixIdrhVaoxuLEvCg7VA== -----END AGE ENCRYPTED FILE----- recipient: age1u8zj599elqqvcmhxn8zuwrufsz8w8w366d3ayrljjejljt2q45kq8mxw9c - enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6R0djMTArVmFySE5DMnFr - WFdBUERFbE1HRVVFd0oyaXJ2eU5HUStBUFN3CnR3ckZ2bkpGZFFScHQwTlBZYTMv - cTBXbGVoTXlWczhId05QVjZDRzFqRjQKLS0tIDNIcVBydHYwTGRnVU1mWWk4WTlR - eVlwQWgxSG5SdmFrWTlOcFo5eXZONWMKgx4huoSnbkRq0wQbsYgsWUKDTxDGNvYR - anVMQg+c7PwDlk1V4JQZ4WrYLx63Ep5qDjGlN/Ssf2Vo6rAuuKetcA== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSByaE5jb3p3VzZ1dE05Wmlx + c1ZoWGhTcTRHRE9oMWtkaCtBY1l1a2QyWWlvCk9MM2FmcEZubXR1REpOcHZ6aHVE + UEkzbllZdkxPYzlTMHNod3VWNnhoaTgKLS0tIGVCdEpqUzhFM0swRjVjb0Z6ckFN + UzlYb1U4UGJqWE9oczdlNVh1S1lqL3cKSKxaJFhun2ee45I1jC55YKNdfYB8MsS+ + dgFOFCvn/4uEcUyKZbUf0STjbIZ5eMHl8NVRTupI4tftGiE559M5Wg== -----END AGE ENCRYPTED FILE----- recipient: age1q47vup0tjhulkg7d6xwmdsgrw64h4ax3la3evzqpxyy4adsmk9fs56qz3y - enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2QjZYejFYbCswQjdmaDA0 - L2ZqUUhtYU12YlpISmxueHQzRG5YL0tQNXh3CndGamMwRzYvUzkvaE9DVnMwTkNC - cFMxczZuOUorS3dnVUpjaVdCUjhLZ2MKLS0tIGlDeGRGa3dRbWgzT3NVYjBaYUJ4 - VW5yeFlvWUZ5MVpNZHA5M1VXR1hxU1kKqii08/MB2aabgP4RQs1ry8AxmFqB8Mn+ - m7B0u64aziKXLSl0u471wqgD+YGRwNcajXT2pHCy8QWLznzvIMSrxA== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2U01jTVZQT1RRRU1nQmVv + Z0lnOGJLN1c0UkNLbnJjV29na3Rub0xzbXhNCjY0aFNuUnFDUnZheDRHT3R5L3Fv + ZkJ3QkF2RENKRVdUYjRzRXMrNklvN2cKLS0tIFJzK2o1MmVTWHZMWmVDWlgzeGY5 + VDg4OGl0NGFYY2VNZGY5UkR1cVdWY0UK1qa3swdfgUw1tnnThRf1sl9f2zTP/d/d + iwcenOEryXatVMJjZVWrLC5C7MwtKz+pIBXa98yaSpCDLqjAMwOCPg== -----END AGE ENCRYPTED FILE----- recipient: age1jhf7vm0005j60mjq63696frrmjhpy8kpc2d66mw044lqap5mjv4snmwvwm - enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrSUc3VnllVDFZTm1jRnlP - ZDBqelhkeHliZ1VlcjVnblQyeFlWclZTWkZjCjhJQk5EWkVoQjdoMHg4Zko4OU1C - NWFnVTIwV3RYbTZSeWttaVRPbjNrMG8KLS0tIDZWUHJSbVVlR0lKUDRtc3VqOTc1 - cGJ3NDBLem9FNUpnbStYRTlqQStHV2sKwxPe4nTULsU0mVeUh8mhr2KX9U0iT5dL - zvHldoQG6mZHgtHK6XI5AQJYf+zUW66OKqNSxAnn+BM20QkAQVZNVw== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBTREI2UEZwS0Y0SmEvK1F4 + Y1hZZjRVWWxMT29mc3ZXcHFDclRUYi96LzA4CkIwZ2h4T2JQRW1OL2hmRTFHWkFY + RHZqQUFqU3hvNlRBZHRiSThPVUE3TG8KLS0tIHh0Z2tlNXREczJsa1g2ekZyZjBt + dW41aVA3VEVqa1NDMHM0T3FKZFNTN0kKGQPRUMSRtibFVFRZR/igsjgxgDo4ogga + dUHBRU65r41er+Wk3lKdtbXJX1g83K8ay7540hNXheOhxFi57trrzA== -----END AGE ENCRYPTED FILE----- recipient: age13lmqgc3jvkyah5e3vcwmj4s5wsc2akctcga0lpc0x8v8du3fxprqp4ldkv - enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0NU9ac2FuRHI1dkQycmc0 - YlhGK29UeTdiZEZXcWtPUW4rMis4Z2NWYWpJCkVldEdMc3ZTaDFidHpaZk5mM283 - bTd0RlN5SHVabGhjeXFnSThydnVoem8KLS0tIElWclYvTmtkb0wyNDlKT3ZEckwx - L2NObzZadlJ5d3MyeGRqKy95L3BOMFEKtoswi6r2TmCZzngUkiGQV5TTsuzisMFS - 5QI0aQZwhexqUMvbPuajYKvcPj+D6a2xaxbL3TBRLjOrFmcp5J7/YA== - -----END AGE ENCRYPTED FILE----- - recipient: age1l272y8udvg60z7edgje42fu49uwt4x2gxn5zvywssnv9h2krms8s094m4k - - enc: | - -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSByUlVjM2xpc281bHZzOGVo - L3VEclhJZDd5SS9mazFiTk9DcmxMaWxPT213ClNPWERKQU03OWk0OEVIY05ib2VG - WXhiZDhuMDZ5ZmFPWHB5RTFKYmpkVzAKLS0tIGppUndCb25wb2dyV01YbENrWjdU - TmMvWVpobnl0eXBIOGQwMW5BSlhJTUkKzua1artJWbZlKfzv27xfZJeBpntBYwUf - c8i1gNlvRwkhFAlrWcKR65vgyxsO3rbkLJRkcwG/q4hHj9zBeC/K2A== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrV1Z2MGNPK28wTStRSUZW + dmFMaUt5MENHeUpVcDVxQnZhd1o5RmJwOWwwCittaGNrcW1hV25VaGJaTEhZRFBJ + Y2toWEpFWFZYT0plb2lkTlJCS2Mwb3MKLS0tIHlkVjVESWFsbWlVQ0xVQ0llclF2 + L1FMbC91YjJsR3BGRjJ0QVQ1MDdBbEUKNnOI9+H1LJcvVcwl9uQMmFp35y5RB+Zl + Mo3z3unweQtS+jiUn7RyVs5daG1E2tfihu2CQDWaktuCuunMpt3Wew== -----END AGE ENCRYPTED FILE----- recipient: age1ufnewppysaq2wwcl4ugngjz8pfzc5a35yg7luq0qmuqvctajcycs5lf6k4 - lastmodified: "2026-06-29T20:19:44Z" - mac: ENC[AES256_GCM,data:GIvQxWt4tZGn0fyiXVtxGFQQoNcFUgilF+/PSz50exVrmzsS0XQUk/TIDFHaQR9jlJI50jqlyc1rBHgjnqC2oPHhPWaaVhgF18vQI55rGKdymNFjsHnaCkblFVdR1RJm0FSB2Ri6y5k8tfN3ywiwromJRz4NYzr1hbmr36azfg4=,iv:V3jspeYt/d2wy13gUrQmPGARm0hxwvSL/mocJAUofdw=,tag:vARoUzBWTJKkONDGoQdzNQ==,type:str] + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqR0hUT3hxTzRwTVhuTm9E + TUFtNFFCYWNUNUNVbHNaNXQ0aEVBa04vSEU0CkRPVzhuYUsrM0wxZjRDdkxvTVA2 + UW9CSjlIQW5yenhaUi9mbEdLYlhWYk0KLS0tIGRXMm9Wa3l4UWxqcGl6ZlBZV3E0 + Z3YrbUYrSmV0NHNTeTdhMkJEdytwSmMKGLpe5+p4FGJNHy6SVgnsDbEsiP0J8lW/ + 0+3X3LQoL0f1S2L5DjexF/H0NGvWjAPaGzDNB6V50VxGDabEIVqjvQ== + -----END AGE ENCRYPTED FILE----- + recipient: age1vyav6kxtvt3z4vtnkkjj38eu8hlts5m7ygyckhskvalg2gpjk52su53d0a + lastmodified: "2026-08-25T15:35:53Z" + mac: ENC[AES256_GCM,data:4/WuSmUb2ONlB56dgbwwekZ8h7E1vvPzPxYCvNM8N68nt1nb9JxoMeAX9PktmljAarYLQJd1O5+49ZEph3Gzx9LrwvWnm9asq0UeeJi08x7wdQDcawNFlBFzXF2qlgxU1JssNHZZOB+5nYH4t8vFzPTq/9BqoifPR2+pLBYhUQI=,iv:N/gVgeWT2/1bfHuR6TanyDBijWmXAqyACXpnwfwDuVA=,tag:pQ4ct4G7SARz2wyjOc7AKQ==,type:str] unencrypted_suffix: _unencrypted - version: 3.13.1 + version: 3.13.3 -- 2.55.0 From b8b0605279e80f62ba05ca62604a728e4a1a499f Mon Sep 17 00:00:00 2001 From: Richie Cahill Date: Tue, 25 Aug 2026 16:45:09 -0400 Subject: [PATCH 04/16] refactor(nixos): make ZFS configuration optional - move ZFS and snapshot manager settings out of the global base - group snapshot files under common/optional/zfs - enable the ZFS module on existing ZFS hosts - make the global tmpfs setting overridable --- common/global/default.nix | 17 +----------- common/optional/zfs/default.nix | 26 +++++++++++++++++++ .../zfs/snapshot.nix} | 0 .../zfs}/snapshot_config.toml | 0 systems/bob/default.nix | 5 ++-- systems/brain/default.nix | 1 + systems/jeeves/default.nix | 1 + systems/rhapsody-in-green/default.nix | 1 + 8 files changed, 33 insertions(+), 18 deletions(-) create mode 100644 common/optional/zfs/default.nix rename common/{global/snapshot_manager.nix => optional/zfs/snapshot.nix} (100%) rename common/{global => optional/zfs}/snapshot_config.toml (100%) diff --git a/common/global/default.nix b/common/global/default.nix index 83b9812..98bd6a8 100644 --- a/common/global/default.nix +++ b/common/global/default.nix @@ -17,16 +17,11 @@ ./nix.nix ./programs.nix ./ssh.nix - ./snapshot_manager.nix ]; boot = { - tmp.useTmpfs = true; + tmp.useTmpfs = lib.mkDefault true; kernelPackages = lib.mkDefault pkgs.linuxPackages_6_12; - zfs = { - package = lib.mkDefault pkgs.zfs_2_4; - forceImportRoot = lib.mkDefault false; - }; }; hardware.enableRedistributableFirmware = true; @@ -50,16 +45,6 @@ # firmware update fwupd.enable = true; - - snapshot_manager = { - enable = lib.mkDefault true; - PYTHONPATH = "${inputs.self}/"; - }; - - zfs = { - trim.enable = lib.mkDefault true; - autoScrub.enable = lib.mkDefault true; - }; }; powerManagement.powertop.enable = lib.mkDefault true; diff --git a/common/optional/zfs/default.nix b/common/optional/zfs/default.nix new file mode 100644 index 0000000..a18e8e6 --- /dev/null +++ b/common/optional/zfs/default.nix @@ -0,0 +1,26 @@ +{ + inputs, + lib, + pkgs, + ... +}: +{ + imports = [ ./snapshot.nix ]; + + boot.zfs = { + package = lib.mkDefault pkgs.zfs_2_4; + forceImportRoot = lib.mkDefault false; + }; + + services = { + snapshot_manager = { + enable = lib.mkDefault true; + PYTHONPATH = "${inputs.self}/"; + }; + + zfs = { + trim.enable = lib.mkDefault true; + autoScrub.enable = lib.mkDefault true; + }; + }; +} diff --git a/common/global/snapshot_manager.nix b/common/optional/zfs/snapshot.nix similarity index 100% rename from common/global/snapshot_manager.nix rename to common/optional/zfs/snapshot.nix diff --git a/common/global/snapshot_config.toml b/common/optional/zfs/snapshot_config.toml similarity index 100% rename from common/global/snapshot_config.toml rename to common/optional/zfs/snapshot_config.toml diff --git a/systems/bob/default.nix b/systems/bob/default.nix index d5eb01e..d404652 100644 --- a/systems/bob/default.nix +++ b/systems/bob/default.nix @@ -6,15 +6,16 @@ "${inputs.self}/users/steve" "${inputs.self}/common/global" "${inputs.self}/common/optional/docker.nix" - "${inputs.self}/common/optional/scanner.nix" "${inputs.self}/common/optional/monitoring-agent.nix" + "${inputs.self}/common/optional/nvidia.nix" + "${inputs.self}/common/optional/scanner.nix" "${inputs.self}/common/optional/steam.nix" "${inputs.self}/common/optional/syncthing_base.nix" "${inputs.self}/common/optional/systemd-boot.nix" "${inputs.self}/common/optional/update.nix" "${inputs.self}/common/optional/yubikey.nix" "${inputs.self}/common/optional/zerotier.nix" - "${inputs.self}/common/optional/nvidia.nix" + "${inputs.self}/common/optional/zfs" ./hardware.nix ./syncthing.nix ./llms.nix diff --git a/systems/brain/default.nix b/systems/brain/default.nix index 902a00b..4033ade 100644 --- a/systems/brain/default.nix +++ b/systems/brain/default.nix @@ -9,6 +9,7 @@ "${inputs.self}/common/optional/systemd-boot.nix" "${inputs.self}/common/optional/update.nix" "${inputs.self}/common/optional/zerotier.nix" + "${inputs.self}/common/optional/zfs" ./docker ./hardware.nix ./programs.nix diff --git a/systems/jeeves/default.nix b/systems/jeeves/default.nix index 1d47f8d..a7f6697 100644 --- a/systems/jeeves/default.nix +++ b/systems/jeeves/default.nix @@ -15,6 +15,7 @@ in "${inputs.self}/common/optional/syncthing_base.nix" "${inputs.self}/common/optional/update.nix" "${inputs.self}/common/optional/zerotier.nix" + "${inputs.self}/common/optional/zfs" ./monitoring ./docker ./services diff --git a/systems/rhapsody-in-green/default.nix b/systems/rhapsody-in-green/default.nix index a339528..b5e8957 100644 --- a/systems/rhapsody-in-green/default.nix +++ b/systems/rhapsody-in-green/default.nix @@ -10,6 +10,7 @@ "${inputs.self}/common/optional/systemd-boot.nix" "${inputs.self}/common/optional/yubikey.nix" "${inputs.self}/common/optional/zerotier.nix" + "${inputs.self}/common/optional/zfs" ./hardware.nix ./open_webui.nix ./programs.nix -- 2.55.0 From 6fa93e1b96e9eeee1bb07ed02a9ed2fd57c298b7 Mon Sep 17 00:00:00 2001 From: Richie Cahill Date: Tue, 25 Aug 2026 17:39:05 -0400 Subject: [PATCH 05/16] feat(portal): add Vultr NixOS host configuration - use the shared global base and Tailscale module - configure Portal SSH and VM memory settings - add Richie's minimal Home Manager profile - document SOPS and nixos-anywhere deployment --- systems/portal_1/README.md | 83 +++++++++++++++++++++++++++++++ systems/portal_1/default.nix | 55 ++++++++++++++++++++ systems/portal_1/disk-config.nix | 59 ++++++++++++++++++++++ users/richie/systems/portal-1.nix | 5 ++ 4 files changed, 202 insertions(+) create mode 100644 systems/portal_1/README.md create mode 100644 systems/portal_1/default.nix create mode 100644 systems/portal_1/disk-config.nix create mode 100644 users/richie/systems/portal-1.nix diff --git a/systems/portal_1/README.md b/systems/portal_1/README.md new file mode 100644 index 0000000..c7b1bbd --- /dev/null +++ b/systems/portal_1/README.md @@ -0,0 +1,83 @@ +# portal_1 + +Minimal NixOS target for a Vultr VM, installed with nixos-anywhere. The Nix +flake target is `portal_1`; the machine hostname is `portal-1` because DNS +hostnames cannot contain underscores. + +## Before deploying + +1. Confirm the VM's system disk is `/dev/vda`. If it is not, update both + references in `disk-config.nix`. +2. Confirm the SSH public key in `default.nix` is the key that should have + administrator access. +3. Boot the VM into a NixOS installer or another nixos-anywhere-compatible + Linux rescue environment with root SSH access. Keep this environment + running while completing the SOPS bootstrap below. + +## Bootstrap SOPS + +Use the rescue environment's SSH host key as the permanent portal identity. +Replace `VM_IP` below: + +```console +ssh root@VM_IP 'cat /etc/ssh/ssh_host_ed25519_key.pub' | \ + nix shell nixpkgs#ssh-to-age --command ssh-to-age +``` + +This prints an `age1...` recipient; it does not copy the private key. Add the +recipient to `.sops.yaml`: + +```yaml +- &system_portal_1 age1... +``` + +Then add `*system_portal_1` to the age recipients for +`users/secrets.yaml`. Re-encrypt the existing file for the new recipient and +add the Tailscale key: + +```console +nix shell nixpkgs#sops --command sops updatekeys users/secrets.yaml +nix shell nixpkgs#sops --command sops users/secrets.yaml +``` + +Add the OAuth client secret from the `Auth Keys: Write` credential in the SOPS +editor and save it: + +```yaml +tailscale_auth_key: tskey-client-... +``` + +## Deploy + +From the repository root, replace `VM_IP` with the VM's public IP: + +```console +nix run github:nix-community/nixos-anywhere -- \ + --copy-host-keys --flake .#portal_1 root@VM_IP +``` + +This repartitions `/dev/vda`, so anything already on that disk is erased. The +layout reserves 8 GiB for swap and assigns the remaining space to the root +filesystem. + +`--copy-host-keys` preserves the same private SSH host key at +`/etc/ssh/ssh_host_ed25519_key` on the installed system. SOPS-Nix converts that +key to an age identity during activation. After the reboot, connect as +`richie` and verify that automatic Tailscale enrollment succeeded: + +```console +ssh -p 278 richie@VM_IP +sudo tailscale status +``` + +The installed OpenSSH service listens on port 278. Port 22 is served by +Endlessh and will not provide an SSH login. + +HAProxy uses the same frontend, routing, and rate-limiting configuration as +Jeeves. Portal manages the ACME certificates for the existing public domains; +their DNS records must resolve to Portal for HTTP-01 issuance and renewal. + +The application backends still use Jeeves' original `127.0.0.1` addresses. +Replace them with the corresponding Tailscale addresses before directing +application traffic through Portal. Ports 80 and 443 are allowed through the +firewall. diff --git a/systems/portal_1/default.nix b/systems/portal_1/default.nix new file mode 100644 index 0000000..3231c6e --- /dev/null +++ b/systems/portal_1/default.nix @@ -0,0 +1,55 @@ +{ + inputs, + lib, + pkgs, + ... +}: +{ + imports = [ + inputs.disko.nixosModules.disko + "${inputs.self}/common/global" + "${inputs.self}/users/richie" + "${inputs.self}/common/optional/tailscale.nix" + ./disk-config.nix + ]; + + nixpkgs.hostPlatform = "x86_64-linux"; + + boot = { + # Avoid consuming the VM's limited memory for /tmp. + tmp.useTmpfs = false; + + # The Vultr system disk and NIC are exposed as virtio devices. + initrd.availableKernelModules = [ + "virtio_pci" + "virtio_blk" + "virtio_scsi" + "sd_mod" + ]; + }; + + networking = { + hostName = "portal-1"; + useDHCP = lib.mkDefault true; + + firewall = { + enable = true; + allowedTCPPorts = [ 278 ]; + }; + }; + + services.openssh.ports = [ 278 ]; + + environment.systemPackages = with pkgs; [ + curl + htop + vim + ]; + + # The VM uses its regular swap instead of compressed RAM swap. + zramSwap.enable = false; + + time.timeZone = "Etc/UTC"; + + system.stateVersion = "24.05"; +} diff --git a/systems/portal_1/disk-config.nix b/systems/portal_1/disk-config.nix new file mode 100644 index 0000000..9e52244 --- /dev/null +++ b/systems/portal_1/disk-config.nix @@ -0,0 +1,59 @@ +{ ... }: +{ + # Vultr's first virtio disk is normally /dev/vda. Change this before + # deployment if the selected image exposes its system disk differently. + disko.devices.disk.main = { + type = "disk"; + device = "/dev/vda"; + content = { + type = "gpt"; + partitions = { + bios = { + size = "1M"; + type = "EF02"; + }; + + ESP = { + size = "512M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ + "fmask=0077" + "dmask=0077" + ]; + }; + }; + + swap = { + size = "8G"; + content = { + type = "swap"; + }; + }; + + root = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + }; + }; + }; + }; + + # Install GRUB for both legacy BIOS and EFI-capable Vultr plans. + boot.loader = { + grub = { + enable = true; + devices = [ "/dev/vda" ]; + efiSupport = true; + efiInstallAsRemovable = true; + }; + efi.canTouchEfiVariables = false; + }; +} diff --git a/users/richie/systems/portal-1.nix b/users/richie/systems/portal-1.nix new file mode 100644 index 0000000..b9d9b52 --- /dev/null +++ b/users/richie/systems/portal-1.nix @@ -0,0 +1,5 @@ +{ + imports = [ + ../home/minimal.nix + ]; +} -- 2.55.0 From 5bc9ee5de9e9aeabbc72d490922a17e311c4aad7 Mon Sep 17 00:00:00 2001 From: Richie Cahill Date: Tue, 25 Aug 2026 18:10:05 -0400 Subject: [PATCH 06/16] =?UTF-8?q?feat(home):=20expand=20Richie=E2=80=99s?= =?UTF-8?q?=20minimal=20profile=20for=20Portal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - move essential CLI tools into the minimal package set - remove duplicated packages from the full programs profile - add the portal-1 SSH host on port 278 --- users/richie/home/minimal.nix | 12 +++++++++++- users/richie/home/programs.nix | 6 ------ users/richie/home/ssh_config.nix | 6 ++++++ 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/users/richie/home/minimal.nix b/users/richie/home/minimal.nix index 30928fa..029bea9 100644 --- a/users/richie/home/minimal.nix +++ b/users/richie/home/minimal.nix @@ -1,4 +1,4 @@ -{ config, ... }: +{ config, pkgs, ... }: { imports = [ ./cli @@ -13,5 +13,15 @@ homeDirectory = "/home/${config.home.username}"; stateVersion = "24.05"; sessionVariables.FLAKE = "$HOME/dotfiles"; + packages = with pkgs; [ + # cli + btop + eza + ripgrep + starship + tmux + # networking + wget + ]; }; } diff --git a/users/richie/home/programs.nix b/users/richie/home/programs.nix index 4cd0eb2..d380da4 100644 --- a/users/richie/home/programs.nix +++ b/users/richie/home/programs.nix @@ -3,8 +3,6 @@ home.packages = with pkgs; [ # cli bat - btop - eza fd ffmpegthumbnailer fzf @@ -18,9 +16,6 @@ p7zip poppler rar - ripgrep - starship - tmux unzip yazi zoxide @@ -37,7 +32,6 @@ # networking iperf3 nmap - wget # python ruff uv diff --git a/users/richie/home/ssh_config.nix b/users/richie/home/ssh_config.nix index e56ed33..2bd41de 100644 --- a/users/richie/home/ssh_config.nix +++ b/users/richie/home/ssh_config.nix @@ -43,6 +43,12 @@ IdentityFile = "~/.ssh/id_ed25519"; Port = 922; }; + portal-1 = { + HostName = "portal-1"; + User = "richie"; + IdentityFile = "~/.ssh/id_ed25519"; + Port = 278; + }; }; }; } -- 2.55.0 From 1c4e0c082891bde116295001d209c0b32f17eb12 Mon Sep 17 00:00:00 2001 From: Richie Cahill Date: Tue, 25 Aug 2026 18:49:03 -0400 Subject: [PATCH 07/16] renamed portal_1 to portal-1 --- systems/{portal_1 => portal-1}/README.md | 0 systems/{portal_1 => portal-1}/default.nix | 0 systems/{portal_1 => portal-1}/disk-config.nix | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename systems/{portal_1 => portal-1}/README.md (100%) rename systems/{portal_1 => portal-1}/default.nix (100%) rename systems/{portal_1 => portal-1}/disk-config.nix (100%) diff --git a/systems/portal_1/README.md b/systems/portal-1/README.md similarity index 100% rename from systems/portal_1/README.md rename to systems/portal-1/README.md diff --git a/systems/portal_1/default.nix b/systems/portal-1/default.nix similarity index 100% rename from systems/portal_1/default.nix rename to systems/portal-1/default.nix diff --git a/systems/portal_1/disk-config.nix b/systems/portal-1/disk-config.nix similarity index 100% rename from systems/portal_1/disk-config.nix rename to systems/portal-1/disk-config.nix -- 2.55.0 From 8245d0f82efd34c13a7fb5c42766806878cc5ba4 Mon Sep 17 00:00:00 2001 From: Richie Cahill Date: Tue, 25 Aug 2026 21:19:01 -0400 Subject: [PATCH 08/16] feat(tailscale): enable mesh networking across NixOS hosts - enable Tailscale on Bob, Brain, Jeeves, and Rhapsody - configure the Jeeves binary cache through Tailscale MagicDNS - trust the existing workshop cache signing key --- common/optional/tailscale.nix | 6 ++++++ systems/bob/default.nix | 1 + systems/brain/default.nix | 1 + systems/jeeves/default.nix | 1 + systems/portal-1/default.nix | 2 +- systems/rhapsody-in-green/default.nix | 1 + 6 files changed, 11 insertions(+), 1 deletion(-) diff --git a/common/optional/tailscale.nix b/common/optional/tailscale.nix index 31528ec..3242c64 100644 --- a/common/optional/tailscale.nix +++ b/common/optional/tailscale.nix @@ -6,6 +6,12 @@ { networking.firewall.trustedInterfaces = [ "tailscale0" ]; + nix.settings = { + trusted-substituters = [ "http://jeeves:5000" ]; + substituters = [ "http://jeeves:5000/?priority=1&want-mass-query=true" ]; + trusted-public-keys = [ "cache.tmmworkshop.com:jHffkpgbmEdstQPoihJPYW9TQe6jnQbWR2LqkNGV3iA=" ]; + }; + services.tailscale = { enable = true; openFirewall = true; diff --git a/systems/bob/default.nix b/systems/bob/default.nix index d404652..5f237cf 100644 --- a/systems/bob/default.nix +++ b/systems/bob/default.nix @@ -12,6 +12,7 @@ "${inputs.self}/common/optional/steam.nix" "${inputs.self}/common/optional/syncthing_base.nix" "${inputs.self}/common/optional/systemd-boot.nix" + "${inputs.self}/common/optional/tailscale.nix" "${inputs.self}/common/optional/update.nix" "${inputs.self}/common/optional/yubikey.nix" "${inputs.self}/common/optional/zerotier.nix" diff --git a/systems/brain/default.nix b/systems/brain/default.nix index 4033ade..d63847f 100644 --- a/systems/brain/default.nix +++ b/systems/brain/default.nix @@ -7,6 +7,7 @@ "${inputs.self}/common/optional/ssh_decrypt.nix" "${inputs.self}/common/optional/syncthing_base.nix" "${inputs.self}/common/optional/systemd-boot.nix" + "${inputs.self}/common/optional/tailscale.nix" "${inputs.self}/common/optional/update.nix" "${inputs.self}/common/optional/zerotier.nix" "${inputs.self}/common/optional/zfs" diff --git a/systems/jeeves/default.nix b/systems/jeeves/default.nix index a7f6697..8f694bb 100644 --- a/systems/jeeves/default.nix +++ b/systems/jeeves/default.nix @@ -13,6 +13,7 @@ in "${inputs.self}/common/optional/monitoring-agent.nix" "${inputs.self}/common/optional/ssh_decrypt.nix" "${inputs.self}/common/optional/syncthing_base.nix" + "${inputs.self}/common/optional/tailscale.nix" "${inputs.self}/common/optional/update.nix" "${inputs.self}/common/optional/zerotier.nix" "${inputs.self}/common/optional/zfs" diff --git a/systems/portal-1/default.nix b/systems/portal-1/default.nix index 3231c6e..a6f7fcd 100644 --- a/systems/portal-1/default.nix +++ b/systems/portal-1/default.nix @@ -7,8 +7,8 @@ { imports = [ inputs.disko.nixosModules.disko - "${inputs.self}/common/global" "${inputs.self}/users/richie" + "${inputs.self}/common/global" "${inputs.self}/common/optional/tailscale.nix" ./disk-config.nix ]; diff --git a/systems/rhapsody-in-green/default.nix b/systems/rhapsody-in-green/default.nix index b5e8957..0e229bb 100644 --- a/systems/rhapsody-in-green/default.nix +++ b/systems/rhapsody-in-green/default.nix @@ -8,6 +8,7 @@ "${inputs.self}/common/optional/steam.nix" "${inputs.self}/common/optional/syncthing_base.nix" "${inputs.self}/common/optional/systemd-boot.nix" + "${inputs.self}/common/optional/tailscale.nix" "${inputs.self}/common/optional/yubikey.nix" "${inputs.self}/common/optional/zerotier.nix" "${inputs.self}/common/optional/zfs" -- 2.55.0 From 7f08a6f788976b6625a9c540eec668c272617f78 Mon Sep 17 00:00:00 2001 From: Richie Cahill Date: Tue, 25 Aug 2026 22:09:44 -0400 Subject: [PATCH 09/16] feat(portal): move edge proxy services from Jeeves - move HAProxy and ACME certificate management to Portal - route application backends to Jeeves over Tailscale - restrict Jeeves backend ports to the Tailscale interface - expose Gems for remote proxy access --- common/optional/tailscale.nix | 2 -- systems/jeeves/networking.nix | 9 +++++++++ systems/jeeves/services/gems.nix | 2 +- systems/jeeves/services/gitea.nix | 1 - systems/jeeves/services/jellyfin.nix | 2 +- systems/jeeves/services/nix_serve.nix | 2 +- systems/jeeves/web_services/default.nix | 9 --------- .../web_services => portal-1/haproxy}/acme.nix | 13 +++++-------- .../haproxy.nix => portal-1/haproxy/default.nix} | 3 +++ .../haproxy}/haproxy.cfg | 16 ++++++++-------- .../web_services => portal-1/haproxy}/robots.txt | 0 11 files changed, 28 insertions(+), 31 deletions(-) delete mode 100644 systems/jeeves/web_services/default.nix rename systems/{jeeves/web_services => portal-1/haproxy}/acme.nix (77%) rename systems/{jeeves/web_services/haproxy.nix => portal-1/haproxy/default.nix} (88%) rename systems/{jeeves/web_services => portal-1/haproxy}/haproxy.cfg (96%) rename systems/{jeeves/web_services => portal-1/haproxy}/robots.txt (100%) diff --git a/common/optional/tailscale.nix b/common/optional/tailscale.nix index 3242c64..f99f500 100644 --- a/common/optional/tailscale.nix +++ b/common/optional/tailscale.nix @@ -4,8 +4,6 @@ ... }: { - networking.firewall.trustedInterfaces = [ "tailscale0" ]; - nix.settings = { trusted-substituters = [ "http://jeeves:5000" ]; substituters = [ "http://jeeves:5000/?priority=1&want-mass-query=true" ]; diff --git a/systems/jeeves/networking.nix b/systems/jeeves/networking.nix index 9be9867..f87c1cf 100644 --- a/systems/jeeves/networking.nix +++ b/systems/jeeves/networking.nix @@ -17,6 +17,15 @@ allowedTCPPorts = [ ]; allowedUDPPorts = [ ]; }; + interfaces.tailscale0.allowedTCPPorts = [ + 5000 # Nix binary cache + 6443 # Gitea HTTP + 8000 # Audiobookshelf + 8001 # Norn Sight + 8002 # Gems + 8091 # Share + 8096 # Jellyfin HTTP + ]; allowedTCPPorts = [ 8070 ]; diff --git a/systems/jeeves/services/gems.nix b/systems/jeeves/services/gems.nix index 1f52e60..9164d1e 100644 --- a/systems/jeeves/services/gems.nix +++ b/systems/jeeves/services/gems.nix @@ -36,7 +36,7 @@ in Type = "simple"; User = "gems"; Group = "gems"; - ExecStart = "${pkgs.my_python}/bin/python -m python.gems.main --host 127.0.0.1 --port 8002"; + ExecStart = "${pkgs.my_python}/bin/python -m python.gems.main --host 0.0.0.0 --port 8002"; Restart = "on-failure"; RestartSec = "5s"; StandardOutput = "journal"; diff --git a/systems/jeeves/services/gitea.nix b/systems/jeeves/services/gitea.nix index 2ea98c4..988da6f 100644 --- a/systems/jeeves/services/gitea.nix +++ b/systems/jeeves/services/gitea.nix @@ -3,7 +3,6 @@ let in { networking.firewall.allowedTCPPorts = [ - 6443 2223 ]; diff --git a/systems/jeeves/services/jellyfin.nix b/systems/jeeves/services/jellyfin.nix index 0044e04..eb8c696 100644 --- a/systems/jeeves/services/jellyfin.nix +++ b/systems/jeeves/services/jellyfin.nix @@ -4,7 +4,7 @@ in { services.jellyfin = { enable = true; - openFirewall = true; + openFirewall = false; dataDir = "${vars.services}/jellyfin"; cacheDir = "${vars.services}/jellyfin/cache"; }; diff --git a/systems/jeeves/services/nix_serve.nix b/systems/jeeves/services/nix_serve.nix index d0811b1..6fdc67a 100644 --- a/systems/jeeves/services/nix_serve.nix +++ b/systems/jeeves/services/nix_serve.nix @@ -7,6 +7,6 @@ in package = pkgs.nix-serve-ng; enable = true; secretKeyFile = "${vars.secrets}/services/nix-cache/cache-priv-key.pem"; - openFirewall = true; + openFirewall = false; }; } diff --git a/systems/jeeves/web_services/default.nix b/systems/jeeves/web_services/default.nix deleted file mode 100644 index 1133fcb..0000000 --- a/systems/jeeves/web_services/default.nix +++ /dev/null @@ -1,9 +0,0 @@ -{ lib, ... }: -{ - imports = - let - files = builtins.attrNames (builtins.readDir ./.); - nixFiles = builtins.filter (name: lib.hasSuffix ".nix" name && name != "default.nix") files; - in - map (file: ./. + "/${file}") nixFiles; -} diff --git a/systems/jeeves/web_services/acme.nix b/systems/portal-1/haproxy/acme.nix similarity index 77% rename from systems/jeeves/web_services/acme.nix rename to systems/portal-1/haproxy/acme.nix index eede8f2..e5a42ac 100644 --- a/systems/jeeves/web_services/acme.nix +++ b/systems/portal-1/haproxy/acme.nix @@ -32,15 +32,12 @@ let ++ map (domain: "acme-${domain}.service") extraDomains; in { - users.users.haproxy.extraGroups = [ "acme" ]; - security.acme = { acceptTerms = true; defaults.email = "Richie@tmmworkshop.com"; certs = builtins.listToAttrs ((map makeCert domains) ++ (map makeExtraCert extraDomains)); }; - # Minimal nginx to serve ACME HTTP-01 challenge files for HAProxy services.nginx = { enable = true; virtualHosts."acme-challenge" = { @@ -56,18 +53,18 @@ in }; }; - # Ensure the challenge directory exists with correct permissions systemd.tmpfiles.rules = [ "d /var/lib/acme/.challenges 0750 acme acme - -" "d /var/lib/acme/.challenges/.well-known 0750 acme acme - -" "d /var/lib/acme/.challenges/.well-known/acme-challenge 0750 acme acme - -" ]; - users.users.nginx.extraGroups = [ "acme" ]; + users.users = { + haproxy.extraGroups = [ "acme" ]; + nginx.extraGroups = [ "acme" ]; + }; - # HAProxy needs certs to exist before it can bind :443. - # NixOS's acme module generates self-signed placeholders on first boot - # via acme-.service — just make HAProxy wait for them. + # HAProxy needs the initial certificates before it can bind to port 443. systemd.services.haproxy = { after = acmeServices; wants = acmeServices; diff --git a/systems/jeeves/web_services/haproxy.nix b/systems/portal-1/haproxy/default.nix similarity index 88% rename from systems/jeeves/web_services/haproxy.nix rename to systems/portal-1/haproxy/default.nix index 47c7c1f..ff8ded6 100644 --- a/systems/jeeves/web_services/haproxy.nix +++ b/systems/portal-1/haproxy/default.nix @@ -1,4 +1,7 @@ +{ ... }: { + imports = [ ./acme.nix ]; + networking.firewall.allowedTCPPorts = [ 80 443 diff --git a/systems/jeeves/web_services/haproxy.cfg b/systems/portal-1/haproxy/haproxy.cfg similarity index 96% rename from systems/jeeves/web_services/haproxy.cfg rename to systems/portal-1/haproxy/haproxy.cfg index 2eefc63..8edd318 100644 --- a/systems/jeeves/web_services/haproxy.cfg +++ b/systems/portal-1/haproxy/haproxy.cfg @@ -120,35 +120,35 @@ backend st_gems_join backend acme_challenge mode http - server acme 127.0.0.1:8402 + server acme jeeves:8402 backend audiobookshelf_nodes mode http - server server 127.0.0.1:8000 + server server jeeves:8000 backend cache_nodes mode http - server server 127.0.0.1:5000 + server server jeeves:5000 backend jellyfin mode http option forwardfor - server jellyfin 127.0.0.1:8096 + server jellyfin jeeves:8096 backend share_nodes mode http - server server 127.0.0.1:8091 + server server jeeves:8091 backend gitea mode http - server server 127.0.0.1:6443 + server server jeeves:6443 backend gems mode http option forwardfor timeout server 1h - server gems 127.0.0.1:8002 + server gems jeeves:8002 backend norn_sight mode http - server server 127.0.0.1:8001 + server server jeeves:8001 diff --git a/systems/jeeves/web_services/robots.txt b/systems/portal-1/haproxy/robots.txt similarity index 100% rename from systems/jeeves/web_services/robots.txt rename to systems/portal-1/haproxy/robots.txt -- 2.55.0 From cb83fc6ea514b2f65ab3b18afed25195dd8e3b49 Mon Sep 17 00:00:00 2001 From: Richie Cahill Date: Wed, 26 Aug 2026 07:12:51 -0400 Subject: [PATCH 10/16] fix 2 imports --- systems/jeeves/default.nix | 1 - systems/portal-1/default.nix | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/systems/jeeves/default.nix b/systems/jeeves/default.nix index 8f694bb..021288f 100644 --- a/systems/jeeves/default.nix +++ b/systems/jeeves/default.nix @@ -20,7 +20,6 @@ in ./monitoring ./docker ./services - ./web_services ./hardware.nix ./networking.nix ./programs.nix diff --git a/systems/portal-1/default.nix b/systems/portal-1/default.nix index a6f7fcd..4bf111e 100644 --- a/systems/portal-1/default.nix +++ b/systems/portal-1/default.nix @@ -11,6 +11,7 @@ "${inputs.self}/common/global" "${inputs.self}/common/optional/tailscale.nix" ./disk-config.nix + ./haproxy ]; nixpkgs.hostPlatform = "x86_64-linux"; -- 2.55.0 From df8908b7087dc644645c3584fe0bd74d12dcbc17 Mon Sep 17 00:00:00 2001 From: Richie Cahill Date: Wed, 26 Aug 2026 07:22:43 -0400 Subject: [PATCH 11/16] feat(services): remove haproxy from validate_system.toml --- systems/jeeves/services/validate_system.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/systems/jeeves/services/validate_system.toml b/systems/jeeves/services/validate_system.toml index 2c73862..11214b8 100644 --- a/systems/jeeves/services/validate_system.toml +++ b/systems/jeeves/services/validate_system.toml @@ -1,7 +1,6 @@ zpool = ["root_pool", "storage", "media"] services = [ "audiobookshelf", - "haproxy", "docker", "jellyfin", ] -- 2.55.0 From 219aea9b5f2c887bdf43959d1faa857641976077 Mon Sep 17 00:00:00 2001 From: Richie Cahill Date: Wed, 26 Aug 2026 07:29:39 -0400 Subject: [PATCH 12/16] fix(acme) used the correct ip for acme --- systems/portal-1/haproxy/haproxy.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/systems/portal-1/haproxy/haproxy.cfg b/systems/portal-1/haproxy/haproxy.cfg index 8edd318..8800b17 100644 --- a/systems/portal-1/haproxy/haproxy.cfg +++ b/systems/portal-1/haproxy/haproxy.cfg @@ -120,7 +120,7 @@ backend st_gems_join backend acme_challenge mode http - server acme jeeves:8402 + server acme 127.0.0.1:8080 backend audiobookshelf_nodes mode http -- 2.55.0 From 515cbae3f8c0d5a1e758a38ced5aaf59c97dcaa9 Mon Sep 17 00:00:00 2001 From: Richie Cahill Date: Wed, 26 Aug 2026 07:39:19 -0400 Subject: [PATCH 13/16] fix(audiobookshelf): updated host --- systems/jeeves/services/audiobookshelf.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/systems/jeeves/services/audiobookshelf.nix b/systems/jeeves/services/audiobookshelf.nix index 47854c7..c530e6a 100644 --- a/systems/jeeves/services/audiobookshelf.nix +++ b/systems/jeeves/services/audiobookshelf.nix @@ -6,6 +6,7 @@ in services.audiobookshelf = { enable = true; port = 8000; + host = "0.0.0.0"; }; systemd.services.audiobookshelf.serviceConfig.WorkingDirectory = lib.mkForce "${vars.docker_configs}/audiobookshelf"; -- 2.55.0 From e0c14973b084e24789dcdf63e6e640c7de6c0745 Mon Sep 17 00:00:00 2001 From: Richie Cahill Date: Wed, 26 Aug 2026 08:28:41 -0400 Subject: [PATCH 14/16] refactor(haproxy): consolidate ACME configuration Merge the ACME module into the HAProxy configuration and start HAProxy only after certificates and Tailscale connectivity are available. --- systems/portal-1/haproxy/acme.nix | 72 ------------------------ systems/portal-1/haproxy/default.nix | 83 ++++++++++++++++++++++++++-- 2 files changed, 77 insertions(+), 78 deletions(-) delete mode 100644 systems/portal-1/haproxy/acme.nix diff --git a/systems/portal-1/haproxy/acme.nix b/systems/portal-1/haproxy/acme.nix deleted file mode 100644 index e5a42ac..0000000 --- a/systems/portal-1/haproxy/acme.nix +++ /dev/null @@ -1,72 +0,0 @@ -let - domains = [ - "audiobookshelf" - "cache" - "gitea" - "gems" - "jellyfin" - "share" - ]; - extraDomains = [ "www.norn-sight.com" ]; - - makeCert = name: { - name = "${name}.tmmworkshop.com"; - value = { - webroot = "/var/lib/acme/.challenges"; - group = "acme"; - reloadServices = [ "haproxy.service" ]; - }; - }; - - makeExtraCert = name: { - inherit name; - value = { - webroot = "/var/lib/acme/.challenges"; - group = "acme"; - reloadServices = [ "haproxy.service" ]; - }; - }; - - acmeServices = - map (domain: "acme-${domain}.tmmworkshop.com.service") domains - ++ map (domain: "acme-${domain}.service") extraDomains; -in -{ - security.acme = { - acceptTerms = true; - defaults.email = "Richie@tmmworkshop.com"; - certs = builtins.listToAttrs ((map makeCert domains) ++ (map makeExtraCert extraDomains)); - }; - - services.nginx = { - enable = true; - virtualHosts."acme-challenge" = { - listen = [ - { - addr = "127.0.0.1"; - port = 8402; - } - ]; - locations."/.well-known/acme-challenge/" = { - root = "/var/lib/acme/.challenges"; - }; - }; - }; - - systemd.tmpfiles.rules = [ - "d /var/lib/acme/.challenges 0750 acme acme - -" - "d /var/lib/acme/.challenges/.well-known 0750 acme acme - -" - "d /var/lib/acme/.challenges/.well-known/acme-challenge 0750 acme acme - -" - ]; - - users.users = { - haproxy.extraGroups = [ "acme" ]; - nginx.extraGroups = [ "acme" ]; - }; - - # HAProxy needs the initial certificates before it can bind to port 443. - systemd.services.haproxy = { - after = acmeServices; - wants = acmeServices; - }; -} diff --git a/systems/portal-1/haproxy/default.nix b/systems/portal-1/haproxy/default.nix index ff8ded6..82c4819 100644 --- a/systems/portal-1/haproxy/default.nix +++ b/systems/portal-1/haproxy/default.nix @@ -1,7 +1,37 @@ -{ ... }: -{ - imports = [ ./acme.nix ]; +let + domains = [ + "audiobookshelf" + "cache" + "gitea" + "gems" + "jellyfin" + "share" + ]; + extraDomains = [ "www.norn-sight.com" ]; + makeCert = name: { + name = "${name}.tmmworkshop.com"; + value = { + webroot = "/var/lib/acme/.challenges"; + group = "acme"; + reloadServices = [ "haproxy.service" ]; + }; + }; + + makeExtraCert = name: { + inherit name; + value = { + webroot = "/var/lib/acme/.challenges"; + group = "acme"; + reloadServices = [ "haproxy.service" ]; + }; + }; + + acmeServices = + map (domain: "acme-${domain}.tmmworkshop.com.service") domains + ++ map (domain: "acme-${domain}.service") extraDomains; +in +{ networking.firewall.allowedTCPPorts = [ 80 443 @@ -10,8 +40,49 @@ # Global robots.txt served by HAProxy for every vhost (see haproxy.cfg). environment.etc."haproxy/robots.txt".source = ./robots.txt; - services.haproxy = { - enable = true; - config = builtins.readFile ./haproxy.cfg; + services = { + haproxy = { + enable = true; + config = builtins.readFile ./haproxy.cfg; + }; + nginx = { + enable = true; + virtualHosts."acme-challenge" = { + listen = [ + { + addr = "127.0.0.1"; + port = 8402; + } + ]; + locations."/.well-known/acme-challenge/" = { + root = "/var/lib/acme/.challenges"; + }; + }; + }; + }; + + security.acme = { + acceptTerms = true; + defaults.email = "Richie@tmmworkshop.com"; + certs = builtins.listToAttrs ((map makeCert domains) ++ (map makeExtraCert extraDomains)); + }; + + systemd = { + services.haproxy = { + # HAProxy needs the initial certificates and Tailscale connectivity before + # it can serve requests to its backends. + after = acmeServices ++ [ "tailscaled-autoconnect.service" ]; + wants = acmeServices ++ [ "tailscaled-autoconnect.service" ]; + }; + tmpfiles.rules = [ + "d /var/lib/acme/.challenges 0750 acme acme - -" + "d /var/lib/acme/.challenges/.well-known 0750 acme acme - -" + "d /var/lib/acme/.challenges/.well-known/acme-challenge 0750 acme acme - -" + ]; + }; + + users.users = { + haproxy.extraGroups = [ "acme" ]; + nginx.extraGroups = [ "acme" ]; }; } -- 2.55.0 From b406b926227c99d873ccb7ebfbf6bd3e601ce894 Mon Sep 17 00:00:00 2001 From: Richie Cahill Date: Wed, 26 Aug 2026 08:37:22 -0400 Subject: [PATCH 15/16] fix(networking): restore Gitea HTTP port in allowedTCPPorts --- systems/jeeves/networking.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/systems/jeeves/networking.nix b/systems/jeeves/networking.nix index f87c1cf..8686a42 100644 --- a/systems/jeeves/networking.nix +++ b/systems/jeeves/networking.nix @@ -19,7 +19,6 @@ }; interfaces.tailscale0.allowedTCPPorts = [ 5000 # Nix binary cache - 6443 # Gitea HTTP 8000 # Audiobookshelf 8001 # Norn Sight 8002 # Gems @@ -27,7 +26,8 @@ 8096 # Jellyfin HTTP ]; allowedTCPPorts = [ - 8070 + 6443 # Gitea HTTP + 8070 # ebook searcher ]; }; useNetworkd = true; -- 2.55.0 From f5a0038ab7654eb0d0d73b8d2e1d99cbb25ccf50 Mon Sep 17 00:00:00 2001 From: Richie Cahill Date: Wed, 26 Aug 2026 10:43:21 -0400 Subject: [PATCH 16/16] feat(portal): proxy Gitea SSH through HAProxy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - expose Gitea SSH through Portal on port 2223 - advertise the correct Gitea SSH user and domain - add the Gitea host to Richie’s SSH config - correct the local ACME challenge backend port --- systems/jeeves/services/gitea.nix | 6 +++++- systems/portal-1/haproxy/default.nix | 1 + systems/portal-1/haproxy/haproxy.cfg | 14 +++++++++++++- users/richie/home/ssh_config.nix | 6 ++++++ 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/systems/jeeves/services/gitea.nix b/systems/jeeves/services/gitea.nix index 988da6f..3e84d3d 100644 --- a/systems/jeeves/services/gitea.nix +++ b/systems/jeeves/services/gitea.nix @@ -26,10 +26,14 @@ in }; service.DISABLE_REGISTRATION = true; server = { - DOMAIN = "tmmworkshop.com"; + DOMAIN = "gitea.tmmworkshop.com"; ROOT_URL = "https://gitea.tmmworkshop.com/"; HTTP_PORT = 6443; + BUILTIN_SSH_SERVER_USER = "gitea"; + SSH_USER = "gitea"; + SSH_DOMAIN = "gitea.tmmworkshop.com"; SSH_PORT = 2223; + SSH_LISTEN_HOST = "0.0.0.0"; SSH_LISTEN_PORT = 2223; START_SSH_SERVER = true; PUBLIC_URL_DETECTION = "auto"; diff --git a/systems/portal-1/haproxy/default.nix b/systems/portal-1/haproxy/default.nix index 82c4819..a885da4 100644 --- a/systems/portal-1/haproxy/default.nix +++ b/systems/portal-1/haproxy/default.nix @@ -35,6 +35,7 @@ in networking.firewall.allowedTCPPorts = [ 80 443 + 2223 ]; # Global robots.txt served by HAProxy for every vhost (see haproxy.cfg). diff --git a/systems/portal-1/haproxy/haproxy.cfg b/systems/portal-1/haproxy/haproxy.cfg index 8800b17..36ada84 100644 --- a/systems/portal-1/haproxy/haproxy.cfg +++ b/systems/portal-1/haproxy/haproxy.cfg @@ -110,6 +110,14 @@ frontend ContentSwitching use_backend gems if host_gems use_backend norn_sight if host_norn_sight +# Proxy Gitea's built-in SSH server over Tailscale to Jeeves. Port 22 remains +# available to Endlessh on Portal. +frontend GiteaSSH + bind *:2223 v4v6 + mode tcp + option tcplog + default_backend gitea_ssh + # Stick-table only (no servers): tracks per-IP request rate to Gitea's compare # endpoint over a 5-minute window so the frontend can cap it at 1 per 5 min. backend st_compare @@ -120,7 +128,7 @@ backend st_gems_join backend acme_challenge mode http - server acme 127.0.0.1:8080 + server acme 127.0.0.1:8402 backend audiobookshelf_nodes mode http @@ -143,6 +151,10 @@ backend gitea mode http server server jeeves:6443 +backend gitea_ssh + mode tcp + server gitea jeeves:2223 + backend gems mode http option forwardfor diff --git a/users/richie/home/ssh_config.nix b/users/richie/home/ssh_config.nix index 2bd41de..9074a26 100644 --- a/users/richie/home/ssh_config.nix +++ b/users/richie/home/ssh_config.nix @@ -49,6 +49,12 @@ IdentityFile = "~/.ssh/id_ed25519"; Port = 278; }; + "gitea.tmmworkshop.com" = { + HostName = "gitea.tmmworkshop.com"; + User = "gitea"; + IdentityFile = "~/.ssh/id_ed25519"; + Port = 2223; + }; }; }; } -- 2.55.0