diff --git a/flake-modules/clan.nix b/flake-modules/clan.nix index 3a1c1df..6b1926e 100644 --- a/flake-modules/clan.nix +++ b/flake-modules/clan.nix @@ -20,6 +20,7 @@ let # duplicated here so we can drop them into /etc/hosts at module-eval time. sunkenShipZTv6 = "fdd5:53a2:de33:d269:6499:93d5:53a2:de33"; phantomShipZTv6 = "fdd5:53a2:de33:d269:6499:936c:48a:bbdc"; + vpsRelayZTv6 = "fdd5:53a2:de33:d269:6499:9305:339f:2ed3"; # Shared across both servers: /etc/hosts entries so data-mesher's # libp2p /dns/.clan/... bootstrap multiaddrs resolve over ZT. @@ -27,6 +28,7 @@ let networking.hosts = { "${sunkenShipZTv6}" = [ "sunken-ship.clan" ]; "${phantomShipZTv6}" = [ "phantom-ship.clan" ]; + "${vpsRelayZTv6}" = [ "vps-relay.clan" ]; }; }; in { @@ -44,6 +46,7 @@ in { # below. inventory.machines.sunken-ship = { }; inventory.machines.phantom-ship = { }; + inventory.machines.vps-relay = { }; # ZeroTier mesh VPN. sunken-ship is the controller (manages network # membership); phantom-ship is a peer. The mac joins manually as an @@ -54,6 +57,7 @@ in { roles.controller.machines.sunken-ship = { }; roles.peer.machines.phantom-ship = { }; roles.peer.machines.sunken-ship = { }; + roles.peer.machines.vps-relay = { }; }; # data-mesher — signed-file gossip protocol over libp2p (port 7946). @@ -99,6 +103,10 @@ in { host = "fdd5:53a2:de33:d269:6499:936c:48a:bbdc"; user = "danny"; }; + roles.default.machines.vps-relay.settings = { + host = "fdd5:53a2:de33:d269:6499:9305:339f:2ed3"; + user = "danny"; + }; }; # Preserve current network / init stack (no systemd-networkd/resolved, @@ -123,6 +131,25 @@ in { ]; }; + machines.vps-relay = { + imports = [ + { + clan.core.enableRecommendedDefaults = false; + # Initial install uses --target-host override; subsequent + # updates go over ZT IPv6 (set once generated, via the + # internet instance above). + } + clanHostsModule + ../nixos/hosts/vps-relay.nix + inputs.home-manager.nixosModules.home-manager + (hmModule { + user = "danny"; + homeDirectory = "/home/danny"; + stateVersion = "25.11"; + }) + ]; + }; + machines.phantom-ship = { imports = [ { diff --git a/nixos/disko-cloud.nix b/nixos/disko-cloud.nix new file mode 100644 index 0000000..dc0a33d --- /dev/null +++ b/nixos/disko-cloud.nix @@ -0,0 +1,34 @@ +# Disko layout for cloud VPS installs (e.g. Hetzner Cloud). +# No LUKS — the provider has physical disk access anyway and there's +# no operator present at boot to enter a passphrase. +{ + disko.devices = { + disk.main = { + type = "disk"; + device = "/dev/sda"; + content = { + type = "gpt"; + partitions = { + ESP = { + size = "512M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "fmask=0022" "dmask=0022" ]; + }; + }; + root = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + }; + }; + }; + }; + }; +} diff --git a/nixos/hosts/sunken-ship.nix b/nixos/hosts/sunken-ship.nix index 0ec4783..442534e 100644 --- a/nixos/hosts/sunken-ship.nix +++ b/nixos/hosts/sunken-ship.nix @@ -120,6 +120,11 @@ # Code: https://github.com/DannyDannyDanny/bigbiggerbiggestbot cloned at /home/danny/tg_fitness_bot # Bot token: ~danny/.secrets/bigbiggerbiggestbot # Deployment: fitness-bot-pull timer below runs every 15 min, git pulls, restarts service on changes. + # + # Mini App URL is fronted by Caddy on the vps-relay host at + # https://bbbot.dannydannydanny.me (VPS → ZeroTier → localhost:8080). + # The bot's start.py honors WEBAPP_URL to skip starting its own + # cloudflared Quick Tunnel when we've got a stable URL from the VPS. systemd.services.fitness-bot = let pythonEnv = pkgs.python3.withPackages (ps: with ps; [ python-telegram-bot @@ -131,7 +136,8 @@ after = [ "network-online.target" ]; wants = [ "network-online.target" ]; wantedBy = [ "multi-user.target" ]; - path = [ pythonEnv pkgs.cloudflared ]; + path = [ pythonEnv ]; + environment.WEBAPP_URL = "https://bbbot.dannydannydanny.me"; serviceConfig = { WorkingDirectory = "/home/danny/tg_fitness_bot"; ExecStart = "${pythonEnv}/bin/python start.py"; diff --git a/nixos/hosts/vps-relay.nix b/nixos/hosts/vps-relay.nix new file mode 100644 index 0000000..8147561 --- /dev/null +++ b/nixos/hosts/vps-relay.nix @@ -0,0 +1,77 @@ +# Hetzner Cloud VPS — public reverse proxy into the clan. +# +# Role: terminates public TLS via Caddy + Let's Encrypt, reverse-proxies +# each declared subdomain over ZeroTier to the appropriate homelab host. +# No navidrome/bbbot data ever hits disk here; this box is a relay. +{ config, lib, pkgs, ... }: +{ + imports = [ ../disko-cloud.nix ]; + + nixpkgs.hostPlatform = "x86_64-linux"; + + # Hetzner Cloud boots EFI with systemd-boot. + boot.loader.systemd-boot.enable = true; + boot.loader.efi.canTouchEfiVariables = true; + + # Cloud provisioners add the initial root SSH key via cloud-init or + # equivalent; we don't run cloud-init. All config is baked at install. + networking.hostName = "vps-relay"; + networking.useDHCP = lib.mkDefault true; + time.timeZone = "Europe/Copenhagen"; + + # --- User + SSH ------------------------------------------------------ + users.users.danny = { + isNormalUser = true; + extraGroups = [ "wheel" ]; + openssh.authorizedKeys.keys = [ + # Same pubkey used to reach sunken-ship; set at install via clan. + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKW/akfIiVU5o63YrTAJVZhMj7kXfYHOnXDtlpVFW7pf danny@sunken-ship" + ]; + }; + users.users.root.openssh.authorizedKeys.keys = + config.users.users.danny.openssh.authorizedKeys.keys; + + security.sudo.wheelNeedsPassword = false; + + services.openssh = { + enable = true; + settings = { + PasswordAuthentication = false; + KbdInteractiveAuthentication = false; + PermitRootLogin = "prohibit-password"; + }; + }; + + # --- Firewall -------------------------------------------------------- + # Public: 22 (SSH), 80 + 443 (Caddy). + # ZT interface: trusted (set in the clan ZT module). + networking.firewall.enable = true; + networking.firewall.allowedTCPPorts = [ 22 80 443 ]; + + # --- Caddy reverse proxy -------------------------------------------- + # Subdomains → clan backends over ZeroTier. IPs are sunken-ship's / + # phantom-ship's ZT IPv6; brackets required in URLs. + services.caddy = { + enable = true; + email = "powerhouseplayer@gmail.com"; + # Tell ACME to use Let's Encrypt's production endpoint (Caddy default). + virtualHosts = { + "navidrome.dannydannydanny.me".extraConfig = '' + reverse_proxy http://[fdd5:53a2:de33:d269:6499:93d5:53a2:de33]:4533 + ''; + "bbbot.dannydannydanny.me".extraConfig = '' + reverse_proxy http://[fdd5:53a2:de33:d269:6499:93d5:53a2:de33]:8080 + ''; + }; + }; + + # --- Basic tooling --------------------------------------------------- + environment.systemPackages = with pkgs; [ + git + htop + tcpdump + ]; + + nix.settings.experimental-features = [ "nix-command" "flakes" ]; + system.stateVersion = "25.11"; +} diff --git a/sops/machines/vps-relay/key.json b/sops/machines/vps-relay/key.json new file mode 100755 index 0000000..5c30825 --- /dev/null +++ b/sops/machines/vps-relay/key.json @@ -0,0 +1,6 @@ +[ + { + "publickey": "age1mlljsdpqf054p4nav9s855rtd5szwyl9av8w2lvg86j59cdtugxqylcn6k", + "type": "age" + } +] \ No newline at end of file diff --git a/sops/secrets/vps-relay-age.key/secret b/sops/secrets/vps-relay-age.key/secret new file mode 100644 index 0000000..aca274e --- /dev/null +++ b/sops/secrets/vps-relay-age.key/secret @@ -0,0 +1,14 @@ +{ + "data": "ENC[AES256_GCM,data:+Cd3Hxr5KzX6J/74M2IZ6VOE6KEDsK8NoVyTleSB7UdsDWWGAS+mgdNZTiVBJEIBx+cmMKMcNj2rNu6T4Z2OCvqH/o6GBAhKBmM=,iv:RllA6vH/qWsx08gTEi5Nl4VkvoeI00Bw56IwPp1TOLk=,tag:PdQJpm0oaYZUZvc1y9Cmcw==,type:str]", + "sops": { + "age": [ + { + "recipient": "age1g6y8gvcampqj5y3yzdajke2h5n7k6ckdg6a424cghy5325px7cmqjmmd28", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAxSmVLKzl3akVZNlE0QWJr\namc0T3NPb0pzM2hvR3VlSEo2TDJ6VDJOQmhBCnJOTkZaOVM2RXpTOEdYUEtWTUht\nS0ZmNDVoVDJzajYxRDVWVFVkTkJLbkUKLS0tICsrUGx0Q2FmZk04NHBVb2wvaU1p\nSktZNVl5bUtKZEJLWm1kYm9wSFl5ZXMKEb+0fq1idxA4mpJAxt3DUWX8kYp8HwYN\nwUQ7SFAlj3k611jfVFwRYdqJZQLYQ0iVbEwy5BfJw4tnqZFeaEBueA==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2026-04-24T11:41:47Z", + "mac": "ENC[AES256_GCM,data:AjAgVpuV7QvCh1E4AvTSP+Oxg/M1at8X08s76C9OxmdCR0Evd67Hb5TaPkujhtX87Qs9IHoOK6yY+aQv2exLXWt6U4uRzapsVIpcofdyA7EUF2q0UaykrqtKLGYW3IY8fXL4XwMMFJ+wmThmwKnJVJO8hUug8AceA83/QVYNccM=,iv:JuxpYvmTROZPv7zawPQ/NpfbWAQqwRfBRp+zhNQnm5I=,tag:v6IMgQTbPP+XEeCSrpVTxg==,type:str]", + "version": "3.12.2" + } +} diff --git a/sops/secrets/vps-relay-age.key/users/danny b/sops/secrets/vps-relay-age.key/users/danny new file mode 120000 index 0000000..215639b --- /dev/null +++ b/sops/secrets/vps-relay-age.key/users/danny @@ -0,0 +1 @@ +../../../users/danny \ No newline at end of file diff --git a/vars/per-machine/vps-relay/zerotier/zerotier-identity-secret/machines/vps-relay b/vars/per-machine/vps-relay/zerotier/zerotier-identity-secret/machines/vps-relay new file mode 120000 index 0000000..5393939 --- /dev/null +++ b/vars/per-machine/vps-relay/zerotier/zerotier-identity-secret/machines/vps-relay @@ -0,0 +1 @@ +../../../../../../sops/machines/vps-relay \ No newline at end of file diff --git a/vars/per-machine/vps-relay/zerotier/zerotier-identity-secret/secret b/vars/per-machine/vps-relay/zerotier/zerotier-identity-secret/secret new file mode 100644 index 0000000..624a47c --- /dev/null +++ b/vars/per-machine/vps-relay/zerotier/zerotier-identity-secret/secret @@ -0,0 +1,18 @@ +{ + "data": "ENC[AES256_GCM,data:pKvAwWARYI+t2dx3E90ime8VWT1LlTaHtkfCbwPzus7GNpOPKDXzhh3aVICSy0FOlonKBuKB35DLialWGb6rLI9T5ITIh+DJj6ijoxjSWJrWmZg2Du7xq89ZtV23HpNfuJnhnF4Wo3/WNDS+vnIynJSS9lx9NXe64Nd52NGSEufHM2HHkWIIgGR7Vgs0EoGxmxrDcxQ21MA2uOMKAYwCWmDJsRO6iXy+t01tTwFjUDg8203GtQjQaR99lY0GLrJybraaowa2bn0gyJvpnFt/zQ7kHNV6jPbdNj4wo4OwZ5JHA3zO5Ep6ePUQm7g8cjVD83eV8HZ/1Hb7LF4S4628CYwydaxHmHJsdbvVz7I1,iv:ydc0gUdniYXGeW4WkQypkWz8C0yZ0GcA2srYWgV165U=,tag:Q06ScyNT7zatscjJqje+lQ==,type:str]", + "sops": { + "age": [ + { + "recipient": "age1g6y8gvcampqj5y3yzdajke2h5n7k6ckdg6a424cghy5325px7cmqjmmd28", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBESE1Nd000SEovTFJQQlhH\ndFBPTmJsK3ZObG9NR0ppWUxqbkhrVmk1eFRNCnlXTUNTejFPZEdmVEROSGZSNmh2\ncEM0NG92Q01MODNZSDFOdE9MMlpmak0KLS0tIHFneDMyMFhMZm1GNDdRYmFmZU5n\nOTRuZTFhaEJxREZ3UkpVSUZqazJ5MUUK4hKiYzkoNhsxYqK0fDP7zweQLFet4WMD\nnQVUYpQIGjxK1fQEImGMybIwGRjIxfsYI51GI7qTkwUAPaLxXUjs+Q==\n-----END AGE ENCRYPTED FILE-----\n" + }, + { + "recipient": "age1mlljsdpqf054p4nav9s855rtd5szwyl9av8w2lvg86j59cdtugxqylcn6k", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxVVJGN3NjQUN3aXRQaVlr\nd1Biei9mV3ZiTWRYRUxyRnhhcHlINjd1RDI0ClZETXJ0V1l6NGViVk43Y3NxSUYz\nZ3JtcDAzcXNZMnNXanl5cEVYaHRLVDAKLS0tIDVIZ2tiblNpVTlnVGJtbloyd3I1\nalZETWRvSVRRM3o2WnBmbGpFalNEQlEK3FkoqpSRrlce/4wFOdF26tUCeY8g1RD2\npYvz/giE8ULnWxYfG2HOTMQkUyUjYFiY0JPJT7oGyhQs4QmkVwhBWg==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2026-04-24T11:41:47Z", + "mac": "ENC[AES256_GCM,data:RlbV4iY8ekGfe4I53Zn8nGaAou8D+jUYW4DXi8EfwdDTiSM3+szyMe8YNMbetM0jiPe2sAVqxTMgkLe99G5lZwZBY6pOrlBljiMPtvHb0NseRr9cnMUySfX9QAhEyD62bWCQyp33jCK7bJjAtmEATnIslYePQCJhmf0OEMO95NI=,iv:XK939qjL9wwZqrJaywnfBziuY6LFI+fAH3d4rNIbdRs=,tag:uhDWeOkpyHom3/5wtEaHrw==,type:str]", + "version": "3.12.2" + } +} diff --git a/vars/per-machine/vps-relay/zerotier/zerotier-identity-secret/users/danny b/vars/per-machine/vps-relay/zerotier/zerotier-identity-secret/users/danny new file mode 120000 index 0000000..48e5c60 --- /dev/null +++ b/vars/per-machine/vps-relay/zerotier/zerotier-identity-secret/users/danny @@ -0,0 +1 @@ +../../../../../../sops/users/danny \ No newline at end of file diff --git a/vars/per-machine/vps-relay/zerotier/zerotier-ip/value b/vars/per-machine/vps-relay/zerotier/zerotier-ip/value new file mode 100644 index 0000000..6f577e1 --- /dev/null +++ b/vars/per-machine/vps-relay/zerotier/zerotier-ip/value @@ -0,0 +1 @@ +fdd5:53a2:de33:d269:6499:9305:339f:2ed3 \ No newline at end of file