Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e316ec56ce | ||
|
|
559115e52b | ||
|
|
9cfcf91b51 | ||
|
|
570fe64497 | ||
|
|
2ac4215237 | ||
|
|
d51a334ec6 | ||
|
|
b81c0aaa89 | ||
|
|
fcd8e55024 | ||
|
|
19e1acda51 | ||
|
|
f5b6b4b55e | ||
|
|
3646d43ac5 | ||
|
|
d8d4a686eb | ||
|
|
b11ff75ca6 | ||
|
|
4735968433 | ||
|
|
02efeb454b | ||
|
|
868cd9eebd | ||
|
|
99fdde666c | ||
|
|
7b706eb3e7 | ||
|
|
6c89a7031a | ||
|
|
4ca5fd3afd |
+54
-9
@@ -13,13 +13,16 @@ Naming: `gw-<city>-<n>`, e.g. `gw-cnx-1`.
|
|||||||
| Function | Implementation |
|
| Function | Implementation |
|
||||||
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| WAN | PPPoE (`pppd`), per-site ISP credentials via clan vars prompts; `wan.vlanId` when the ISP tags the session (AIS: 10); `wan.macAddress` to clone the old router's MAC if the ISP has it pinned |
|
| WAN | PPPoE (`pppd`), per-site ISP credentials via clan vars prompts; `wan.vlanId` when the ISP tags the session (AIS: 10); `wan.macAddress` to clone the old router's MAC if the ISP has it pinned |
|
||||||
| LAN | VLAN-filtering bridge `br0` over the trunk ports (networkd) |
|
| LAN | VLAN-filtering bridge `br0` over the trunk ports (networkd); `accessPorts` pin a port untagged to one VLAN — convention: the last copper port is an untagged `mgmt` recovery port |
|
||||||
| Firewall/NAT | nftables: default-deny WAN, no inter-VLAN, MSS clamp, v4 NAT |
|
| Firewall/NAT | nftables: default-deny WAN, no inter-VLAN, MSS clamp, v4 NAT |
|
||||||
| DHCP | Kea, one subnet per VLAN |
|
| DHCP | Kea, one subnet per VLAN |
|
||||||
| DNS | Blocky (blocklist resolver), metrics on :4000 scraped by control |
|
| DNS | Blocky (blocklist resolver), metrics on :4000 scraped by control |
|
||||||
| IPv6 | DHCPv6-PD on ppp0, /64 per VLAN via SLAAC |
|
| IPv6 | DHCPv6-PD on ppp0, /64 per VLAN via SLAAC |
|
||||||
| Bans | CrowdSec + nftables bouncer (sshd log parsing) |
|
| Bans | CrowdSec + nftables bouncer (sshd log parsing) |
|
||||||
| Omada | Optional per site: TP-Link Omada controller as a podman container |
|
| Omada | Optional per site: TP-Link Omada controller as a podman container |
|
||||||
|
| Proxy | Optional: Caddy reverse proxy for internal services under `*.<site><n>.cnx.network` with a real Let's Encrypt wildcard (DNS-01 against ns1) |
|
||||||
|
| Diagnostics | iperf3 server on 5201, reachable from every VLAN and the mesh (`iperf3 -c <router-vlan-addr>`); CLI toolkit: tcpdump, mtr, ethtool, conntrack, kdig, iftop, librespeed-cli |
|
||||||
|
| Speed test | Hourly librespeed run (`speedtest.timer`) → `speedtest_*` metrics via node_exporter; vmalert flags download < 50% of the link's own 7-day median (`WANSpeedDegraded`) |
|
||||||
| Management | ZeroTier mesh: SSH, node_exporter, journald upload — like the fleet |
|
| Management | ZeroTier mesh: SSH, node_exporter, journald upload — like the fleet |
|
||||||
| Storage | Single-disk ZFS (zstd, `/var` its own dataset for snapshots) |
|
| Storage | Single-disk ZFS (zstd, `/var` its own dataset for snapshots) |
|
||||||
|
|
||||||
@@ -39,9 +42,13 @@ should also shorten `dhcp.leaseTime` (default 86400 s) so the pool recycles.
|
|||||||
First user: `gw-cnx-2` (site 2) runs the public WiFi — guest VLAN 30 at
|
First user: `gw-cnx-2` (site 2) runs the public WiFi — guest VLAN 30 at
|
||||||
`10.2.128.0/22`, pool `10.2.128.100 – 10.2.131.250`, `dhcp.leaseTime = 3600`.
|
`10.2.128.0/22`, pool `10.2.128.100 – 10.2.131.250`, `dhcp.leaseTime = 3600`.
|
||||||
|
|
||||||
| Site | siteId | mgmt | lan |
|
| Site | siteId | mgmt | lan | site-specific VLANs |
|
||||||
| ---- | ------ | -------------- | -------------- |
|
| ---- | ------ | -------------- | -------------- | --------------------------------------------------------------- |
|
||||||
| cnx | 1 | `10.1.10.0/24` | `10.1.20.0/24` |
|
| cnx | 1 | `10.1.10.0/24` | `10.1.20.0/24` | iot 40, voip 50, dmz 60, unit1–5 110/120/130/140/150 (all /24s) |
|
||||||
|
|
||||||
|
Static DHCP leases are declared per VLAN via `dhcp.reservations` (attribute
|
||||||
|
name = hostname, plus `hwAddress`/`ipAddress`); park them outside the pool,
|
||||||
|
in the `.2–.99` infra range or `.200–.254`.
|
||||||
|
|
||||||
Trust model: mgmt → everything; other VLANs → router DNS/DHCP + internet only
|
Trust model: mgmt → everything; other VLANs → router DNS/DHCP + internet only
|
||||||
(no inter-VLAN); WAN → nothing inbound; mesh → SSH, metrics, Omada UI.
|
(no inter-VLAN); WAN → nothing inbound; mesh → SSH, metrics, Omada UI.
|
||||||
@@ -57,17 +64,24 @@ Trust model: mgmt → everything; other VLANs → router DNS/DHCP + internet onl
|
|||||||
(dynamic PPPoE IP; clan connects over the mesh).
|
(dynamic PPPoE IP; clan connects over the mesh).
|
||||||
3. Add its node_exporter and Blocky scrape targets in
|
3. Add its node_exporter and Blocky scrape targets in
|
||||||
`modules/monitoring/server.nix`.
|
`modules/monitoring/server.nix`.
|
||||||
4. `git add` the new machine directory — flake evaluation only sees
|
4. If the site uses the internal proxy (`cnx.router.proxy.enable`), wire ns1:
|
||||||
|
import `(import ../../modules/dns/acme-gw-secret.nix "gw-<city>-<n>")` in
|
||||||
|
**both** the gateway's and ns1's configuration, and on ns1 add the
|
||||||
|
`dns-acme-gw-<city>-<n>-knot` generator, its `keyFiles` entry, and an
|
||||||
|
`acl_acme_gw_<city>_<n>` scoped to `_acme-challenge.<city><n>` on the
|
||||||
|
`cnx.network` zone (copy the `gw-cnx-1` blocks). Then
|
||||||
|
`clan vars generate ns1` and redeploy ns1.
|
||||||
|
5. `git add` the new machine directory — flake evaluation only sees
|
||||||
git-tracked files, so an untracked `machines/gw-…/` is silently ignored.
|
git-tracked files, so an untracked `machines/gw-…/` is silently ignored.
|
||||||
5. `clan vars generate gw-<city>-<n>` — prompts for the site's PPPoE
|
6. `clan vars generate gw-<city>-<n>` — prompts for the site's PPPoE
|
||||||
credentials, mints the ZeroTier identity etc. (`nix flake check` fails
|
credentials, mints the ZeroTier identity etc. (`nix flake check` fails
|
||||||
until this has run, because mesh-hosts reads the ZeroTier IP var.)
|
until this has run, because mesh-hosts reads the ZeroTier IP var.)
|
||||||
6. Boot the box from a NixOS installer USB on the local network, then:
|
7. Boot the box from a NixOS installer USB on the local network, then:
|
||||||
`clan machines install gw-<city>-<n> --target-host root@<lan-ip>`
|
`clan machines install gw-<city>-<n> --target-host root@<lan-ip>`
|
||||||
7. Check `facter.json` for the real NIC names, fix `wan.interface` /
|
8. Check `facter.json` for the real NIC names, fix `wan.interface` /
|
||||||
`trunkPorts` if the enumeration differs, and
|
`trunkPorts` if the enumeration differs, and
|
||||||
`clan machines update gw-<city>-<n>` (rides the mesh from then on).
|
`clan machines update gw-<city>-<n>` (rides the mesh from then on).
|
||||||
8. Add a row to the site table above and to the machines table in
|
9. Add a row to the site table above and to the machines table in
|
||||||
[Overview](./overview.md).
|
[Overview](./overview.md).
|
||||||
|
|
||||||
## Omada controller
|
## Omada controller
|
||||||
@@ -80,6 +94,33 @@ gateway's mesh address from an admin machine. Controller state is under
|
|||||||
`/var/lib/omada`, declared as clan state (`clan.core.state.omada`); wiring it
|
`/var/lib/omada`, declared as clan state (`clan.core.state.omada`); wiring it
|
||||||
into the borgbackup instance is still a follow-up.
|
into the borgbackup instance is still a follow-up.
|
||||||
|
|
||||||
|
## Internal reverse proxy
|
||||||
|
|
||||||
|
`cnx.router.proxy.enable` puts Caddy on the gateway, terminating TLS for
|
||||||
|
`<service>.<site><n>.cnx.network` (e.g. `https://omada.cnx1.cnx.network`) and
|
||||||
|
forwarding to internal backends:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
cnx.router.proxy = {
|
||||||
|
enable = true;
|
||||||
|
services.omada = {
|
||||||
|
backend = "https://127.0.0.1:8043";
|
||||||
|
insecureSkipVerify = true; # Omada's cert is self-signed
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Cert**: one real Let's Encrypt wildcard `*.<site><n>.cnx.network` per
|
||||||
|
gateway, issued via ACME DNS-01 (lego/RFC2136) against ns1 — works behind
|
||||||
|
PPPoE with zero inbound reachability, and browsers trust it without a CA
|
||||||
|
install. Each gateway has its own TSIG key (`acme_gw_<city>_<n>`) that ns1
|
||||||
|
scopes to `_acme-challenge.<site><n>` TXT records only.
|
||||||
|
- **Resolution**: the names exist only internally — Blocky answers
|
||||||
|
`*.<site><n>.cnx.network` with the router's `lan` address; the public
|
||||||
|
`cnx.network` zone never carries them.
|
||||||
|
- **Access**: `proxy.allowVlans` (default `mgmt` + `lan`) get 443 (and 80 for
|
||||||
|
the HTTP→HTTPS redirect). Not exposed to WAN, guest VLANs, or the mesh.
|
||||||
|
|
||||||
## Runbook
|
## Runbook
|
||||||
|
|
||||||
- **PPPoE down**: `systemctl status pppd-wan`, `journalctl -u pppd-wan` on the
|
- **PPPoE down**: `systemctl status pppd-wan`, `journalctl -u pppd-wan` on the
|
||||||
@@ -89,6 +130,10 @@ into the borgbackup instance is still a follow-up.
|
|||||||
port's `bridgeVLANs` tagging and that the switch trunk carries the VLAN.
|
port's `bridgeVLANs` tagging and that the switch trunk carries the VLAN.
|
||||||
- **DNS/blocklist issues**: Blocky metrics are in VictoriaMetrics (job
|
- **DNS/blocklist issues**: Blocky metrics are in VictoriaMetrics (job
|
||||||
`blocky`); `journalctl -u blocky` on the gateway.
|
`blocky`); `journalctl -u blocky` on the gateway.
|
||||||
|
- **Slow internet reported / `WANSpeedDegraded` firing**: check the
|
||||||
|
`speedtest_download_mbps` / `speedtest_ping_ms` trend in VictoriaMetrics,
|
||||||
|
then `librespeed-cli` and `mtr <isp-gw>` on the gateway to separate ISP
|
||||||
|
degradation from local saturation (`iftop` on ppp0 shows who's eating it).
|
||||||
- **Banned yourself**: `cscli decisions list` / `cscli decisions delete --ip <ip>`
|
- **Banned yourself**: `cscli decisions list` / `cscli decisions delete --ip <ip>`
|
||||||
on the gateway. The ZeroTier mesh `/88` is whitelisted at the parser stage
|
on the gateway. The ZeroTier mesh `/88` is whitelisted at the parser stage
|
||||||
(`cnx/mesh-whitelist`), so admin access over the mesh can never be banned.
|
(`cnx/mesh-whitelist`), so admin access over the mesh can never be banned.
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ there is picked up):
|
|||||||
- **CNX Uptime** (`uptime.json`) — per-host up/down status, current uptime,
|
- **CNX Uptime** (`uptime.json`) — per-host up/down status, current uptime,
|
||||||
availability over the selected window, and up/down history. Label-driven, so
|
availability over the selected window, and up/down history. Label-driven, so
|
||||||
every scraped host appears automatically.
|
every scraped host appears automatically.
|
||||||
|
- **CNX WAN Speed** (`wan.json`) — per-gateway WAN speed from the hourly
|
||||||
|
librespeed runs: latest download/upload/ping, throughput history with each
|
||||||
|
link's 7-day median as reference, latency/jitter, and test status. See
|
||||||
|
[Site gateways](./gateways.md).
|
||||||
- **parsedmarc** — DMARC aggregate/forensic report viewer. Auto-provisioned by
|
- **parsedmarc** — DMARC aggregate/forensic report viewer. Auto-provisioned by
|
||||||
the `parsedmarc` module (not from `dashboards/`); reads its own Elasticsearch
|
the `parsedmarc` module (not from `dashboards/`); reads its own Elasticsearch
|
||||||
datasource, not VictoriaMetrics. See [DMARC reports](#dmarc-reports) below.
|
datasource, not VictoriaMetrics. See [DMARC reports](#dmarc-reports) below.
|
||||||
|
|||||||
Generated
+4
@@ -247,6 +247,10 @@
|
|||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"clan-core": "clan-core",
|
"clan-core": "clan-core",
|
||||||
|
"flake-parts": [
|
||||||
|
"clan-core",
|
||||||
|
"flake-parts"
|
||||||
|
],
|
||||||
"nixos-mailserver": "nixos-mailserver",
|
"nixos-mailserver": "nixos-mailserver",
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
"clan-core",
|
"clan-core",
|
||||||
|
|||||||
@@ -7,13 +7,16 @@
|
|||||||
inputs.nixos-mailserver.url = "gitlab:simple-nixos-mailserver/nixos-mailserver/nixos-26.05";
|
inputs.nixos-mailserver.url = "gitlab:simple-nixos-mailserver/nixos-mailserver/nixos-26.05";
|
||||||
inputs.nixos-mailserver.inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixos-mailserver.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
|
||||||
|
inputs.flake-parts.follows = "clan-core/flake-parts";
|
||||||
|
|
||||||
outputs =
|
outputs =
|
||||||
{
|
inputs@{
|
||||||
self,
|
self,
|
||||||
clan-core,
|
clan-core,
|
||||||
nixpkgs,
|
nixpkgs,
|
||||||
|
flake-parts,
|
||||||
...
|
...
|
||||||
}@inputs:
|
}:
|
||||||
let
|
let
|
||||||
# Usage see: https://docs.clan.lol
|
# Usage see: https://docs.clan.lol
|
||||||
clan = clan-core.lib.clan {
|
clan = clan-core.lib.clan {
|
||||||
@@ -36,38 +39,31 @@
|
|||||||
"age-plugin-fido2-hmac"
|
"age-plugin-fido2-hmac"
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
in
|
||||||
|
flake-parts.lib.mkFlake { inherit inputs; } {
|
||||||
systems = [
|
systems = [
|
||||||
"x86_64-linux"
|
"x86_64-linux"
|
||||||
"aarch64-linux"
|
"aarch64-linux"
|
||||||
"aarch64-darwin"
|
"aarch64-darwin"
|
||||||
"x86_64-darwin"
|
"x86_64-darwin"
|
||||||
];
|
];
|
||||||
forAllSystems = nixpkgs.lib.genAttrs systems;
|
|
||||||
pkgsFor = system: clan-core.inputs.nixpkgs.legacyPackages.${system};
|
|
||||||
treefmtFor = system: inputs.treefmt-nix.lib.evalModule (pkgsFor system) ./fmt.nix;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
inherit (clan.config) nixosConfigurations nixosModules clanInternals;
|
|
||||||
clan = clan.config;
|
|
||||||
|
|
||||||
# `nix fmt` and the `nix flake check` formatting gate.
|
flake = {
|
||||||
formatter = forAllSystems (system: (treefmtFor system).config.build.wrapper);
|
inherit (clan.config) nixosConfigurations nixosModules clanInternals;
|
||||||
checks = forAllSystems (system: {
|
clan = clan.config;
|
||||||
formatting = (treefmtFor system).config.build.check self;
|
};
|
||||||
});
|
|
||||||
|
|
||||||
# Add the Clan cli tool to the dev shell.
|
perSystem =
|
||||||
# Use "nix develop" to enter the dev shell.
|
{ system, ... }:
|
||||||
devShells = forAllSystems (
|
|
||||||
system:
|
|
||||||
let
|
let
|
||||||
|
pkgs = clan-core.inputs.nixpkgs.legacyPackages.${system};
|
||||||
|
treefmtEval = inputs.treefmt-nix.lib.evalModule pkgs ./fmt.nix;
|
||||||
clanCli = clan-core.packages.${system}.clan-cli;
|
clanCli = clan-core.packages.${system}.clan-cli;
|
||||||
# `clan machines update a b c` normally runs machines in parallel,
|
# `clan machines update a b c` normally runs machines in parallel,
|
||||||
# which interleaves their output and buries the YubiKey PIN prompts.
|
# which interleaves their output and buries the YubiKey PIN prompts.
|
||||||
# This wrapper (first in PATH) runs them one at a time instead; any
|
# This wrapper (first in PATH) runs them one at a time instead; any
|
||||||
# flags fall through to the real CLI untouched.
|
# flags fall through to the real CLI untouched.
|
||||||
clanSequential = (pkgsFor system).writeShellScriptBin "clan" ''
|
clanSequential = pkgs.writeShellScriptBin "clan" ''
|
||||||
if [ "$#" -gt 3 ] && [ "$1" = machines ] && [ "$2" = update ]; then
|
if [ "$#" -gt 3 ] && [ "$1" = machines ] && [ "$2" = update ]; then
|
||||||
shift 2
|
shift 2
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
@@ -84,14 +80,19 @@
|
|||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
default = (pkgsFor system).mkShell {
|
# `nix fmt` and the `nix flake check` formatting gate.
|
||||||
|
formatter = treefmtEval.config.build.wrapper;
|
||||||
|
checks.formatting = treefmtEval.config.build.check self;
|
||||||
|
|
||||||
|
# Add the Clan cli tool to the dev shell.
|
||||||
|
# Use "nix develop" to enter the dev shell.
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
packages = [
|
packages = [
|
||||||
clanSequential
|
clanSequential
|
||||||
clanCli
|
clanCli
|
||||||
(treefmtFor system).config.build.wrapper
|
treefmtEval.config.build.wrapper
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,9 @@
|
|||||||
},
|
},
|
||||||
"web01": {
|
"web01": {
|
||||||
"installedAt": 1781983723
|
"installedAt": 1781983723
|
||||||
|
},
|
||||||
|
"gw-cnx-1": {
|
||||||
|
"installedAt": 1785494267
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
imports = [
|
imports = [
|
||||||
../../modules/router
|
../../modules/router
|
||||||
../../modules/monitoring/exporters.nix
|
../../modules/monitoring/exporters.nix
|
||||||
|
(import ../../modules/dns/acme-gw-secret.nix "gw-cnx-1")
|
||||||
];
|
];
|
||||||
|
|
||||||
clan.core.sops.defaultGroups = [ "admins" ];
|
clan.core.sops.defaultGroups = [ "admins" ];
|
||||||
@@ -24,18 +25,65 @@
|
|||||||
site = "cnx";
|
site = "cnx";
|
||||||
siteId = 1;
|
siteId = 1;
|
||||||
wan.interface = "enp1s0";
|
wan.interface = "enp1s0";
|
||||||
wan.vlanId = 10; # AIS delivers PPPoE tagged on VLAN 10
|
wan.vlanId = null; # this ISP runs PPPoE untagged on the port
|
||||||
trunkPorts = [
|
trunkPorts = [
|
||||||
"enp2s0"
|
"enp2s0"
|
||||||
"enp3s0"
|
# "enp3s0" # STAGING: serves as the uplink below until cutover
|
||||||
"enp4s0"
|
|
||||||
];
|
];
|
||||||
|
# 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.<vlanId>.0/24, router .1, pool .100-.199). The old
|
||||||
|
# untagged LAN becomes tagged mgmt — infra switch ports get PVID 10.
|
||||||
vlans = {
|
vlans = {
|
||||||
mgmt.id = 10; # 10.1.10.0/24 — APs, switches, Omada, admin
|
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
|
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.
|
# This site runs the Omada controller for its APs/switches.
|
||||||
omada.enable = true;
|
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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# STAGING (remove at cutover, and restore enp3s0 to trunkPorts): DHCP-client
|
||||||
|
# uplink into the existing OPNsense LAN so the box has internet + mesh while
|
||||||
|
# it runs alongside the old router. Default-deny firewall on this interface
|
||||||
|
# (it's in no VLAN zone); PPPoE simply retries until the WAN port is cabled.
|
||||||
|
# Do NOT connect the trunk ports to the production switch while staging —
|
||||||
|
# Kea on tag 10 would fight the OPNsense LAN DHCP in one broadcast domain.
|
||||||
|
systemd.network.networks."05-staging" = {
|
||||||
|
matchConfig.Name = "enp3s0";
|
||||||
|
networkConfig.DHCP = "ipv4";
|
||||||
};
|
};
|
||||||
|
|
||||||
time.timeZone = "Etc/GMT-7"; # UTC+7 (Thailand, fixed offset, no DST)
|
time.timeZone = "Etc/GMT-7"; # UTC+7 (Thailand, fixed offset, no DST)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,7 @@ in
|
|||||||
../../modules/dns/authoritative.nix
|
../../modules/dns/authoritative.nix
|
||||||
../../modules/dns/acme-mx1-secret.nix
|
../../modules/dns/acme-mx1-secret.nix
|
||||||
../../modules/dns/acme-web01-secret.nix
|
../../modules/dns/acme-web01-secret.nix
|
||||||
|
(import ../../modules/dns/acme-gw-secret.nix "gw-cnx-1")
|
||||||
../../modules/static-ipv6.nix
|
../../modules/static-ipv6.nix
|
||||||
../../modules/monitoring/exporters.nix
|
../../modules/monitoring/exporters.nix
|
||||||
];
|
];
|
||||||
@@ -93,10 +94,29 @@ in
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# ACME DNS-01, dedicated gateway keys. Same pattern as web01: each gateway
|
||||||
|
# holds its own TSIG key (acme_gw_<site>_<n>), rendered from the shared
|
||||||
|
# secret generator imported above. acl_acme_gw_<site>_<n> scopes it to TXT
|
||||||
|
# updates at _acme-challenge.<site><n> on cnx.network — the owner its internal
|
||||||
|
# wildcard *.<site><n>.cnx.network challenge uses — and nothing else.
|
||||||
|
clan.core.vars.generators.dns-acme-gw-cnx-1-knot = {
|
||||||
|
files."acme.conf" = {
|
||||||
|
secret = true;
|
||||||
|
owner = "knot";
|
||||||
|
group = "knot";
|
||||||
|
};
|
||||||
|
dependencies = [ "dns-acme-gw-cnx-1-secret" ];
|
||||||
|
script = ''
|
||||||
|
printf 'key:\n - id: acme_gw_cnx_1\n algorithm: hmac-sha256\n secret: %s\n' \
|
||||||
|
"$(cat "$in"/dns-acme-gw-cnx-1-secret/secret)" > "$out"/acme.conf
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
services.knot.keyFiles = [
|
services.knot.keyFiles = [
|
||||||
config.clan.core.vars.generators.dns-acme-tsig.files."acme.conf".path
|
config.clan.core.vars.generators.dns-acme-tsig.files."acme.conf".path
|
||||||
config.clan.core.vars.generators.dns-acme-mx1-knot.files."acme.conf".path
|
config.clan.core.vars.generators.dns-acme-mx1-knot.files."acme.conf".path
|
||||||
config.clan.core.vars.generators.dns-acme-web01-knot.files."acme.conf".path
|
config.clan.core.vars.generators.dns-acme-web01-knot.files."acme.conf".path
|
||||||
|
config.clan.core.vars.generators.dns-acme-gw-cnx-1-knot.files."acme.conf".path
|
||||||
];
|
];
|
||||||
|
|
||||||
services.knot.settings.acl = [
|
services.knot.settings.acl = [
|
||||||
@@ -133,6 +153,17 @@ in
|
|||||||
# i.e. _acme-challenge at the cnx.network apex (where this acl is attached).
|
# i.e. _acme-challenge at the cnx.network apex (where this acl is attached).
|
||||||
"update-owner-name" = [ "_acme-challenge" ];
|
"update-owner-name" = [ "_acme-challenge" ];
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
id = "acl_acme_gw_cnx_1";
|
||||||
|
key = "acme_gw_cnx_1";
|
||||||
|
action = [ "update" ];
|
||||||
|
"update-type" = [ "TXT" ];
|
||||||
|
"update-owner" = "name";
|
||||||
|
"update-owner-match" = "sub-or-equal";
|
||||||
|
# The internal wildcard *.cnx1.cnx.network places its challenge at
|
||||||
|
# _acme-challenge.cnx1.cnx.network.
|
||||||
|
"update-owner-name" = [ "_acme-challenge.cnx1" ];
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
# Automatic DNSSEC signing policy (primary only). ECDSA P-256/SHA-256 with
|
# Automatic DNSSEC signing policy (primary only). ECDSA P-256/SHA-256 with
|
||||||
@@ -168,6 +199,9 @@ in
|
|||||||
"acl_acme"
|
"acl_acme"
|
||||||
]
|
]
|
||||||
++ lib.optionals (d == "cnx.email") [ "acl_acme_mx1" ]
|
++ lib.optionals (d == "cnx.email") [ "acl_acme_mx1" ]
|
||||||
++ lib.optionals (d == "cnx.network") [ "acl_acme_web01" ];
|
++ lib.optionals (d == "cnx.network") [
|
||||||
|
"acl_acme_web01"
|
||||||
|
"acl_acme_gw_cnx_1"
|
||||||
|
];
|
||||||
}) domains;
|
}) domains;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Shared TSIG secret for a gateway's dedicated ACME key (function: machine
|
||||||
|
# name -> module). The acme_gw_<x> key lets that gateway — and only it — write
|
||||||
|
# _acme-challenge.<label> TXT records on ns1 to obtain its internal wildcard
|
||||||
|
# cert via DNS-01. ns1 scopes it with a matching acl on the cnx.network zone.
|
||||||
|
# Import on BOTH ns1 and the gateway machine, applied with the machine name:
|
||||||
|
# (import ../../modules/dns/acme-gw-secret.nix "gw-cnx-1")
|
||||||
|
machine:
|
||||||
|
{ pkgs, ... }:
|
||||||
|
{
|
||||||
|
clan.core.vars.generators."dns-acme-${machine}-secret" = {
|
||||||
|
share = true;
|
||||||
|
files."secret".secret = true;
|
||||||
|
runtimeInputs = [ pkgs.openssl ];
|
||||||
|
# 32 random bytes, base64 — a valid hmac-sha256 TSIG secret.
|
||||||
|
script = ''openssl rand -base64 32 | tr -d '\n' > "$out"/secret'';
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -4,4 +4,5 @@
|
|||||||
"cnx.network"
|
"cnx.network"
|
||||||
"buildfor.life"
|
"buildfor.life"
|
||||||
"cnx.email"
|
"cnx.email"
|
||||||
|
"newedge.house"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
$ORIGIN newedge.house.
|
||||||
|
$TTL 3600
|
||||||
|
|
||||||
|
@ IN SOA ns1.cnx.network. hostmaster.cnx.network. (
|
||||||
|
2026061401 ; serial (ignored: Knot auto-assigns a dateserial on signing)
|
||||||
|
3600 ; refresh
|
||||||
|
900 ; retry
|
||||||
|
604800 ; expire
|
||||||
|
300 ) ; negative-cache TTL
|
||||||
|
|
||||||
|
; Served by the same nameservers (out-of-bailiwick, no glue needed here).
|
||||||
|
@ IN NS ns1.cnx.network.
|
||||||
|
@ IN NS ns2.cnx.network.
|
||||||
@@ -59,6 +59,31 @@ in
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
# WAN quality on the site gateways, fed by the hourly speedtest timer
|
||||||
|
# (modules/router/speedtest.nix) via the node_exporter textfile
|
||||||
|
# collector. Degradation is judged against the link's own 7-day median,
|
||||||
|
# so no per-site/per-ISP-plan threshold needs maintaining.
|
||||||
|
name = "wan";
|
||||||
|
rules = [
|
||||||
|
{
|
||||||
|
alert = "WANSpeedDegraded";
|
||||||
|
expr = "speedtest_download_mbps < 0.5 * quantile_over_time(0.5, speedtest_download_mbps[7d])";
|
||||||
|
for = "3h";
|
||||||
|
labels.severity = "warning";
|
||||||
|
annotations.summary = "WAN download on {{ $labels.instance }} is below half its 7-day median";
|
||||||
|
annotations.description = "Sustained for 3h (several test runs) — likely ISP degradation, line fault, or saturation. Compare speedtest_upload_mbps/speedtest_ping_ms and run `librespeed-cli` manually on the gateway.";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
alert = "WANSpeedTestFailing";
|
||||||
|
expr = "speedtest_success == 0";
|
||||||
|
for = "3h";
|
||||||
|
labels.severity = "warning";
|
||||||
|
annotations.summary = "Speed tests on {{ $labels.instance }} have been failing for 3h";
|
||||||
|
annotations.description = "librespeed-cli cannot complete a test while other traffic may still flow; check `journalctl -u speedtest` on the gateway.";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
{
|
{
|
||||||
name = "backup";
|
name = "backup";
|
||||||
rules = [
|
rules = [
|
||||||
|
|||||||
@@ -0,0 +1,267 @@
|
|||||||
|
{
|
||||||
|
"uid": "cnx-wan",
|
||||||
|
"title": "CNX WAN Speed",
|
||||||
|
"tags": ["wan", "speedtest", "gateway", "cnx"],
|
||||||
|
"timezone": "browser",
|
||||||
|
"schemaVersion": 39,
|
||||||
|
"version": 1,
|
||||||
|
"refresh": "5m",
|
||||||
|
"time": { "from": "now-7d", "to": "now" },
|
||||||
|
"templating": { "list": [] },
|
||||||
|
"annotations": { "list": [] },
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"type": "row",
|
||||||
|
"title": "WAN speed (site gateways)",
|
||||||
|
"id": 1,
|
||||||
|
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "stat",
|
||||||
|
"title": "Download (latest)",
|
||||||
|
"description": "Most recent hourly librespeed result per gateway.",
|
||||||
|
"id": 2,
|
||||||
|
"datasource": { "type": "prometheus", "uid": "victoriametrics" },
|
||||||
|
"gridPos": { "h": 5, "w": 6, "x": 0, "y": 1 },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "bps",
|
||||||
|
"color": { "mode": "fixed", "fixedColor": "green" },
|
||||||
|
"noValue": "no data"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": ["lastNotNull"],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "area",
|
||||||
|
"textMode": "value_and_name",
|
||||||
|
"orientation": "auto"
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": { "type": "prometheus", "uid": "victoriametrics" },
|
||||||
|
"expr": "speedtest_download_mbps * 1e6",
|
||||||
|
"legendFormat": "{{instance}}",
|
||||||
|
"instant": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "stat",
|
||||||
|
"title": "Upload (latest)",
|
||||||
|
"description": "Most recent hourly librespeed result per gateway.",
|
||||||
|
"id": 3,
|
||||||
|
"datasource": { "type": "prometheus", "uid": "victoriametrics" },
|
||||||
|
"gridPos": { "h": 5, "w": 6, "x": 6, "y": 1 },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "bps",
|
||||||
|
"color": { "mode": "fixed", "fixedColor": "blue" },
|
||||||
|
"noValue": "no data"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": ["lastNotNull"],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "area",
|
||||||
|
"textMode": "value_and_name",
|
||||||
|
"orientation": "auto"
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": { "type": "prometheus", "uid": "victoriametrics" },
|
||||||
|
"expr": "speedtest_upload_mbps * 1e6",
|
||||||
|
"legendFormat": "{{instance}}",
|
||||||
|
"instant": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "stat",
|
||||||
|
"title": "Ping (latest)",
|
||||||
|
"description": "Latency to the librespeed test server. Yellow over 50 ms, red over 100 ms.",
|
||||||
|
"id": 4,
|
||||||
|
"datasource": { "type": "prometheus", "uid": "victoriametrics" },
|
||||||
|
"gridPos": { "h": 5, "w": 6, "x": 12, "y": 1 },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "ms",
|
||||||
|
"color": { "mode": "thresholds" },
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{ "color": "green", "value": null },
|
||||||
|
{ "color": "yellow", "value": 50 },
|
||||||
|
{ "color": "red", "value": 100 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"noValue": "no data"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": ["lastNotNull"],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "none",
|
||||||
|
"textMode": "value_and_name",
|
||||||
|
"orientation": "auto"
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": { "type": "prometheus", "uid": "victoriametrics" },
|
||||||
|
"expr": "speedtest_ping_ms",
|
||||||
|
"legendFormat": "{{instance}}",
|
||||||
|
"instant": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "stat",
|
||||||
|
"title": "Test status",
|
||||||
|
"description": "Whether the last speed test on each gateway completed. FAILING also fires the WANSpeedTestFailing alert after 3h.",
|
||||||
|
"id": 5,
|
||||||
|
"datasource": { "type": "prometheus", "uid": "victoriametrics" },
|
||||||
|
"gridPos": { "h": 5, "w": 6, "x": 18, "y": 1 },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": { "mode": "thresholds" },
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [{ "color": "green", "value": null }]
|
||||||
|
},
|
||||||
|
"noValue": "no data",
|
||||||
|
"mappings": [
|
||||||
|
{
|
||||||
|
"type": "value",
|
||||||
|
"options": {
|
||||||
|
"0": { "text": "FAILING", "color": "red", "index": 0 },
|
||||||
|
"1": { "text": "OK", "color": "green", "index": 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": ["lastNotNull"],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"colorMode": "background",
|
||||||
|
"graphMode": "none",
|
||||||
|
"textMode": "value_and_name",
|
||||||
|
"orientation": "auto"
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": { "type": "prometheus", "uid": "victoriametrics" },
|
||||||
|
"expr": "speedtest_success",
|
||||||
|
"legendFormat": "{{instance}}",
|
||||||
|
"instant": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "timeseries",
|
||||||
|
"title": "Download",
|
||||||
|
"description": "Hourly download rate per gateway, with each link's own 7-day median as reference — the WANSpeedDegraded alert fires when a link stays below half its median for 3h.",
|
||||||
|
"id": 6,
|
||||||
|
"datasource": { "type": "prometheus", "uid": "victoriametrics" },
|
||||||
|
"gridPos": { "h": 9, "w": 12, "x": 0, "y": 6 },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": { "unit": "bps", "min": 0 },
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"matcher": { "id": "byRegexp", "options": ".*7d median.*" },
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "custom.lineStyle",
|
||||||
|
"value": { "fill": "dash", "dash": [10, 10] }
|
||||||
|
},
|
||||||
|
{ "id": "custom.fillOpacity", "value": 0 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": { "type": "prometheus", "uid": "victoriametrics" },
|
||||||
|
"expr": "speedtest_download_mbps * 1e6",
|
||||||
|
"legendFormat": "{{instance}}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"refId": "B",
|
||||||
|
"datasource": { "type": "prometheus", "uid": "victoriametrics" },
|
||||||
|
"expr": "quantile_over_time(0.5, speedtest_download_mbps[7d]) * 1e6",
|
||||||
|
"legendFormat": "{{instance}} 7d median"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "timeseries",
|
||||||
|
"title": "Upload",
|
||||||
|
"description": "Hourly upload rate per gateway.",
|
||||||
|
"id": 7,
|
||||||
|
"datasource": { "type": "prometheus", "uid": "victoriametrics" },
|
||||||
|
"gridPos": { "h": 9, "w": 12, "x": 12, "y": 6 },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": { "unit": "bps", "min": 0 },
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": { "type": "prometheus", "uid": "victoriametrics" },
|
||||||
|
"expr": "speedtest_upload_mbps * 1e6",
|
||||||
|
"legendFormat": "{{instance}}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "timeseries",
|
||||||
|
"title": "Latency & jitter",
|
||||||
|
"description": "Ping and jitter towards the librespeed test server. A latency climb with stable throughput usually means bufferbloat or an ISP routing change.",
|
||||||
|
"id": 8,
|
||||||
|
"datasource": { "type": "prometheus", "uid": "victoriametrics" },
|
||||||
|
"gridPos": { "h": 8, "w": 24, "x": 0, "y": 15 },
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": { "unit": "ms", "min": 0 },
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": { "type": "prometheus", "uid": "victoriametrics" },
|
||||||
|
"expr": "speedtest_ping_ms",
|
||||||
|
"legendFormat": "{{instance}} ping"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"refId": "B",
|
||||||
|
"datasource": { "type": "prometheus", "uid": "victoriametrics" },
|
||||||
|
"expr": "speedtest_jitter_ms",
|
||||||
|
"legendFormat": "{{instance}} jitter"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
{
|
{
|
||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
|
pkgs,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
@@ -67,6 +68,25 @@ let
|
|||||||
e.g. public-WiFi guest VLANs (3600-7200), so the pool recycles.
|
e.g. public-WiFi guest VLANs (3600-7200), so the pool recycles.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
reservations = lib.mkOption {
|
||||||
|
type = lib.types.attrsOf (
|
||||||
|
lib.types.submodule {
|
||||||
|
options = {
|
||||||
|
hwAddress = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
example = "aa:bb:cc:dd:ee:ff";
|
||||||
|
description = "Client MAC address.";
|
||||||
|
};
|
||||||
|
ipAddress = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "Fixed address handed to this client (inside the VLAN's subnet, outside the pool).";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
default = { };
|
||||||
|
description = "Static DHCP leases; the attribute name becomes the client's hostname.";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
allowWan = lib.mkOption {
|
allowWan = lib.mkOption {
|
||||||
type = lib.types.bool;
|
type = lib.types.bool;
|
||||||
@@ -84,6 +104,9 @@ in
|
|||||||
./dns-dhcp.nix
|
./dns-dhcp.nix
|
||||||
./crowdsec.nix
|
./crowdsec.nix
|
||||||
./omada.nix
|
./omada.nix
|
||||||
|
./proxy.nix
|
||||||
|
./iperf.nix
|
||||||
|
./speedtest.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
options.cnx.router = {
|
options.cnx.router = {
|
||||||
@@ -137,6 +160,19 @@ in
|
|||||||
description = "LAN ports carrying all VLANs tagged (incl. any 10G SFP+ ports).";
|
description = "LAN ports carrying all VLANs tagged (incl. any 10G SFP+ ports).";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
accessPorts = lib.mkOption {
|
||||||
|
type = lib.types.attrsOf lib.types.str;
|
||||||
|
default = { };
|
||||||
|
example = {
|
||||||
|
enp4s0 = "mgmt";
|
||||||
|
};
|
||||||
|
description = ''
|
||||||
|
Ports acting as untagged access ports on a single VLAN (port name ->
|
||||||
|
VLAN name). Frames are untagged on the wire; the bridge tags them with
|
||||||
|
the VLAN's PVID. Use for an always-available on-site mgmt port.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
vlans = lib.mkOption {
|
vlans = lib.mkOption {
|
||||||
type = lib.types.attrsOf (lib.types.submodule vlanModule);
|
type = lib.types.attrsOf (lib.types.submodule vlanModule);
|
||||||
description = "VLANs served at this site; `mgmt` and `lan` are mandatory.";
|
description = "VLANs served at this site; `mgmt` and `lan` are mandatory.";
|
||||||
@@ -149,6 +185,27 @@ in
|
|||||||
assertion = cfg.vlans ? mgmt && cfg.vlans ? lan;
|
assertion = cfg.vlans ? mgmt && cfg.vlans ? lan;
|
||||||
message = "cnx.router: every site must define the `mgmt` and `lan` VLANs.";
|
message = "cnx.router: every site must define the `mgmt` and `lan` VLANs.";
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
assertion = lib.all (v: cfg.vlans ? ${v}) (lib.attrValues cfg.accessPorts);
|
||||||
|
message = "cnx.router: every accessPorts value must name a defined VLAN.";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = lib.all (p: !(cfg.accessPorts ? ${p})) cfg.trunkPorts;
|
||||||
|
message = "cnx.router: a port cannot be both a trunk and an access port.";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
# Router diagnostics toolkit: packets (tcpdump), path (mtr), link
|
||||||
|
# negotiation (ethtool), NAT state (conntrack), DNS (kdig), per-flow
|
||||||
|
# bandwidth (iftop), WAN throughput (librespeed-cli; iperf3 covers LAN).
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
tcpdump
|
||||||
|
mtr
|
||||||
|
ethtool
|
||||||
|
conntrack-tools
|
||||||
|
knot-dns
|
||||||
|
iftop
|
||||||
|
librespeed-cli
|
||||||
];
|
];
|
||||||
|
|
||||||
networking.useNetworkd = true;
|
networking.useNetworkd = true;
|
||||||
@@ -234,6 +291,21 @@ in
|
|||||||
};
|
};
|
||||||
}) cfg.trunkPorts
|
}) cfg.trunkPorts
|
||||||
)
|
)
|
||||||
|
// lib.mapAttrs' (
|
||||||
|
port: vlanName:
|
||||||
|
lib.nameValuePair "25-access-${port}" {
|
||||||
|
matchConfig.Name = port;
|
||||||
|
networkConfig.Bridge = "br0";
|
||||||
|
bridgeVLANs = [
|
||||||
|
{
|
||||||
|
VLAN = cfg.vlans.${vlanName}.id;
|
||||||
|
PVID = cfg.vlans.${vlanName}.id;
|
||||||
|
EgressUntagged = cfg.vlans.${vlanName}.id;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
linkConfig.RequiredForOnline = "no";
|
||||||
|
}
|
||||||
|
) cfg.accessPorts
|
||||||
// lib.mapAttrs' (
|
// lib.mapAttrs' (
|
||||||
name: vlan:
|
name: vlan:
|
||||||
lib.nameValuePair "40-${vlanIf name}" {
|
lib.nameValuePair "40-${vlanIf name}" {
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ in
|
|||||||
interface = "vlan-${name}";
|
interface = "vlan-${name}";
|
||||||
valid-lifetime = vlan.dhcp.leaseTime;
|
valid-lifetime = vlan.dhcp.leaseTime;
|
||||||
pools = [ { pool = "${vlan.dhcp.pool.from} - ${vlan.dhcp.pool.to}"; } ];
|
pools = [ { pool = "${vlan.dhcp.pool.from} - ${vlan.dhcp.pool.to}"; } ];
|
||||||
|
reservations = lib.mapAttrsToList (host: res: {
|
||||||
|
hostname = host;
|
||||||
|
hw-address = res.hwAddress;
|
||||||
|
ip-address = res.ipAddress;
|
||||||
|
}) vlan.dhcp.reservations;
|
||||||
option-data = [
|
option-data = [
|
||||||
{
|
{
|
||||||
name = "routers";
|
name = "routers";
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# iperf3 server on every gateway, for throughput testing from any LAN segment
|
||||||
|
# (e.g. validating AP/switch links: `iperf3 -c 10.<siteId>.<vlan>.1`) and from
|
||||||
|
# admin machines over the mesh. Never reachable from the WAN (default-deny).
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
cfg = config.cnx.router;
|
||||||
|
mesh = import ../mesh-hosts.nix { inherit config lib; };
|
||||||
|
vlanIfs = lib.mapAttrsToList (name: _: "vlan-${name}") cfg.vlans;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
services.iperf3.enable = true;
|
||||||
|
|
||||||
|
networking.firewall.interfaces = lib.genAttrs vlanIfs (_: {
|
||||||
|
allowedTCPPorts = [ 5201 ];
|
||||||
|
allowedUDPPorts = [ 5201 ];
|
||||||
|
});
|
||||||
|
|
||||||
|
networking.firewall.extraInputRules = ''
|
||||||
|
ip6 saddr ${mesh.subnet} tcp dport 5201 accept comment "iperf3 over the mesh"
|
||||||
|
ip6 saddr ${mesh.subnet} udp dport 5201 accept comment "iperf3 over the mesh"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
# Internal reverse proxy for the gateway: Caddy terminates TLS for
|
||||||
|
# <service>.<site><siteId>.cnx.network (e.g. omada.cnx1.cnx.network) and
|
||||||
|
# forwards to backends by their internal address. The cert is a real Let's
|
||||||
|
# Encrypt wildcard (*.<site><siteId>.cnx.network) obtained via ACME DNS-01
|
||||||
|
# against ns1 with a gateway-scoped TSIG key, so browsers trust it without
|
||||||
|
# any CA install; the names only *resolve* internally — Blocky answers them
|
||||||
|
# with the router's LAN address, the public zone never carries them.
|
||||||
|
#
|
||||||
|
# Requires the machine to also import the shared secret generator:
|
||||||
|
# (import ../../modules/dns/acme-gw-secret.nix "<hostname>")
|
||||||
|
# and ns1 to hold the matching key + acl (see machines/ns1/configuration.nix).
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
cfg = config.cnx.router;
|
||||||
|
hosts = import ../hosts.nix;
|
||||||
|
hostname = config.networking.hostName;
|
||||||
|
tsigKey = "acme_${lib.replaceStrings [ "-" ] [ "_" ] hostname}";
|
||||||
|
certName = "${cfg.site}${toString cfg.siteId}.cnx.network";
|
||||||
|
|
||||||
|
serviceModule = {
|
||||||
|
options = {
|
||||||
|
backend = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
example = "https://127.0.0.1:8043";
|
||||||
|
description = "URL Caddy forwards to (internal/mesh address).";
|
||||||
|
};
|
||||||
|
insecureSkipVerify = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Skip TLS verification towards the backend (self-signed upstreams like Omada).";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.cnx.router.proxy = {
|
||||||
|
enable = lib.mkEnableOption "internal reverse proxy (Caddy, wildcard cert via DNS-01)";
|
||||||
|
|
||||||
|
services = lib.mkOption {
|
||||||
|
type = lib.types.attrsOf (lib.types.submodule serviceModule);
|
||||||
|
default = { };
|
||||||
|
description = "Proxied services; attr name becomes <name>.${certName}.";
|
||||||
|
};
|
||||||
|
|
||||||
|
allowVlans = lib.mkOption {
|
||||||
|
type = lib.types.listOf lib.types.str;
|
||||||
|
default = [
|
||||||
|
"mgmt"
|
||||||
|
"lan"
|
||||||
|
];
|
||||||
|
description = "VLANs whose clients may reach the proxy (443, plus 80 for the redirect).";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf (cfg.enable && cfg.proxy.enable) {
|
||||||
|
assertions = [
|
||||||
|
{
|
||||||
|
assertion = lib.all (v: cfg.vlans ? ${v}) cfg.proxy.allowVlans;
|
||||||
|
message = "cnx.router.proxy.allowVlans must name VLANs defined in cnx.router.vlans.";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
# Render the shared per-gateway TSIG secret into a lego rfc2136 env file;
|
||||||
|
# same pattern as web01 (modules/web-proxy.nix), scoped on ns1 to
|
||||||
|
# _acme-challenge.<site><siteId> TXT records only.
|
||||||
|
clan.core.vars.generators."dns-acme-${hostname}-rfc2136" = {
|
||||||
|
files."rfc2136.env".secret = true; # root-owned; systemd reads it as root
|
||||||
|
dependencies = [ "dns-acme-${hostname}-secret" ];
|
||||||
|
script = ''
|
||||||
|
printf 'RFC2136_NAMESERVER=${hosts.ns1.ipv4}:53\nRFC2136_TSIG_ALGORITHM=hmac-sha256.\nRFC2136_TSIG_KEY=${tsigKey}\nRFC2136_TSIG_SECRET=%s\n' \
|
||||||
|
"$(cat "$in"/dns-acme-${hostname}-secret/secret)" > "$out"/rfc2136.env
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
security.acme = {
|
||||||
|
acceptTerms = true;
|
||||||
|
defaults.email = "postmaster@cnx.email";
|
||||||
|
# One wildcard for every proxied service; DNS-01 against ns1, so issuance
|
||||||
|
# works behind PPPoE with no inbound reachability at all.
|
||||||
|
certs.${certName} = {
|
||||||
|
domain = "*.${certName}";
|
||||||
|
dnsProvider = "rfc2136";
|
||||||
|
environmentFile =
|
||||||
|
config.clan.core.vars.generators."dns-acme-${hostname}-rfc2136".files."rfc2136.env".path;
|
||||||
|
# ns1 is the only nameserver that accepts this key's UPDATE; check
|
||||||
|
# propagation against it directly rather than a public resolver.
|
||||||
|
dnsResolver = "${hosts.ns1.ipv4}:53";
|
||||||
|
# Caddy reads the cert from explicit file paths (tls directive below),
|
||||||
|
# so it won't notice a renewal on its own.
|
||||||
|
reloadServices = [ "caddy.service" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# The lego-issued cert is owned group=acme; Caddy needs to read the key.
|
||||||
|
users.users.caddy.extraGroups = [ "acme" ];
|
||||||
|
|
||||||
|
# The explicit `tls cert key` points Caddy at the wildcard cert and disables
|
||||||
|
# its automatic ACME, so no extra issuance happens.
|
||||||
|
services.caddy = {
|
||||||
|
enable = true;
|
||||||
|
virtualHosts = lib.mapAttrs' (
|
||||||
|
name: svc:
|
||||||
|
lib.nameValuePair "${name}.${certName}" {
|
||||||
|
extraConfig = ''
|
||||||
|
tls /var/lib/acme/${certName}/cert.pem /var/lib/acme/${certName}/key.pem
|
||||||
|
${
|
||||||
|
if svc.insecureSkipVerify then
|
||||||
|
''
|
||||||
|
reverse_proxy ${svc.backend} {
|
||||||
|
transport http {
|
||||||
|
tls_insecure_skip_verify
|
||||||
|
}
|
||||||
|
}''
|
||||||
|
else
|
||||||
|
"reverse_proxy ${svc.backend}"
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
) cfg.proxy.services;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Blocky answers <anything>.<site><siteId>.cnx.network (customDNS covers
|
||||||
|
# subdomains) with the router's LAN address — clients on any allowed VLAN
|
||||||
|
# reach that address through the router's input path.
|
||||||
|
services.blocky.settings.customDNS.mapping.${certName} = cfg.vlans.lan.address;
|
||||||
|
|
||||||
|
# 443 serves the proxy; 80 only carries Caddy's automatic HTTP->HTTPS
|
||||||
|
# redirect. mgmt is already a trusted interface; listed anyway so shrinking
|
||||||
|
# trustedInterfaces later doesn't silently break the proxy.
|
||||||
|
networking.firewall.interfaces = lib.genAttrs (map (v: "vlan-${v}") cfg.proxy.allowVlans) (_: {
|
||||||
|
allowedTCPPorts = [
|
||||||
|
80
|
||||||
|
443
|
||||||
|
];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# Periodic WAN speed test so ISP degradation shows up as a trend instead of a
|
||||||
|
# complaint. A timer runs librespeed-cli and writes the results as Prometheus
|
||||||
|
# metrics into node_exporter's textfile collector — they ride the existing
|
||||||
|
# 9100 scrape to VictoriaMetrics, where alerts.nix compares each run against
|
||||||
|
# the link's own 7-day median (no per-site threshold to maintain).
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
cfg = config.cnx.router;
|
||||||
|
textfileDir = "/var/lib/speedtest";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.cnx.router.speedtest.interval = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "hourly";
|
||||||
|
description = "systemd OnCalendar spec for the WAN speed test.";
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
services.prometheus.exporters.node.extraFlags = [
|
||||||
|
"--collector.textfile.directory=${textfileDir}"
|
||||||
|
];
|
||||||
|
|
||||||
|
systemd.services.speedtest = {
|
||||||
|
description = "WAN speed test to Prometheus textfile metrics";
|
||||||
|
after = [ "network-online.target" ];
|
||||||
|
wants = [ "network-online.target" ];
|
||||||
|
path = [
|
||||||
|
pkgs.librespeed-cli
|
||||||
|
pkgs.jq
|
||||||
|
];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
StateDirectory = "speedtest";
|
||||||
|
# One test at boot would race PPPoE and log a spurious failure.
|
||||||
|
ExecCondition = "${pkgs.iproute2}/bin/ip link show ppp0";
|
||||||
|
};
|
||||||
|
script = ''
|
||||||
|
tmp="${textfileDir}/.speedtest.prom.tmp"
|
||||||
|
if result=$(librespeed-cli --json); then
|
||||||
|
jq -r '.[0]
|
||||||
|
| "speedtest_download_mbps \(.download)",
|
||||||
|
"speedtest_upload_mbps \(.upload)",
|
||||||
|
"speedtest_ping_ms \(.ping)",
|
||||||
|
"speedtest_jitter_ms \(.jitter)",
|
||||||
|
"speedtest_success 1"' <<<"$result" > "$tmp"
|
||||||
|
else
|
||||||
|
echo "speedtest_success 0" > "$tmp"
|
||||||
|
fi
|
||||||
|
mv "$tmp" "${textfileDir}/speedtest.prom"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.timers.speedtest = {
|
||||||
|
wantedBy = [ "timers.target" ];
|
||||||
|
timerConfig = {
|
||||||
|
OnCalendar = cfg.speedtest.interval;
|
||||||
|
RandomizedDelaySec = "10m";
|
||||||
|
Persistent = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
../../../groups/admins
|
||||||
@@ -3,8 +3,12 @@
|
|||||||
"sops": {
|
"sops": {
|
||||||
"age": [
|
"age": [
|
||||||
{
|
{
|
||||||
"recipient": "age1yubikey1qd859y9ehz2ya8j2cftwrtmdeqhuk7r7yc52zp64wpff6068gwrac3q6nsa",
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNRWlKNFVSOThSWlI3LzAz\ndnQxbnNGSlhoVmZrQmpIVEJieStlUzlseDJJCmdGbldCSU1oTGszZmtzSWhJaWta\nVDhoQU8vVXB0bG1VUkxoWDlyR0I1a0EKLS0tIDFzdklCTTBjeXBoT3FqVW1Pbkxl\nYVZ5Vm55OFp4YWwzS3dVNXRoZmpBT1EK0pNbk20gwPe5Q1bZR3XRHMWz+x+8FzRa\niq5tLcGkgfMO6WTzj8vtB7g0Q3sFgmPXTqb8yMzqAkZd4z9FKJGt6A==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHBpdi1wMjU2IHFWcTVydyBBcmJ4cXNt\nL3k4UmtqQUJZOGRoZFhab0pRSTdBUm9ZZkRxdHNvaCtRc05JVgphbHhHQThiRDNF\neW8xVGhXQ1pvSkRDR2hod2Zpa3pwaWZDVUpGZlpPWkNZCi0tLSB2T3JCcElnbVU2\nekJ5ZTdyQzZwbmR4aHBJUTc3M3loUm0wbHpHTGM0ZW1RCs0kHes9rkjd3uwHzxhi\n8x8AcQcQTR+CGsV5XPzGVMKKUZpwXWeNnvcHfVwYcqOnXucbUdCxLc6d57c9GQpC\nd3Y=\n-----END AGE ENCRYPTED FILE-----\n"
|
"recipient": "age1hlzrpqqgndcthq5m5yj9egfgyet2fzrxwa6ynjzwx2r22uy6m3hqr3rd06"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHBpdi1wMjU2IHFWcTVydyBBcFBHaFpS\nbjJiL0ZnOXlzMVVNdjhvUGxmaEJhTklSR2ZaTnhPWXFCaC8vbgpLdE04aW16dEls\neGw3VllEYVAzbGJ5TVZoOXhkR3dCUkREdUkxMU13clVzCi0tLSA3eW93QnFsNW1r\nK0ZiWEtrWFVqeTJyTk1maGVTMzdBTkJUdk5WN2N3WlprCp5oak+Bh/g4+ikE1LR5\n7kYrWQlzjC0Z+F5GegEtRjGCPnolWllnPUH4dGGjfuyAxy9qfi1J6Bi6m8uzo5rY\n+zs=\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1yubikey1qd859y9ehz2ya8j2cftwrtmdeqhuk7r7yc52zp64wpff6068gwrac3q6nsa"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"lastmodified": "2026-06-14T05:20:21Z",
|
"lastmodified": "2026-06-14T05:20:21Z",
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
../../../groups/admins
|
||||||
@@ -3,12 +3,17 @@
|
|||||||
"sops": {
|
"sops": {
|
||||||
"age": [
|
"age": [
|
||||||
{
|
{
|
||||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHBpdi1wMjU2IHFWcTVydyBBcC92dDND\nUFFiMGYzSE5zWFdoRXlSV1lQdHc4TDBab21wSWpxMGxsSDVweQplTW5lcUY3dXdR\nMUJPamMzREZlN0N6WTVUbE9kaXlZMVVDUFBRVnU5TkJjCi0tLSBzZW5SYXMrRTlT\nYmJLSDRlWTg5bFdNRmw1WkhpQVQ5Zi9GeG91SUtxM3RVClP9r+sAX8BVngpVGPJh\n/tYHqmSl67sFL5K6TJog1QQ1Gj2GiiTXE/kLTN+vsPbQaepqXamCKNrMXZWYOA2t\ntWw=\n-----END AGE ENCRYPTED FILE-----\n",
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBLRkppdUoza2l0SnFpME5n\nM0xBRVk4c2lhaHMvazFNaGNTdkJDcnJEQkVrCjhFUlZMUitnZ20zQlFEcm5WQmVG\nWmJVeUh6S09MS0xpQUR4aDF1V1dCZFkKLS0tICtOMEJ6QmRRR3BkaU5sZHdzOElO\nSnV4K0dHMHhDOURPTEpucXFuNEUwaU0Kx4xAaKTrw1bKJPQVCHcByTcapFhslwrV\nqssEBnYWXqFgI3vh3CatBXuZY1Lm3eTKhDDQ2ZyPKgc/l0gXyFwH2g==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1hlzrpqqgndcthq5m5yj9egfgyet2fzrxwa6ynjzwx2r22uy6m3hqr3rd06"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHBpdi1wMjU2IHFWcTVydyBBMHFKQTNG\nazdEcThReFZGVVBtVFBDY1luQk1UTkl0a0p1bzQ0cUxBZWVyYwo4aU5qaEs0czJB\nUzZFcG9uTUZ1OUFScW9ZeHljbDBEbjkzZ08vS2F1Qjd3Ci0tLSBHTXhVWjNsWHhK\nbWtrMWw3Nm8wSU9MNXZwcWxVZXRJS1dTK0hycnFoVlRJChemDXVb7LgM7OeUUeqa\nTl0IuDHmIIx/lUYnJWGZmWQHz2znk679j36VQQNWcK6eZlKUIqYmBj3fMu+OYir2\nMLI=\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
"recipient": "age1yubikey1qd859y9ehz2ya8j2cftwrtmdeqhuk7r7yc52zp64wpff6068gwrac3q6nsa"
|
"recipient": "age1yubikey1qd859y9ehz2ya8j2cftwrtmdeqhuk7r7yc52zp64wpff6068gwrac3q6nsa"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"lastmodified": "2026-07-28T09:03:13Z",
|
"lastmodified": "2026-07-28T09:03:13Z",
|
||||||
"mac": "ENC[AES256_GCM,data:MKIyykYwtQqpUnXqZQKzKwoRhSon2PH0wYiTDTvogaVREHfpEPs/mdOjQBUHtbr1QGqWNh4AxdBTKfpjXTlncHAEtcIdVH3qPccVxsXqfqRmZPDFzcHZAIw+VSvDsKwD3anixiXDeHmeLawwbyb1rnS0P8KquXbmjvirwGUIQLU=,iv:+hdLXMJMCby/6Q6JGxp4p95GgPKbdA/OTAZloyylr08=,tag:ZZ4dKjhjpE2xqcoNf4qeoA==,type:str]",
|
"mac": "ENC[AES256_GCM,data:MKIyykYwtQqpUnXqZQKzKwoRhSon2PH0wYiTDTvogaVREHfpEPs/mdOjQBUHtbr1QGqWNh4AxdBTKfpjXTlncHAEtcIdVH3qPccVxsXqfqRmZPDFzcHZAIw+VSvDsKwD3anixiXDeHmeLawwbyb1rnS0P8KquXbmjvirwGUIQLU=,iv:+hdLXMJMCby/6Q6JGxp4p95GgPKbdA/OTAZloyylr08=,tag:ZZ4dKjhjpE2xqcoNf4qeoA==,type:str]",
|
||||||
|
"unencrypted_suffix": "_unencrypted",
|
||||||
"version": "3.13.1"
|
"version": "3.13.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
../../../groups/admins
|
||||||
@@ -3,8 +3,12 @@
|
|||||||
"sops": {
|
"sops": {
|
||||||
"age": [
|
"age": [
|
||||||
{
|
{
|
||||||
"recipient": "age1yubikey1qd859y9ehz2ya8j2cftwrtmdeqhuk7r7yc52zp64wpff6068gwrac3q6nsa",
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHBpdi1wMjU2IHFWcTVydyBBcU01TXl0\nRmU5ZlZOK3NZZE8vNktOWXBkRnZtZHlUbnBHYlVQbDdzZlRDbApFeFI0OE1KdndQ\nZWczZGRYZXBONTc2aG4vZTJEZ1hCdGljNFJBcmF0UGpRCi0tLSBGMGJVWUtJMWFP\nQUZwWVZTWUFKZnVJUTJSMHlRMGRNcExPSEpXTFpLSkVNCj1WEuRyATcxp0GYeoqC\nACpQpcqgIBVD1dYB0pPo+I6yDXvx+JbwVdgGl+2Cxj40HQOfyvJecoOO3efOZrB3\nUZU=\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHBpdi1wMjU2IHFWcTVydyBBemRVWUs1\naXFpVm80UmloUUpnR1BVaGsxampHV1JFNWFnSUhiY0orOTU0cgppVEJ5QVcvbGpC\nZlErd01XUzh0MnZOekkvTkc5Z3owS3pBbkNCUDJ6SlJRCi0tLSBRVVgrd3hIVEla\nMEhoN3FDSG1HdGk5YU8ydzlVUGd1RW5zM1lka3VuSHFNClrBItoFo7uqpS5fuvaG\nOvh6mTylk4bHgcSRGKTk383srgsvPdZaioJfUs7fbVmLD3+bhfM/Wgv7tI6kO1dj\nmDg=\n-----END AGE ENCRYPTED FILE-----\n"
|
"recipient": "age1yubikey1qd859y9ehz2ya8j2cftwrtmdeqhuk7r7yc52zp64wpff6068gwrac3q6nsa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBRb0ZvUmN2RWhtQkpXMXJr\nY1o2MXZsQ0dXNFYxa2RLUGlJWnN3Z25zbnp3CmtLbkYvdHR2MUZIY2U1MzhyU1VD\ndm5MTmMveEJLOHZWWnVWdVpDcTFsTmMKLS0tIFEwYzZYeU5uc29BTEVxN2YvN0Zw\nd0VSeTNhQk1pNVdaZVdTVk1oNjFwQWcKwEbHyT354Cbam9wuiIhapre+nbqtMr9h\nbOjF53LR9RZ9/mbpTNDLOkq2QW2aM4tKkRfdUgP1XgboilzcTjba8A==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1hlzrpqqgndcthq5m5yj9egfgyet2fzrxwa6ynjzwx2r22uy6m3hqr3rd06"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"lastmodified": "2026-06-14T05:20:24Z",
|
"lastmodified": "2026-06-14T05:20:24Z",
|
||||||
|
|||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
../../../groups/admins
|
||||||
@@ -3,8 +3,12 @@
|
|||||||
"sops": {
|
"sops": {
|
||||||
"age": [
|
"age": [
|
||||||
{
|
{
|
||||||
"recipient": "age1yubikey1qd859y9ehz2ya8j2cftwrtmdeqhuk7r7yc52zp64wpff6068gwrac3q6nsa",
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHBpdi1wMjU2IHFWcTVydyBBaTVGQWRC\nZUJocGx5Zjd0c1JpUEdHbGczNUh1RjRqSERRNHNSbnRndWFUMAoxbFBEU0Ezajhr\nSUlIRStWUUdVTDJMQWxmSkw0c0FNYnFxNEJIdnExTmx3Ci0tLSBMRGRPLzlXTEN3\ncHZCMGF3WDZTcFhTd2srL3Vpc2dRdExrVzFJcWhjQVJrCrA1y2SapkZoppI3z4LD\nH71D/WxIcTHp3fIzICNvgxWHn3+vkDDZcpcgH+gRiPCDqrgDp0NZcZiDlz6zyHGr\nmaM=\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHBpdi1wMjU2IHFWcTVydyBBL2doZFpO\na2Y2QzZSckJaZmZsTlFtQU1mckxJWTBiUXA2SUljVWZDemFJYgpwcFl6RHh2ZXdn\nNTBsaEVpWm5oYWtteU5WZmxWK1RRK2NoYW0yWlpCd3dBCi0tLSBsZVNpczR4dUdv\nSmtqQzMvc1ZYY0xUY0JMQmt2YzB5YmZOMEtXcW9vWUcwCnCdgcgiwF2rkWsV0IdO\n/6cs35FBypWpfflGwOwP0GnTrNizc9HphcMcwAi5NMQVi6X90Xgm5aFdpAsSWepX\n/SI=\n-----END AGE ENCRYPTED FILE-----\n"
|
"recipient": "age1yubikey1qd859y9ehz2ya8j2cftwrtmdeqhuk7r7yc52zp64wpff6068gwrac3q6nsa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0Rmp1S29hVE5LNkZpLzRV\nMGZJL242YmdHNkFXR2d0TmlVWXViTlJFNGpnClBSdEhaRlNWbVNrb05tTHh0Q3Rk\naVFNdUd1clNVckREajVoRmZCenFEaVkKLS0tIExOaHd1WWxZRFM1UFk4K0E0cWpE\nK2duUE1KeEtmeVorRnc3MFdDZXFGZjAKKw4ykWu+HzBN3WAj/gx0mMYk0RiyRR50\nYv7hg1B/+x4lDCTjAJIWrrBEjLJBZTZRnECmTMTlJ39uOhxeUOizCA==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1hlzrpqqgndcthq5m5yj9egfgyet2fzrxwa6ynjzwx2r22uy6m3hqr3rd06"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"lastmodified": "2026-06-14T05:20:27Z",
|
"lastmodified": "2026-06-14T05:20:27Z",
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
../../../../../../sops/groups/admins
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
../../../../../../sops/machines/gw-cnx-1
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"data": "ENC[AES256_GCM,data:4OPDbpmYb7nhHbUvu8dYmF7B/LipnKNE5g8vE33LMzUVg5S+EAyRu2u9dbeUDayqq+ms8WT/oqN/Q0hjsE67+3PCEYBbeAgb5tibsn3tmh1Erp0QUlA4V/erhDwvH+Y8GA3UEJITQ1TtPhCSCKnQbGm85+Tir0nsocjQi3g9Q67TRqUAF7US8H+MVM+YEnIE7HcUH/LKc9z3kg43AaMa2Ey83PjmkMmRdQ==,iv:3VdMulw2DUIReb2werCE4+VLMlxn4FeVUwl8R9Bn3Vw=,tag:TZJZcRiBSCjeTzd10a3rHA==,type:str]",
|
||||||
|
"sops": {
|
||||||
|
"age": [
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBTXlXNzBJRisyeVM3YmRO\nK3IyTTFOcW5VVCtJcnVySFU2RE9PdUdpeWpJCnpscGhsakZkL1hDenhXUW1yY3JH\naHFzUS8velQxV3lYc2hPL0FWcWRpYUEKLS0tIE9BckgrUVRKM1hOWGg0N0V2Z2ds\nUzlhZ0luWitPS3U0UHpWQzZwdG1temMKf5SKmUh3LYD50tObR5tfEvNRoVegg/Tu\n4jXlMKNVg7yOgXET3CNS/Sm4ork4Wz4bSA6gYhLD5JIW356d5WBO4w==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1g8a6fqhqg8q8qu4jyuhxc7hf00t5qvra55chyfm0qk45wlmvc9qs2cft8t"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAybzRIVjhDUHZBVHFSTDF1\nZUNScTRoZzFzdkRISFMzZ0E3M0ZWeFF3bncwCi9rbzg2bUV0OVhBNlROQlhSQnpF\nSm9mVkF2MzhycnNqRUwvTFV2bW5QRWsKLS0tIHJUc2NyOVprMzBjOTgyM1IvZDU2\na0RSenNHYXNqSzR1K0JRVVBpMlpZN0kKk63q9i5zCP3lAZ+JiknOgFcp/quFGYdp\nzoAmkEeGLK8nR5lOVdUutHktG9Rneci+7Q49bk2S4gqeLDt2AnOCsw==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1hlzrpqqgndcthq5m5yj9egfgyet2fzrxwa6ynjzwx2r22uy6m3hqr3rd06"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHBpdi1wMjU2IHFWcTVydyBBcXRoTXMy\nOG01R2hEY3R2NTd1bzdPTVUvOWorN1NOYlVLYWk3VThtUEpoUgpzOHNYN1N0bzlv\naWhDVCs0b1ozQ0FBREpiVXpiTkxOeEl5b3YzdkR5YXowCi0tLSBBbTZUZDVpN0I0\ndWRPUXdVaGx5Mk56UzFUbzBvT0xBa0h3dFdmajNXak9RCmDpIFatc4U+k3NDDw7m\nn/D+azJwB6KS2s9YeUy85RoClEqiNSGC9t7qqffVFdiNS8Zww6MTzY++lgj1bYlK\nLMc=\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1yubikey1qd859y9ehz2ya8j2cftwrtmdeqhuk7r7yc52zp64wpff6068gwrac3q6nsa"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lastmodified": "2026-07-31T03:07:42Z",
|
||||||
|
"mac": "ENC[AES256_GCM,data:aXgLJn6HmSS01GjwYi/lpDil2/FmQitKZOZHUEr1k1r31gTb87RYVJd11MXuGiCCtc5ASKsbZIxoRuPf9DLiiJfuCaVY6Sv4fH/JXkSaLh9YoRCPeX7lrcyYiqficAhmC6+0D3Y0e9tpLSuEdaNB9wHwvslEZ4LSkVscTPSM8VQ=,iv:eY9+6mPJaVDNkZH4LvPBzggEHtyj4H/3QPfN0cjJdx4=,tag:iZyexO/rlt/8XOJ9yR4zgA==,type:str]",
|
||||||
|
"version": "3.13.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
../../../../../../sops/users/berwn
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
../../../../../../sops/groups/admins
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
../../../../../../sops/machines/ns1
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"data": "ENC[AES256_GCM,data:Y0fYYbD4wqi/WNt+dzVFkJ6ZH4PLC34T3dbmPw0bVFCNCbNF/twHwALVihVc18Oc2PbLpudY5DKr3s0zwfoG7YzPn0Hlc6+KsotoG+8R71WRo3D9yZbGz2ajdDCuglfLbhFmuDAlQjAHFWVUV5HH,iv:CbNFBo0As6cu/1n/BuwqqMn8wbx76i1HZ1Y3g6DiBcA=,tag:0VlCjAK6ZwP0eDxRcqowvg==,type:str]",
|
||||||
|
"sops": {
|
||||||
|
"age": [
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5NndmSXF2MWc2MEtZZ09u\nR0xQWUlGWWo5VFRMTFgwOCtFMVdJbXB0MlcwCktRNG9tQVJJOUlPbXlBbTFyb1NE\nSmZ2OVpXNUxUdjZhYTl1aG1DV3ZkNzAKLS0tIEtiOGVaMTZrbHBRZGVBRlQ2VE1G\ncWhkVzZ4Yk1GY3ZXNnJzcFk3NUs0N2sK44Dw4YMuUSiu+XVR/By1Tdm+ikGZr8sW\nsOr8BVOUH14yF3L5pGd3DoJIYPKqcaUiDpg6qSsqYXD8UYaB8c6d4Q==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1fanu282vm7njjweqhrpcfcwpttuhce8js4tsyfry98l0neaqpewqs5s7nt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrSkppQU1sbm9WaDIrUGEx\nRjJWakpReXFCVTBaR2RqN3pNVDV6c1hySVVrCjdtRlFTMHNEc3J1NjMwczlvVWdx\nSVBjVXhGQVFFbXdEbVVFWHFHbEppQ3MKLS0tIG1DdzFqYURxNFBWQjF6NXl6YVU3\nZ2tYWjN4MVNMVGNjSmVJYWs2SnVjQVkK5kS5kpZO22EAAdokjM44g7hZy+bdOKrc\nrC6DjBwpI+BtJjXdO8nltBIXKhOBz+8sHI0XPuQP58UjKRD3YtaaJA==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1hlzrpqqgndcthq5m5yj9egfgyet2fzrxwa6ynjzwx2r22uy6m3hqr3rd06"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHBpdi1wMjU2IHFWcTVydyBBMnFiMFhr\nci9GdmZvRnhaVXNtR1NDcDZrcXhKd0V1Nm4wQnpUeWNiY253WApnUlJVNG4wZStX\nRkZIOEVBVTlNcS8zUWVqM0xldGRXVjNTWlZqWE5lTXpNCi0tLSBxSkVLY0k4dkpH\nbjRJOWhWekhhbXV4bldUWVE2UXRRbXlpeUJtUWVJcU1nCmxN/fEWLPJuzt6bYdNA\n+FUSn7Q3WLLgIGmCMxy+xxNBh3Sbu9ZRd9and2tNwcwBcCiywsrESQc5SD0RFqNU\ncqQ=\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1yubikey1qd859y9ehz2ya8j2cftwrtmdeqhuk7r7yc52zp64wpff6068gwrac3q6nsa"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lastmodified": "2026-07-31T03:07:42Z",
|
||||||
|
"mac": "ENC[AES256_GCM,data:0Ugi6vc7hWdXxCbVuRoq0yuAl15N0Udndf3QISlteWdiV9F3Cg99S/NpCa9PPk7EKOGJ9+Ow0mJAZVJqz9l5Zuvq4wpClU9hSe7VMuUO2sF8EambdEQtrHccV560DecFZn/lgYhnJIhJOUAKuqR7PoyGN5A+64vSc18KgNU8Euc=,iv:cnhktstJBZIAt1RU84fX8YV4IPrYPUQAjNGlXweNJFo=,tag:D2Qqyu+utno09ADAPXUkVA==,type:str]",
|
||||||
|
"version": "3.13.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
../../../../../../sops/users/berwn
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
../../../../../sops/groups/admins
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
../../../../../sops/machines/gw-cnx-1
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
../../../../../sops/machines/ns1
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"data": "ENC[AES256_GCM,data:juFDgvxu7l6xf8wXVvlMz6m8bOaBAG3JTok/5YNwEIs/XxmL+GqkI0WuYr0=,iv:/Ga2ENxUUMK1U/WoYU8McatIPT9lRyFYfSZiTZTMIFs=,tag:Rxxkwu73i45HlCLBgSqi+g==,type:str]",
|
||||||
|
"sops": {
|
||||||
|
"age": [
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBmMXpyMko0d3NMeFZMc1kx\nWEtyZjVvWkpwK2tCNy9WQmNwRFpsb3hXUENzCjF1ZXhQRTVxSm40aStZLy9oWGxn\nMU5BSDFsd0ZNVTROVGs4SkcweFgvTDgKLS0tIHhEcEdScFluT1pGTTBXVW9jTzV5\nalhPT0FyRXNveHBwUTArMjhrNDJMR3cKiBBQheppYxyJ4uwS8MIWSuo7EGm3GYcc\nE4vdoIEp2NkDlPEWNudMKlV0fMoOdAt1psPFXUy37XEWLsOgJTIMEg==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1fanu282vm7njjweqhrpcfcwpttuhce8js4tsyfry98l0neaqpewqs5s7nt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBMWlNVS2VDR2pRUWdlUlFG\nUjJPNzl6YUpSRnc3bHRveU9Xd1kyTTU5N0I4CmZ1bDRzU2o3YVNFRnE0MGhFMVYx\ndXcrZGc4TEFJQjNwZEVDaW5hTVZTblEKLS0tIDlRbHkrdEFJM2NsckJ1d2VSb1Bh\na3Y4VXE0UjRVUU41ODBzeDc3T3AvcjQKBS9bhuSlag2xuUyhnPniY1EFIXzsSNDE\nONpxAmsuGm/aQ6u2+W+46Vm0Ow3Oc+cIkB35XeB0D9wXQjbJt8r0Rw==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1g8a6fqhqg8q8qu4jyuhxc7hf00t5qvra55chyfm0qk45wlmvc9qs2cft8t"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqcUlLTU5HOVFxTEwrSFQ5\nTkkzd0JxN0V5RkM5UXZ6OENnYU02T2Jtd0dzCjBmOEZ1SFBmdGZERVE2bThMREs4\nbmVWemlqRkFQVzBqTk9hZTd5QkhWSWMKLS0tIFcvdGVpQXBxSjZCbk52WVlhVnpD\nUjhxRWdZbk5tSTUxeUt4cHhQVlVFbHMKg0VVYZwf9ykVaZ17pqrha3HG7ZRjcRdk\nsTTuTZ9c+5IhBThrv24+8vkidQhJKSJY+aLiYL9BraKS86lvZSPzpg==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1hlzrpqqgndcthq5m5yj9egfgyet2fzrxwa6ynjzwx2r22uy6m3hqr3rd06"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHBpdi1wMjU2IHFWcTVydyBBN2RKU0FW\nakJJcXB6dHllbytBSldiV1NvZFFneEVtdmw5L2xQc1kvWnJwRQoyNGkyUUtUZ29K\nS2hudkhPTDVtMVg1ZTFpakxZb3BkM0JQYXBSS21iNEtNCi0tLSBVWWpGRUFpOEEy\nNU1SUmVOQnozcjdwcDJkQnY3bldFeHptTUJ2bjJPamlFCre4Bzl0E8yj1Ld79Snk\nTRE/1l2kep1/9ufwq59R1eyE7nTCmnuaMUj1nB2na64S1orjmVa69buBEQORaoNp\n8yg=\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1yubikey1qd859y9ehz2ya8j2cftwrtmdeqhuk7r7yc52zp64wpff6068gwrac3q6nsa"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lastmodified": "2026-07-31T03:07:39Z",
|
||||||
|
"mac": "ENC[AES256_GCM,data:MVoUIB0J8GvTEgEVdFD3CWmqaJkuvhx21ApnfMJgTABwKN/q+luPhn2i/2LI9S1NQx26k6fbiYtq5DypuOBfxW+wmeDwOMFVmA5tQLCuvzn6HR1hC3EMGAI5AmXulAdOWpP86R8tfVjrPNyb94iq6haNkFJ+N8Vacq5Iq8FyKyk=,iv:9vTa0xZD5wscJ614ELcTzu7SpMu7+BjRxx3soBwlMwA=,tag:/MPGGTQN2DLBubm93FY/LA==,type:str]",
|
||||||
|
"version": "3.13.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
../../../../../sops/users/berwn
|
||||||
Reference in New Issue
Block a user