Upgrade clan to 26.05

Bump clan-core and nixos-mailserver to 26.05 (NixOS 26.05) and adapt to
the breaking changes surfaced by nix flake check:

- mesh-hosts/clan.nix: read the new shared, instance-scoped zerotier vars
  (zerotier-ip-<machine>-zerotier, zerotier-network-zerotier); admit
  external members via the controller's native allowedIds.
- monitoring/server.nix: Grafana lost its built-in secret_key default;
  mint one via a clan generator and pass it with $__file{}.
- dns/authoritative.nix: services.resolved.extraConfig removed -> settings.
- mail.nix: SNM cert API change (x509.useACMEHost + acme extraDomainNames)
  and accounts/dkim option renames.
- docs: mesh runbook updated for the new var paths and allowedIds.
This commit is contained in:
Berwn
2026-06-25 10:17:32 +07:00
parent 9bcc5ae2e3
commit 48fcc3058b
8 changed files with 98 additions and 108 deletions
+1 -14
View File
@@ -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
];
+9 -11
View File
@@ -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/<m>/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-<machine>-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.
Generated
+44 -63
View File
@@ -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": {
+2 -2
View File
@@ -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 =
+1 -1
View File
@@ -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;
+12 -10
View File
@@ -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
+11 -7
View File
@@ -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/<m>/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-<machine>-<instance>/ip and the network id at
# vars/shared/zerotier-network-<instance>/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";
+18
View File
@@ -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;