Commit 076028be557e
Changed files (6)
infra/lutece/README.md
@@ -0,0 +1,116 @@
+# lutece — Scaleway Dedibox VPS (VPS-START-2-S, Paris)
+
+NixOS VPS at `64.31.63.130`, installed with `nixos-anywhere` + disko. Two roles:
+
+- **homelab mesh client** on `wg0` (`10.100.0.3`), peer of carthage
+- **travel exit node** on `wg1` (`10.101.0.0/24`) — full tunnel giving a French
+ IP from abroad, plus access to the homelab
+
+## Why no Terraform
+
+Dedibox is on the legacy Online.net platform and has **no resources in the
+`scaleway/scaleway` Terraform provider** (its `scaleway_baremetal_server` is
+Elastic Metal, a different product). It is scriptable through the `dedibox/v1`
+API (`scw dedibox ...`), but a VPS on monthly commitment is a pet, ordered once
+by hand. The declarative part lives in `systems/lutece/`.
+
+## Hardware facts (verified on the running host)
+
+| | |
+|---|---|
+| Disk | `/dev/vda`, 30 GB, virtio |
+| Firmware | BIOS (GPT + `EF02` boot partition, GRUB) |
+| CPU / RAM | 2 vCPU / 1.9 GB + 4 GB swapfile |
+| IPv4 | `64.31.63.130/24`, gw `64.31.63.1` — **static, no DHCP** |
+| IPv6 | `2a11:840:72:1b::/64` via SLAAC; stable address ends `:216:3eff:fea8:6cf3` |
+
+The static IPv4 is the important one: a DHCP assumption leaves the box
+unreachable after reboot. IPv6 is intentionally *not* pinned — its gateway is
+the provider router's link-local address.
+
+## Reinstall
+
+```console
+nix run github:nix-community/nixos-anywhere -- \
+ --flake .#lutece --build-on local root@64.31.63.130
+```
+
+Run it from a host whose key is in the VPS's `authorized_keys` (set at order
+time in the Dedibox console). It must be usable **non-interactively**, so a
+Yubikey/touch key will not do. Keep an IPv4 reachable —
+`nixos-anywhere` has known failures on IPv6-only Scaleway targets
+(nix-community/nixos-anywhere#227).
+
+Note the two keys under `/etc/wireguard/` are **not** in the Nix store and do
+not survive a reinstall; regenerate them as below.
+
+## WireGuard keys (manual, once)
+
+```console
+# mesh identity (wg0) — public key goes to globals.machines.lutece.net.vpn.pubkey
+umask 077; wg genkey > /etc/wireguard/private.key; wg pubkey < /etc/wireguard/private.key
+
+# travel server identity (wg1) — public key goes into travel client configs
+umask 077; wg genkey > /etc/wireguard/travel.key; wg pubkey < /etc/wireguard/travel.key
+```
+
+## Travel tunnel
+
+Peers are declared in `globals.net.travel.peers` (name → travel IP); their
+public keys are reused from the machine's mesh identity in
+`globals.machines.<name>.net.vpn.pubkey`.
+
+Client config, e.g. for hokkaido:
+
+```ini
+[Interface]
+PrivateKey = <the device's existing mesh private key>
+Address = 10.101.0.5/32
+DNS = 1.1.1.1
+MTU = 1340
+
+[Peer]
+PublicKey = d28ha+85FlP188kDkCDMX6I8iM20PtbTy6kj0sHvHwo=
+Endpoint = 64.31.63.130:51820
+AllowedIPs = 0.0.0.0/0, ::/0
+PersistentKeepalive = 25
+```
+
+Only `Address` changes per device:
+
+| Device | mesh | travel |
+|---|---|---|
+| hokkaido | `10.100.0.5` | `10.101.0.5/32` |
+| osaka (Boox tablet) | `10.100.0.64` | `10.101.0.64/32` |
+| suzu | `10.100.0.65` | `10.101.0.65/32` |
+| houbeb-makcbook-pro | `10.100.0.71` | `10.101.0.71/32` |
+
+`AllowedIPs = 0.0.0.0/0` is what makes it a full tunnel: web traffic exits in
+Paris with a French IP, and `10.100.0.0/24` reaches the homelab through
+lutece's `wg0`.
+
+**How the return path works:** lutece masquerades `10.101.0.0/24` on
+POSTROUTING with no output interface, so homelab hosts see traffic as coming
+from `10.100.0.3` and need no route for the travel subnet. That is why adding
+this feature required no change to rhea/aion/carthage's config.
+
+Caveat: this is a hosting IP range, not residential. Geo-restricted services
+that block datacenter ranges may still refuse it.
+
+## Deploy order
+
+1. Generate `/etc/wireguard/travel.key` on lutece (above).
+2. Deploy lutece: `nixos-rebuild switch --flake .#lutece --target-host root@64.31.63.130`
+3. Redeploy carthage so it accepts lutece as a mesh peer.
+4. Add the travel tunnel to the device and test: `curl ifconfig.me` should show
+ `64.31.63.130`.
+
+## Open items
+
+- Root only accepts touch-required keys, so nothing can manage lutece
+ non-interactively. Add an `aomi-tpm` entry for `lutece` in
+ `globals.ssh.vincent` if automation is wanted.
+- SSH is still public; restrict it to the VPN address once the mesh is up (see
+ the TODO in `systems/lutece/extra.nix`).
+- Travel peers currently reuse their mesh keypair. Separate keys would be
+ cleaner; it means provisioning a new keypair per device.
systems/lutece/boot.nix
@@ -0,0 +1,35 @@
+{ lib, ... }:
+{
+ console.keyMap = lib.mkForce "us";
+
+ # disko handles GRUB device configuration
+ boot.loader.grub.enable = lib.mkForce true;
+ boot.loader.systemd-boot.enable = lib.mkForce false;
+ boot.initrd.systemd.enable = lib.mkForce false;
+
+ # Scaleway virtual instance (QEMU/KVM guest)
+ boot.initrd.availableKernelModules = [
+ "virtio_net"
+ "virtio_pci"
+ "virtio_mmio"
+ "virtio_blk"
+ "virtio_scsi"
+ "9p"
+ "9pnet_virtio"
+ ];
+ boot.initrd.kernelModules = [
+ "virtio_balloon"
+ "virtio_console"
+ "virtio_rng"
+ ];
+
+ # Make the Scaleway web serial console usable
+ boot.kernelParams = [ "console=ttyS0" ];
+
+ # VPS optimization: No physical hardware, no firmware needed
+ hardware.enableRedistributableFirmware = lib.mkForce false;
+ hardware.enableAllFirmware = lib.mkForce false;
+
+ # VPS optimization: No firmware update service needed
+ services.fwupd.enable = lib.mkForce false;
+}
systems/lutece/extra.nix
@@ -0,0 +1,46 @@
+{
+ globals,
+ lib,
+ libx,
+ ...
+}:
+{
+ imports = [
+ ../common/services/openssh.nix
+ ];
+
+ # Disable TPM2 (VPS has no TPM hardware)
+ security.tpm2.enable = lib.mkForce false;
+
+ # ── Travel exit node ───────────────────────────────────────────
+ # wg0 is the homelab mesh client (to carthage, auto-derived from globals).
+ # wg1 gives travel devices a French IP: they set AllowedIPs = 0.0.0.0/0 and
+ # everything exits here. The module's POSTROUTING masquerade has no output
+ # interface, so it covers both internet egress via eth0 and traffic forwarded
+ # into wg0 towards the homelab — which is why no other host needs a route for
+ # the travel subnet.
+ services.wireguard.server = {
+ enable = true;
+ interface = "wg1";
+ privateKeyFile = "/etc/wireguard/travel.key";
+ natSource = globals.net.travel.subnet;
+ ips = [ "10.101.0.1/24" ];
+ peers = lib.mapAttrsToList (name: ip: {
+ publicKey = globals.machines.${name}.net.vpn.pubkey;
+ allowedIPs = libx.wg-ips [ ip ];
+ }) globals.net.travel.peers;
+ };
+
+ networking.firewall = {
+ allowPing = true;
+ allowedTCPPorts = [
+ 80 # HTTP
+ 443 # HTTPS
+ ];
+ };
+
+ # TODO: once the WireGuard mesh is up, restrict SSH to the VPN interface like
+ # carthage does:
+ # services.openssh.listenAddresses = [ { addr = <vpn ip>; port = 22; } ];
+ # services.openssh.openFirewall = lib.mkForce false;
+}
systems/lutece/hardware.nix
@@ -0,0 +1,80 @@
+{ lib, ... }:
+{
+ # Disko declarative partitioning for Scaleway (DEV1-* local SSD)
+ # Verify the disk device with `lsblk` on the temporary Ubuntu before running
+ # nixos-anywhere: local `l_ssd` volumes show up as /dev/vda, block storage
+ # (`sbs_volume`, used by PLAY2/PRO2/POP2) as /dev/sda.
+ disko.devices = {
+ disk = {
+ main = {
+ device = "/dev/vda";
+ type = "disk";
+ content = {
+ type = "gpt";
+ partitions = {
+ boot = {
+ size = "1M";
+ type = "EF02"; # BIOS boot partition for GRUB
+ };
+ root = {
+ size = "100%";
+ content = {
+ type = "filesystem";
+ format = "ext4";
+ mountpoint = "/";
+ mountOptions = [
+ "defaults"
+ "noatime"
+ ];
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+
+ swapDevices = [
+ {
+ device = "/swapfile";
+ size = 4096; # 2x RAM — the VPS only has 2GB
+ }
+ ];
+
+ # Dedibox VPS networking is STATIC — there is no DHCP server on this segment.
+ # Values taken from the Debian rescue install (`ip -br a`, `ip r`, `ip -6 r`).
+ # Interface names are pinned to eth0 (unpredictable naming) so the static
+ # config below cannot break on a kernel/udev rename.
+ networking = {
+ useDHCP = false;
+ usePredictableInterfaceNames = lib.mkForce false;
+
+ interfaces.eth0 = {
+ ipv4.addresses = [
+ {
+ address = "64.31.63.130";
+ prefixLength = 24;
+ }
+ ];
+ };
+
+ defaultGateway = {
+ address = "64.31.63.1";
+ interface = "eth0";
+ };
+
+ # IPv6 is autoconfigured from router advertisements (SLAAC), yielding the
+ # stable EUI-64 address 2a11:840:72:1b:216:3eff:fea8:6cf3 in
+ # 2a11:840:72:1b::/64. Deliberately not pinned: the v6 gateway is the
+ # provider router's link-local address, which is theirs to change.
+
+ nameservers = [
+ "1.1.1.1"
+ "8.8.8.8"
+ ];
+ };
+
+ # Keep honouring RAs even if IP forwarding is enabled later (accept_ra=1 is
+ # ignored once a host forwards; 2 keeps SLAAC working).
+ boot.kernel.sysctl."net.ipv6.conf.eth0.accept_ra" = 2;
+}
flake.nix
@@ -112,6 +112,12 @@
homeInput = inputs.home-manager-26_05;
agenixInput = inputs.agenix-26_05;
};
+ lutece = libx.mkHost {
+ hostname = "lutece";
+ pkgsInput = inputs.nixpkgs-26_05;
+ homeInput = inputs.home-manager-26_05;
+ agenixInput = inputs.agenix-26_05;
+ };
carthage = libx.mkHost {
hostname = "carthage";
pkgsInput = inputs.nixpkgs-26_05;
globals.nix
@@ -185,6 +185,27 @@ _: {
vpn = {
endpoint = "46.224.100.116";
};
+ # Travel tunnel: lutece (Paris) as a full-tunnel exit node, used to get a
+ # French IP from abroad. Separate subnet and separate interface (wg1) from
+ # the 10.100.0.0/24 homelab mesh.
+ #
+ # Traffic from these peers is masqueraded on lutece, so the rest of the
+ # mesh sees 10.100.0.3 as the source and needs no route for 10.101.0.0/24.
+ travel = {
+ subnet = "10.101.0.0/24";
+ server = "lutece";
+ # Public key of /etc/wireguard/travel.key on lutece, for client configs.
+ pubkey = "d28ha+85FlP188kDkCDMX6I8iM20PtbTy6kj0sHvHwo=";
+ # name -> travel IP. Public keys are reused from the machine's mesh
+ # identity (globals.machines.<name>.net.vpn.pubkey); last octet mirrors
+ # the machine's VPN address for readability.
+ peers = {
+ hokkaido = "10.101.0.5";
+ osaka = "10.101.0.64";
+ suzu = "10.101.0.65";
+ houbeb-makcbook-pro = "10.101.0.71";
+ };
+ };
nfs = {
defaultHosts = {
rhea = {
@@ -414,6 +435,25 @@ _: {
vincent = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGxstR3xEf87leVVDS3GVPx8Ap9+eP+OfkSvM26V54XP vincent@shikoku";
};
};
+ # Scaleway Dedibox VPS (VPS-START-2-S, Paris) — see infra/lutece
+ # Mesh client on wg0 (10.100.0.3) and travel exit server on wg1
+ # (see globals.net.travel).
+ lutece = {
+ net = {
+ ips = [ "64.31.63.130" ];
+ vpn = {
+ pubkey = "H7q7Qz7JqAKXlEbNMBW3quac2GDASAZ0SYvCvJ96NTo=";
+ ips = [ "10.100.0.3" ];
+ };
+ names = [
+ "lutece.vpn"
+ "lutece.sbr.pm"
+ ];
+ };
+ ssh = {
+ hostKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII0QxlxWPQfd4t8IxqhLKFRXPVfrRSJBpUSXipIcKOVm"; # ssh-keyscan -q -t ed25519 64.31.63.130
+ };
+ };
# Hetzner Cloud VPS
# TODO: Update pubkey, hostKey, and syncthing id after provisioning
carthage = {