Harden Dynu/Traefik DNS correlation and validation
This commit is contained in:
@@ -18,9 +18,74 @@ This repository includes a **read-only** Dynu DNS inventory workflow for `lan.dd
|
||||
- No Ansible Dynu mutation tasks are introduced.
|
||||
- API secrets are read from environment variables and are never logged.
|
||||
|
||||
## Correlation logic
|
||||
|
||||
`scripts/dynu/correlate_dynu_with_traefik.py` uses compose files as the source of truth and parses them as YAML.
|
||||
|
||||
It supports both common label formats:
|
||||
|
||||
- list style:
|
||||
|
||||
```yaml
|
||||
labels:
|
||||
- "traefik.http.routers.app.rule=Host(`app.lan.ddnsgeek.com`)"
|
||||
```
|
||||
|
||||
- map style:
|
||||
|
||||
```yaml
|
||||
labels:
|
||||
traefik.http.routers.app.rule: "Host(`app.lan.ddnsgeek.com`)"
|
||||
```
|
||||
|
||||
The parser extracts hostnames from router rules such as:
|
||||
|
||||
- `Host(`a`)`
|
||||
- `Host("a")`
|
||||
- `Host('a')`
|
||||
- multi-host rules (comma-delimited)
|
||||
- combined expressions such as `Host(...) && PathPrefix(...)`
|
||||
|
||||
## Route metadata in inventory
|
||||
|
||||
Each discovered hostname mapping includes:
|
||||
|
||||
- fqdn
|
||||
- compose service name
|
||||
- compose file path
|
||||
- stack area (`apps`, `monitoring`, `core`)
|
||||
- router label key(s)
|
||||
- raw router rule
|
||||
- `uses_tls`
|
||||
- `tls_options`
|
||||
- `middlewares`
|
||||
- `uses_mtls`
|
||||
- `uses_authelia`
|
||||
|
||||
mTLS is metadata only and **never blocks mapping**.
|
||||
|
||||
## Validation model
|
||||
|
||||
The generated JSON/Markdown include a top-level `validation` section with:
|
||||
|
||||
- `allowed_unmapped_hostnames`
|
||||
- `unexpected_unmapped_hostnames`
|
||||
- `duplicate_hostnames`
|
||||
- `ambiguous_hostnames`
|
||||
- `validation_ok`
|
||||
|
||||
Current policy:
|
||||
|
||||
- `edge.lan.ddnsgeek.com` is the only allowed unmapped DNS hostname.
|
||||
- every other `*.lan.ddnsgeek.com` DNS hostname should map to a compose/Traefik-discovered service.
|
||||
|
||||
Optional strict mode:
|
||||
|
||||
- Set `DYNU_ENFORCE_VALIDATION=true` to make the correlate script exit non-zero when unexpected unmapped hostnames exist.
|
||||
|
||||
## Required Environment Variables
|
||||
|
||||
- `DYNU_API_KEY` (required)
|
||||
- `DYNU_API_KEY` (required for fetch)
|
||||
- `DYNU_BASE_URL` (optional, defaults to `https://api.dynu.com`)
|
||||
- `DYNU_READ_ONLY` (**must** be `true`)
|
||||
|
||||
@@ -33,6 +98,7 @@ DYNU_BASE_URL=https://api.dynu.com
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- Keep values unquoted unless required by your shell.
|
||||
- `scripts/dynu/build_dns_inventory.sh` will auto-load `secrets/dynu.env` when present.
|
||||
|
||||
|
||||
@@ -4,93 +4,64 @@
|
||||
|
||||
- Base domain: `lan.ddnsgeek.com`
|
||||
- Dynu fetched at: `2026-04-21T03:55:09+00:00`
|
||||
- Inventory generated at: `2026-04-21T03:55:09+00:00`
|
||||
- Inventory generated at: `2026-04-21T04:08:43+00:00`
|
||||
|
||||
## Summary
|
||||
|
||||
- Traefik hostnames discovered: **5**
|
||||
- Traefik hostnames discovered: **15**
|
||||
- Dynu hostnames discovered: **20**
|
||||
- Matched: **5**
|
||||
- Missing in Dynu: **0**
|
||||
- Dynu DNS only: **15**
|
||||
- Duplicate Traefik hostnames: **1**
|
||||
- Mapped hostnames: **15**
|
||||
- DNS-only hostnames: **5**
|
||||
- Traefik-only hostnames: **0**
|
||||
- Ambiguous hostnames: **0**
|
||||
|
||||
## Dynu Records
|
||||
## Validation
|
||||
|
||||
| Hostname | Type | Value | TTL |
|
||||
|---|---|---|---|
|
||||
| `auth.lan.ddnsgeek.com` | `A` | `` | `120` |
|
||||
| `edge.lan.ddnsgeek.com` | `A` | `` | `120` |
|
||||
| `familytree.lan.ddnsgeek.com` | `A` | `` | `120` |
|
||||
| `gitea.lan.ddnsgeek.com` | `A` | `` | `120` |
|
||||
| `gotify.lan.ddnsgeek.com` | `A` | `` | `120` |
|
||||
| `grafana.lan.ddnsgeek.com` | `A` | `` | `120` |
|
||||
| `influxdb.lan.ddnsgeek.com` | `A` | `` | `120` |
|
||||
| `kuma.lan.ddnsgeek.com` | `A` | `120.155.63.223` | `60` |
|
||||
| `lan.ddnsgeek.com` | `SOA` | `` | `120` |
|
||||
| `monitor-kuma.lan.ddnsgeek.com` | `A` | `` | `120` |
|
||||
| `mtls-bridge.lan.ddnsgeek.com` | `A` | `` | `120` |
|
||||
| `nextcloud.lan.ddnsgeek.com` | `A` | `` | `120` |
|
||||
| `node-red.lan.ddnsgeek.com` | `A` | `` | `120` |
|
||||
| `passbolt.lan.ddnsgeek.com` | `A` | `` | `120` |
|
||||
| `portainer.lan.ddnsgeek.com` | `A` | `` | `120` |
|
||||
| `prometheus.lan.ddnsgeek.com` | `A` | `` | `120` |
|
||||
| `searxng.lan.ddnsgeek.com` | `A` | `` | `120` |
|
||||
| `shifts.lan.ddnsgeek.com` | `A` | `` | `120` |
|
||||
| `stockfill.lan.ddnsgeek.com` | `A` | `` | `120` |
|
||||
| `traefik.lan.ddnsgeek.com` | `A` | `` | `120` |
|
||||
- Validation ok: **false**
|
||||
- Allowed unmapped hostnames: `edge.lan.ddnsgeek.com`
|
||||
- Unexpected unmapped hostnames: **3**
|
||||
- Duplicate hostnames: **1**
|
||||
- Ambiguous hostnames: **0**
|
||||
|
||||
## Correlation
|
||||
### Allowed unmapped hostnames
|
||||
|
||||
| Hostname | Status | Service(s) | Source compose file(s) | DNS records |
|
||||
|---|---|---|---|---|
|
||||
| `auth.lan.ddnsgeek.com` | `dns_only` | - | - | A: |
|
||||
| `edge.lan.ddnsgeek.com` | `dns_only` | - | - | A: |
|
||||
| `familytree.lan.ddnsgeek.com` | `dns_only` | - | - | A: |
|
||||
| `gitea.lan.ddnsgeek.com` | `matched` | apps/gitea | apps/gitea/docker-compose.yml | A: |
|
||||
| `gotify.lan.ddnsgeek.com` | `dns_only` | - | - | A: |
|
||||
| `grafana.lan.ddnsgeek.com` | `dns_only` | - | - | A: |
|
||||
| `influxdb.lan.ddnsgeek.com` | `dns_only` | - | - | A: |
|
||||
| `kuma.lan.ddnsgeek.com` | `dns_only` | - | - | A:120.155.63.223 |
|
||||
| `lan.ddnsgeek.com` | `dns_only` | - | - | SOA: |
|
||||
| `monitor-kuma.lan.ddnsgeek.com` | `dns_only` | - | - | A: |
|
||||
| `mtls-bridge.lan.ddnsgeek.com` | `matched` | monitoring/mtls-bridge | monitoring/mtls-bridge/docker-compose.yml | A: |
|
||||
| `nextcloud.lan.ddnsgeek.com` | `dns_only` | - | - | A: |
|
||||
| `node-red.lan.ddnsgeek.com` | `dns_only` | - | - | A: |
|
||||
| `passbolt.lan.ddnsgeek.com` | `dns_only` | - | - | A: |
|
||||
| `portainer.lan.ddnsgeek.com` | `dns_only` | - | - | A: |
|
||||
| `prometheus.lan.ddnsgeek.com` | `dns_only` | - | - | A: |
|
||||
| `searxng.lan.ddnsgeek.com` | `matched` | apps/searxng-webapp | apps/searxng/docker-compose.yml | A: |
|
||||
| `shifts.lan.ddnsgeek.com` | `dns_only` | - | - | A: |
|
||||
| `stockfill.lan.ddnsgeek.com` | `matched` | apps/stockfill | apps/stockfill/docker-compose.yml | A: |
|
||||
| `traefik.lan.ddnsgeek.com` | `matched` | core/traefik | core/traefik/docker-compose.yml | A: |
|
||||
- `edge.lan.ddnsgeek.com`
|
||||
|
||||
## Matched records
|
||||
### Unexpected unmapped hostnames
|
||||
|
||||
- `gitea.lan.ddnsgeek.com`
|
||||
- `mtls-bridge.lan.ddnsgeek.com`
|
||||
- `searxng.lan.ddnsgeek.com`
|
||||
- `kuma.lan.ddnsgeek.com`
|
||||
- `shifts.lan.ddnsgeek.com`
|
||||
- `stockfill.lan.ddnsgeek.com`
|
||||
- `traefik.lan.ddnsgeek.com`
|
||||
|
||||
## Traefik hostnames missing in Dynu
|
||||
### Duplicate hostnames
|
||||
|
||||
- `mtls-bridge.lan.ddnsgeek.com`
|
||||
|
||||
### Ambiguous hostnames
|
||||
|
||||
_None._
|
||||
|
||||
## Dynu DNS records not mapped to known Traefik services
|
||||
## Correlation
|
||||
|
||||
- `auth.lan.ddnsgeek.com`
|
||||
- `edge.lan.ddnsgeek.com`
|
||||
- `familytree.lan.ddnsgeek.com`
|
||||
- `gotify.lan.ddnsgeek.com`
|
||||
- `grafana.lan.ddnsgeek.com`
|
||||
- `influxdb.lan.ddnsgeek.com`
|
||||
- `kuma.lan.ddnsgeek.com`
|
||||
- `lan.ddnsgeek.com`
|
||||
- `monitor-kuma.lan.ddnsgeek.com`
|
||||
- `nextcloud.lan.ddnsgeek.com`
|
||||
- `node-red.lan.ddnsgeek.com`
|
||||
- `passbolt.lan.ddnsgeek.com`
|
||||
- `portainer.lan.ddnsgeek.com`
|
||||
- `prometheus.lan.ddnsgeek.com`
|
||||
- `shifts.lan.ddnsgeek.com`
|
||||
| Hostname | Status | Reasons | Service(s) | Route metadata | DNS records |
|
||||
|---|---|---|---|---|---|
|
||||
| `auth.lan.ddnsgeek.com` | `mapped` | `mapped` | core/authelia | authelia [tls=true, mtls=false, authelia=false, tls_options=-, middlewares=-] | A: |
|
||||
| `edge.lan.ddnsgeek.com` | `allowed_unmapped` | `allowed_unmapped, dns_only` | - | - | A: |
|
||||
| `familytree.lan.ddnsgeek.com` | `mapped` | `mapped` | apps/grampsweb | gramps [tls=true, mtls=false, authelia=false, tls_options=-, middlewares=-] | A: |
|
||||
| `gitea.lan.ddnsgeek.com` | `mapped` | `mapped` | apps/gitea | gitea [tls=true, mtls=false, authelia=false, tls_options=-, middlewares=-] | A: |
|
||||
| `gotify.lan.ddnsgeek.com` | `mapped` | `mapped` | monitoring/gotify | gotify [tls=true, mtls=true, authelia=false, tls_options=mtls-private-admin@file, middlewares=-] | A: |
|
||||
| `grafana.lan.ddnsgeek.com` | `mapped` | `mapped` | monitoring/grafana | grafana [tls=true, mtls=true, authelia=false, tls_options=mtls-private-admin@file, middlewares=-] | A: |
|
||||
| `influxdb.lan.ddnsgeek.com` | `mapped` | `mapped` | monitoring/influxdb | influxdb [tls=true, mtls=true, authelia=true, tls_options=mtls-private-admin@file, middlewares=authelia] | A: |
|
||||
| `kuma.lan.ddnsgeek.com` | `unexpected_unmapped` | `unexpected_unmapped, dns_only` | - | - | A:120.155.63.223 |
|
||||
| `lan.ddnsgeek.com` | `dns_only` | `dns_only` | - | - | SOA: |
|
||||
| `monitor-kuma.lan.ddnsgeek.com` | `mapped` | `mapped` | monitoring/monitor-kuma | monitor [tls=true, mtls=true, authelia=false, tls_options=mtls-private-admin@file, middlewares=-] | A: |
|
||||
| `mtls-bridge.lan.ddnsgeek.com` | `mapped` | `mapped` | monitoring/mtls-bridge | mtls-bridge [tls=true, mtls=true, authelia=false, tls_options=-, middlewares=mtls-bridge-auth,mtls-bridge-cors]<br>mtls-bridge-preflight [tls=true, mtls=true, authelia=false, tls_options=-, middlewares=mtls-bridge-cors] | A: |
|
||||
| `nextcloud.lan.ddnsgeek.com` | `mapped` | `mapped` | apps/nextcloud-webapp | nextcloud [tls=true, mtls=false, authelia=false, tls_options=-, middlewares=nextcloud-dav,nextcloud-webfinger] | A: |
|
||||
| `node-red.lan.ddnsgeek.com` | `mapped` | `mapped` | monitoring/node-red | node-red [tls=true, mtls=true, authelia=true, tls_options=mtls-private-admin@file, middlewares=authelia] | A: |
|
||||
| `passbolt.lan.ddnsgeek.com` | `mapped` | `mapped` | apps/passbolt-webapp | passbolt [tls=true, mtls=false, authelia=false, tls_options=-, middlewares=-] | A: |
|
||||
| `portainer.lan.ddnsgeek.com` | `mapped` | `mapped` | monitoring/portainer | portainer [tls=true, mtls=true, authelia=false, tls_options=mtls-private-admin@file, middlewares=-] | A: |
|
||||
| `prometheus.lan.ddnsgeek.com` | `mapped` | `mapped` | monitoring/prometheus | prometheus [tls=true, mtls=true, authelia=true, tls_options=mtls-private-admin@file, middlewares=authelia] | A: |
|
||||
| `searxng.lan.ddnsgeek.com` | `mapped` | `mapped` | apps/searxng-webapp | searxng [tls=true, mtls=false, authelia=false, tls_options=-, middlewares=-] | A: |
|
||||
| `shifts.lan.ddnsgeek.com` | `unexpected_unmapped` | `unexpected_unmapped, dns_only` | - | - | A: |
|
||||
| `stockfill.lan.ddnsgeek.com` | `unexpected_unmapped` | `unexpected_unmapped, dns_only` | - | - | A: |
|
||||
| `traefik.lan.ddnsgeek.com` | `mapped` | `mapped` | core/traefik | traefik [tls=true, mtls=true, authelia=true, tls_options=mtls-private-admin@file, middlewares=authelia] | A: |
|
||||
|
||||
Reference in New Issue
Block a user