diff --git a/flake.nix b/flake.nix index 3c65eeb..937f997 100644 --- a/flake.nix +++ b/flake.nix @@ -39,6 +39,17 @@ ]; }; + # Hetzner Cloud host (cpx32, nbg1). Provisions via `terraform/` + nixos-infect. + # Used in parallel with cc-ci (Incus) during transition; becomes canonical after cutover. + # See terraform/README.md for the full apply + Stage 2 (nixos-rebuild switch) workflow. + nixosConfigurations.cc-ci-hetzner = nixpkgs.lib.nixosSystem { + inherit system; + modules = [ + sops-nix.nixosModules.sops + ./nix/hosts/cc-ci-hetzner/configuration.nix + ]; + }; + devShells.${system} = { # Devshell for working on the harness/bridge locally (tools + lint toolchain). default = pkgs.mkShell { diff --git a/nix/hosts/cc-ci-hetzner/configuration.nix b/nix/hosts/cc-ci-hetzner/configuration.nix new file mode 100644 index 0000000..1bf3e8b --- /dev/null +++ b/nix/hosts/cc-ci-hetzner/configuration.nix @@ -0,0 +1,75 @@ +# cc-ci on Hetzner Cloud — NixOS configuration. +# Extends the shared cc-ci modules (same services as the Incus host) with +# Hetzner-specific hardware + networking. Run in parallel with the Incus cc-ci +# host during transition; make this the canonical cc-ci after cutover (plan §7). +# +# To apply after `terraform apply` + nixos-infect: +# git clone --recursive https://git.autonomic.zone/recipe-maintainers/cc-ci.git /etc/cc-ci +# install -m600 /var/lib/sops-nix/key.txt +# nixos-rebuild switch --flake /etc/cc-ci#cc-ci-hetzner +{ pkgs, lib, ... }: +{ + imports = [ + ./hardware.nix + ./networking.nix + ../../modules/packages.nix + ../../modules/secrets.nix + ../../modules/swarm.nix + ../../modules/docker-prune.nix + ../../modules/abra.nix + ../../modules/proxy.nix + ../../modules/drone.nix + ../../modules/drone-runner.nix + ../../modules/bridge.nix + ../../modules/dashboard.nix + ../../modules/backupbot.nix + ../../modules/harness.nix + ../../modules/warm-keycloak.nix + ../../modules/nightly-sweep.nix + ]; + + # Timezone (same as Incus host — see configuration.nix there for rationale). + time.timeZone = "UTC"; + environment.etc."timezone".text = "UTC\n"; + + # Tailscale — keeps the orchestrator→cc-ci access path unchanged (direct peer). + # On the Hetzner host the auth key is also seeded via /etc/ts-auth-key. + services.tailscale = { + enable = true; + authKeyFile = "/etc/ts-auth-key"; + extraUpFlags = [ "--hostname=cc-ci" ]; + }; + + # SSH — allow root login over tailscale (same as Incus host). + services.openssh = { + enable = true; + settings.PermitRootLogin = "yes"; + }; + + # Root SSH authorized keys — preserved across nixos-rebuild switches. + users.users.root.openssh.authorizedKeys.keys = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOk8NaeBdPbS2gfUvbny8h0AkZlVjGYHzx4QPXSJ38gd claude@claude-vm" + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJVlfoLBPseQ9fA9534KmRg2KWcksKZGzAJIpHJ2JpsI mfowler.email@protonmail.com" + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAcyTGb/wVgdhg5oBCZZvBaR1RuUQRY/3WHnOQpNDCsp claude-cc-ci-sandbox@20260526" + ]; + + # Firewall — Hetzner has a public IP, so open 80+443 for Traefik. + # Tailscale interface is trusted (no port restrictions for orchestrator access). + # Plan §6: v1 keeps the sops wildcard cert; evaluate ACME-on-public-IP as follow-up. + networking.firewall = { + enable = true; + trustedInterfaces = [ "tailscale0" ]; + allowedTCPPorts = [ 22 80 443 ]; + }; + + environment.systemPackages = with pkgs; [ + curl + git + jq + openssh + ]; + + nix.settings.experimental-features = [ "nix-command" "flakes" ]; + + system.stateVersion = "24.11"; +} diff --git a/nix/hosts/cc-ci-hetzner/hardware.nix b/nix/hosts/cc-ci-hetzner/hardware.nix new file mode 100644 index 0000000..d19d0ac --- /dev/null +++ b/nix/hosts/cc-ci-hetzner/hardware.nix @@ -0,0 +1,35 @@ +# Hardware configuration for cc-ci on Hetzner Cloud (cpx32: AMD 4 vCPU / 8 GB / x86_64). +# Generated by nixos-infect from a Debian 12 base image, then committed here. +# +# nixos-infect uses GRUB + EFI on Hetzner (not systemd-boot), with a qemu-guest profile +# because Hetzner Cloud uses KVM virtualisation. +# +# IMPORTANT: networking.nix (below) contains the server's static public IP. +# When provisioning a new server via `terraform apply`, copy the fresh networking.nix +# from /etc/nixos/networking.nix on the new host and commit it here before rebuilding. +{ modulesPath, ... }: +{ + imports = [ (modulesPath + "/profiles/qemu-guest.nix") ]; + + boot.loader = { + efi.efiSysMountPoint = "/boot/efi"; + grub = { + efiSupport = true; + efiInstallAsRemovable = true; + device = "nodev"; + }; + }; + + fileSystems."/boot/efi" = { + device = "/dev/disk/by-uuid/D978-69EE"; + fsType = "vfat"; + }; + + boot.initrd.availableKernelModules = [ "ata_piix" "uhci_hcd" "xen_blkfront" "vmw_pvscsi" ]; + boot.initrd.kernelModules = [ "nvme" ]; + + fileSystems."/" = { + device = "/dev/sda1"; + fsType = "ext4"; + }; +} diff --git a/nix/hosts/cc-ci-hetzner/networking.nix b/nix/hosts/cc-ci-hetzner/networking.nix new file mode 100644 index 0000000..6d9a629 --- /dev/null +++ b/nix/hosts/cc-ci-hetzner/networking.nix @@ -0,0 +1,40 @@ +# Hetzner static networking — generated by nixos-infect at provision time. +# +# This file is server-specific: the IP, gateway, and MAC address are tied to a +# particular Hetzner instance. When provisioning a new server: +# 1. After `terraform apply` + nixos-infect completes, run: +# ssh root@ 'cat /etc/nixos/networking.nix' +# 2. Replace this file's contents with the output and commit. +# 3. Then: `nixos-rebuild switch --flake .#cc-ci-hetzner --target-host root@` +# +# Current instance: 91.98.47.73 (fsn1, Hetzner server 134485294, provisioned 2026-05-31). +{ lib, ... }: { + networking = { + nameservers = [ + "185.12.64.1" + "185.12.64.2" + ]; + defaultGateway = "172.31.1.1"; + defaultGateway6 = { + address = ""; + interface = "eth0"; + }; + dhcpcd.enable = false; + usePredictableInterfaceNames = lib.mkForce false; + interfaces = { + eth0 = { + ipv4.addresses = [ + { address = "91.98.47.73"; prefixLength = 32; } + ]; + ipv6.addresses = [ + { address = "fe80::9000:8ff:fe04:152e"; prefixLength = 64; } + ]; + ipv4.routes = [{ address = "172.31.1.1"; prefixLength = 32; }]; + ipv6.routes = [{ address = ""; prefixLength = 128; }]; + }; + }; + }; + services.udev.extraRules = '' + ATTR{address}=="92:00:08:04:15:2e", NAME="eth0" + ''; +}