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/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/tailscale.nix b/common/optional/tailscale.nix new file mode 100644 index 0000000..f99f500 --- /dev/null +++ b/common/optional/tailscale.nix @@ -0,0 +1,37 @@ +{ + config, + inputs, + ... +}: +{ + 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; + 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/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/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 = diff --git a/systems/bob/default.nix b/systems/bob/default.nix index d5eb01e..5f237cf 100644 --- a/systems/bob/default.nix +++ b/systems/bob/default.nix @@ -6,15 +6,17 @@ "${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/tailscale.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..d63847f 100644 --- a/systems/brain/default.nix +++ b/systems/brain/default.nix @@ -7,8 +7,10 @@ "${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" ./docker ./hardware.nix ./programs.nix diff --git a/systems/jeeves/default.nix b/systems/jeeves/default.nix index 1d47f8d..021288f 100644 --- a/systems/jeeves/default.nix +++ b/systems/jeeves/default.nix @@ -13,12 +13,13 @@ 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" ./monitoring ./docker ./services - ./web_services ./hardware.nix ./networking.nix ./programs.nix diff --git a/systems/jeeves/networking.nix b/systems/jeeves/networking.nix index 9be9867..8686a42 100644 --- a/systems/jeeves/networking.nix +++ b/systems/jeeves/networking.nix @@ -17,8 +17,17 @@ allowedTCPPorts = [ ]; allowedUDPPorts = [ ]; }; + interfaces.tailscale0.allowedTCPPorts = [ + 5000 # Nix binary cache + 8000 # Audiobookshelf + 8001 # Norn Sight + 8002 # Gems + 8091 # Share + 8096 # Jellyfin HTTP + ]; allowedTCPPorts = [ - 8070 + 6443 # Gitea HTTP + 8070 # ebook searcher ]; }; useNetworkd = true; 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"; 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..3e84d3d 100644 --- a/systems/jeeves/services/gitea.nix +++ b/systems/jeeves/services/gitea.nix @@ -3,7 +3,6 @@ let in { networking.firewall.allowedTCPPorts = [ - 6443 2223 ]; @@ -27,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/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/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", ] diff --git a/systems/jeeves/web_services/acme.nix b/systems/jeeves/web_services/acme.nix deleted file mode 100644 index eede8f2..0000000 --- a/systems/jeeves/web_services/acme.nix +++ /dev/null @@ -1,75 +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 -{ - 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" = { - listen = [ - { - addr = "127.0.0.1"; - port = 8402; - } - ]; - locations."/.well-known/acme-challenge/" = { - root = "/var/lib/acme/.challenges"; - }; - }; - }; - - # 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" ]; - - # 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. - systemd.services.haproxy = { - after = acmeServices; - wants = acmeServices; - }; -} 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/haproxy.nix b/systems/jeeves/web_services/haproxy.nix deleted file mode 100644 index 47c7c1f..0000000 --- a/systems/jeeves/web_services/haproxy.nix +++ /dev/null @@ -1,14 +0,0 @@ -{ - networking.firewall.allowedTCPPorts = [ - 80 - 443 - ]; - - # 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; - }; -} 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..4bf111e --- /dev/null +++ b/systems/portal-1/default.nix @@ -0,0 +1,56 @@ +{ + inputs, + lib, + pkgs, + ... +}: +{ + imports = [ + inputs.disko.nixosModules.disko + "${inputs.self}/users/richie" + "${inputs.self}/common/global" + "${inputs.self}/common/optional/tailscale.nix" + ./disk-config.nix + ./haproxy + ]; + + 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/systems/portal-1/haproxy/default.nix b/systems/portal-1/haproxy/default.nix new file mode 100644 index 0000000..a885da4 --- /dev/null +++ b/systems/portal-1/haproxy/default.nix @@ -0,0 +1,89 @@ +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 + 2223 + ]; + + # 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; + }; + 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" ]; + }; +} diff --git a/systems/jeeves/web_services/haproxy.cfg b/systems/portal-1/haproxy/haproxy.cfg similarity index 92% rename from systems/jeeves/web_services/haproxy.cfg rename to systems/portal-1/haproxy/haproxy.cfg index 2eefc63..36ada84 100644 --- a/systems/jeeves/web_services/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 @@ -124,31 +132,35 @@ backend acme_challenge 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 gitea_ssh + mode tcp + server gitea jeeves:2223 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 diff --git a/systems/rhapsody-in-green/default.nix b/systems/rhapsody-in-green/default.nix index a339528..0e229bb 100644 --- a/systems/rhapsody-in-green/default.nix +++ b/systems/rhapsody-in-green/default.nix @@ -8,8 +8,10 @@ "${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" ./hardware.nix ./open_webui.nix ./programs.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..029bea9 --- /dev/null +++ b/users/richie/home/minimal.nix @@ -0,0 +1,27 @@ +{ config, pkgs, ... }: +{ + imports = [ + ./cli + ]; + + programs = { + home-manager.enable = true; + }; + + home = { + username = "richie"; + 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..9074a26 100644 --- a/users/richie/home/ssh_config.nix +++ b/users/richie/home/ssh_config.nix @@ -43,6 +43,18 @@ IdentityFile = "~/.ssh/id_ed25519"; Port = 922; }; + portal-1 = { + HostName = "portal-1"; + User = "richie"; + IdentityFile = "~/.ssh/id_ed25519"; + Port = 278; + }; + "gitea.tmmworkshop.com" = { + HostName = "gitea.tmmworkshop.com"; + User = "gitea"; + IdentityFile = "~/.ssh/id_ed25519"; + Port = 2223; + }; }; }; } 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 + ]; +} 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