feat(clan): add vps-relay + strip bbbot cloudflared 🚢

Stage 4.5: declare a Hetzner-hosted reverse-proxy VPS as a clan machine.

- nixos/hosts/vps-relay.nix: Debian→NixOS cx23 in hel1. Caddy at public
  80/443 reverse-proxies navidrome.dannydannydanny.me and
  bbbot.dannydannydanny.me over ZT to sunken-ship.
- nixos/disko-cloud.nix: simple GPT + ext4 root, no LUKS — cloud provider
  has physical disk anyway and there's no operator at boot.
- flake-modules/clan.nix: register vps-relay as an inventory machine,
  zerotier peer, internet networking target at its clan-generated ZT
  IPv6, and add vps-relay.clan to clanHostsModule /etc/hosts.
- sunken-ship fitness-bot: drop pkgs.cloudflared from PATH + set
  WEBAPP_URL=https://bbbot.dannydannydanny.me. Paired with the bbbot
  upstream patch (start.py honors env WEBAPP_URL and skips cloudflared
  when set) — once the 15-min fitness-bot-pull timer pulls that change,
  bbbot will stop churning trycloudflare.com URLs.

Vars (zerotier identity/ip + sops machine key) generated on sunken-ship
because clan's hermetic sandbox on macOS fails to run the zerotier
identity generator (same workaround as for data-mesher earlier).

VPS install flow: Hetzner-created Debian box, then `clan machines
install vps-relay --target-host root@<public-ipv4>` reinstalls to
NixOS; subsequent updates go over ZT.
This commit is contained in:
DannyDannyDanny 2026-04-24 13:43:21 +02:00
parent b0c8664f5c
commit 47fc658523
11 changed files with 187 additions and 1 deletions

View file

@ -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/<machine>.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 = [
{

34
nixos/disko-cloud.nix Normal file
View file

@ -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 = "/";
};
};
};
};
};
};
}

View file

@ -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";

77
nixos/hosts/vps-relay.nix Normal file
View file

@ -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";
}

View file

@ -0,0 +1,6 @@
[
{
"publickey": "age1mlljsdpqf054p4nav9s855rtd5szwyl9av8w2lvg86j59cdtugxqylcn6k",
"type": "age"
}
]

View file

@ -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"
}
}

View file

@ -0,0 +1 @@
../../../users/danny

View file

@ -0,0 +1 @@
../../../../../../sops/machines/vps-relay

View file

@ -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"
}
}

View file

@ -0,0 +1 @@
../../../../../../sops/users/danny

View file

@ -0,0 +1 @@
fdd5:53a2:de33:d269:6499:9305:339f:2ed3