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:
parent
b0c8664f5c
commit
47fc658523
11 changed files with 187 additions and 1 deletions
|
|
@ -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
34
nixos/disko-cloud.nix
Normal 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 = "/";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -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
77
nixos/hosts/vps-relay.nix
Normal 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";
|
||||
}
|
||||
6
sops/machines/vps-relay/key.json
Executable file
6
sops/machines/vps-relay/key.json
Executable file
|
|
@ -0,0 +1,6 @@
|
|||
[
|
||||
{
|
||||
"publickey": "age1mlljsdpqf054p4nav9s855rtd5szwyl9av8w2lvg86j59cdtugxqylcn6k",
|
||||
"type": "age"
|
||||
}
|
||||
]
|
||||
14
sops/secrets/vps-relay-age.key/secret
Normal file
14
sops/secrets/vps-relay-age.key/secret
Normal 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"
|
||||
}
|
||||
}
|
||||
1
sops/secrets/vps-relay-age.key/users/danny
Symbolic link
1
sops/secrets/vps-relay-age.key/users/danny
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../../users/danny
|
||||
|
|
@ -0,0 +1 @@
|
|||
../../../../../../sops/machines/vps-relay
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
../../../../../../sops/users/danny
|
||||
1
vars/per-machine/vps-relay/zerotier/zerotier-ip/value
Normal file
1
vars/per-machine/vps-relay/zerotier/zerotier-ip/value
Normal file
|
|
@ -0,0 +1 @@
|
|||
fdd5:53a2:de33:d269:6499:9305:339f:2ed3
|
||||
Loading…
Add table
Add a link
Reference in a new issue