Reusable cnx.router.* module for the Topton 1U boxes replacing OPNsense: PPPoE WAN (optionally VLAN-tagged, AIS: 10, secret credentials incl. username), VLAN-filtering bridge, nftables NAT/firewall with MSS clamp, Kea DHCP with per-VLAN lease time, Blocky DNS, DHCPv6-PD, CrowdSec with the ZeroTier mesh whitelisted, optional Omada controller, ZFS disk. Fleet baseline rides along: admins sops group is now derived for every machine in clan.nix (secrets encrypt to it from the first vars generate) and time sync is chrony everywhere instead of systemd-timesyncd.
182 lines
6.2 KiB
Nix
182 lines
6.2 KiB
Nix
# Monitoring server, imported by control only: VictoriaMetrics (TSDB + scraper)
|
|
# and Grafana. VictoriaMetrics binds loopback (only Grafana, on the same host,
|
|
# reads it). Grafana is reachable over the ZeroTier mesh, scoped by the firewall
|
|
# rule at the bottom; the Hetzner cloud firewall keeps it off the public net.
|
|
{
|
|
config,
|
|
lib,
|
|
pkgs,
|
|
...
|
|
}:
|
|
let
|
|
mesh = import ../mesh-hosts.nix { inherit config lib; };
|
|
probes = import ./blackbox-probes.nix { inherit lib; };
|
|
vmPort = 8428;
|
|
logsPort = 9428;
|
|
grafanaPort = 3000;
|
|
controlV6 = mesh.hosts.control;
|
|
|
|
# A single scrape target with a friendly instance label. IPv6 mesh addresses
|
|
# must be bracketed for Prometheus-style targets.
|
|
target = name: addr: port: {
|
|
targets = [ "${addr}:${toString port}" ];
|
|
labels.instance = name;
|
|
};
|
|
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 = {
|
|
enable = true;
|
|
listenAddress = "127.0.0.1:${toString vmPort}";
|
|
retentionPeriod = "180d";
|
|
# The scraper dials IPv4-only by default; our ns1/ns2 targets are mesh ULAs,
|
|
# so without this VM drops them with "no suitable address found (try -enableTCP6)".
|
|
extraOptions = [ "-enableTCP6" ];
|
|
prometheusConfig = {
|
|
global.scrape_interval = "30s";
|
|
scrape_configs = [
|
|
{
|
|
job_name = "node";
|
|
static_configs = [
|
|
# control scrapes its own node_exporter over loopback so host metrics
|
|
# survive even if the mesh is down; ns1/ns2 are scraped over the mesh.
|
|
(target "control" "127.0.0.1" 9100)
|
|
(target "ns1" (v6 mesh.hosts.ns1) 9100)
|
|
(target "ns2" (v6 mesh.hosts.ns2) 9100)
|
|
(target "mx1" (v6 mesh.hosts.mx1) 9100)
|
|
(target "web01" (v6 mesh.hosts.web01) 9100)
|
|
(target "gw-cnx-1" (v6 mesh.hosts.gw-cnx-1) 9100)
|
|
];
|
|
}
|
|
{
|
|
job_name = "blocky";
|
|
static_configs = [
|
|
(target "gw-cnx-1" (v6 mesh.hosts.gw-cnx-1) 4000)
|
|
];
|
|
}
|
|
{
|
|
job_name = "knot";
|
|
static_configs = [
|
|
(target "ns1" (v6 mesh.hosts.ns1) 9433)
|
|
(target "ns2" (v6 mesh.hosts.ns2) 9433)
|
|
];
|
|
}
|
|
]
|
|
# Outside-in DNS probes via the blackbox exporter (blackbox.nix). The job
|
|
# list is generated from the same probe definitions the exporter uses.
|
|
++ probes.scrapeConfigs;
|
|
};
|
|
};
|
|
|
|
# Centralized logs: VictoriaLogs ingests journald from all three hosts, each
|
|
# of which runs systemd-journal-upload against /insert/journald (exporters.nix).
|
|
# Binds all interfaces because ns1/ns2 push over the mesh; the firewall rule at
|
|
# the bottom scopes 9428 to the mesh subnet and the Hetzner firewall closes the
|
|
# public side. Retention is set via extraOptions (no dedicated NixOS option).
|
|
services.victorialogs = {
|
|
enable = true;
|
|
listenAddress = ":${toString logsPort}";
|
|
# -enableTCP6: like the scraper above, VictoriaLogs is IPv4-only by default
|
|
# for *listening* too — ":9428" binds 0.0.0.0 only, so ns1/ns2 pushing over
|
|
# the IPv6 mesh get "connection refused". This makes it bind [::] (dual-stack)
|
|
# so the mesh can reach it. Retention has no dedicated NixOS option.
|
|
extraOptions = [
|
|
"-retentionPeriod=30d"
|
|
"-enableTCP6"
|
|
];
|
|
};
|
|
|
|
# Admin password generated once and stored as a clan secret. Retrieve with:
|
|
# clan vars get control grafana-admin/password
|
|
clan.core.vars.generators.grafana-admin = {
|
|
files."password" = {
|
|
secret = true;
|
|
owner = "grafana";
|
|
group = "grafana";
|
|
};
|
|
runtimeInputs = [ pkgs.openssl ];
|
|
script = ''
|
|
openssl rand -base64 24 | tr -d "\n" > "$out"/password
|
|
'';
|
|
};
|
|
|
|
# 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,
|
|
# alongside the metrics datasource.
|
|
declarativePlugins = [ pkgs.grafanaPlugins.victoriametrics-logs-datasource ];
|
|
settings = {
|
|
server = {
|
|
http_addr = "::";
|
|
http_port = grafanaPort;
|
|
root_url = "http://${v6 controlV6}:${toString grafanaPort}/";
|
|
};
|
|
security = {
|
|
admin_user = "admin";
|
|
admin_password = "$__file{${adminPasswordFile}}";
|
|
secret_key = "$__file{${secretKeyFile}}";
|
|
};
|
|
"auth.anonymous".enabled = false;
|
|
users.allow_sign_up = false;
|
|
};
|
|
provision = {
|
|
enable = true;
|
|
datasources.settings = {
|
|
apiVersion = 1;
|
|
datasources = [
|
|
{
|
|
name = "VictoriaMetrics";
|
|
type = "prometheus";
|
|
uid = "victoriametrics";
|
|
access = "proxy";
|
|
url = "http://127.0.0.1:${toString vmPort}";
|
|
isDefault = true;
|
|
}
|
|
{
|
|
name = "VictoriaLogs";
|
|
type = "victoriametrics-logs-datasource";
|
|
uid = "victorialogs";
|
|
access = "proxy";
|
|
url = "http://127.0.0.1:${toString logsPort}";
|
|
}
|
|
];
|
|
};
|
|
dashboards.settings = {
|
|
apiVersion = 1;
|
|
providers = [
|
|
{
|
|
name = "cnx";
|
|
options.path = ./dashboards;
|
|
options.foldersFromFilesStructure = false;
|
|
}
|
|
];
|
|
};
|
|
};
|
|
};
|
|
|
|
# Grafana (admin laptops + servers) and VictoriaLogs ingestion (ns1/ns2 push
|
|
# journald over the mesh) reachable only from the ZeroTier mesh.
|
|
networking.firewall.extraInputRules = ''
|
|
ip6 saddr ${mesh.subnet} tcp dport { ${toString grafanaPort}, ${toString logsPort} } accept
|
|
'';
|
|
}
|