Author SHA1 Message Date
kurogeek e316ec56ce flake: use flake-parts 2026-08-05 10:19:32 +07:00
kurogeek 559115e52b inventory.json: update install time of gw-cnx-1 2026-07-31 17:37:47 +07:00
kurogeek 9cfcf91b51 inventory.json: update install time of gw-cnx-1 2026-07-31 16:08:14 +07:00
kurogeek 570fe64497 inventory.json: update install time of gw-cnx-1 2026-07-31 15:09:07 +07:00
kurogeek 2ac4215237 inventory.json: update install time of gw-cnx-1 2026-07-31 13:03:43 +07:00
kurogeek d51a334ec6 inventory.json: update install time of gw-cnx-1 2026-07-31 13:00:57 +07:00
kurogeek b81c0aaa89 machines/gw-cnx-1/facter.json: update hardware configuration 2026-07-31 13:00:57 +07:00
Berwn fcd8e55024 Add untagged access ports and a staging uplink for gw-cnx-1
New cnx.router.accessPorts option pins a port untagged to one VLAN via
bridge PVID/EgressUntagged; convention is the last copper port as an
always-available mgmt recovery port. gw-cnx-1 port roles: enp1s0 WAN,
enp2s0 trunk, enp3s0 temporary DHCP uplink into the old OPNsense LAN
(back to trunk at cutover), enp4s0 untagged mgmt.
2026-07-31 12:57:34 +07:00
Berwn 19e1acda51 Renumber gw-cnx-1 to the fleet addressing scheme (OPNsense replacement)
The newedge.house site adopts the 10.1.<vlanId>.0/24 convention: mgmt 10
(the old untagged LAN), lan 20, iot 40, voip 50, dmz 60, unit1-5 110-150.
PPPoE is untagged at this site. Static leases move to a new per-VLAN
dhcp.reservations option rendered into Kea host reservations.
2026-07-31 11:39:54 +07:00
Berwn f5b6b4b55e Serve newedge.house from the fleet nameservers 2026-07-31 11:39:54 +07:00
Berwn 3646d43ac5 Add CNX WAN Speed dashboard for the gateway speed tests
Per-gateway download/upload/ping from the hourly librespeed runs, with
each link's 7-day median overlaid so the WANSpeedDegraded alert
condition is visible at a glance. Label-driven — new sites appear
automatically once scraped.
2026-07-31 11:03:31 +07:00
Berwn d8d4a686eb Add gateway diagnostics: iperf3, CLI toolkit, periodic WAN speed test
iperf3 serves throughput tests from every VLAN and the mesh; an hourly
librespeed run feeds speedtest_* metrics through node_exporter's textfile
collector, and vmalert flags download rates below half the link's own
7-day median so ISP degradation surfaces without per-site thresholds.
2026-07-31 10:44:52 +07:00
Berwn b11ff75ca6 Add internal reverse proxy for gateways (Caddy, wildcard via DNS-01)
cnx.router.proxy serves <service>.<site><n>.cnx.network with a real Let's
Encrypt wildcard obtained via a gateway-scoped TSIG key against ns1; Blocky
resolves the names to the router's LAN address, so they exist only
internally. First user: Omada UI on gw-cnx-1 (omada.cnx1.cnx.network).
2026-07-31 10:09:08 +07:00
Berwn 4735968433 vars: update via generator dns-acme-gw-cnx-1-knot (machine: ns1) 2026-07-31 10:09:08 +07:00
Berwn 02efeb454b vars: update via generator dns-acme-gw-cnx-1-rfc2136 (machine: gw-cnx-1) 2026-07-31 10:09:08 +07:00
Berwn 868cd9eebd vars: update via generator dns-acme-gw-cnx-1-secret (shared) 2026-07-31 10:09:08 +07:00
Berwn 99fdde666c secrets: add group admins to secret gw-cnx-1-age.key 2026-07-31 10:09:08 +07:00
Berwn 7b706eb3e7 secrets: add group admins to secret ns2-age.key 2026-07-31 10:09:08 +07:00
Berwn 6c89a7031a secrets: add group admins to secret ns1-age.key 2026-07-31 10:09:08 +07:00
Berwn 4ca5fd3afd secrets: add group admins to secret control-age.key 2026-07-31 10:09:08 +07:00
kurogeek 7958ddd771 .gitignore 2026-07-31 09:38:57 +07:00
kurogeek 0c11aca91f machines/gw-cnx-1: disko update 2026-07-31 09:38:07 +07:00
kurogeek 292a699a2b clan.core.sops.defaultGroups to all machines 2026-07-31 09:18:18 +07:00
Berwn 7d549a9c17 Migrate deprecated admin service to sshd + users
sshd carries the admin SSH keys and now also CA-signed host certificates
(TOFU-less verification via the shared openssh-ca); the users service
generates per-machine root passwords (share = false, no prompts).
2026-07-28 17:17:56 +07:00
Berwn ac60d527ae vars: update via generator openssh-cert (machine: web01) 2026-07-28 17:15:55 +07:00
Berwn 3471faa466 vars: update via generator openssh-cert (machine: ns2) 2026-07-28 17:15:54 +07:00
Berwn db22afc6e3 vars: update via generator openssh-cert (machine: ns1) 2026-07-28 17:15:54 +07:00
Berwn 04d3518c03 vars: update via generator openssh-cert (machine: mx1) 2026-07-28 17:15:53 +07:00
Berwn 94daf06814 vars: update via generator openssh-cert (machine: gw-cnx-1) 2026-07-28 17:15:53 +07:00
Berwn 3093ed7044 vars: update via generator openssh-cert (machine: control) 2026-07-28 17:15:52 +07:00
Berwn c1fad33ad7 vars: update via generator user-password-root (machine: web01) 2026-07-28 17:15:47 +07:00
Berwn 60f6884f35 vars: update via generator user-password-root (machine: ns2) 2026-07-28 17:15:47 +07:00
Berwn d2b352543b vars: update via generator user-password-root (machine: ns1) 2026-07-28 17:15:46 +07:00
Berwn 7639d477c8 vars: update via generator user-password-root (machine: mx1) 2026-07-28 17:15:45 +07:00
Berwn 8327e4997a vars: update via generator user-password-root (machine: gw-cnx-1) 2026-07-28 17:15:44 +07:00
Berwn 060091247a vars: update via generator user-password-root (machine: control) 2026-07-28 17:15:43 +07:00
Berwn 9ccfbc3959 vars: update via generator openssh-ca (shared) 2026-07-28 17:15:43 +07:00
Berwn 4585fe7b7f Update flake inputs (clan-core 2026-07-08, nixos-mailserver, treefmt-nix) 2026-07-28 17:11:00 +07:00
Berwn 89eb50fcd7 vars: re-encrypt gw-cnx-1 emergency-access and openssh for admins group 2026-07-28 17:06:11 +07:00
Berwn 430c9996ae devshell: run multi-machine clan updates sequentially
Parallel updates interleave output and bury the YubiKey PIN prompts; a
clan wrapper in the dev shell loops one machine at a time instead.
2026-07-28 17:06:11 +07:00
Berwn 158252323f Add site gateway role (modules/router) and gw-cnx-1
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.
2026-07-28 17:06:07 +07:00
Berwn a87b579eb7 docs: add mail runbook page (mailbox and domain add procedures) 2026-07-28 17:06:07 +07:00
Berwn 7cd44d957c vars: update via generator pppoe-credentials (machine: gw-cnx-1) 2026-07-28 16:34:28 +07:00
Berwn dc2183a83a vars: update via generator pppoe-credentials (machine: gw-cnx-1) 2026-07-28 16:31:09 +07:00
Berwn b0aa12ac07 secrets: fix vars for gw-cnx-1 2026-07-28 16:30:29 +07:00
Berwn 745b82a4ee secrets: fix vars for gw-cnx-1 2026-07-28 16:30:16 +07:00
Berwn 88bbba0eae secrets: fix vars for gw-cnx-1 2026-07-28 16:30:04 +07:00
Berwn 3f9559766f vars: update via generator zerotier-ip-gw-cnx-1-zerotier (shared) 2026-07-28 16:03:23 +07:00
Berwn d53a1e6524 vars: update via generator tor_tor (machine: gw-cnx-1) 2026-07-28 16:03:18 +07:00
Berwn b7317ea4ff vars: update via generator state-version (machine: gw-cnx-1) 2026-07-28 16:03:15 +07:00
Berwn 832dcb1af9 vars: update via generator root-password (machine: gw-cnx-1) 2026-07-28 16:03:15 +07:00
Berwn 41a2d22be0 vars: update via generator openssh (machine: gw-cnx-1) 2026-07-28 16:03:14 +07:00
Berwn 2fbdd8e2c0 vars: update via generator emergency-access (machine: gw-cnx-1) 2026-07-28 16:03:14 +07:00
Berwn 62b0b81d19 vars: update via generator zerotier-identity-gw-cnx-1 (shared) 2026-07-28 16:03:13 +07:00
Berwn 4fa491fd63 secrets: add machine gw-cnx-1 2026-07-28 16:03:13 +07:00
Berwn b8622b3e93 secrets: update gw-cnx-1-age.key 2026-07-28 16:03:13 +07:00
155 changed files with 6423 additions and 67 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
result
result*
.direnv/
+41 -12
View File
@@ -1,30 +1,54 @@
let
hosts = import ./modules/hosts.nix;
# Single source of the machine list: inventory AND the per-machine baseline
# below are both derived from it, so no machine can be added without the
# baseline (e.g. admins group encryption for all its generated secrets).
fleet = {
control = { };
ns1 = { };
ns2 = { };
mx1 = { };
web01 = { };
# Site gateways (Topton 1U routers): dynamic PPPoE WAN, so they are NOT in
# modules/hosts.nix / the `internet` instance — clan reaches them over the
# zerotier mesh (or Tor) instead.
gw-cnx-1 = { };
};
in
{
# Ensure this is unique among all clans you want to use.
meta.name = "cnx-network-clan";
meta.domain = "cnx-network.internal";
inventory.machines = {
control = { };
ns1 = { };
ns2 = { };
mx1 = { };
web01 = { };
};
inventory.machines = fleet;
inventory.instances = {
admin = {
roles.default.tags.all = { };
roles.default.settings.allowedKeys = {
# Admin SSH keys + root password, split per the clan-core migration off
# the deprecated `admin` service (sshd handles keys, users the password).
sshd = {
roles.server.tags.all = { };
roles.server.settings.authorizedKeys = {
"berwn" = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIENAjhGQGraQoAjJzsomKP8GAmQPeGL1rNRNHgRcLqtT";
"kurogeek" =
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEcZ/p1Ofa9liwIzPWzNtONhJ7+FUWd2lCz33r81t8+w kurogeek@kurogeek";
};
};
root-user = {
module = {
name = "users";
input = "clan-core";
};
roles.default.tags.all = { };
roles.default.settings = {
user = "root";
prompt = false; # auto-generate, like the old admin service
share = false; # per-machine password, not fleet-wide
};
};
zerotier = {
roles.controller.machines."control" = { };
roles.peer.tags.all = { };
@@ -65,6 +89,11 @@ in
};
};
machines = {
};
# Fleet-wide baseline applied to every machine. Secrets minted by
# `clan vars generate` are encrypted for the admins group from the very
# first run — generating before this took effect is what forced the
# re-encryption dance (`clan vars fix`) on gw-cnx-1.
machines = builtins.mapAttrs (_: _: {
clan.core.sops.defaultGroups = [ "admins" ];
}) fleet;
}
+2
View File
@@ -3,5 +3,7 @@
- [Overview](./overview.md)
- [ZeroTier mesh](./mesh.md)
- [DNS](./dns.md)
- [Mail](./mail.md)
- [Site gateways](./gateways.md)
- [Monitoring](./monitoring.md)
- [Backups](./backups.md)
+145
View File
@@ -0,0 +1,145 @@
# Site gateways
NixOS routers (OPNsense replacements) on Topton 1U fanless boxes — Intel N300,
4x i226-V 2.5G ports (some units add 2x 10G SFP+), 16GB DDR5, 256GB mSATA.
Four sites are planned; the first is `gw-cnx-1` (Chiang Mai). Everything lives
in the reusable `modules/router/` module (`cnx.router.*` options); a site's
machine config only sets port names, VLANs, and feature flags.
Naming: `gw-<city>-<n>`, e.g. `gw-cnx-1`.
## What each gateway runs
| 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 |
| 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 |
| DHCP | Kea, one subnet per VLAN |
| DNS | Blocky (blocklist resolver), metrics on :4000 scraped by control |
| IPv6 | DHCPv6-PD on ppp0, /64 per VLAN via SLAAC |
| Bans | CrowdSec + nftables bouncer (sshd log parsing) |
| 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 |
| Storage | Single-disk ZFS (zstd, `/var` its own dataset for snapshots) |
## Addressing plan
Each site owns `10.<siteId>.0.0/16`. Defaults per VLAN: subnet
`10.<siteId>.<vlanId>.0/24`, router at `.1`, DHCP pool `.100.199`
(`.2.99` static/infra, `.200.254` reserved).
VLAN id convention (fleet-wide): **10 = mgmt**, **20 = lan** (both mandatory at
every site), 30 = guest, 40 = iot (reserved). Sites add their own beyond that.
Larger subnets (e.g. public-WiFi guest networks that outgrow a /24) are carved
from the **upper half** `10.<siteId>.128.0/17` and set explicitly on the VLAN.
The lower half stays reserved for /24s indexed by VLAN id. High-churn VLANs
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
`10.2.128.0/22`, pool `10.2.128.100 10.2.131.250`, `dhcp.leaseTime = 3600`.
| Site | siteId | mgmt | lan | site-specific VLANs |
| ---- | ------ | -------------- | -------------- | --------------------------------------------------------------- |
| cnx | 1 | `10.1.10.0/24` | `10.1.20.0/24` | iot 40, voip 50, dmz 60, unit15 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
(no inter-VLAN); WAN → nothing inbound; mesh → SSH, metrics, Omada UI.
## Adding a site
1. Copy `machines/gw-cnx-1/` to `machines/gw-<city>-<n>/`; set `site`,
`siteId` (next free number), port names, VLANs, and `omada.enable`.
Fill the real disk id in `disko.nix` (from the installer:
`ls -l /dev/disk/by-id/`). Keep the `mgmt`/`lan` VLANs.
2. Add the machine to `inventory.machines` in `clan.nix` and to the machine
list in `modules/mesh-hosts.nix`. Do **not** add it to `modules/hosts.nix`
(dynamic PPPoE IP; clan connects over the mesh).
3. Add its node_exporter and Blocky scrape targets in
`modules/monitoring/server.nix`.
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.
6. `clan vars generate gw-<city>-<n>` — prompts for the site's PPPoE
credentials, mints the ZeroTier identity etc. (`nix flake check` fails
until this has run, because mesh-hosts reads the ZeroTier IP var.)
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>`
8. Check `facter.json` for the real NIC names, fix `wan.interface` /
`trunkPorts` if the enumeration differs, and
`clan machines update gw-<city>-<n>` (rides the mesh from then on).
9. Add a row to the site table above and to the machines table in
[Overview](./overview.md).
## Omada controller
Sites with TP-Link Omada APs/switches set `cnx.router.omada.enable = true`.
The controller runs as a podman container (`mbentley/omada-controller` — there
is no nixpkgs package) with host networking on the mgmt VLAN, where adoption
broadcasts (UDP 29810) live. UI: `https://<mgmt-addr>:8043` from mgmt, or the
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
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
- **PPPoE down**: `systemctl status pppd-wan`, `journalctl -u pppd-wan` on the
gateway (over the mesh). ISP credentials live in the `pppoe-credentials`
vars generator; re-enter with `clan vars generate gw-<city>-<n> --regenerate`.
- **A VLAN gets no leases**: `systemctl status kea-dhcp4-server`; check the
port's `bridgeVLANs` tagging and that the switch trunk carries the VLAN.
- **DNS/blocklist issues**: Blocky metrics are in VictoriaMetrics (job
`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>`
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.
## Follow-ups
- Borgbackup client for Omada + Kea lease state.
- Suricata (IDS-only) if CPU headroom allows — deliberately skipped for now.
- Remaining three sites.
+90
View File
@@ -0,0 +1,90 @@
# Mail
A single mail server, `mx1`, runs the [Simple NixOS Mailserver](https://nixos-mailserver.readthedocs.io/)
stack (Postfix + Dovecot + Rspamd + OpenDKIM) for the `cnx.email` domain. All of
it is declared in `modules/mail.nix`, imported by `machines/mx1/configuration.nix`.
Mailboxes are **virtual** (not system users): each address is a login account
whose password is a four-word passphrase auto-generated by a clan vars generator
(e.g. `otter-lantern-cobalt-driftwood-42`). The generator stores both the
passphrase (handed to the user) and its sha-512 hash (consumed by SNM); plaintext
never lands in the repo.
The mail cert is obtained by `mx1` itself via ACME DNS-01 against `ns1` (key
`acme_mx1`), and carries `mx1.cnx.email` plus the `mta-sts.cnx.email` and
`mail.cnx.email` SANs. See [DNS → ACME DNS-01](./dns.md#acme-dns-01).
## Add a mailbox
1. Append the address to `accounts` in `modules/mail.nix`:
```nix
accounts = [
"postmaster@cnx.email"
"alice@cnx.email" # new
];
```
2. Mint its generated password (user-run, YubiKey touch):
```
clan vars generate mx1
```
3. Redeploy: `clan machines update mx1`.
4. Hand the passphrase to the user. The generator name replaces `@`→`-at-` and
`.`→`-`:
```
clan vars get mx1 mail-passwd-alice-at-cnx-email/passphrase
```
The DMARC report inbox (`dmarc@cnx.email`) is special: its password comes from
the **shared** `mail-dmarc-cred` generator (not the per-machine set) so parsedmarc
on `control` can read the same passphrase over the mesh.
## Add a domain
Adding a second mail domain is more than a config line — it needs its own DNS
records and DKIM key. Steps:
1. **Serve the zone** (if not already): add it in `modules/dns/domains.nix` and
drop a `<domain>.zone` file — see [DNS](./dns.md).
2. **Declare the domain** in `modules/mail.nix`:
```nix
mailserver.domains = [ "cnx.email" "newdomain.tld" ];
```
Add at least a `postmaster@newdomain.tld` account (RFC 5321 requires it) the
same way as above.
3. **Publish the mail DNS records** in the new zone, mirroring `cnx.email`'s
`; ---- Mail ----` block:
- `MX 10 mx1.cnx.email.` — point at the existing MX; do **not** stand up a new
hostname unless you really want a separate server.
- SPF: `@ TXT "v=spf1 mx -all"`.
- DKIM: SNM generates a key per domain on first start under
`mailserver.dkimKeyDirectory` (selector `mail`). After deploy, read the
public key off `mx1` and paste it as `mail._domainkey TXT`. The private keys
are declared as clan state and backed up.
- DMARC: `_dmarc TXT "v=DMARC1; p=quarantine; rua=mailto:dmarc@cnx.email; ..."`
— reports can keep flowing to the existing `dmarc@cnx.email` inbox.
4. **DANE / MTA-STS are tied to `mx1.cnx.email`**, the MX hostname — they do **not**
need duplicating per sending-domain. A new domain that uses `mx1.cnx.email` as
its MX inherits the existing TLSA and MTA-STS policy. Only add new TLSA/MTA-STS
records if you introduce a new MX hostname.
5. `clan vars generate mx1` (if you added accounts), then
`clan machines update mx1`.
## DKIM, DANE, MTA-STS reminders
These three are easy to get subtly wrong; the live records for `cnx.email` and
the exact commands to regenerate them are in `modules/dns/zones/cnx.email.zone`:
- **DKIM** key rotation = regenerate under `dkimKeyDirectory`, then republish the
`mail._domainkey TXT`.
- **DANE TLSA** (`_25._tcp.mx1 TLSA 3 1 1 …`) is the SHA-256 of `mx1`'s cert
**public key**. It stays valid across renewals because lego runs with
`--reuse-key`; only recompute it if the key changes. The `openssl` one-liner is
in the zone file.
- **MTA-STS**: bump the `_mta-sts TXT` id **every time** the policy in
`modules/mail.nix` changes, or senders keep the cached policy.
## DMARC reporting
`mx1` delivers aggregate/forensic DMARC reports to `dmarc@cnx.email`; parsedmarc
on `control` polls that mailbox over the mesh and feeds the results into Grafana.
See [Monitoring](./monitoring.md).
+4
View File
@@ -52,6 +52,10 @@ there is picked up):
- **CNX Uptime** (`uptime.json`) — per-host up/down status, current uptime,
availability over the selected window, and up/down history. Label-driven, so
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
the `parsedmarc` module (not from `dashboards/`); reads its own Elasticsearch
datasource, not VictoriaMetrics. See [DMARC reports](#dmarc-reports) below.
+10 -1
View File
@@ -7,12 +7,13 @@ this book is built from `docs/` and served on `control` over the ZeroTier mesh.
## Machines
| Machine | Role | Public IPv4 | Public IPv6 |
| --------- | -------------------------------------- | ---------------- | ----------------------- |
| ---------- | -------------------------------------- | ---------------- | ----------------------- |
| `control` | ZeroTier controller, monitoring, docs | `77.42.68.181` | `2a01:4f9:c013:e6d0::1` |
| `ns1` | Knot DNS **primary** (master) | `46.224.170.206` | `2a01:4f8:c014:b5c5::1` |
| `ns2` | Knot DNS **secondary** (slave) | `157.180.70.82` | `2a01:4f9:c014:6d87::1` |
| `mx1` | Mail server (**MX** for cnx.email) | `5.223.65.38` | `2a01:4ff:2f0:1963::1` |
| `web01` | Public reverse proxy (TLS termination) | `5.223.55.246` | `2a01:4ff:2f0:2d8f::1` |
| `gw-cnx-1` | Site gateway Chiang Mai (router) | dynamic (PPPoE) | — |
## Access
@@ -21,6 +22,14 @@ this book is built from `docs/` and served on `control` over the ZeroTier mesh.
- clan reaches machines by their public IPs first (the `internet` instance), with
the mesh and Tor as automatic fallbacks.
## Host baseline
Every machine sets a fixed-offset `time.timeZone` and runs **chrony** for time
sync (`services.chrony.enable`). chrony is the single NTP implementation across
the fleet — do **not** use `systemd-timesyncd`; the two are mutually exclusive
and we standardise on chrony everywhere. New hosts must set both the timezone and
chrony as part of their baseline config.
## Editing these docs
Commit-to-edit: change the markdown under `docs/src/`, commit, and redeploy
Generated
+14 -10
View File
@@ -29,11 +29,11 @@
"treefmt-nix": "treefmt-nix"
},
"locked": {
"lastModified": 1782204050,
"narHash": "sha256-o8PXnm00Ecim/7WIQWxnFciWxvxgbll9fxH5MJ3a1Kg=",
"rev": "246e2bcba4dc9c009a96d017c030c7a53c83955e",
"lastModified": 1783497933,
"narHash": "sha256-TxmwEews6URFPqOWEHNychtXbFDgLZjbOfEXtvtOm6U=",
"rev": "3dc0221ca09033599fe98055e9bbc81bdf32732a",
"type": "tarball",
"url": "https://git.clan.lol/api/v1/repos/clan/clan-core/archive/246e2bcba4dc9c009a96d017c030c7a53c83955e.tar.gz"
"url": "https://git.clan.lol/api/v1/repos/clan/clan-core/archive/3dc0221ca09033599fe98055e9bbc81bdf32732a.tar.gz"
},
"original": {
"type": "tarball",
@@ -217,11 +217,11 @@
]
},
"locked": {
"lastModified": 1781301671,
"narHash": "sha256-rq6WOopxq3U2AGEWO80o9LIJDYcYIdgw6jyl+y+19w8=",
"lastModified": 1785198005,
"narHash": "sha256-falBPi+PJtMx0vwII8L24wjDGXNZMwRiVXsw8WTXLEo=",
"owner": "simple-nixos-mailserver",
"repo": "nixos-mailserver",
"rev": "661ec59a97ccee13a63f79280b282eb6f7d3f817",
"rev": "d357b9f048c5532ec81b0e0034c0b8463d5ddd46",
"type": "gitlab"
},
"original": {
@@ -247,6 +247,10 @@
"root": {
"inputs": {
"clan-core": "clan-core",
"flake-parts": [
"clan-core",
"flake-parts"
],
"nixos-mailserver": "nixos-mailserver",
"nixpkgs": [
"clan-core",
@@ -320,11 +324,11 @@
]
},
"locked": {
"lastModified": 1780220602,
"narHash": "sha256-eynAfOmbmxJnkp7YewvCEbShNnnYJ9gLLqkzsYtBPeM=",
"lastModified": 1784369104,
"narHash": "sha256-47cxbcZODibHv3rELFQ9vZly0vUNkND/atn/U7HLeb0=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "db947814a175b7ca6ded66e21383d938df01c227",
"rev": "df3c0640565d04a0261253cdd89fce78ec50168a",
"type": "github"
},
"original": {
+44 -17
View File
@@ -7,13 +7,16 @@
inputs.nixos-mailserver.url = "gitlab:simple-nixos-mailserver/nixos-mailserver/nixos-26.05";
inputs.nixos-mailserver.inputs.nixpkgs.follows = "nixpkgs";
inputs.flake-parts.follows = "clan-core/flake-parts";
outputs =
{
inputs@{
self,
clan-core,
nixpkgs,
flake-parts,
...
}@inputs:
}:
let
# Usage see: https://docs.clan.lol
clan = clan-core.lib.clan {
@@ -36,36 +39,60 @@
"age-plugin-fido2-hmac"
];
};
in
flake-parts.lib.mkFlake { inherit inputs; } {
systems = [
"x86_64-linux"
"aarch64-linux"
"aarch64-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
{
flake = {
inherit (clan.config) nixosConfigurations nixosModules clanInternals;
clan = clan.config;
};
perSystem =
{ system, ... }:
let
pkgs = clan-core.inputs.nixpkgs.legacyPackages.${system};
treefmtEval = inputs.treefmt-nix.lib.evalModule pkgs ./fmt.nix;
clanCli = clan-core.packages.${system}.clan-cli;
# `clan machines update a b c` normally runs machines in parallel,
# which interleaves their output and buries the YubiKey PIN prompts.
# This wrapper (first in PATH) runs them one at a time instead; any
# flags fall through to the real CLI untouched.
clanSequential = pkgs.writeShellScriptBin "clan" ''
if [ "$#" -gt 3 ] && [ "$1" = machines ] && [ "$2" = update ]; then
shift 2
for arg in "$@"; do
case "$arg" in
-*) exec ${clanCli}/bin/clan machines update "$@" ;;
esac
done
for machine in "$@"; do
${clanCli}/bin/clan machines update "$machine" || exit
done
exit 0
fi
exec ${clanCli}/bin/clan "$@"
'';
in
{
# `nix fmt` and the `nix flake check` formatting gate.
formatter = forAllSystems (system: (treefmtFor system).config.build.wrapper);
checks = forAllSystems (system: {
formatting = (treefmtFor system).config.build.check self;
});
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 = forAllSystems (system: {
default = (pkgsFor system).mkShell {
devShells.default = pkgs.mkShell {
packages = [
clan-core.packages.${system}.clan-cli
(treefmtFor system).config.build.wrapper
clanSequential
clanCli
treefmtEval.config.build.wrapper
];
};
});
};
};
}
+3
View File
@@ -14,6 +14,9 @@
},
"web01": {
"installedAt": 1781983723
},
"gw-cnx-1": {
"installedAt": 1785494267
}
}
}
+1 -1
View File
@@ -23,7 +23,7 @@ in
};
time.timeZone = "Etc/GMT-3"; # UTC+3 (fixed offset, no DST)
services.timesyncd.enable = true;
services.chrony.enable = true;
# Public Hetzner Cloud firewalls, synced from this config on every deploy.
# Rules live in their own data file; see that file for the no-public-SSH note.
+91
View File
@@ -0,0 +1,91 @@
# Site gateway Chiang Mai (site 1): Topton 1U, Intel N300, 4x i226-V 2.5G.
# Port roles below use the expected igc names — verify against facter.json
# after the first install and adjust if the box enumerates differently.
{ config, lib, ... }:
{
imports = [
../../modules/router
../../modules/monitoring/exporters.nix
(import ../../modules/dns/acme-gw-secret.nix "gw-cnx-1")
];
clan.core.sops.defaultGroups = [ "admins" ];
# Until the install generates facter.json (which normally provides this).
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
# ZFS (disko.nix) needs a stable machine-unique hostId; derive it from the
# hostname so every gateway gets one for free when copied for a new site.
networking.hostId = builtins.substring 0 8 (
builtins.hashString "sha256" config.networking.hostName
);
cnx.router = {
enable = true;
site = "cnx";
siteId = 1;
wan.interface = "enp1s0";
wan.vlanId = null; # this ISP runs PPPoE untagged on the port
trunkPorts = [
"enp2s0"
# "enp3s0" # STAGING: serves as the uplink below until cutover
];
# 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 = {
mgmt = {
id = 10; # 10.1.10.0/24 — servers, APs, switches, Omada, admin
dhcp.reservations.storinator01 = {
hwAddress = "7c:c2:55:e0:d6:40";
ipAddress = "10.1.10.53";
};
};
lan.id = 20; # 10.1.20.0/24 — trusted clients
iot.id = 40; # 10.1.40.0/24
voip.id = 50; # 10.1.50.0/24
dmz.id = 60; # 10.1.60.0/24
unit1.id = 110; # 10.1.110.0/24
unit2.id = 120; # 10.1.120.0/24
unit3.id = 130; # 10.1.130.0/24
unit4.id = 140; # 10.1.140.0/24
unit5 = {
id = 150; # 10.1.150.0/24
dhcp.reservations.newt = {
hwAddress = "7c:d3:0a:21:58:0b";
ipAddress = "10.1.150.22";
};
};
};
# This site runs the Omada controller for its APs/switches.
omada.enable = true;
# Internal reverse proxy: real wildcard cert *.cnx1.cnx.network; Blocky
# resolves the names to the router's LAN address for mgmt+lan clients.
proxy = {
enable = true;
services.omada = {
# Omada's UI is HTTPS with a self-signed cert on the host network.
backend = "https://127.0.0.1:8043";
insecureSkipVerify = true;
};
};
};
# 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)
services.chrony.enable = true;
}
+97
View File
@@ -0,0 +1,97 @@
{ ... }:
let
hashDisk = disk: "os-${builtins.substring 0 5 (builtins.hashString "sha256" disk)}";
os = "/dev/disk/by-id/ata-TS256GMSA230S_J927900030";
in
{
boot.loader = {
systemd-boot = {
enable = true;
};
efi = {
canTouchEfiVariables = true;
};
};
boot.zfs.forceImportRoot = true;
disko.devices = {
disk = {
"os-${hashDisk os}" = {
type = "disk";
device = os;
content = {
type = "gpt";
partitions = {
ESP = {
size = "1G";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "nofail" ];
};
};
system = {
size = "100%";
content = {
type = "zfs";
pool = "zroot";
};
};
swap = {
size = "16G";
content = {
type = "swap";
};
};
};
};
};
};
zpool = {
zroot = {
type = "zpool";
rootFsOptions = {
mountpoint = "none";
compression = "lz4";
acltype = "posixacl";
xattr = "sa";
"com.sun:auto-snapshot" = "true";
};
options.ashift = "12";
datasets = {
"root" = {
type = "zfs_fs";
options.mountpoint = "none";
};
"root/nixos" = {
type = "zfs_fs";
options.mountpoint = "/";
mountpoint = "/";
};
"root/home" = {
type = "zfs_fs";
options.mountpoint = "/home";
mountpoint = "/home";
};
"root/tmp" = {
type = "zfs_fs";
mountpoint = "/tmp";
options = {
mountpoint = "/tmp";
sync = "disabled";
};
};
"root/var" = {
type = "zfs_fs";
options.mountpoint = "/var";
mountpoint = "/var";
};
};
};
};
};
}
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -19,5 +19,5 @@ in
};
time.timeZone = "Etc/GMT-8"; # UTC+8 (Singapore, fixed offset, no DST)
services.timesyncd.enable = true;
services.chrony.enable = true;
}
+36 -2
View File
@@ -14,6 +14,7 @@ in
../../modules/dns/authoritative.nix
../../modules/dns/acme-mx1-secret.nix
../../modules/dns/acme-web01-secret.nix
(import ../../modules/dns/acme-gw-secret.nix "gw-cnx-1")
../../modules/static-ipv6.nix
../../modules/monitoring/exporters.nix
];
@@ -38,7 +39,7 @@ in
};
time.timeZone = "Etc/GMT-1"; # UTC+1 (fixed offset, no DST)
services.timesyncd.enable = true;
services.chrony.enable = true;
# ACME DNS-01 (RFC 2136), general key. A dedicated TSIG key scoped by acl_acme
# (referenced by every zone below) to TXT updates at or under _acme-challenge.
@@ -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 = [
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-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 = [
@@ -133,6 +153,17 @@ in
# i.e. _acme-challenge at the cnx.network apex (where this acl is attached).
"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
@@ -168,6 +199,9 @@ in
"acl_acme"
]
++ 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;
}
+1 -1
View File
@@ -20,7 +20,7 @@ in
};
time.timeZone = "Etc/GMT-3"; # UTC+3 (fixed offset, no DST)
services.timesyncd.enable = true;
services.chrony.enable = true;
# ns2 = secondary (slave): pulls every zone from ns1 and accepts its NOTIFY.
services.knot.settings.zone = map (d: {
+1 -1
View File
@@ -18,5 +18,5 @@ in
};
time.timeZone = "Etc/GMT-8"; # UTC+8 (Singapore, fixed offset, no DST)
services.timesyncd.enable = true;
services.chrony.enable = true;
}
+17
View File
@@ -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'';
};
}
+1
View File
@@ -4,4 +4,5 @@
"cnx.network"
"buildfor.life"
"cnx.email"
"newedge.house"
]
+13
View File
@@ -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.
+1
View File
@@ -23,6 +23,7 @@ let
"ns2"
"mx1"
"web01"
"gw-cnx-1"
] readIp;
# RFC 4193 prefix of this ZeroTier network: fd + the 8-byte network id + the
+25
View File
@@ -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";
rules = [
+267
View File
@@ -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"
}
]
}
]
}
+7
View File
@@ -48,6 +48,13 @@ in
(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)
];
}
{
+52
View File
@@ -0,0 +1,52 @@
# CrowdSec security engine + nftables bouncer: parses sshd auth attempts from
# the journal and bans offending source IPs at the firewall. Log-based (no
# inline DPI) so it costs the N300 next to nothing.
{
config,
lib,
...
}:
let
cfg = config.cnx.router;
mesh = import ../mesh-hosts.nix { inherit config lib; };
in
{
config = lib.mkIf cfg.enable {
services.crowdsec = {
enable = true;
autoUpdateService = true;
hub.collections = [
"crowdsecurity/linux"
"crowdsecurity/sshd"
];
localConfig = {
acquisitions = [
{
source = "journalctl";
journalctl_filter = [ "_SYSTEMD_UNIT=sshd.service" ];
labels.type = "syslog";
}
];
# Never ban the ZeroTier mesh — it is the only admin path to these
# boxes (no public SSH), so a false positive would lock us out.
# Parser-stage whitelist: mesh events are dropped before any scenario.
parsers.s02Enrich = [
{
name = "cnx/mesh-whitelist";
description = "Whitelist the ZeroTier management mesh";
whitelist = {
reason = "ZeroTier mesh is the admin path";
cidr = [ mesh.subnet ];
};
}
];
};
};
services.crowdsec-firewall-bouncer = {
enable = true;
registerBouncer.enable = true;
settings.mode = "nftables";
};
};
}
+325
View File
@@ -0,0 +1,325 @@
# Site gateway (OPNsense replacement) for the Topton 1U boxes: PPPoE WAN,
# VLAN-filtering bridge over the LAN ports, and per-VLAN L3 interfaces.
# Imported by machines/gw-<city>-<n>; everything is driven by cnx.router.*.
#
# Fleet addressing convention: each site owns 10.<siteId>.0.0/16. A VLAN's
# subnet defaults to 10.<siteId>.<vlanId>.0/24 with the router at .1 and the
# DHCP pool at .100-.199. VLANs that need more space (e.g. public-wifi guest)
# override `subnet`/`address`/`dhcp.pool` and take a wider block from the
# upper half (10.<siteId>.128.0/17), e.g. guest -> 10.<siteId>.128.0/22.
# VLAN ids: 10 = mgmt, 20 = lan (mandatory); 30 = guest, 40 = iot (reserved).
{
config,
lib,
pkgs,
...
}:
let
cfg = config.cnx.router;
vlanIf = name: "vlan-${name}";
site = toString cfg.siteId;
vlanModule =
{ name, config, ... }:
let
octet = toString config.id;
in
{
options = {
id = lib.mkOption {
type = lib.types.ints.between 1 4094;
description = "802.1Q VLAN id (fleet convention: 10 mgmt, 20 lan, 30 guest, 40 iot).";
};
address = lib.mkOption {
type = lib.types.str;
default = "10.${site}.${octet}.1";
description = "Router address on this VLAN.";
};
prefixLength = lib.mkOption {
type = lib.types.ints.between 8 30;
default = 24;
};
subnet = lib.mkOption {
type = lib.types.str;
default = "10.${site}.${octet}.0/24";
description = "The VLAN's network in CIDR form (must contain `address`).";
};
dhcp = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
};
pool = {
from = lib.mkOption {
type = lib.types.str;
default = "10.${site}.${octet}.100";
};
to = lib.mkOption {
type = lib.types.str;
default = "10.${site}.${octet}.199";
};
};
leaseTime = lib.mkOption {
type = lib.types.ints.positive;
default = 86400;
description = ''
Lease validity in seconds. Lower it for high-churn networks,
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 {
type = lib.types.bool;
default = true;
description = "Whether clients on this VLAN may reach the internet.";
};
};
};
in
{
imports = [
./pppoe.nix
./ipv6.nix
./firewall.nix
./dns-dhcp.nix
./crowdsec.nix
./omada.nix
./proxy.nix
./iperf.nix
./speedtest.nix
];
options.cnx.router = {
enable = lib.mkEnableOption "site gateway (router) role";
site = lib.mkOption {
type = lib.types.str;
description = "City code of the site, e.g. \"cnx\".";
};
siteId = lib.mkOption {
type = lib.types.ints.between 1 254;
description = "Site number; drives the 10.<siteId>.<vlan>.0/24 addressing.";
};
wan.interface = lib.mkOption {
type = lib.types.str;
description = "Physical WAN port the PPPoE session runs on.";
};
wan.vlanId = lib.mkOption {
type = lib.types.nullOr (lib.types.ints.between 1 4094);
default = null;
description = ''
802.1Q tag the ISP requires for the PPPoE session (AIS Thailand: 10);
null for untagged PPPoE directly on the port. Unrelated to the LAN
VLANs this tag exists only on the WAN port.
'';
};
wan.macAddress = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "aa:bb:cc:dd:ee:ff";
description = ''
Spoofed MAC for the WAN port, e.g. to keep the MAC the ISP has
pinned (cloned from the old router). null keeps the hardware MAC.
'';
};
wan.pppInterface = lib.mkOption {
type = lib.types.str;
internal = true;
readOnly = true;
default = if cfg.wan.vlanId == null then cfg.wan.interface else "wan-vlan";
description = "Interface pppd dials on (the WAN port or its ISP VLAN).";
};
trunkPorts = lib.mkOption {
type = lib.types.listOf lib.types.str;
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 {
type = lib.types.attrsOf (lib.types.submodule vlanModule);
description = "VLANs served at this site; `mgmt` and `lan` are mandatory.";
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.vlans ? mgmt && cfg.vlans ? lan;
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.useDHCP = false;
systemd.network.enable = true;
systemd.network.netdevs = {
"20-br0" = {
netdevConfig = {
Name = "br0";
Kind = "bridge";
};
bridgeConfig.VLANFiltering = true;
};
}
// lib.optionalAttrs (cfg.wan.vlanId != null) {
"15-wan-vlan" = {
netdevConfig = {
Name = "wan-vlan";
Kind = "vlan";
};
vlanConfig.Id = cfg.wan.vlanId;
};
}
// lib.mapAttrs' (
name: vlan:
lib.nameValuePair "30-${vlanIf name}" {
netdevConfig = {
Name = vlanIf name;
Kind = "vlan";
};
vlanConfig.Id = vlan.id;
}
) cfg.vlans;
systemd.network.networks =
let
taggedAll = lib.mapAttrsToList (_: vlan: { VLAN = vlan.id; }) cfg.vlans;
in
{
# WAN port carries only the PPPoE session; no IP config of its own.
"10-wan" = {
matchConfig.Name = cfg.wan.interface;
networkConfig.LinkLocalAddressing = "no";
vlan = lib.optional (cfg.wan.vlanId != null) "wan-vlan";
linkConfig = {
RequiredForOnline = "carrier";
}
# The wan-vlan subinterface (and thus the PPPoE session) inherits
# the parent port's MAC, so spoofing here covers both cases.
// lib.optionalAttrs (cfg.wan.macAddress != null) {
MACAddress = cfg.wan.macAddress;
};
};
}
// lib.optionalAttrs (cfg.wan.vlanId != null) {
# The ISP-side VLAN subinterface pppd dials on (e.g. AIS tags PPPoE).
"15-wan-vlan" = {
matchConfig.Name = "wan-vlan";
networkConfig.LinkLocalAddressing = "no";
linkConfig.RequiredForOnline = "no";
};
}
// {
# The bridge itself is L2-only; L3 lives on the vlan-* interfaces,
# which hang off the bridge (tagged on the bridge "self" port).
"20-br0" = {
matchConfig.Name = "br0";
networkConfig.LinkLocalAddressing = "no";
vlan = lib.mapAttrsToList (name: _: vlanIf name) cfg.vlans;
bridgeVLANs = taggedAll;
linkConfig.RequiredForOnline = "no";
};
}
// lib.listToAttrs (
map (port: {
name = "25-trunk-${port}";
value = {
matchConfig.Name = port;
networkConfig.Bridge = "br0";
bridgeVLANs = taggedAll;
linkConfig.RequiredForOnline = "no";
};
}) 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' (
name: vlan:
lib.nameValuePair "40-${vlanIf name}" {
matchConfig.Name = vlanIf name;
address = [ "${vlan.address}/${toString vlan.prefixLength}" ];
networkConfig = {
IPv6AcceptRA = false;
# Announce a /64 carved from the DHCPv6-PD prefix on ppp0 (SLAAC).
IPv6SendRA = true;
DHCPPrefixDelegation = true;
};
dhcpPrefixDelegationConfig.SubnetId = "auto";
linkConfig.RequiredForOnline = "no";
}
) cfg.vlans;
};
}
+84
View File
@@ -0,0 +1,84 @@
# LAN DHCP (Kea) and DNS (Blocky). Fully declarative: one Kea subnet per VLAN
# with dhcp.enable, Blocky as the blocklist resolver every DHCP lease points
# at. Blocky's HTTP listener (:4000) serves Prometheus metrics, scraped by
# control over the mesh (firewall.nix scopes it to the mesh subnet).
{
config,
lib,
...
}:
let
cfg = config.cnx.router;
dhcpVlans = lib.filterAttrs (_: vlan: vlan.dhcp.enable) cfg.vlans;
in
{
config = lib.mkIf cfg.enable {
services.kea.dhcp4 = {
enable = true;
settings = {
interfaces-config.interfaces = lib.mapAttrsToList (name: _: "vlan-${name}") dhcpVlans;
lease-database = {
type = "memfile";
persist = true;
name = "/var/lib/kea/dhcp4.leases";
};
valid-lifetime = 86400;
subnet4 = lib.mapAttrsToList (name: vlan: {
id = vlan.id;
subnet = vlan.subnet;
interface = "vlan-${name}";
valid-lifetime = vlan.dhcp.leaseTime;
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 = [
{
name = "routers";
data = vlan.address;
}
{
name = "domain-name-servers";
data = vlan.address;
}
];
}) dhcpVlans;
};
};
services.blocky = {
enable = true;
settings = {
ports = {
dns = 53;
http = 4000;
};
upstreams.groups.default = [
"9.9.9.9"
"149.112.112.112"
"2620:fe::fe"
];
blocking = {
denylists.ads = [
"https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts"
];
clientGroupsBlock.default = [ "ads" ];
};
caching = {
minTime = "5m";
prefetching = true;
};
prometheus.enable = true;
};
};
# The router itself resolves via public resolvers, not via Blocky, so DNS
# for deploys/updates survives a broken local resolver.
networking.nameservers = [
"9.9.9.9"
"1.1.1.1"
];
};
}
+62
View File
@@ -0,0 +1,62 @@
# Router firewall/NAT policy (nftables). Trust model:
# mgmt VLAN -> trusted: router services, all VLANs, WAN
# other VLANs -> DNS/DHCP on the router + WAN (if allowWan); no inter-VLAN
# WAN (ppp0) -> nothing inbound beyond established/related
# mesh -> admin SSH + metrics scrapes (same trust boundary as the fleet)
{
config,
lib,
...
}:
let
cfg = config.cnx.router;
mesh = import ../mesh-hosts.nix { inherit config lib; };
vlanIfs = lib.mapAttrsToList (name: _: "vlan-${name}") cfg.vlans;
wanVlanIfs = lib.mapAttrsToList (name: _: "vlan-${name}") (
lib.filterAttrs (_: vlan: vlan.allowWan) cfg.vlans
);
nonMgmtIfs = lib.filter (i: i != "vlan-mgmt") vlanIfs;
ifSet = ifs: "{ ${lib.concatStringsSep ", " (map (i: "\"${i}\"") ifs)} }";
in
{
config = lib.mkIf cfg.enable {
networking.nftables.enable = true;
# SSH reachable only from the mgmt VLAN (trusted) and the mesh — never
# from the WAN or the other VLANs.
services.openssh.openFirewall = false;
networking.firewall = {
enable = true;
filterForward = true;
trustedInterfaces = [ "vlan-mgmt" ];
# Non-mgmt VLANs may only talk to the router's DNS and DHCP.
interfaces = lib.genAttrs nonMgmtIfs (_: {
allowedTCPPorts = [ 53 ];
allowedUDPPorts = [
53
67
];
});
extraInputRules = ''
ip6 saddr ${mesh.subnet} tcp dport 22 accept comment "admin ssh over the mesh"
ip6 saddr ${mesh.subnet} tcp dport 4000 accept comment "blocky metrics scrape from control"
'';
extraForwardRules = ''
tcp flags syn tcp option maxseg size set rt mtu comment "MSS clamp for PPPoE mtu 1492"
iifname "vlan-mgmt" accept comment "mgmt reaches all VLANs and the WAN"
iifname ${ifSet wanVlanIfs} oifname "ppp0" accept comment "LAN to internet"
'';
};
networking.nat = {
enable = true;
externalInterface = "ppp0";
internalInterfaces = vlanIfs;
};
};
}
+28
View File
@@ -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"
'';
};
}
+31
View File
@@ -0,0 +1,31 @@
# IPv6 on the PPPoE uplink: run networkd's DHCPv6 client on ppp0 to obtain a
# delegated prefix; each vlan-* interface (default.nix) carves a /64 out of it
# via DHCPPrefixDelegation and announces it to clients with SLAAC.
{
config,
lib,
...
}:
let
cfg = config.cnx.router;
in
{
config = lib.mkIf cfg.enable {
systemd.network.networks."45-ppp0" = {
matchConfig.Name = "ppp0";
networkConfig = {
DHCP = "ipv6";
# pppd owns the v4 address/route on this link; don't let networkd
# tear them down.
KeepConfiguration = "static";
# Default v6 route comes from the ISP's RA when they send one.
IPv6AcceptRA = true;
};
# Many PPPoE ISPs never send an RA with the M flag; solicit regardless.
dhcpV6Config.WithoutRA = "solicit";
linkConfig.RequiredForOnline = "no";
};
boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = lib.mkDefault 1;
};
}
+43
View File
@@ -0,0 +1,43 @@
# TP-Link Omada SDN controller for sites with Omada APs/switches. There is no
# nixpkgs package, so it runs as a podman container (mbentley/omada-controller,
# the de-facto standard image). Host networking because device adoption relies
# on L2 broadcast discovery (UDP 29810) on the mgmt VLAN; the default-deny
# input firewall keeps its ports unreachable from WAN and non-mgmt VLANs.
{
config,
lib,
...
}:
let
cfg = config.cnx.router;
mesh = import ../mesh-hosts.nix { inherit config lib; };
in
{
options.cnx.router.omada.enable =
lib.mkEnableOption "TP-Link Omada SDN controller (podman container)";
config = lib.mkIf (cfg.enable && cfg.omada.enable) {
virtualisation.podman.enable = true;
virtualisation.oci-containers = {
backend = "podman";
containers.omada = {
image = "docker.io/mbentley/omada-controller:5.15";
extraOptions = [ "--network=host" ];
environment.TZ = config.time.timeZone;
volumes = [
"/var/lib/omada/data:/opt/tplink/EAPController/data"
"/var/lib/omada/logs:/opt/tplink/EAPController/logs"
];
};
};
# Admin UI (8043) also reachable over the mesh, like Grafana on control.
networking.firewall.extraInputRules = ''
ip6 saddr ${mesh.subnet} tcp dport 8043 accept comment "omada ui over the mesh"
'';
# Controller state (adopted devices, site config, cert) — declared as clan
# state so a borgbackup client can pick it up; backup wiring is a later step.
clan.core.state.omada.folders = [ "/var/lib/omada" ];
};
}
+64
View File
@@ -0,0 +1,64 @@
# PPPoE WAN session. ISP credentials are entered once at `clan vars generate`
# (prompts). Both are secret — AIS often uses the same string for username and
# password — so neither may land in the Nix store: pppd reads the username from
# an included secret options file and the password from chap/pap-secrets.
{
config,
lib,
...
}:
let
cfg = config.cnx.router;
creds = config.clan.core.vars.generators.pppoe-credentials;
in
{
config = lib.mkIf cfg.enable {
clan.core.vars.generators.pppoe-credentials = {
prompts.username = {
description = "PPPoE username (from the ISP)";
type = "hidden";
};
prompts.password = {
description = "PPPoE password (from the ISP)";
type = "hidden";
};
files."user-opts".secret = true;
files."chap-secrets".secret = true;
script = ''
user="$(cat "$prompts"/username)"
pass="$(cat "$prompts"/password)"
printf 'user "%s"\n' "$user" > "$out"/user-opts
printf '"%s" * "%s"\n' "$user" "$pass" > "$out"/chap-secrets
'';
};
services.pppd = {
enable = true;
peers.wan = {
autostart = true;
config = ''
plugin pppoe.so ${cfg.wan.pppInterface}
ifname ppp0
file ${creds.files."user-opts".path}
noipdefault
defaultroute
noauth
hide-password
persist
maxfail 0
holdoff 5
lcp-echo-interval 15
lcp-echo-failure 3
+ipv6
mtu 1492
mru 1492
'';
};
};
# pppd looks up the password for `user` in these files at dial time; both
# point at the same generated `"<user>" * "<pass>"` line (PAP and CHAP).
environment.etc."ppp/chap-secrets".source = creds.files."chap-secrets".path;
environment.etc."ppp/pap-secrets".source = creds.files."chap-secrets".path;
};
}
+141
View File
@@ -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
];
});
};
}
+67
View File
@@ -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;
};
};
};
}
+6
View File
@@ -0,0 +1,6 @@
[
{
"publickey": "age1g8a6fqhqg8q8qu4jyuhxc7hf00t5qvra55chyfm0qk45wlmvc9qs2cft8t",
"type": "age"
}
]
+1
View File
@@ -0,0 +1 @@
../../../groups/admins
+6 -2
View File
@@ -3,8 +3,12 @@
"sops": {
"age": [
{
"recipient": "age1yubikey1qd859y9ehz2ya8j2cftwrtmdeqhuk7r7yc52zp64wpff6068gwrac3q6nsa",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHBpdi1wMjU2IHFWcTVydyBBcmJ4cXNt\nL3k4UmtqQUJZOGRoZFhab0pRSTdBUm9ZZkRxdHNvaCtRc05JVgphbHhHQThiRDNF\neW8xVGhXQ1pvSkRDR2hod2Zpa3pwaWZDVUpGZlpPWkNZCi0tLSB2T3JCcElnbVU2\nekJ5ZTdyQzZwbmR4aHBJUTc3M3loUm0wbHpHTGM0ZW1RCs0kHes9rkjd3uwHzxhi\n8x8AcQcQTR+CGsV5XPzGVMKKUZpwXWeNnvcHfVwYcqOnXucbUdCxLc6d57c9GQpC\nd3Y=\n-----END AGE ENCRYPTED FILE-----\n"
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNRWlKNFVSOThSWlI3LzAz\ndnQxbnNGSlhoVmZrQmpIVEJieStlUzlseDJJCmdGbldCSU1oTGszZmtzSWhJaWta\nVDhoQU8vVXB0bG1VUkxoWDlyR0I1a0EKLS0tIDFzdklCTTBjeXBoT3FqVW1Pbkxl\nYVZ5Vm55OFp4YWwzS3dVNXRoZmpBT1EK0pNbk20gwPe5Q1bZR3XRHMWz+x+8FzRa\niq5tLcGkgfMO6WTzj8vtB7g0Q3sFgmPXTqb8yMzqAkZd4z9FKJGt6A==\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",
+1
View File
@@ -0,0 +1 @@
../../../groups/admins
+19
View File
@@ -0,0 +1,19 @@
{
"data": "ENC[AES256_GCM,data:bsb98446kjvnDAVWNIqvH64uD6XyfEpohPFZs38jDaxxTp1wqJmFdeDW+p3SGld8Q+4h6rq7rFtTUJnQyslvgsPfYN5tBhJuTbI=,iv:pPUf3RgHqS8LlRIBdZ7jmzxp5VmlP68nrX8TCVP8aGU=,tag:SseZa3Sf5LuMXWN8CNSbxw==,type:str]",
"sops": {
"age": [
{
"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"
}
],
"lastmodified": "2026-07-28T09:03:13Z",
"mac": "ENC[AES256_GCM,data:MKIyykYwtQqpUnXqZQKzKwoRhSon2PH0wYiTDTvogaVREHfpEPs/mdOjQBUHtbr1QGqWNh4AxdBTKfpjXTlncHAEtcIdVH3qPccVxsXqfqRmZPDFzcHZAIw+VSvDsKwD3anixiXDeHmeLawwbyb1rnS0P8KquXbmjvirwGUIQLU=,iv:+hdLXMJMCby/6Q6JGxp4p95GgPKbdA/OTAZloyylr08=,tag:ZZ4dKjhjpE2xqcoNf4qeoA==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.13.1"
}
}
+1
View File
@@ -0,0 +1 @@
../../../users/berwn
+1
View File
@@ -0,0 +1 @@
../../../groups/admins
+6 -2
View File
@@ -3,8 +3,12 @@
"sops": {
"age": [
{
"recipient": "age1yubikey1qd859y9ehz2ya8j2cftwrtmdeqhuk7r7yc52zp64wpff6068gwrac3q6nsa",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHBpdi1wMjU2IHFWcTVydyBBemRVWUs1\naXFpVm80UmloUUpnR1BVaGsxampHV1JFNWFnSUhiY0orOTU0cgppVEJ5QVcvbGpC\nZlErd01XUzh0MnZOekkvTkc5Z3owS3pBbkNCUDJ6SlJRCi0tLSBRVVgrd3hIVEla\nMEhoN3FDSG1HdGk5YU8ydzlVUGd1RW5zM1lka3VuSHFNClrBItoFo7uqpS5fuvaG\nOvh6mTylk4bHgcSRGKTk383srgsvPdZaioJfUs7fbVmLD3+bhfM/Wgv7tI6kO1dj\nmDg=\n-----END AGE ENCRYPTED FILE-----\n"
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHBpdi1wMjU2IHFWcTVydyBBcU01TXl0\nRmU5ZlZOK3NZZE8vNktOWXBkRnZtZHlUbnBHYlVQbDdzZlRDbApFeFI0OE1KdndQ\nZWczZGRYZXBONTc2aG4vZTJEZ1hCdGljNFJBcmF0UGpRCi0tLSBGMGJVWUtJMWFP\nQUZwWVZTWUFKZnVJUTJSMHlRMGRNcExPSEpXTFpLSkVNCj1WEuRyATcxp0GYeoqC\nACpQpcqgIBVD1dYB0pPo+I6yDXvx+JbwVdgGl+2Cxj40HQOfyvJecoOO3efOZrB3\nUZU=\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",
+1
View File
@@ -0,0 +1 @@
../../../groups/admins
+6 -2
View File
@@ -3,8 +3,12 @@
"sops": {
"age": [
{
"recipient": "age1yubikey1qd859y9ehz2ya8j2cftwrtmdeqhuk7r7yc52zp64wpff6068gwrac3q6nsa",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHBpdi1wMjU2IHFWcTVydyBBL2doZFpO\na2Y2QzZSckJaZmZsTlFtQU1mckxJWTBiUXA2SUljVWZDemFJYgpwcFl6RHh2ZXdn\nNTBsaEVpWm5oYWtteU5WZmxWK1RRK2NoYW0yWlpCd3dBCi0tLSBsZVNpczR4dUdv\nSmtqQzMvc1ZYY0xUY0JMQmt2YzB5YmZOMEtXcW9vWUcwCnCdgcgiwF2rkWsV0IdO\n/6cs35FBypWpfflGwOwP0GnTrNizc9HphcMcwAi5NMQVi6X90Xgm5aFdpAsSWepX\n/SI=\n-----END AGE ENCRYPTED FILE-----\n"
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHBpdi1wMjU2IHFWcTVydyBBaTVGQWRC\nZUJocGx5Zjd0c1JpUEdHbGczNUh1RjRqSERRNHNSbnRndWFUMAoxbFBEU0Ezajhr\nSUlIRStWUUdVTDJMQWxmSkw0c0FNYnFxNEJIdnExTmx3Ci0tLSBMRGRPLzlXTEN3\ncHZCMGF3WDZTcFhTd2srL3Vpc2dRdExrVzFJcWhjQVJrCrA1y2SapkZoppI3z4LD\nH71D/WxIcTHp3fIzICNvgxWHn3+vkDDZcpcgH+gRiPCDqrgDp0NZcZiDlz6zyHGr\nmaM=\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",
@@ -0,0 +1 @@
9a9e2f23526941b204dacfe8fc77dad5f4b1c7aff945cdbf301ec61129959228
@@ -0,0 +1 @@
ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIHiQTWR/pRz9vlrtQSe+u2g8XPDZvFpRBCpF7bQ6mBPaAAAAIH+Ix/XbsbqHU87Kg7Y9frnhYgfUEMNKOmQ3DSlb9LCmAAAAAAAAAAAAAAACAAAAB2NvbnRyb2wAAAAgAAAAHGNvbnRyb2wuY254LW5ldHdvcmsuaW50ZXJuYWwAAAAAAAAAAP//////////AAAAAAAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACCboWwGkseX7mqk2GVtM2o1WPRzh8U3X6A3woanYG9xoQAAAFMAAAALc3NoLWVkMjU1MTkAAABAbWgsXjGd/QGdbYHNfHs4LiMgF1weHHirWk4ZLMuTLRDXObwdJ4tbc9IGDfNBZnAfOSxBBy+U/sjGbKQtRWt4Aw== /tmp/vars-irrhty3h/in/openssh/ssh.id_ed25519.pub
@@ -0,0 +1 @@
../../../../../../sops/groups/admins
@@ -0,0 +1 @@
../../../../../../sops/machines/control
@@ -0,0 +1,22 @@
{
"data": "ENC[AES256_GCM,data:r0i0hSQpZQfTP/2VhM7DkFAits86qDRlowbnwZqs4JrtvNjmReJe/mkF8xPT4eKusIzB/THAY4udgltrrOP0iFRMBPQqeFsd0w==,iv:+oYEln5OVeaFujy22IGKxKEgsExw0pSK3+bYQ82riPI=,tag:/FP5zwVfUszr3oAv6nwyVA==,type:str]",
"sops": {
"age": [
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBWSkthdHhQMHJ5THMxNUdS\nTGRZUVFDMEd6OVBPWFN3bXVzcFNnT0lrR1hrCkZ2bVRvVnJlM3BLc0ZqNUUwQ1VD\nMlJ1bmpkdzVYYURPOUorNkZKWDk5NGMKLS0tIC80Yy81K2owMDh6SFVMSmNOeERq\nVEJWNFRLV1gxOUx5WWJwWE5uNDR1eTgKdEcfmvFy5E81t2SWWEdPCbQy9nxGsMCT\nsZZxIbGwN0W3pf+QoPtIHl2tAixKy+gHPp8UNTGToDuK6ILSf8OoSQ==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1env5y2eey0p3dggs8tydwmtyhtwqylpg7spuququ4vvax6arzp2qjnm4tx"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjTU8vOVRNbjBNOWpBWWpJ\nTUtIY1g4R1VVb2krNWxDOFpobkhuSmVkTFV3ClA0azFHVzkrY0xJdERZV0tLZkhN\nbldVYjJObjBIZm1RelB5VzBiU3dqK2sKLS0tIEdxRFRJMTI3RzI3ampRYVM2YWJI\nSU1pTWtRWVU2RDRHcmZtaGxhWElwd2sKK1zZtqqkdRMdITSKbJ/JilpAAflkgG5G\nnaeHwBIw75pZa6lQDM2aRWN+yFUMQtbJ3i7Zw/Du+emikyN1+hz5cg==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1hlzrpqqgndcthq5m5yj9egfgyet2fzrxwa6ynjzwx2r22uy6m3hqr3rd06"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHBpdi1wMjU2IHFWcTVydyBBdzdqU0M0\nOGNqMHJLZ0V4RHhnclE2SUY0aVFVMjB1azVlUlE5VXBTbGRsTgpFWjNPaDErU0cy\nNFFGR0VIODRTamc0bWxCTFN4MU9CdllaWjhvOUJaeWU4Ci0tLSBlK09xeGpRenk0\nZjlLTkhtR0NZZ1Jna2xHbFpZeUl3aEZ4cndhcHFOQkk0CuX8TF7mW7GowFZ2X75v\ng8zrxp3xtMzPp/C+dbuu02Lfe9f/1jaBOTtJjR6BnV0ucfYjUhtw7OQ3qCUV7a+Z\nG0c=\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1yubikey1qd859y9ehz2ya8j2cftwrtmdeqhuk7r7yc52zp64wpff6068gwrac3q6nsa"
}
],
"lastmodified": "2026-07-28T10:15:43Z",
"mac": "ENC[AES256_GCM,data:uq8xDJBop1yPYMb6gNNWhKxJAiBdMjuYHhA3fvpCauDAakKgEGcSmc7/cnSAtds4NbsPkxi6tn32aJvF8QsKlSIycnqDpxGqpGVgYjvt/lasM50Ukwk6W+YGwTx8bnCmFazimJuaNg92G0VZAGI0uybITR3EEHws/9na69470jc=,iv:hJcVMW1Gaf9Rle5K3LJV/P9Eo7AVBss40rZ5csQml1o=,tag:5ql+BvXOYlgOgbNDl5o/vw==,type:str]",
"version": "3.13.1"
}
}
@@ -0,0 +1 @@
../../../../../../sops/users/berwn
@@ -0,0 +1 @@
../../../../../../sops/groups/admins
@@ -0,0 +1,18 @@
{
"data": "ENC[AES256_GCM,data:TyTbzOZ9AOAaP5e96sleUEuuSq/FPjbfHZ6aeR5iOwvDtoc8ZhLYXEO+AF1p+DKS5hDA,iv:9VgMtKiHt8oYnNY8E9+bodU5ei3riw+Gx2KiOQBhoyk=,tag:zJXsCVITlqqrw72QqE/coQ==,type:str]",
"sops": {
"age": [
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDdWJBOEkwM01BZDlMVS9W\nekFmT0JuMnZHNnBBOVhYK05Ra3VzMW9kK0ZrCkkzRk1JZHdzVlhBeEtTamNHb21x\nU1RkaDZYVEVFV1pyL0lSb1JXeWN6ZDgKLS0tIEVqWDZwZ1RBSS8rVVJLM1pSVkRY\nNEVub1diUytCUHZ1c3NPaWlCUHFYc2MK2lqzBTaw9qIFB6cucfxbMQGhLfSyHYhi\nRJf+pwI0BehAteT87IAbA63Sdx82QNb21mU1AYHPxLdDdrrHWnFxJA==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1hlzrpqqgndcthq5m5yj9egfgyet2fzrxwa6ynjzwx2r22uy6m3hqr3rd06"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHBpdi1wMjU2IHFWcTVydyBBOUhYNXIz\nMU9VU3ZMN0NVT1JSZ2N0Q1hxS2dnZm5iY1Z2WUtsaWlXVks0MApxbEtmWWVmQlZ5\nNzZrLy85M1ZiSlc1Y01OM1VEYy9rZ2FhdU5uNTFPa3U0Ci0tLSBkM0ppSUJtMkVP\nWC9jN0NOam9iSy9xakNhOUN3NjBtVllvUU13Tlo4RUdNCtY/2d3ANDMh2Sx1aFlm\n/b3GKbXeeBeeHuFBAsQmuZXqb5EHGJsB9JpjsR0TJJVflCgYYSjn0L1xg9DG/ANa\nn1k=\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1yubikey1qd859y9ehz2ya8j2cftwrtmdeqhuk7r7yc52zp64wpff6068gwrac3q6nsa"
}
],
"lastmodified": "2026-07-28T10:15:43Z",
"mac": "ENC[AES256_GCM,data:hVEI3iNwXa0O9ZkZWzb0OP8tnsTXWB0znLPg6FIx7Z7D9yPSwCpVu4qdtLwFnB6SEk1tnFykQofCKJ+UQtf6AuziGymwONUQWwUW7pF3zoKtxJ4bJQEKAVChgJqKx7GTp5HXxFkbrp03USJqaHqE9M0leRWIamdBeyXI9rdX9OE=,iv:dn4BqTra2ljHmXfoEoVFwRXXtwiLOs3j5fRNbvndIgA=,tag:zOaRMxl23pJRIvJo3ettEg==,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,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 @@
$y$j9T$tSEgEho.KxGPAaQTjU03W1$M6aXoOlLF6B/QaH5XZl/kAGxuC5VClCUpYlr9gOCJt/
@@ -0,0 +1 @@
../../../../../../sops/groups/admins
@@ -0,0 +1,19 @@
{
"data": "ENC[AES256_GCM,data:WtA3B4lfAERXHgc6hyjaBdFxNMlW26VeAYVb6qosXuFGxwqzbf7XXrEWJ0CSFhb5CSJO0SFJ,iv:ATeO9O1uZ3M40Y8q6Udi5qhmGsjSR7GxIRCpGnls8bg=,tag:NKiLATUjXWVJ2SXNI984Ag==,type:str]",
"sops": {
"age": [
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHBpdi1wMjU2IHFWcTVydyBBOWVrRjh2\nZjgxSHdYeUFkRFVLRjc0WVZPNjFoU0wyMnB3cDU3ODJLeU4rQwpxdXRLOWtuMEhx\nR0NROE5xeHdBL1NhQ3NrSlQ5eUE0OGcrcGt6NGdRdTRvCi0tLSA2aDdydHB2TWpR\nQ09kakg3V09iNHpvdVYxck9DeVgwMFhMZ2g5cUxyWXo0CleMojWF4rB5GlAAM1D1\nKEES65mAa/3GPM0vHhd/YBwwyzyv4HeZHSVao5O10FzMWc226Rc029nhNwcTAlA7\nphU=\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1yubikey1qd859y9ehz2ya8j2cftwrtmdeqhuk7r7yc52zp64wpff6068gwrac3q6nsa"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5aFlkMktXRlIxZE8xTXNL\nRzFzWisrQkxEYXJ4STU5VkxZN2d0QjN2V0hrCnZxalRXZno3bk9TUG9oWFhnVkZM\nT3VLK1lLMWNtaHZ3Qk5odm9rRXFRTVEKLS0tIGd2K3ZNZUxoQXROVng0b1c0UU1r\nUWJxWnpCeWxEK3A2SnNBWjAxczZkeWcKOl+DtJQkkKaS1YYaDzqmasnxiY8iEuAd\n57f0zP+peA9Q4U3TZg3UGG94V9kuweNMTWPfj1GZvwBvZP9sZYuFYw==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1hlzrpqqgndcthq5m5yj9egfgyet2fzrxwa6ynjzwx2r22uy6m3hqr3rd06"
}
],
"lastmodified": "2026-07-28T09:03:14Z",
"mac": "ENC[AES256_GCM,data:8lemWJvcdPy0sg0K+Zn9A7xWd5X4q3XGTb+R5DiJMSo8BWxpP+QGNqxEuni8sNxP2d0bkrsEI3vG89lx37x3iot667Gds0qAaHq7h8l+3iwAvGMXAEzwIms9GLdIcJiUWYlFiAcicfUINWtPQ+adxACFzTOBIBEGqD6iQ5B+KsM=,iv:OuBeq83CopTNeJJM/UHUrI7RKIGVJ3tM+DcckuFxLBc=,tag:JAXpfqwSDRr4e58wiJeMNQ==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.13.1"
}
}
@@ -0,0 +1 @@
../../../../../../sops/users/berwn
@@ -0,0 +1 @@
210641414b2391ee4659814fb67a1869d8f1920716b53f7b5226be36887ec7ce
@@ -0,0 +1 @@
ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAICg/CnZm/jIFijKdkXTrJS2Fman7vhPtUcpTFVhLAoyLAAAAIP+kt1X3TTJhWQYakYQzrr2lI8tanZ74uEahnD4meuCaAAAAAAAAAAAAAAACAAAACGd3LWNueC0xAAAAIQAAAB1ndy1jbngtMS5jbngtbmV0d29yay5pbnRlcm5hbAAAAAAAAAAA//////////8AAAAAAAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIJuhbAaSx5fuaqTYZW0zajVY9HOHxTdfoDfChqdgb3GhAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEAhM9fTE9iCnV1wGKunJcRqOmNWbu0iZjPqXNcPp8rpwFJm/8CW0qC/TV/H5im7sX68K+ZOOaOTuOqu+D6U+lIA /tmp/vars-gc02c4xe/in/openssh/ssh.id_ed25519.pub
@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIP+kt1X3TTJhWQYakYQzrr2lI8tanZ74uEahnD4meuCa
@@ -0,0 +1 @@
../../../../../../sops/groups/admins
@@ -0,0 +1 @@
../../../../../../sops/machines/gw-cnx-1
@@ -0,0 +1,23 @@
{
"data": "ENC[AES256_GCM,data:C+f7BWR2JNfMIqrfCrfkBC6fouAMLFXAEvoltExJU9FkiE8JN+AxtMVEH38PTui9NzlPnmLFc95MGHstj8Kb3rViwuoY2lCHCRM3uHPTau0Cxp8Ekq5ILMebjKLVeeyisQXKelMXg0zYBX+HBUHYJi+SYqQM7Pw3iI1WRStW43PGPjcSExAhgQts8V5xaTjseCf7EqgtjfOmya/uQXe6asL+GVjH8icpc0A9+AyrZci4Vr3lsJ1qgy0ej4a17glfuXnjaXjnlw4vDxDAz0c7xjx88xRxagqtN19Yi0jLRIuLU7Brka9wDMbu6yj6HzaYRaha+4HLDTbg+0Gd+iyH6z0tiXAj7p35VtBRefL/0CW/kIzwyl7GWAZpaX9xPUzZre20TYnPpxToP6n/6SJNccJhNd5GiGYtVzHXj9ni1kulX5N7A9QEikZGyujxPi+LFQVIKnDL1cxS8EeDqiRuAEi5LJa7xYKBXBIFdFO/9DMDesMQcvPP3HjpdFtaaybvpzai,iv:0dUn8InsV5N893LRfJNmYieSFSzPv1ceqXZzwkAmoIw=,tag:0eVSEfOHzD6AcuCDLY+ggw==,type:str]",
"sops": {
"age": [
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBld0NYcG9weENnN1JadG5G\nSFVPQzE1TG1POFc0RFZnSk5PbWZJNWpFY2tjCnBxdWM1R0kvOXUzUCtqUjMxcjMr\nQ2Rsd2d1QWd2bzZxaUN4VTAwdHZPSHcKLS0tIFhWNmZYSzJhR255NFRFa1VBcWtK\nVXF1YTVRVVVMLzVMQ0l2RnJLVDVWVU0KMGbb1Nnw5eEDFpGGi0Y3Dr15J6lyLeKY\n4hQJN4gEYcBokEbAV3wTYbkDQqDuQ1dKfS+zgybA1pomKcubkDCWlQ==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1g8a6fqhqg8q8qu4jyuhxc7hf00t5qvra55chyfm0qk45wlmvc9qs2cft8t"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHBpdi1wMjU2IHFWcTVydyBBK2E4TWdr\nTCttMjFmN1NMRWQ4YmpUZGNWa3NWTkJWUnRJSEthcnJncEhSRgpFTGx5L1VDSVhY\nWE1Udmt5NUkyOHdTd0JqUmlxdGt6UFEyK3RHakNHVklBCi0tLSBVbVFQSWMxdGlj\nTmdSN3Q3NVZaOVRWQk03K1o5WmphMGFFNHUwOGdCcUV3CmqsQ6E3U20tvO0ciXqb\nUIdmcfuy7u4SGQY4qskEefYQrn1TaD/zVM9yL+Uw6GvxxSnKeytgkwxnCTTjh10C\nVB0=\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1yubikey1qd859y9ehz2ya8j2cftwrtmdeqhuk7r7yc52zp64wpff6068gwrac3q6nsa"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSOWVZT0hRQWJiSG9OcFg3\ndm9rN0ZyZm45MGE1bDJ0N3dFWi9BV0R2WGhNClY1MmZsSWxESFljM3FQTFlkSlha\nenF2QkRmU3VSTWNqOG5qeEU4TXA5U3MKLS0tIENlNXpvRHJIQk83Q3ZIV24rdGU0\nWFB5NllWTU1ldUtNWmNGN21XMXBYZGsKhlAkMshZJjY48PBgAd8W7JAISnC/BGjL\n4sZez/+Qnz06y/ZyvatV3yIDmUTvp2jSqFzvwvDBeHuuAowpa6Yg4w==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1hlzrpqqgndcthq5m5yj9egfgyet2fzrxwa6ynjzwx2r22uy6m3hqr3rd06"
}
],
"lastmodified": "2026-07-28T09:03:14Z",
"mac": "ENC[AES256_GCM,data:07tbbE0nqs0P0svDnzg5f6bPklOjTVFLGIsDc+ozllTU3VLYWHwvvkdgoJbPDaFH5mXykK6JyMZiI5jorx8iG/0+3s1bVMR0lCom7cXOqzai/h5meFZ8ymkON0cVXqTfwy7QTAlxg7ysjAqHLvJlHorTNFVUjxZelfDapa4g/ew=,iv:0SdRCRUcD+QxSZdOrzJ6Ri3eK1rvysFWDOrxSNc/5B8=,tag:c6R5gzhvlaBOl+J8zNjiOw==,type:str]",
"unencrypted_suffix": "_unencrypted",
"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,22 @@
{
"data": "ENC[AES256_GCM,data:KEymUXwLPa5F2Y2W3LHGKnzGwmekRR/Wjs0aIg==,iv:g8LUbeDkjavPuo9zeEgJaWt9WOMTFXjAoZH6freFzHA=,tag:JiJRR5lOMTiBNxnnByv1dw==,type:str]",
"sops": {
"age": [
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5RHZqdkUvK0MyWkg3L2RK\nbjE2ZGdsSGRrYTRtRW1uQlRsVExCS2FkdEd3CkpkTGl6TTZndGRPRDh0VmRuTzk3\nZmdWUEJqSTdIZUEwQzJqM3QwOW1VS0EKLS0tIFU0QWx0d3h5c2F1U21qUzRMMExS\nbTFTc2lEWDVQSHZFYURxaEcyQTloQTAK6yIaBPEVqj3nlvsIl3srGWm9o6a9aE91\nZelv3QXjKUE1yQlrStZPKFNo4cZN/bt/81IEZIKcASE/b2DOJWW+ag==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1g8a6fqhqg8q8qu4jyuhxc7hf00t5qvra55chyfm0qk45wlmvc9qs2cft8t"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBIOTFQTWxlWkFXTXUyczIw\nV1U0RXBya3FiUXdrQzRLcWNCVG1lM1hobjFvCmZLcS9VTmJORDN2U0JjUFJnTGNx\nWW5BQzNRUlU2UTlTNVFsaGFwWldBYkEKLS0tIDhkTEk4dlllYms4WVM5dmhlcTZW\neE5tUzl1RVRjOTV0c0tRSmYvWlVyYlkKRCoMiXvMWSeZpgbI8xZ3FoioZjzCPHhW\nGQhcl3oTicZfyhSsDn51Y4GGHQJxCoRJFVs1jw2/R/V+q3/rUEwjXw==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1hlzrpqqgndcthq5m5yj9egfgyet2fzrxwa6ynjzwx2r22uy6m3hqr3rd06"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHBpdi1wMjU2IHFWcTVydyBBdjRpYm9L\nd25aTjJKdkJxbnErVU5Wb2dBbjdkaTZ0YmcyOUR1OEhIR1REYQo1WENYcjQrSXJJ\nM0c2Zldpd1ByYUxmNE9vOFRNclZZQk1uT0Y1SWlyQTB3Ci0tLSBSOXd4MDRYNHlI\nZUdGajVmTjU1aXJLNXpLS0docS9GZUZlSng0aVpaeW1rCkLpdGashC/Dmvjp2nOb\n3oH8GROhquOXPBJemxH4Xr3sZE4NzO5UdAf5ErBMLgmLCKEOM3A+DXOffGyHVPBf\ndZY=\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1yubikey1qd859y9ehz2ya8j2cftwrtmdeqhuk7r7yc52zp64wpff6068gwrac3q6nsa"
}
],
"lastmodified": "2026-07-28T09:34:28Z",
"mac": "ENC[AES256_GCM,data:/cKylOQQRWKIoKwOlL/SBe/cPWyIFixUDfeh/KOIHOSdJFa6dcV78IHZgiy+aNF4ogjX+y5cy1TXsUFiagFiBRpsO9IOLj6NhSWp8ox2q0vwj0Vjk5RF+SmASLSREuGe7MvsNwwesEkbvOpRE2PAJer518EGk9EvFwjUWE5Y7z8=,iv:GuHbdaf9hq0XBhniVsYZcfo9WstOQJM1UGbd9Gcsmgk=,tag:bZQiL2NJA6R254KAcajfgQ==,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,22 @@
{
"data": "ENC[AES256_GCM,data:mmdooyUC9Kh7y64dTTBn1wtT,iv:syGCZO3EzGXKiBe/gudF93dc6oJz0wWxqpbd4UX2hcU=,tag:L1EzmPnx5B/vGscoKYYjeQ==,type:str]",
"sops": {
"age": [
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBaaXV3amcxV3M2dEFOLzhO\nV2prS0ZsbC9OYmg5TU5CbThDd3p5TGNGL0JVCnNMMkI5NU02a0RNZDdqcVUwRXVQ\nQURQOVB2MTg5QzJjeVpoZmJnUk93WncKLS0tIGJHOHdqVmRBQTJDMjdCWFFOeVkw\nekdEb2x5a0NseityMFVWeWFLckZsK2sKstOfMyYSgw2+DELgzHZl6GY5S0Ph45Jv\nTLQlyRPUd1IqvLVtmQOieUB+9e5k9TUTfLdvxUxIqSNKxcxVnJ7nsw==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1g8a6fqhqg8q8qu4jyuhxc7hf00t5qvra55chyfm0qk45wlmvc9qs2cft8t"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSByL0x4bHJNNVZPTEttK3A0\nNEc4Q2RQU09nWkFVQmxCL0NPem04ODNOY2l3ClJ0UTE4VEd0ZWNWSjdrbVhqcEF6\nbUYzQUFIeFJvT2QxMU8xNDlROE5WQ1EKLS0tIFlJSUR4NTBUSUFseUVNUk8wTkVi\nS29YWVJTM1o4VG01Y3pYSFBUb3dkQk0KM1VGs6ZIMO7/uj8aoLScsRevwhDBgWiW\nCLfEllrjicWomfjHBUBFeRKqKhvI9eWtBi64gNL+d+DUZ4NMx3fDyg==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1hlzrpqqgndcthq5m5yj9egfgyet2fzrxwa6ynjzwx2r22uy6m3hqr3rd06"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHBpdi1wMjU2IHFWcTVydyBBeFM0UDJU\nTytwOE82QUV5RjNaYkVaWDJhTDQxTWRoUzJ6VjN5R0tWa1phdgpUVCtsWW1paE4v\nMmdSbTZvbURaZUcwb3U3OXRMdXZLRHFXS1NPZmlZY3pJCi0tLSBYMysydUFORmJi\nbHVrU1BtaFBRNCtLYjRRVndjZGR4R1hYdHZicnNDQlg0CkTyVvREnp/73rMF7XOq\nsvP1jr09gIy5o3URUcv+IanFLkiBCZciyMoTnOJKJOwWlf+1SXBAKO86hUrgOvW2\nYOM=\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1yubikey1qd859y9ehz2ya8j2cftwrtmdeqhuk7r7yc52zp64wpff6068gwrac3q6nsa"
}
],
"lastmodified": "2026-07-28T09:34:28Z",
"mac": "ENC[AES256_GCM,data:EIGsqX6qnOocm0pxEZAsZf6tprPst88csaAIX0PnbgJSkO7DlYT6dC9tXeTtGSujkolAhLG9fB5FGfT1hCFMzHzgZCUbPaT9tei8X74XwC7nABqc5mSjQDS4vUGpoToH33yu3UStFQGVstOEG+FSjQpiY2fFKVJ+8u+Y2EcghDA=,iv:E56Tu6Fihtaj+NlZw8EQ3sam7SxRjPPZ/KgFjzBu4DE=,tag:hR5o2f/Jrigtp+Zjm695KA==,type:str]",
"version": "3.13.1"
}
}
@@ -0,0 +1 @@
../../../../../../sops/users/berwn
@@ -0,0 +1 @@
8804124153
@@ -0,0 +1 @@
../../../../../../sops/groups/admins
@@ -0,0 +1 @@
../../../../../../sops/machines/gw-cnx-1
@@ -0,0 +1,23 @@
{
"data": "ENC[AES256_GCM,data:HxPAe9vXu18iI/mvwCGgQFgcZjOdzUwNlwel/89e4eFD3RUQX3RaD+Vb3HylYQ/Y/gahdWM9NYBoOf/fOMTzXFN+D/VkQHrKXw==,iv:u6NuNha/Z60x5ig11O/S1mAO+P74u7vFvjhxR17KRE4=,tag:4ae0Si08BiHFMDNKRMLRTg==,type:str]",
"sops": {
"age": [
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHBpdi1wMjU2IHFWcTVydyBBL0QzY2lq\nU3NqRGY0dVpRZWtxRWhmNmZvd0FaSElBekl6UmdXVkkyeXp6RwpSamdORnluMlhK\nOEJ2OGNCZUh6Z1VjTHJoMytoOC9vcGFBMmtIVHlPRSt3Ci0tLSBxR1BCNy95NlZy\nek55bSs5OXRrSkdCWmpqVzFWMmtVTVd2a1NqNWh4MlhRCktog39bvtPgDWcSNLGr\nL36h37Q0h4/dYO1enkre0v7UvgVDjC0FmP5jQEUl1YEtCIvOSsSTPHYvzuQ5JUJB\nd5I=\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1yubikey1qd859y9ehz2ya8j2cftwrtmdeqhuk7r7yc52zp64wpff6068gwrac3q6nsa"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3L1RPcHlZbXQxMEh3WEFP\nRGhEYWhwRVhQTk5XM1N5NXZTMjh6cTErYWo4CkliZWcvZzVXNEhlaTgvTFpVc1M3\nNGdETDEyS1JlZnZ5eStQOHdhYTh0aE0KLS0tIG9mQ2hQaWR6dTUyRmNob0p3bE9R\nYmZwZGZ6TDZGdEtsTUVUMHdQOVMxbE0Ko48BMimAX8Zu5o0lUeJcpm/1G0Ed/P07\nOVVc5hHIQJCmgUOVvYQvA1uHdzfFOl4jChjwk7bejDXGoOrNwht57Q==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1g8a6fqhqg8q8qu4jyuhxc7hf00t5qvra55chyfm0qk45wlmvc9qs2cft8t"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4WEFHNmg1SmhqOVkzbkpl\nUnpZZkNHMTcrMklzSGo3RkhzSG9sK3Njb25VCjgvb2NzeDJNKytXQ2RGRHRsRmM4\nZUw5RlRsblRaZ3Y5d052QnlGT3cxbzQKLS0tIFlQZ1FHSlUwM1FpOVNxK3dIK0h0\nVTFTQzhCRHcyc0NzNTFlbGF6TzN1MVUKTCUyyPE0YxTYEwORmK6c/wWuCvia3oJL\nWOTR149gsMpPfgaUqU5ncPyqgkR6fEF1T+iqaVdwOXuBPZJ8qlYCtQ==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1hlzrpqqgndcthq5m5yj9egfgyet2fzrxwa6ynjzwx2r22uy6m3hqr3rd06"
}
],
"lastmodified": "2026-07-28T09:03:15Z",
"mac": "ENC[AES256_GCM,data:qPfMhfrfjpvZGqyrhDuudG6+GmGWThZD3sTAal/9E0x98FPg3GrJbD1UCmr2nWXltkrPDpBRZPfRjUzSKUEtH7Sy2mZ8m4mZgQTeS1wLUe6/oaid/zz9vRNbNXZS+UregW+6Xd1qWwfuZ/rcciNyR/ijRMj4Mm434ZvsuMirzgw=,iv:wT2TJ/ZwgvpmjHEkDOj5hrBO30IgbPcQ/yle06V4/8E=,tag:BkH1Blt9Mg73PjK3zpchRQ==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.13.1"
}
}
@@ -0,0 +1 @@
../../../../../../sops/users/berwn
@@ -0,0 +1 @@
../../../../../../sops/groups/admins
@@ -0,0 +1,19 @@
{
"data": "ENC[AES256_GCM,data:Yr5kdmNaa42lDVwyTx/C7fKgluBKMBijLTO20y7mH8G+xsn1eQd+/fNl+FJks3QYiiE=,iv:FyZ7lLikZwxBo6V7gbGg0HX6/ljtowNX8M8H7K1/9nI=,tag:6N61vAWpayMPDojAfZvmOg==,type:str]",
"sops": {
"age": [
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHBpdi1wMjU2IHFWcTVydyBBbm1RcUI3\nWmRFSEdwSmRCamNKM2JNczN2Um9mUFhxMEk1dW95anpTZXNQNApETGtVUmFWTGZs\nNHpMTU5XUXhUN2lSQ0dIYXpMemFmVFhTLytyL1R2UWM0Ci0tLSBqWlkzMVJ2ODZp\nRTZBbjFxWUhRQkdNNktrRVpER0NEN3ZIY1RBVVllckl3CrQGPl5f4kv5Mt9omQ3A\niu9izVDlqt6H+knUpPBjEvCz3lJ6wE43V1513X7pabYese0WsBQY/PK8qBJpug2M\nro0=\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1yubikey1qd859y9ehz2ya8j2cftwrtmdeqhuk7r7yc52zp64wpff6068gwrac3q6nsa"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBoWkxHaXFkOTBTVG9GTDYy\nV3NXNGttN2ovazMvUzNsU2hiaEcvUFNQZG1ZCmFiZVV5M09LNkc4QW5JOGQyQ3Fj\nejdNTjVrU2tZb2FpaEFJYklzZXhEWDAKLS0tIHZITnBuU093Wks1YTBUNnZkR1NF\ncFJWSzFqQW1CVWJCb3o5ZFZRR3RLZjgKsxstaYtfL4WA/WlkOmSq/5YT8Zu83iMG\nTQCNHCvHvwprtmRfeUe0x1wUXqynVPRRcxbQCi5KiHh5zZR2d73Dsg==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1hlzrpqqgndcthq5m5yj9egfgyet2fzrxwa6ynjzwx2r22uy6m3hqr3rd06"
}
],
"lastmodified": "2026-07-28T09:03:15Z",
"mac": "ENC[AES256_GCM,data:A9lKOiRMDamC3QXSpRhS1x+xefOkk7ONCGG338dQ4J6tFRBKS8sJvMA/DlHxucCAKaWA6y8fcEf6k1Q0z+469OolrIAphG8TKYEKvOd7X5isvz/u6zXpwafV19mrHDcPB2WFGHM1g0A7wfSVzHnvwJ2vlcSfh/PYLiir1MbD+G4=,iv:C84tv9oUZ5uxg1ivfkCXgCamtl16UhPEVJ3OFIrAZQ0=,tag:cRT/RGqpvyzt3GHUYM9kuQ==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.13.1"
}
}
@@ -0,0 +1 @@
../../../../../../sops/users/berwn
@@ -0,0 +1 @@
26.05
@@ -0,0 +1 @@
../../../../../../sops/groups/admins
@@ -0,0 +1 @@
../../../../../../sops/machines/gw-cnx-1
@@ -0,0 +1,23 @@
{
"data": "ENC[AES256_GCM,data:mXJtT9SvhkiJq5R1kONHBGCuAXaJb8fDrkDZxNw0uu7DFaYAzuhTd6OvjHj9dljgnZDQjAVBPDj3ykZNVvAv,iv:bGGww3Dx94EeTFRVXkT8mK/a+Ai961aM0Po0QxEUgr8=,tag:0loC/c+GoT6my1ESs+3Bnw==,type:str]",
"sops": {
"age": [
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHBpdi1wMjU2IHFWcTVydyBBdEtaRHh6\nN0RaNVpudDBYYk9iRG9QNGt0NHNuNlowNk5LbHZLSm9FOXByMgpiMHlRdGRnTjI1\nbUt4YWxDdGtjUnd5L2R2dnlCN3o2bUtzOTc2WWh1WVlRCi0tLSBWMjIyS3VhMkRm\nT0lkdldwVC93QzdMc0taZWRqZmJIaFFFSkx1Nnh0K0U0CidB2Un7vuPdC4+sX6q6\n+rCmDpFbPqWy+AENgrOdXlRnAcXH7dBWmgLGt85l6XtxAjIUAS9euWlLRkO0OpUY\nc7c=\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1yubikey1qd859y9ehz2ya8j2cftwrtmdeqhuk7r7yc52zp64wpff6068gwrac3q6nsa"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjalVSdlRvRFkvZkcwekR4\nOXF6MkczUWNSSTVFcFB6OWVBQ3MzemNqaFcwCmVKRUdxbldUY0JRQW0yR1MxeEJs\nejVRVmVyYUN4QUlJWjc4ZDRrTmZBK1kKLS0tIHU5RldrckFGbzNOWGZRaFB4cURs\nQUtuRC83MERUeTB6RFRRQVE0SnNCRUUKMh4JmS5Tv3Yluw2HtFSdnjp0rJHb+Pm9\njgWcRxX1a7/qxGaYDEKFJ30RW5O2xebiyE3f77YZUmfmfw0/qRUvmA==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1hlzrpqqgndcthq5m5yj9egfgyet2fzrxwa6ynjzwx2r22uy6m3hqr3rd06"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1YkllWEhjSStBSnU5eGcz\nQU9XRWVQOTg1Q29hK0pGd3RwNUhkUXFDd0RJCnE2TkduNkIwaDdYdjQwVVRpQ3lz\nWDVMaW9JZ085YTFtQjNhN3REK0t2MFEKLS0tIEVZYU5iWk9IV0JYYnhPK2UrcVps\nTk9qR0pqYmtXSnZmVDMvR1lVQXc2YWsKUBXa610I4RKHAUXrr3GOyHlnzOtxPxdW\n4+UgXK5ByrwqV5Ul+JKCmU2WpfcRYJlGVGGNcA1r37Df9QFRezgw0g==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1g8a6fqhqg8q8qu4jyuhxc7hf00t5qvra55chyfm0qk45wlmvc9qs2cft8t"
}
],
"lastmodified": "2026-07-28T09:03:17Z",
"mac": "ENC[AES256_GCM,data:dEk6VblChVD9XtqnMWOCuzXtpF1kMJ4KNO8E/xxo6SxvIdR0FwjaPrL3B5mRKJRLX9IdehqfeDVfZykCS4oSfog6AGKCl+FtDu7lXZwJfV3JpM59VuaI48H0u1lMupw0jR6nkd6wAvwqXAGM/KKMD9cKs83WbSbp8wpow/Ejk38=,iv:QqTPg0n53MeMF5oI39jf4KKg5AkZuPeEqNrFpWhXdkI=,tag:hjX+wFgSRAbSWvMnxLXrGQ==,type:str]",
"unencrypted_suffix": "_unencrypted",
"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,23 @@
{
"data": "ENC[AES256_GCM,data:UH44nJ+3zH5HyXGDvH3SLBL2kUPcAZ5eLjXZ9lFQvvvYTMs/eTq2VsBmaxOyBYs97zzGbk/EFY5MpGPrYL4kYyce+8nYRAlzwazXmIIilZVjpGkAKyLCzWvQUUhkDOUY,iv:e7sH5RfpRUThCl5QlZzoJH5+V0uikARNBdNJ7PCc3m4=,tag:vEQHtyZ8rhifXWO4O73xLg==,type:str]",
"sops": {
"age": [
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHBpdi1wMjU2IHFWcTVydyBBclR5YnZT\nQUZTMnV6Y2hJZW40V1hxMUhkclVtK0E5TVVvOEVjeFB1aHF2ZwpSYmRrME5hb0p4\nSG1YTDJ5N1VLVzYzYmJPNVBvSTJtV29KU0FVRDFXTUk4Ci0tLSBUdnJWa1BtUjBJ\naGpuc2hNZGJydk9rc2ZEQWFnUkpaQ3ZEWWNVUXJIUmtZCjn49OtidX03qZT9Goe8\nWUobnO6o6zbz0Sn4G4bXbLcxunapI9IlCLRtAlPfD3D6DZWYqR+C8Jua2GnlUX/q\nHn0=\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1yubikey1qd859y9ehz2ya8j2cftwrtmdeqhuk7r7yc52zp64wpff6068gwrac3q6nsa"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBibXB0OVVmZldIS0JITE9k\nNCtrL2llY0cyOXZCKzREWFU3azZydFNxS0hnCjFwa1ZDOGV0bGpMUWtraG9wUFM4\nUGJXVmpTVUJKRDgzZnJkZld3YlppMXMKLS0tIE9Xei9BUE55Z3hzcTNZa3M5TmNi\ncm5lNE82RkUrYytOTzVXMGVTUVUwdTAKievevNumIned89qb2ox9dPs69lxpLwDx\n1orikXSjVGr/gEZcsyx7B2+m3OoiZFwM94neJz9ckmprNmTHuW3K0Q==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1hlzrpqqgndcthq5m5yj9egfgyet2fzrxwa6ynjzwx2r22uy6m3hqr3rd06"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzRlFPRXFrQll6cUhGekRq\nbWhidGt2Ykl4T1NrYWVZcVZvMUhPS1hmYXhRCk5VcER5QWhqRFMvcVBKazhpMkF5\nTS9UU21nZlRVYVhJeUI1dFZFRk5mMGsKLS0tIHhidU5Id2JrOHdpZ1V4NkU3T29x\nb1FMM3NudUxPVlYrYVYvU2p2emtITU0KDcf+hNrvRkRmtEZWQx2MNzDFbwQQS/PS\nBmTs1phNbmZ2H3ZwpgTFWgQVjbfH6CzBt5hXS/K4RRr69C3tnHlwhA==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1g8a6fqhqg8q8qu4jyuhxc7hf00t5qvra55chyfm0qk45wlmvc9qs2cft8t"
}
],
"lastmodified": "2026-07-28T09:03:18Z",
"mac": "ENC[AES256_GCM,data:z/wf3LsHRJ9ayCiLIkBnNDJaQ+GX0XokWQcBBFA+F8QZ832THVvEmJ9yWzmSXK7GB/LXOwVTyMX30ezSBOutJcySaLPJ5ebFnhyyKW2khU8qdXFVvbeb55K14yZcm+dFyiUkpqDtj5tk20GZD8aunKjmGy+J8UtURynr5pYIR4c=,iv:4+gquB+HkbVeJgSQCXpb2tAACzzAkJfOFKD7hexU5Lc=,tag:/2E7DCrBhoLFYbMF1EcUBg==,type:str]",
"unencrypted_suffix": "_unencrypted",
"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,22 @@
{
"data": "ENC[AES256_GCM,data:o8lBgZVy0lcXhXgLzpklYbB0zkFuiAM6eYg2Kx1j4mVcslsVtmNp+fXjh/CR0djDVjs58j0YxSW2ubM8PiSwZMxsMxWTFBKZwg==,iv:5YBcsmfDZ1+D5Q87p1xlQyOsF8x6BFYfJi4nkATiKv0=,tag:YO21I3RQW57iN+aFm5PpXA==,type:str]",
"sops": {
"age": [
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqYmhLM2xJUENKRlpRVHVu\nd3pMeStxYWlkVTlYeEFlQk52K3hqc0VRZVJNClVIbnkyY1RnSU5TOFVrMVVrekRX\nWE1IaVJ6eGxWS282NEVvQkYwbHB2ZEUKLS0tIEp3eldaNkRQTzNrOThRc0FVWlZS\nNmplR2tUeXNVRHVNMXRCeHNEdHpvR3cKR7bS4YFvEUM8hgcPR/N3wz0WSZaZbOfK\nsM/naGpy/lKdxufOmrHU5WaesBIMK8s6e07FWL3vgGDoycZrnKbZUg==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1g8a6fqhqg8q8qu4jyuhxc7hf00t5qvra55chyfm0qk45wlmvc9qs2cft8t"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKUEtEWmt6cjBEekgydm5M\nMzg0WFdrbjFQR1JOcmtxbkZMMlNLdVRKZmh3CkNVK0V2dndPUHlMZE53b1k4b3p2\nUVp0OWJrU0hWVW9SbUh2aEZ2aHBidDgKLS0tIGFjNGFuQWpDdmRKNDg0ZElLeXB3\nZFZZTlNLSE1VVEhRY2dSVFhSS28zZ2sKYwvkbwSy56N4sedQAkOZOCHkYjfqlOMv\n2HOCDPxS6gtWqFaJ4LLDyEcWUbJMlx/hvbrI4AI/fzqMsXlZ+MNwRw==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1hlzrpqqgndcthq5m5yj9egfgyet2fzrxwa6ynjzwx2r22uy6m3hqr3rd06"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHBpdi1wMjU2IHFWcTVydyBBcGFOamdJ\na1R6dHNVYmY4cE5YSTVBM1NCckhnTy9DaWNEWXFyK0dwRWgwMApkdlVpbCtIWlRr\nUTlUUzZCWnplL25KaU92M2pPWlNUVlNmM003SitGajhzCi0tLSBvRms1MG1BdEw3\ncWpYVlorUk5FemtTSVNkSnNNN1U2YTBiczhONUtxdnMwCllrHDvB9cUxh5V+eDHE\n5hCWb/xr2xvQj5e6jQvlq2KQm9ZLtIdKlLckeGHNowlMHs/rmsKSF0LdFhTU/5BM\nPuw=\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1yubikey1qd859y9ehz2ya8j2cftwrtmdeqhuk7r7yc52zp64wpff6068gwrac3q6nsa"
}
],
"lastmodified": "2026-07-28T10:15:44Z",
"mac": "ENC[AES256_GCM,data:wn1vo4qYXs4BknTsRVhO13k3Pn+IMOWJ5dZsbKXGOME1bOHFIJo6KQkBAMFm9z6YboHvW4gFJgKmzeqOyPtQYLPDmwTE7vYwdlPwERwFHkEp21n75Qm6gMZU7IDRcL+ndVmU65vflscAtBM7EkjZ3N5C0S5ShOCgaoisexKUFEE=,iv:D2Nugo4IuaZxqs+mHzp0opxgsm1VlOSUvw79OXhnMvI=,tag:sIQs5ORYx3hxPLnMIjqkOw==,type:str]",
"version": "3.13.1"
}
}
@@ -0,0 +1 @@
../../../../../../sops/users/berwn
@@ -0,0 +1 @@
../../../../../../sops/groups/admins
@@ -0,0 +1,18 @@
{
"data": "ENC[AES256_GCM,data:6ViETqNUeQL7XaLEQfJDove2uEJwct+KVRMos25qP5wl5XjW/23AUgVmXoBF6DbA,iv:lhZNPf5IXvcaDrYapXk0SqPx7oFv6iS13lAog/l/AEs=,tag:TkmUSNegrpuPXLIru6js6g==,type:str]",
"sops": {
"age": [
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzd290UTgwUnJ4TkpOcGFD\nTE9jZzl2ZkdObndtaEdLZWlLQjJGMWJ1Z1VRClh4UHp4Ny9zN0t0T3g1bXdOcDJI\nbXJYQlFySElrYkRXVlpxZlRoTFJLNEkKLS0tIGNxZlFiUkwzN3o5enlMeklwbTF0\nb1BCS04vK0xsK1FMeG80SjRVTnBLRUUKSL4y1C+x3RKNU/6wyoHtX8dWjOUZf+d4\nZt2xjpX3jY04HtHS6xsWljhz7USMb9m9+n67IdKh9CLeh892iII3ww==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1hlzrpqqgndcthq5m5yj9egfgyet2fzrxwa6ynjzwx2r22uy6m3hqr3rd06"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHBpdi1wMjU2IHFWcTVydyBBcmVyejhK\nbURGbkxpQTZDMXNjYzUvbHI5OVJSRi9wYVZrdkRGSVBONTRWNgpxYmh0Y2xsVGt3\nRlR5c0FRRzhUb2l4bmV2ZTl0VGlxYkI0NGx4WlhiSzB3Ci0tLSBoQUxkMmcyQmJ5\nMGkvZXhnbTdRSUE2NGFFYzkzSktqNVZnUU0wRklRS3Y0ChYI6yBNFPgVplfs1W0Y\n+NhEvvebTS/bz/Lw1+HOFBs9u00wIHO0iEgOVQ3TjV0DtT4qWPcbVTc8jzA4Lslj\nf0E=\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1yubikey1qd859y9ehz2ya8j2cftwrtmdeqhuk7r7yc52zp64wpff6068gwrac3q6nsa"
}
],
"lastmodified": "2026-07-28T10:15:44Z",
"mac": "ENC[AES256_GCM,data:T0R1BeWWbsdV9rxdEQfyRdU06z6iUoSeVXqj1TfbB2Ru2Hmi94h1JuyWbNTqjW3YK/ZCj43w1FeDlhLrkzH2U3pFOCUTl1oxmvKK8CYl7rU4ar/zKzH9lEtH94jYN5xnFM+gFd0EYsWxexOaSKyNA66dPRgzU9osV16eDRK02ek=,iv:SyCC54qtwLEUEc6ednIB3HXYOV9M4AooQcaoo35atgY=,tag:uh/TU8RpVVHCGWCX/cAg0A==,type:str]",
"version": "3.13.1"
}
}

Some files were not shown because too many files have changed in this diff Show More