{ config, lib, ... }: let hosts = import ./modules/hosts.nix; mesh = import ./modules/mesh-hosts.nix { dir = config.directory; inherit lib; }; # Single source of the machine list: inventory AND the per-machine baseline # below are both derived from it, so no machine can be added without the # baseline (e.g. admins group encryption for all its generated secrets). fleet = { control = { }; ns1 = { }; ns2 = { }; mx1 = { }; web01 = { }; # Site gateways (Topton 1U routers): dynamic PPPoE WAN, so they are NOT in # modules/hosts.nix / the `internet` instance — clan reaches them over the # zerotier mesh (or Tor) instead. gw-cnx-1 = { }; }; in { # Ensure this is unique among all clans you want to use. meta.name = "cnx-network-clan"; meta.domain = "cnx-network.internal"; inventory.machines = fleet; # Services defined in this repo (module.input = "self"). See modules/clan/. modules.router = ./modules/clan/router; inventory.instances = { # Admin SSH keys + root password, split per the clan-core migration off # the deprecated `admin` service (sshd handles keys, users the password). sshd = { roles.server.tags.all = { }; roles.server.settings.authorizedKeys = { "berwn" = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIENAjhGQGraQoAjJzsomKP8GAmQPeGL1rNRNHgRcLqtT"; "kurogeek" = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEcZ/p1Ofa9liwIzPWzNtONhJ7+FUWd2lCz33r81t8+w kurogeek@kurogeek"; }; }; root-user = { module = { name = "users"; input = "clan-core"; }; roles.default.tags.all = { }; roles.default.settings = { user = "root"; prompt = false; # auto-generate, like the old admin service share = false; # per-machine password, not fleet-wide }; }; zerotier = { roles.controller.machines."control" = { }; roles.peer.tags.all = { }; # External members admitted by ZeroTier node id (stable per device). # Inventory machines are auto-accepted; this is only for peers outside the # clan. Node id comes from `zerotier-cli info` on the joining device. roles.controller.settings.allowedIds = [ "8802c8d7e0" # alex-nixos "2bd36db8cc" # kurogeek-thinkpad ]; }; tor = { roles.server.tags.nixos = { }; }; # Direct SSH to public IPs — clan's priority-1 connection path, with the # ZeroTier mesh and Tor kept as automatic fallbacks. Raw IPs (from # modules/hosts.nix, not the ns1/ns2 DNS names) so reaching these hosts never # depends on their own DNS being up. internet.roles.default.machines = builtins.mapAttrs (_: h: { settings.host = h.ipv4; }) hosts; # Recovery root password for console access when a machine fails to boot. emergency-access = { roles.default.tags.nixos = { }; }; # Encrypted, deduplicating backups. control hosts the repos; ns1 is the # only client, backing up its declared clan.core.state (the Knot DNSSEC # keystore) over the mesh. Repo lives at /var/lib/borgbackup/ns1 on control. # Cross-host so an ns1 loss is recoverable; repokey encryption means control # never holds plaintext. Run `clan vars generate ns1` (YubiKey) before deploy. borgbackup = { roles.server.machines.control = { }; roles.client.machines.ns1 = { }; }; # Site gateways: one router instance per site, settings = the site's # port/VLAN layout. Everything else (PPPoE, Kea/Blocky, nftables, IPv6-PD, # CrowdSec, iperf3, speedtest) follows from modules/clan/router. router = { module = { name = "router"; input = "self"; }; # Fleet-wide settings: admin paths (SSH, scrapes, iperf3, Omada UI) are # opened to the mesh; internal proxy names live under # .cnx.network with wildcard certs issued via DNS-01 against # ns1 (which must declare the gateway's acme-secret.nix generator). roles.default.settings = { mesh.subnet = mesh.subnet; proxy = { domain = "cnx.network"; acme.nameserver = hosts.ns1.ipv4; acme.email = "postmaster@cnx.email"; }; }; # Chiang Mai (site 1): Topton 1U, Intel N300, 4x i226-V 2.5G. Port roles # use the expected igc names — verify against facter.json after the # first install and adjust if the box enumerates differently. roles.default.machines.gw-cnx-1.settings = { site = "cnx"; siteId = 1; wan.interface = "enp1s0"; wan.vlanId = null; # this ISP runs PPPoE untagged on the port wan.macAddress = "a8:b8:e0:01:06:87"; trunkPorts = [ "enp2s0" # "enp3s0" # STAGING: serves as the uplink until cutover (see the machine config) ]; # Dedicated on-site recovery port: untagged mgmt, always available even # if the switch config is broken. accessPorts.enp4s0 = "mgmt"; # Replaces the newedge.house OPNsense box; renumbered to the fleet # convention (10.1..0/24, router .1, pool .100-.199). The old # untagged LAN becomes tagged mgmt — infra switch ports get PVID 10. vlans = { mgmt = { id = 10; # 10.1.10.0/24 — servers, APs, switches, Omada, admin dhcp.reservations.storinator01 = { hwAddress = "7c:c2:55:e0:d6:40"; ipAddress = "10.1.10.53"; }; }; lan.id = 20; # 10.1.20.0/24 — trusted clients iot.id = 40; # 10.1.40.0/24 voip.id = 50; # 10.1.50.0/24 dmz.id = 60; # 10.1.60.0/24 unit1.id = 110; # 10.1.110.0/24 unit2.id = 120; # 10.1.120.0/24 unit3.id = 130; # 10.1.130.0/24 unit4.id = 140; # 10.1.140.0/24 unit5 = { id = 150; # 10.1.150.0/24 dhcp.reservations.newt = { hwAddress = "7c:d3:0a:21:58:0b"; ipAddress = "10.1.150.22"; }; }; }; # This site runs the Omada controller for its APs/switches. omada.enable = true; # Internal reverse proxy: real wildcard cert *.cnx1.cnx.network; Blocky # resolves the names to the router's LAN address for mgmt+lan clients. proxy = { enable = true; services.omada = { # Omada's UI is HTTPS with a self-signed cert on the host network. backend = "https://127.0.0.1:8043"; insecureSkipVerify = true; }; }; }; }; }; # Fleet-wide baseline applied to every machine. Secrets minted by # `clan vars generate` are encrypted for the admins group from the very # first run — generating before this took effect is what forced the # re-encryption dance (`clan vars fix`) on gw-cnx-1. machines = builtins.mapAttrs (_: _: { clan.core.sops.defaultGroups = [ "admins" ]; }) fleet; }