diff --git a/clan.nix b/clan.nix index 584b47f..15cc8ea 100644 --- a/clan.nix +++ b/clan.nix @@ -1,18 +1,5 @@ let hosts = import ./modules/hosts.nix; - - # This clan-core pins the zerotier `allowedIps` interface (admit by network - # IPv6), but node IDs are the stable per-device handle (what `zerotier-cli - # info` prints). Derive a member's IP on THIS network from the controller's - # network id so external members can be listed by node id, as below. - ztNetworkId = builtins.readFile ./vars/per-machine/control/zerotier/zerotier-network-id/value; - ztMemberIp = - nodeId: - let - full = "fd" + ztNetworkId + "9993" + nodeId; - h = i: builtins.substring (i * 4) 4 full; - in - "${h 0}:${h 1}:${h 2}:${h 3}:${h 4}:${h 5}:${h 6}:${h 7}"; in { # Ensure this is unique among all clans you want to use. @@ -44,7 +31,7 @@ in # 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.allowedIps = map ztMemberIp [ + roles.controller.settings.allowedIds = [ "8802c8d7e0" # alex-nixos "2bd36db8cc" # kurogeek-thinkpad ]; diff --git a/docs/src/mesh.md b/docs/src/mesh.md index 17ecaf0..53020e9 100644 --- a/docs/src/mesh.md +++ b/docs/src/mesh.md @@ -10,10 +10,12 @@ transfers and metrics scraping ride this mesh, never the public net. ## The mesh map `modules/mesh-hosts.nix` does **not** hardcode addresses. It reads each machine's -IP from the public clan vars that clan-core's zerotier generator already writes -(`vars/per-machine//zerotier/zerotier-ip/value`) and derives the `/88` subnet -from `control`'s `zerotier-network-id`. Regenerate or re-key a node and the map -follows automatically. +IP from the public clan vars that clan-core's zerotier service writes. As of +clan-core 26.05 these are shared, instance-scoped generators +(`vars/shared/zerotier-ip--zerotier/ip/value`), and the `/88` subnet is +derived from the instance network id +(`vars/shared/zerotier-network-zerotier/network-id/value`). Regenerate or re-key a +node and the map follows automatically. Consumers: `modules/dns/authoritative.nix` (transfer ACLs), `modules/monitoring/*` (scrape targets and firewall scoping). @@ -21,12 +23,11 @@ Consumers: `modules/dns/authoritative.nix` (transfer ACLs), `modules/monitoring/ ## Admitting external members Inventory machines are auto-accepted. External devices (admin laptops) are listed -in `clan.nix` under the controller's `allowedIps`. Because this clan-core pins the -`allowedIps` interface (admit by network IPv6), we keep a **node-id** list and a -`ztMemberIp` helper derives each device's IP on this network: +in `clan.nix` under the controller's `allowedIds` — clan-core 26.05 admits members +by their 10-char ZeroTier node id directly, so no IP derivation is needed: ```nix -roles.controller.settings.allowedIps = map ztMemberIp [ +roles.controller.settings.allowedIds = [ "8802c8d7e0" # alex-nixos "2bd36db8cc" # kurogeek-thinkpad ]; @@ -34,6 +35,3 @@ roles.controller.settings.allowedIps = map ztMemberIp [ A device's 10-char node id comes from `zerotier-cli info` on that device. After editing, deploy `control`; the controller admits the new member on its next run. - -> A newer clan-core exposes `allowedIds` (admit by node id directly), but adopting -> it means a zerotier vars-schema migration, so we stay on the IP-derivation path. diff --git a/flake.lock b/flake.lock index e343a4d..012d963 100644 --- a/flake.lock +++ b/flake.lock @@ -23,22 +23,21 @@ "flake-parts": "flake-parts", "nix-darwin": "nix-darwin", "nix-select": "nix-select", - "nixos-facter-modules": "nixos-facter-modules", "nixpkgs": "nixpkgs", "sops-nix": "sops-nix", "systems": "systems", "treefmt-nix": "treefmt-nix" }, "locked": { - "lastModified": 1779116933, - "narHash": "sha256-BoSQT4vbuWbGcDbXp4nqBd1I9X8r+dLSPtTbc5yJd28=", - "rev": "10d86d5d8291fe5162a5535f433a4d50efc4b2bc", + "lastModified": 1782204050, + "narHash": "sha256-o8PXnm00Ecim/7WIQWxnFciWxvxgbll9fxH5MJ3a1Kg=", + "rev": "246e2bcba4dc9c009a96d017c030c7a53c83955e", "type": "tarball", - "url": "https://git.clan.lol/api/v1/repos/clan/clan-core/archive/10d86d5d8291fe5162a5535f433a4d50efc4b2bc.tar.gz" + "url": "https://git.clan.lol/api/v1/repos/clan/clan-core/archive/246e2bcba4dc9c009a96d017c030c7a53c83955e.tar.gz" }, "original": { "type": "tarball", - "url": "https://git.clan.lol/clan/clan-core/archive/25.11.tar.gz" + "url": "https://git.clan.lol/clan/clan-core/archive/26.05.tar.gz" } }, "data-mesher": { @@ -57,11 +56,11 @@ ] }, "locked": { - "lastModified": 1767582502, - "narHash": "sha256-WVcYGWcAlWzVt38OaTC5i5Q3QkIKJKZsJ7LcQZVVxeE=", - "rev": "31f2e3ecf207fd2760e3cebf7c2cf3cb7170ea3d", + "lastModified": 1778718524, + "narHash": "sha256-pXLoI6Ax0EnUK6r34UM1vibVC7CfTu6j72R2692ZzPs=", + "rev": "12c552ad547d87254f33f33bddd1a2cdbeac754d", "type": "tarball", - "url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/31f2e3ecf207fd2760e3cebf7c2cf3cb7170ea3d.tar.gz" + "url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/12c552ad547d87254f33f33bddd1a2cdbeac754d.tar.gz" }, "original": { "type": "tarball", @@ -76,11 +75,11 @@ ] }, "locked": { - "lastModified": 1766150702, - "narHash": "sha256-P0kM+5o+DKnB6raXgFEk3azw8Wqg5FL6wyl9jD+G5a4=", + "lastModified": 1780290312, + "narHash": "sha256-eTAlX0CwgB84Ts3GaBd944A3DRXVMzgA0EqroZBISUo=", "owner": "nix-community", "repo": "disko", - "rev": "916506443ecd0d0b4a0f4cf9d40a3c22ce39b378", + "rev": "115e5211780054d8a890b41f0b7734cafad54dfe", "type": "github" }, "original": { @@ -94,13 +93,13 @@ "locked": { "lastModified": 1767039857, "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", - "owner": "edolstra", + "owner": "NixOS", "repo": "flake-compat", "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", "type": "github" }, "original": { - "owner": "edolstra", + "owner": "NixOS", "repo": "flake-compat", "type": "github" } @@ -113,11 +112,11 @@ ] }, "locked": { - "lastModified": 1777988971, - "narHash": "sha256-qIoWPDs+0/8JecyYgE3gpKQxW/4bLW/gp45vow9ioCQ=", + "lastModified": 1778716662, + "narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "0678d8986be1661af6bb555f3489f2fdfc31f6ff", + "rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb", "type": "github" }, "original": { @@ -139,11 +138,11 @@ ] }, "locked": { - "lastModified": 1772893680, - "narHash": "sha256-JDqZMgxUTCq85ObSaFw0HhE+lvdOre1lx9iI6vYyOEs=", + "lastModified": 1778507602, + "narHash": "sha256-kTwur1wV+01SdqskVMSo6JMEpg71ps3HpbFY2GsflKs=", "owner": "cachix", "repo": "git-hooks.nix", - "rev": "8baab586afc9c9b57645a734c820e4ac0a604af9", + "rev": "61ab0e80d9c7ab14c256b5b453d8b3fb0189ba0a", "type": "github" }, "original": { @@ -182,16 +181,15 @@ ] }, "locked": { - "lastModified": 1772129556, - "narHash": "sha256-Utk0zd8STPsUJPyjabhzPc5BpPodLTXrwkpXBHYnpeg=", + "lastModified": 1779036909, + "narHash": "sha256-zXcwYQGCT6pzinK+1dBB2ekTVtfxGZAapb3Evdcu4fY=", "owner": "nix-darwin", "repo": "nix-darwin", - "rev": "ebec37af18215214173c98cf6356d0aca24a2585", + "rev": "56c666e108467d87d13508936aade6d567f2a501", "type": "github" }, "original": { "owner": "nix-darwin", - "ref": "nix-darwin-25.11", "repo": "nix-darwin", "type": "github" } @@ -209,21 +207,6 @@ "url": "https://git.clan.lol/clan/nix-select/archive/main.tar.gz" } }, - "nixos-facter-modules": { - "locked": { - "lastModified": 1773858690, - "narHash": "sha256-oW0/lC0oRG5H5LaK6Rmh9L1wmkn9TbenM4bXwnIEDKA=", - "owner": "nix-community", - "repo": "nixos-facter-modules", - "rev": "139dcef4dfc97009629c445806f197883351ab4a", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "nixos-facter-modules", - "type": "github" - } - }, "nixos-mailserver": { "inputs": { "blobs": "blobs", @@ -234,34 +217,31 @@ ] }, "locked": { - "lastModified": 1773912645, - "narHash": "sha256-QHzRqq6gh+t3F/QU9DkP7X63dDDcuIQmaDz12p7ANTg=", + "lastModified": 1781301671, + "narHash": "sha256-rq6WOopxq3U2AGEWO80o9LIJDYcYIdgw6jyl+y+19w8=", "owner": "simple-nixos-mailserver", "repo": "nixos-mailserver", - "rev": "25e6dbb8fca3b6e779c5a46fd03bd760b2165bb5", + "rev": "661ec59a97ccee13a63f79280b282eb6f7d3f817", "type": "gitlab" }, "original": { "owner": "simple-nixos-mailserver", - "ref": "nixos-25.11", + "ref": "nixos-26.05", "repo": "nixos-mailserver", "type": "gitlab" } }, "nixpkgs": { "locked": { - "lastModified": 1778003029, - "narHash": "sha256-q/nkKLDtHIyLjZpKhWk3cSK5IYsFqtMd6UtXF3ddjgA=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "0c88e1f2bdb93d5999019e99cb0e61e1fe2af4c5", - "type": "github" + "lastModified": 1780453794, + "narHash": "sha256-hhAl/iKiurXPn7rdzDgiSuRB8tqOB6f0buWkh8Y9mkY=", + "rev": "6b316287bae2ee04c9b93c8c858d930fd07d7338", + "type": "tarball", + "url": "https://releases.nixos.org/nixos/26.05/nixos-26.05.1183.6b316287bae2/nixexprs.tar.xz" }, "original": { - "owner": "nixos", - "ref": "nixos-25.11", - "repo": "nixpkgs", - "type": "github" + "type": "tarball", + "url": "https://nixos.org/channels/nixos-26.05/nixexprs.tar.xz" } }, "root": { @@ -283,11 +263,11 @@ ] }, "locked": { - "lastModified": 1777944972, - "narHash": "sha256-VfGRo1qTBKOe3s2gOv8LSoA6Fk19PvBlwQ1ECN0Evn8=", + "lastModified": 1780547341, + "narHash": "sha256-Gq8KNx5A7hBB3uGJaj6eQfLDIz5YdLu92gqBcvHvoUo=", "owner": "Mic92", "repo": "sops-nix", - "rev": "c591bf665727040c6cc5cb409079acb22dcce33c", + "rev": "9ed65852b6257fbeae4355bc24ecfea307ca759a", "type": "github" }, "original": { @@ -298,15 +278,16 @@ }, "systems": { "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "lastModified": 1774449309, + "narHash": "sha256-brhZ8DmuGtzkCYHJg4HEd602amKm89Y9ytsFZ5uWD1w=", "owner": "nix-systems", "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "rev": "c29398b59d2048c4ab79345812849c9bd15e9150", "type": "github" }, "original": { "owner": "nix-systems", + "ref": "future-26.11", "repo": "default", "type": "github" } @@ -319,11 +300,11 @@ ] }, "locked": { - "lastModified": 1768158989, - "narHash": "sha256-67vyT1+xClLldnumAzCTBvU0jLZ1YBcf4vANRWP3+Ak=", + "lastModified": 1780220602, + "narHash": "sha256-eynAfOmbmxJnkp7YewvCEbShNnnYJ9gLLqkzsYtBPeM=", "owner": "numtide", "repo": "treefmt-nix", - "rev": "e96d59dff5c0d7fddb9d113ba108f03c3ef99eca", + "rev": "db947814a175b7ca6ded66e21383d938df01c227", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index f95b97d..2adf7e1 100644 --- a/flake.nix +++ b/flake.nix @@ -1,10 +1,10 @@ { - inputs.clan-core.url = "https://git.clan.lol/clan/clan-core/archive/25.11.tar.gz"; + inputs.clan-core.url = "https://git.clan.lol/clan/clan-core/archive/26.05.tar.gz"; inputs.nixpkgs.follows = "clan-core/nixpkgs"; inputs.treefmt-nix.url = "github:numtide/treefmt-nix"; inputs.treefmt-nix.inputs.nixpkgs.follows = "nixpkgs"; # Simple NixOS Mailserver, pinned to the branch matching clan-core's nixpkgs. - inputs.nixos-mailserver.url = "gitlab:simple-nixos-mailserver/nixos-mailserver/nixos-25.11"; + inputs.nixos-mailserver.url = "gitlab:simple-nixos-mailserver/nixos-mailserver/nixos-26.05"; inputs.nixos-mailserver.inputs.nixpkgs.follows = "nixpkgs"; outputs = diff --git a/modules/dns/authoritative.nix b/modules/dns/authoritative.nix index 29de972..e793ca7 100644 --- a/modules/dns/authoritative.nix +++ b/modules/dns/authoritative.nix @@ -32,7 +32,7 @@ in # knot binds 0.0.0.0@53 and ::@53, so free port 53 by disabling the # systemd-resolved stub listener. Resolution still works via nss-resolve. - services.resolved.extraConfig = "DNSStubListener=no"; + services.resolved.settings.Resolve.DNSStubListener = "no"; services.knot = { enable = true; diff --git a/modules/mail.nix b/modules/mail.nix index 480af79..acfdee9 100644 --- a/modules/mail.nix +++ b/modules/mail.nix @@ -98,29 +98,31 @@ in mailserver = { enable = true; - # Fresh install: declare the latest layout the nixos-25.11 branch ships (3), + # Fresh install: declare the latest layout the nixos-26.05 branch ships (3), # so SNM uses the current dovecot mail directory layout with nothing to migrate. stateVersion = 3; inherit fqdn; domains = [ "cnx.email" ]; - inherit loginAccounts; + accounts = loginAccounts; # Consume a security.acme cert we obtain ourselves via DNS-01 (below); no - # web server and no inbound HTTP needed, so port 80 stays closed. Add the - # MTA-STS host as a SAN so the one cert also covers the policy endpoint. - certificateScheme = "acme"; - certificateDomains = [ - mtaStsHost - clientHost - ]; + # web server and no inbound HTTP needed, so port 80 stays closed. The extra + # SAN hosts (MTA-STS, client alias) are attached to that cert via + # security.acme.certs.${fqdn}.extraDomainNames below. + x509.useACMEHost = fqdn; - dkimSelector = "mail"; + dkim.defaults.selector = "mail"; }; security.acme = { acceptTerms = true; defaults.email = "postmaster@cnx.email"; certs.${fqdn} = { + # The MTA-STS endpoint and client-facing alias ride this one cert as SANs. + extraDomainNames = [ + mtaStsHost + clientHost + ]; dnsProvider = "rfc2136"; environmentFile = config.clan.core.vars.generators.dns-acme-rfc2136.files."rfc2136.env".path; # ns1 is the only nameserver that accepts the acme_mx1 UPDATE; check diff --git a/modules/mesh-hosts.nix b/modules/mesh-hosts.nix index beebc1c..bfe738b 100644 --- a/modules/mesh-hosts.nix +++ b/modules/mesh-hosts.nix @@ -2,16 +2,20 @@ # DNS zone transfers and metrics scraping ride this mesh, never the public net. # # Rather than hardcoding the addresses, we read them from the public clan vars -# that clan-core's zerotier generator already writes per machine -# (vars/per-machine//zerotier/zerotier-ip/value). This keeps the mesh map in +# that clan-core's zerotier service writes. As of clan-core 26.05 these are +# shared, instance-scoped generators: the per-machine IP lives at +# vars/shared/zerotier-ip--/ip and the network id at +# vars/shared/zerotier-network-/network-id (instance = "zerotier", +# the inventory.instances.zerotier name in clan.nix). This keeps the mesh map in # lockstep with the actual identities: regenerate or re-key a node and its # address here follows automatically. Call as: import ../mesh-hosts.nix { inherit config lib; }. { config, lib }: let dir = config.clan.core.settings.directory; + instance = "zerotier"; - readVar = - machine: file: builtins.readFile "${dir}/vars/per-machine/${machine}/zerotier/${file}/value"; + readIp = + machine: builtins.readFile "${dir}/vars/shared/zerotier-ip-${machine}-${instance}/ip/value"; hosts = lib.genAttrs [ "control" @@ -19,13 +23,13 @@ let "ns2" "mx1" "web01" - ] (m: readVar m "zerotier-ip"); + ] readIp; # RFC 4193 prefix of this ZeroTier network: fd + the 8-byte network id + the - # 0x9993 marker. The network id is a public var on the controller (control). + # 0x9993 marker. The network id is a public, shared var for the instance. # The /88 (11 bytes) covers fd + network id + 0x99 + 0x93, i.e. every mesh peer, # and is used to scope mesh-only firewall rules. - networkId = readVar "control" "zerotier-network-id"; + networkId = builtins.readFile "${dir}/vars/shared/zerotier-network-${instance}/network-id/value"; full = "fd" + networkId + "9993"; # 22 hex chars = 11 bytes hextet = i: builtins.substring (i * 4) 4 full; subnet = "${hextet 0}:${hextet 1}:${hextet 2}:${hextet 3}:${hextet 4}:${builtins.substring 20 2 full}00::/88"; diff --git a/modules/monitoring/server.nix b/modules/monitoring/server.nix index 9973b09..71f5842 100644 --- a/modules/monitoring/server.nix +++ b/modules/monitoring/server.nix @@ -25,6 +25,7 @@ let v6 = addr: "[${addr}]"; adminPasswordFile = config.clan.core.vars.generators.grafana-admin.files."password".path; + secretKeyFile = config.clan.core.vars.generators.grafana-secret-key.files."secret-key".path; in { services.victoriametrics = { @@ -95,6 +96,22 @@ in ''; }; + # NixOS 26.05 removed the built-in default for services.grafana's secret_key + # (used to sign/encrypt DB secrets). Mint our own instead of hardcoding the old + # upstream default. Safe to generate fresh here: the provisioned datasources + # carry no credentials, so nothing in Grafana's DB is encrypted with it. + clan.core.vars.generators.grafana-secret-key = { + files."secret-key" = { + secret = true; + owner = "grafana"; + group = "grafana"; + }; + runtimeInputs = [ pkgs.openssl ]; + script = '' + openssl rand -base64 32 | tr -d "\n" > "$out"/secret-key + ''; + }; + services.grafana = { enable = true; # VictoriaLogs datasource plugin so journald is greppable from Grafana, @@ -109,6 +126,7 @@ in security = { admin_user = "admin"; admin_password = "$__file{${adminPasswordFile}}"; + secret_key = "$__file{${secretKeyFile}}"; }; "auth.anonymous".enabled = false; users.allow_sign_up = false;