dns: Technitium-master sync to NetBird managed zone + cleanup
- scripts/dns-sync.py: reconcile Technitium named A-records -> NetBird managed zone via API (cron */10 on dns LXC 107). Single authoring source; kills the manual drift behind the auth/sso/nfs-export saga. - secrets/netbird-pat.yaml: sops-encrypted NetBird API PAT for the sync. - dns.md / 107-dns.md: document the sync model + why forward-to-Technitium was abandoned (NetBird self-IP / nameserver-group quirks). - Cleanup: removed inert Mac secondary; reverted primary AXFR; home-lab-dns -> [192.168.8.2] (1/1 Available); deleted vestigial Proxmox Names group. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -62,4 +62,13 @@ creation_rules:
|
|||||||
age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6,
|
age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6,
|
||||||
age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0,
|
age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0,
|
||||||
age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6
|
age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6
|
||||||
|
|
||||||
|
- path_regex: ^secrets/netbird-pat\.yaml$
|
||||||
|
# NetBird API Personal Access Token. Consumed by the dns-sync job on the
|
||||||
|
# `dns` LXC (107) to reconcile Technitium -> NetBird managed DNS zone.
|
||||||
|
# (When 107 is enrolled, add its age_pubkey here and updatekeys.)
|
||||||
|
age: >-
|
||||||
|
age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6,
|
||||||
|
age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0,
|
||||||
|
age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6
|
||||||
# webhook noop 2026-05-20T18:16:57+02:00
|
# webhook noop 2026-05-20T18:16:57+02:00
|
||||||
|
|||||||
@@ -24,9 +24,11 @@ Authoritative split-horizon DNS for `hubris.network` on the LAN/mesh, plus recur
|
|||||||
- API: `http://192.168.8.2:5380/api/...` (token via `/api/user/login`). Zone was built via the API.
|
- API: `http://192.168.8.2:5380/api/...` (token via `/api/user/login`). Zone was built via the API.
|
||||||
|
|
||||||
## Who points here
|
## Who points here
|
||||||
- **NetBird mesh peers:** nameserver group `home-lab-dns` (`hubris.network` → this server). *Note: the [NetBird managed DNS zone](124-authentik.md) still answers most app names directly, bypassing this — see the single-source-of-truth decision.*
|
- **NetBird mesh peers:** resolve via the **NetBird managed DNS zone**, which is kept in sync *from* this Technitium (see dns-sync below). The `home-lab-dns` nameserver group (`→ 192.168.8.2`) is a thin fallback forwarder.
|
||||||
- **Plain LAN clients:** router DHCP should hand out `192.168.8.2` (update the router — the old `.180`/dnsmasq is gone).
|
- **Plain LAN clients:** router DHCP should hand out `192.168.8.2` (the old `.180`/dnsmasq is gone).
|
||||||
- **Secondary:** Mac Mini (planned, Phase 3) via AXFR zone transfer.
|
|
||||||
|
## dns-sync (Technitium = authoring source)
|
||||||
|
`/opt/dns-sync/sync.py` (cron `*/10`, logs `/var/log/dns-sync.log`) reconciles this zone's named A-records → the NetBird managed DNS zone via the NetBird API (`/api/dns/zones/{id}/records`). Token at `/opt/dns-sync/netbird-token` (mode 600; source of truth in sops `secrets/netbird-pat.yaml`). **Edit DNS only here**; the sync propagates to the mesh. It deletes NetBird records absent from Technitium. Tracked: [scripts/dns-sync.py](../scripts/dns-sync.py). *Why this exists:* NetBird won't forward to Technitium for mesh peers (self-IP / nameserver-group quirks), so we sync into the managed zone instead — see [dns.md](../infrastructure/dns.md).
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
- [124 — authentik](124-authentik.md) — retired host of the old dnsmasq
|
- [124 — authentik](124-authentik.md) — retired host of the old dnsmasq
|
||||||
@@ -35,5 +37,8 @@ Authoritative split-horizon DNS for `hubris.network` on the LAN/mesh, plus recur
|
|||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
### 2026-06-03 — dns-sync added (Technitium → NetBird managed zone)
|
||||||
|
This Technitium became the single DNS authoring source; `/opt/dns-sync/sync.py` (cron */10) reconciles named A-records into the NetBird managed zone via the API. Fixed previously-broken mesh names (`sso`, `nfs-export`, `mcp`, `secrets`) by adding them to the managed zone; reaped obsolete `files`/`photos-new`. See [dns.md](../infrastructure/dns.md).
|
||||||
|
|
||||||
### 2026-06-01 — created; replaced dnsmasq on 124
|
### 2026-06-01 — created; replaced dnsmasq on 124
|
||||||
Stood up Technitium at `192.168.8.2`, imported the split-horizon zone (specific A + wildcard + MX/SPF/CAA), made it the primary nameserver in the NetBird `home-lab-dns` group. Verified all names resolve with dnsmasq/124 stopped; [LXC 124 retired](124-authentik.md).
|
Stood up Technitium at `192.168.8.2`, imported the split-horizon zone (specific A + wildcard + MX/SPF/CAA), made it the primary nameserver in the NetBird `home-lab-dns` group. Verified all names resolve with dnsmasq/124 stopped; [LXC 124 retired](124-authentik.md).
|
||||||
|
|||||||
@@ -80,6 +80,17 @@ Either:
|
|||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
### 2026-06-03 — single *authoring* source: Technitium → NetBird managed zone sync
|
||||||
|
The "delete NetBird managed zone → forward everything to Technitium" plan was **abandoned** — NetBird's DNS defeats it: it **won't apply a nameserver group that contains the peer's own mesh IP** (the Mac's `100.122.234.17` → `Nameservers: 0/0 Available`), and nameserver-group forwarding to Technitium never actually took effect for mesh peers (the **managed zone was doing all the real work**; disabling it broke all mesh resolution). So the model is now:
|
||||||
|
|
||||||
|
- **Technitium (`192.168.8.2`) is the single place you author DNS** (UI/API, MX/SPF/CAA, full zone).
|
||||||
|
- A **sync job on [dns (107)](../containers/107-dns.md)** (`/opt/dns-sync/sync.py`, cron */10) reconciles Technitium's named A-records → the **NetBird managed DNS zone** via the NetBird API (`/api/dns/zones/{id}/records`, PAT in sops `secrets/netbird-pat.yaml`). Mesh peers keep using the managed zone (which works); non-mesh LAN clients query Technitium directly; undefined names fall to the public IONOS wildcard (`.79`) — correct.
|
||||||
|
- This **killed the manual drift** that caused the whole `auth`/`sso`/`nfs-export` saga. Never hand-edit the NetBird managed zone again — edit Technitium; the sync propagates.
|
||||||
|
|
||||||
|
**Cleanup done same day:** removed the inert Mac-Mini Technitium secondary (mesh-only, served nobody); reverted the primary's `zoneTransfer=Allow`; fixed `home-lab-dns` group → `[192.168.8.2]` (dropped the self-referencing Mac IP → now `1/1 Available`); deleted the vestigial `Proxmox Names` group.
|
||||||
|
|
||||||
|
> Reference: [scripts/dns-sync.py](../scripts/dns-sync.py). The sync's source of truth is Technitium; it **deletes** NetBird records absent from Technitium (so obsolete names like `files`, `photos-new` get reaped).
|
||||||
|
|
||||||
### 2026-06-01 — dnsmasq replaced by Technitium on [dns (107)](../containers/107-dns.md); LXC 124 retired
|
### 2026-06-01 — dnsmasq replaced by Technitium on [dns (107)](../containers/107-dns.md); LXC 124 retired
|
||||||
Split-horizon DNS moved off [124](../containers/124-authentik.md) to a dedicated **Technitium** LXC at **`192.168.8.2`** (zone: specific A overrides + wildcard→VPS + replicated MX/SPF/CAA). NetBird `home-lab-dns` nameserver group cut over to `192.168.8.2` (with `.180` as a now-dead fallback). dnsmasq stopped, all names verified via Technitium, **LXC 124 shut down**. **Caveat:** the [NetBird managed DNS zone](../containers/124-authentik.md) still answers most app names *directly* (bypassing the nameserver group) — three overlapping DNS sources remain; see the single-source-of-truth decision (Phase 4). **Action needed:** update router DHCP DNS from the dead `.180` → `192.168.8.2` for any plain-LAN (non-mesh) clients.
|
Split-horizon DNS moved off [124](../containers/124-authentik.md) to a dedicated **Technitium** LXC at **`192.168.8.2`** (zone: specific A overrides + wildcard→VPS + replicated MX/SPF/CAA). NetBird `home-lab-dns` nameserver group cut over to `192.168.8.2` (with `.180` as a now-dead fallback). dnsmasq stopped, all names verified via Technitium, **LXC 124 shut down**. **Caveat:** the [NetBird managed DNS zone](../containers/124-authentik.md) still answers most app names *directly* (bypassing the nameserver group) — three overlapping DNS sources remain; see the single-source-of-truth decision (Phase 4). **Action needed:** update router DHCP DNS from the dead `.180` → `192.168.8.2` for any plain-LAN (non-mesh) clients.
|
||||||
|
|
||||||
|
|||||||
49
scripts/dns-sync.py
Normal file
49
scripts/dns-sync.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Sync Technitium hubris.network A-records -> NetBird managed DNS zone.
|
||||||
|
Technitium is the single authoring source; this reconciles NetBird to match."""
|
||||||
|
import json, urllib.request, urllib.parse, ssl, sys
|
||||||
|
|
||||||
|
NB_API = "https://netbird.hubris.network/api"
|
||||||
|
ZONE_ID = "d7gaad00qfrc73er3n1g"
|
||||||
|
NB_TOKEN = open("/opt/dns-sync/netbird-token").read().strip()
|
||||||
|
TECH = "http://127.0.0.1:5380/api"
|
||||||
|
TECH_PW = open("/opt/technitium/admin_password.txt").read().strip()
|
||||||
|
|
||||||
|
ctx = ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE
|
||||||
|
|
||||||
|
def jget(url):
|
||||||
|
with urllib.request.urlopen(url, context=ctx, timeout=15) as r: return json.load(r)
|
||||||
|
|
||||||
|
def nb(method, path, data=None):
|
||||||
|
req = urllib.request.Request(NB_API+path, method=method)
|
||||||
|
req.add_header("Authorization", "Token "+NB_TOKEN)
|
||||||
|
body=None
|
||||||
|
if data is not None:
|
||||||
|
req.add_header("Content-Type","application/json"); body=json.dumps(data).encode()
|
||||||
|
with urllib.request.urlopen(req, data=body, context=ctx, timeout=15) as r:
|
||||||
|
t=r.read(); return json.loads(t) if t else {}
|
||||||
|
|
||||||
|
# 1. source of truth: Technitium named A-records
|
||||||
|
tok = jget(f"{TECH}/user/login?user=admin&pass={urllib.parse.quote(TECH_PW)}&includeInfo=false")["token"]
|
||||||
|
recs = jget(f"{TECH}/zones/records/get?token={tok}&zone=hubris.network&domain=hubris.network&listZone=true")["response"]["records"]
|
||||||
|
source = { r["name"]: r["rData"]["ipAddress"]
|
||||||
|
for r in recs
|
||||||
|
if r.get("type")=="A" and not r["name"].startswith("*") and r["name"]!="hubris.network" }
|
||||||
|
|
||||||
|
# 2. current NetBird records
|
||||||
|
cur = { r["name"]: (r["id"], r["content"]) for r in nb("GET", f"/dns/zones/{ZONE_ID}/records") if r.get("type")=="A" }
|
||||||
|
|
||||||
|
# 3. reconcile
|
||||||
|
c=u=d=0; changes=[]
|
||||||
|
for name, ip in source.items():
|
||||||
|
if name not in cur:
|
||||||
|
nb("POST", f"/dns/zones/{ZONE_ID}/records", {"name":name,"type":"A","content":ip,"ttl":300}); c+=1; changes.append(f"+ {name} {ip}")
|
||||||
|
elif cur[name][1] != ip:
|
||||||
|
nb("DELETE", f"/dns/zones/{ZONE_ID}/records/{cur[name][0]}")
|
||||||
|
nb("POST", f"/dns/zones/{ZONE_ID}/records", {"name":name,"type":"A","content":ip,"ttl":300}); u+=1; changes.append(f"~ {name} {cur[name][1]}->{ip}")
|
||||||
|
for name,(rid,ip) in cur.items():
|
||||||
|
if name not in source:
|
||||||
|
nb("DELETE", f"/dns/zones/{ZONE_ID}/records/{rid}"); d+=1; changes.append(f"- {name} {ip}")
|
||||||
|
|
||||||
|
print(f"dns-sync: {len(source)} source records | +{c} ~{u} -{d}")
|
||||||
|
for ch in changes: print(" "+ch)
|
||||||
36
secrets/netbird-pat.yaml
Normal file
36
secrets/netbird-pat.yaml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
#ENC[AES256_GCM,data:yB1EyrABkGl4G+Mq67djNXCuySBS1TDZjKwtko8hfZi15br6+L9dlwhVTHrzvHAdGYOvkZu/dCssii0kKXC+3QeJws+SfwUV,iv:r7bb8QjkyfeaATPKN3VzO4H+ykQTyUKRMxoE9bmGo+E=,tag:TA0actRKQmYsgCMAcvQkjA==,type:comment]
|
||||||
|
#ENC[AES256_GCM,data:oVH2uanu0pyv7B0SKiaSKfAXWmKAimXKR2mHKi/+hvFIQGzf3mYTZ5zGMQdBCLoy,iv:pgQrEqMZmynEHfIIHHi52xSNwn0/uQ2ccnK0T9T/Z7U=,tag:8hdflKb6l7HlqHTto8eEyQ==,type:comment]
|
||||||
|
netbird_pat: ENC[AES256_GCM,data:cz9pdu7aLR+vKV9QbQ2PtKWf5j5PWxcHEWZ8IEseHS/qPdR4M8282g==,iv:3MCOU0q/7nqQCD6+7qWq++XR9uHi5QHZoenCIT2aYIk=,tag:m4ZE4pDycjwGxoLi1Li1bg==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpYVlocHFacWxtTnhPeTV1
|
||||||
|
SnZqVUUwbXVId0dKNTdmaVQzSUcrT3gxYTA0CloyYm1oQlU3QXVUNVIxTXpGQkN6
|
||||||
|
cTBxTHViZ0E5bEF0dmx4S3l5WVJZOW8KLS0tIEQ5UmJGZG5aVDhGblFDREc5M1dR
|
||||||
|
ZlJ1UEVLeWFsQm0zQ0Q1Z2pTbEpFenMKsw+gfYZ+sby1crIyfoy+yGIaQ2WZ2KYU
|
||||||
|
YgsXY4Vt5b5DC0u/sHKaqXH3bw8ooBQ7tR4WX3JvU6emIAJU0rAuwg==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBaM3NXTExiRzZ0VWdRR1RH
|
||||||
|
RUlBVVBTWFpUQWtCTnc2T0VqMDJITVFiOFNnClZRbElxRFlGb1orbThmdGI3dFRM
|
||||||
|
V1Jqck9pRzVjWGhVQlIvVUFLMXkyT1EKLS0tIGY5K3g3VDJjUHpjSTNrOTRBUEpz
|
||||||
|
dHBmQ0FpeHRSc1ZyMjJHdU1pM1FzbFEKqIJ0qC0SY0aSceh0ZhGVDMb/WCkh1JZZ
|
||||||
|
E3cP7r4Sa6rMZXRR4u23wTHnrckdHgwxSSXYYymy4ilXhgiFQ3ZTBA==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhSElYV3dkMU5hR1Vnb0tl
|
||||||
|
L1R0SG0zeFRuZUd2aTVVaU1leU14bHJjVWl3CjhibVp4NWFsazRYWkl4R1NWTUdk
|
||||||
|
TFdrOXc2OWY0aWxUb3NqM3R1T2xVVEkKLS0tIHlUZGUrRmM5VVVTOEZNYTBSZ2s0
|
||||||
|
VWVTdXlZcmJqeU4xc2NLSDY3MWNUUVkKix4IZEbRI7icZciI7bYzIvDiW4M3Nb7O
|
||||||
|
uopkLc9bC/E+qK9H7jfySkk/zIeW1s/s7fBEKB5utUVpvmKzUB24pA==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6
|
||||||
|
lastmodified: "2026-06-03T19:57:46Z"
|
||||||
|
mac: ENC[AES256_GCM,data:zez2CaP1NPJJnUbEWT58If6jKxFiAudRRPKVJCzbjtp0loFohvOHt1nFY3g0XldqkNngsSZ83GOeJ9g+CvdD7/plkmjz0C04uHWahnNhjAwNRiN32xt31XFqEejScOHW4dbOunX8wXuQ0Q4SrSLTl5G9O/qbLwlwEiqp8KxwRWA=,iv:ctqQ9TDUM65POxn4v1iILubygU2lloCLz9u0bAP6gsY=,tag:05FpkqIhPRMX5hYopFNK2Q==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.1
|
||||||
Reference in New Issue
Block a user