Path-route Headplane under /admin on the same vhost as headscale instead of its own subdomain - Caddy handle blocks split on the prefix, headscale gets everything else. base_url drops to the site root since Headplane appends /admin (and the OIDC callback path) itself. Wire Zitadel as the OIDC provider. client_id/client_secret/the headscale API key can't be real until Zitadel and headscale are actually deployed and an application/key exist, so those are REPLACE_ME placeholders for now (documented in services/headplane.nix) - direct API-key login stays enabled as a fallback so this can't lock anyone out in the meantime. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
104 lines
4.7 KiB
Nix
104 lines
4.7 KiB
Nix
{ config, pkgs, lib, ... }:
|
|
|
|
# netcup VPS (UEFI, /dev/vda). Public reverse proxy + tailnet node.
|
|
{
|
|
imports = [
|
|
./hardware-configuration.nix
|
|
./disk-config.nix # disko: vda partitions + filesystems
|
|
./secrets.nix # sops-nix: tailscale authkey
|
|
../../common.nix # shared base: user / ssh / nix / firewall
|
|
../../services/caddy.nix
|
|
../../services/tailscale.nix
|
|
../../services/zitadel.nix
|
|
../../services/headscale.nix
|
|
../../services/headplane.nix
|
|
];
|
|
|
|
# ---- Boot (UEFI) ----
|
|
boot.loader.systemd-boot.enable = true;
|
|
boot.loader.efi.canTouchEfiVariables = true;
|
|
# Root is on the virtio disk — pin these so stage-1 mounts it regardless of
|
|
# what nixos-generate-config detects in the installer.
|
|
boot.initrd.availableKernelModules = [ "virtio_pci" "virtio_blk" "virtio_scsi" ];
|
|
|
|
networking.hostName = "neptun";
|
|
|
|
# ---- Static networking (netcup) ----
|
|
# No LAN fallback: get this right or the box is unreachable (use netcup's
|
|
# VNC console / rescue system to fix). Values captured from the running VPS.
|
|
networking.useDHCP = false;
|
|
networking.usePredictableInterfaceNames = false; # keep the NIC named eth0
|
|
networking.interfaces.eth0 = {
|
|
ipv4.addresses = [ { address = "159.195.64.117"; prefixLength = 22; } ];
|
|
ipv6.addresses = [ { address = "2a0a:4cc0:c2:19e1:44b4:8dff:fe4d:c7d7"; prefixLength = 64; } ];
|
|
};
|
|
networking.defaultGateway = { address = "159.195.64.1"; interface = "eth0"; };
|
|
# netcup IPv6 gateway is conventionally fe80::1 — VERIFY with `ip -6 route`
|
|
# on the running VPS; wrong v6 gw won't break v4 reachability.
|
|
networking.defaultGateway6 = { address = "fe80::1"; interface = "eth0"; };
|
|
# Public resolvers for early boot; tailscale MagicDNS overrides once up.
|
|
networking.nameservers = [ "9.9.9.9" "1.1.1.1" "2620:fe::fe" ];
|
|
|
|
# firewall (enable + 22), caddy (80/443), tailscale (trust tailscale0 + join
|
|
# headscale) come from ../../common.nix and ../../services/{caddy,tailscale}.nix.
|
|
|
|
# ---- Public reverse proxy vhosts ----
|
|
# Caddy gets automatic public HTTPS (Let's Encrypt) for real domains.
|
|
# Proxies to jupiter's audiobookshelf over the tailnet (MagicDNS name).
|
|
# Needs a public A record -> this VPS IP (ports 80/443 opened by the module).
|
|
services.caddy.virtualHosts."audiobookshelf.mgaction.town".extraConfig = ''
|
|
reverse_proxy http://jupiter.hosts.mgaction.town:8000
|
|
'';
|
|
# TODO: port your other VPS services' vhosts here before deploying.
|
|
|
|
# ---- Zitadel (identity/OIDC provider) ----
|
|
# Runs locally on neptun (see services/zitadel.nix); Caddy just terminates
|
|
# TLS and proxies to it.
|
|
services.zitadel.settings.ExternalDomain = "auth.mgaction.town";
|
|
services.caddy.virtualHosts."auth.mgaction.town".extraConfig = ''
|
|
reverse_proxy http://localhost:8080
|
|
'';
|
|
|
|
# ---- Headscale + Headplane (tailnet control server + its web UI) ----
|
|
# Both run locally on neptun (see services/{headscale,headplane}.nix), path
|
|
# -routed on the same vhost: Headplane owns /admin* (its whole app,
|
|
# including static assets and the OIDC callback, lives under that prefix —
|
|
# `handle` keeps the prefix in the forwarded path, unlike `handle_path`,
|
|
# since Headplane needs to see it), everything else goes to headscale
|
|
# itself (tailnet client traffic). headscale's node-update endpoint is a
|
|
# long-poll, hence `flush_interval -1` — without it Caddy buffers the
|
|
# response and clients see stale state.
|
|
services.caddy.virtualHosts."vpn.mgaction.town".extraConfig = ''
|
|
handle /admin* {
|
|
reverse_proxy http://localhost:3000
|
|
}
|
|
handle {
|
|
reverse_proxy http://localhost:8082 {
|
|
flush_interval -1
|
|
}
|
|
}
|
|
'';
|
|
|
|
# ---- Gitea SSH forward ----
|
|
# Caddy only proxies HTTP; git-over-ssh needs a raw TCP forward. Gitea's
|
|
# own built-in SSH server runs on jupiter:2222 (see services/gitea.nix —
|
|
# not :222, the unpriv gitea user can't bind <1024). Forward this VPS's
|
|
# public :2222 to it over the tailnet, so `ssh://git@git.mgaction.town:2222/...`
|
|
# reaches gitea. Needs a matching inbound-2222 rule in netcup's *edge*
|
|
# firewall panel too (separate from this box's own, and not managed by Nix).
|
|
systemd.services.gitea-ssh-forward = {
|
|
description = "Forward :2222 to jupiter's gitea SSH server over tailscale";
|
|
after = [ "network-online.target" "tailscaled.service" ];
|
|
wants = [ "network-online.target" ];
|
|
wantedBy = [ "multi-user.target" ];
|
|
serviceConfig = {
|
|
DynamicUser = true;
|
|
ExecStart = "${pkgs.socat}/bin/socat TCP-LISTEN:2222,fork,reuseaddr TCP:jupiter.hosts.mgaction.town:2222";
|
|
Restart = "always";
|
|
};
|
|
};
|
|
networking.firewall.allowedTCPPorts = [ 2222 ];
|
|
|
|
system.stateVersion = "26.05"; # set at install time; do NOT bump on upgrades
|
|
}
|