2026-03-17 21:42:16 +00:00
variable " tls_secret_name " { }
variable " tier " { type = string }
variable " homepage_token " { }
variable " mysql_host " { type = string }
truenas deprecation: migrate all non-immich storage to proxmox NFS
- Migrate 7 backup CronJobs to Proxmox host NFS (192.168.1.127)
(etcd, mysql, postgresql, nextcloud, redis, vaultwarden, plotting-book)
- Migrate headscale backup, ebook2audiobook, osm_routing to Proxmox NFS
- Migrate servarr (lidarr, readarr, soulseek) NFS refs to Proxmox
- Remove 79 orphaned TrueNAS NFS module declarations from 49 stacks
- Delete stacks/platform/modules/ (27 dead module copies, 65MB)
- Update nfs-truenas StorageClass to point to Proxmox (192.168.1.127)
- Remove iscsi DNS record from config.tfvars
- Fix woodpecker persistence config and alertmanager PV
Only Immich (8 PVCs, ~1.4TB) remains on TrueNAS.
2026-04-12 14:35:39 +01:00
variable " postgresql_host " { type = string }
2026-03-17 21:42:16 +00:00
variable " technitium_username " { type = string }
variable " technitium_password " {
type = string
sensitive = true
}
resource " kubernetes_namespace " " technitium " {
metadata {
name = " technitium "
labels = {
tier = var . tier
}
# stale cache error when trying to resolve
# labels = {
# "istio-injection" : "enabled"
# }
}
[infra] Suppress Goldilocks vpa-update-mode label drift on all namespaces [ci skip]
## Context
Wave 3B-continued: the Goldilocks VPA dashboard (stacks/vpa) runs a Kyverno
ClusterPolicy `goldilocks-vpa-auto-mode` that mutates every namespace with
`metadata.labels["goldilocks.fairwinds.com/vpa-update-mode"] = "off"`. This
is intentional — Terraform owns container resource limits, and Goldilocks
should only provide recommendations, never auto-update. The label is how
Goldilocks decides per-namespace whether to run its VPA in `off` mode.
Effect on Terraform: every `kubernetes_namespace` resource shows the label
as pending-removal (`-> null`) on every `scripts/tg plan`. Dawarich survey
2026-04-18 confirmed the drift. Cluster-side count: 88 namespaces carry the
label (`kubectl get ns -o json | jq ... | wc -l`). Every TF-managed namespace
is affected.
This commit brings the intentional admission drift under the same
`# KYVERNO_LIFECYCLE_V1` discoverability marker introduced in c9d221d5 for
the ndots dns_config pattern. The marker now stands generically for any
Kyverno admission-webhook drift suppression; the inline comment records
which specific policy stamps which specific field so future grep audits
show why each suppression exists.
## This change
107 `.tf` files touched — every stack's `resource "kubernetes_namespace"`
resource gets:
```hcl
lifecycle {
# KYVERNO_LIFECYCLE_V1: goldilocks-vpa-auto-mode ClusterPolicy stamps this label on every namespace
ignore_changes = [metadata[0].labels["goldilocks.fairwinds.com/vpa-update-mode"]]
}
```
Injection was done with a brace-depth-tracking Python pass (`/tmp/add_goldilocks_ignore.py`):
match `^resource "kubernetes_namespace" ` → track `{` / `}` until the
outermost closing brace → insert the lifecycle block before the closing
brace. The script is idempotent (skips any file that already mentions
`goldilocks.fairwinds.com/vpa-update-mode`) so re-running is safe.
Vault stack picked up 2 namespaces in the same file (k8s-users produces
one, plus a second explicit ns) — confirmed via file diff (+8 lines).
## What is NOT in this change
- `stacks/trading-bot/main.tf` — entire file is `/* … */` commented out
(paused 2026-04-06 per user decision). Reverted after the script ran.
- `stacks/_template/main.tf.example` — per-stack skeleton, intentionally
minimal. User keeps it that way. Not touched by the script (file
has no real `resource "kubernetes_namespace"` — only a placeholder
comment).
- `.terraform/` copies (e.g. `stacks/metallb/.terraform/modules/...`) —
gitignored, won't commit; the live path was edited.
- `terraform fmt` cleanup of adjacent pre-existing alignment issues in
authentik, freedify, hermes-agent, nvidia, vault, meshcentral. Reverted
to keep the commit scoped to the Goldilocks sweep. Those files will
need a separate fmt-only commit or will be cleaned up on next real
apply to that stack.
## Verification
Dawarich (one of the hundred-plus touched stacks) showed the pattern
before and after:
```
$ cd stacks/dawarich && ../../scripts/tg plan
Before:
Plan: 0 to add, 2 to change, 0 to destroy.
# kubernetes_namespace.dawarich will be updated in-place
(goldilocks.fairwinds.com/vpa-update-mode -> null)
# module.tls_secret.kubernetes_secret.tls_secret will be updated in-place
(Kyverno generate.* labels — fixed in 8d94688d)
After:
No changes. Your infrastructure matches the configuration.
```
Injection count check:
```
$ rg -c 'KYVERNO_LIFECYCLE_V1: goldilocks-vpa-auto-mode' stacks/ | awk -F: '{s+=$2} END {print s}'
108
```
## Reproduce locally
1. `git pull`
2. Pick any stack: `cd stacks/<name> && ../../scripts/tg plan`
3. Expect: no drift on the namespace's goldilocks.fairwinds.com/vpa-update-mode label.
Closes: code-dwx
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 21:15:27 +00:00
lifecycle {
# KYVERNO_LIFECYCLE_V1: goldilocks-vpa-auto-mode ClusterPolicy stamps this label on every namespace
ignore_changes = [ metadata [ 0 ] . labels [ " goldilocks.fairwinds.com/vpa-update-mode " ] ]
}
2026-03-17 21:42:16 +00:00
}
module " tls_secret " {
source = " ../../../../modules/kubernetes/setup_tls_secret "
namespace = kubernetes_namespace . technitium . metadata [ 0 ] . name
tls_secret_name = var . tls_secret_name
}
# CoreDNS Corefile - manages cluster DNS resolution
2026-04-06 11:54:45 +03:00
# The viktorbarzin.lan block forwards to Technitium via ClusterIP (stable, LB-independent).
2026-03-17 21:42:16 +00:00
# A template regex in the viktorbarzin.lan block short-circuits junk queries
# caused by ndots:5 search domain expansion (e.g. www.cloudflare.com.viktorbarzin.lan,
# redis.redis.svc.cluster.local.viktorbarzin.lan) by returning NXDOMAIN for any
# query with 2+ labels before .viktorbarzin.lan. Legitimate single-label queries
# (e.g. idrac.viktorbarzin.lan) fall through to Technitium.
resource " kubernetes_config_map " " coredns " {
metadata {
name = " coredns "
namespace = " kube-system "
}
data = {
Corefile = < < - EOF
. : 53 {
#log
errors
health {
lameduck 5 s
}
ready
kubernetes cluster . local in - addr . arpa ip6 . arpa {
pods insecure
fallthrough in - addr . arpa ip6 . arpa
ttl 30
}
prometheus : 9153
[dns] DNS reliability & hardening — Technitium + CoreDNS + alerts + readiness gate
Workstreams A, B, G, H, I of the DNS reliability plan (code-q2e).
Follow-ups for C, D, E, F filed as code-2k6, code-k0d, code-o6j, code-dw8.
**Technitium (WS A)**
- Primary deployment: add Kyverno lifecycle ignore_changes on dns_config
(secondary/tertiary already had it) — eliminates per-apply ndots drift.
- All 3 instances: raise memory request+limit from 512Mi to 1Gi (primary
was restarting near the ceiling; CPU limits stay off per cluster policy).
- zone-sync CronJob: parse API responses, push status/failures/last-run and
per-instance zone_count gauges to Pushgateway, fail the job on any
create error (was silently passing).
**CoreDNS (WS B)**
- Corefile: add policy sequential + health_check 5s + max_fails 2 on root
forward, health_check on viktorbarzin.lan forward, serve_stale
3600s/86400s on both cache blocks — pfSense flap no longer takes the
cluster down; upstream outage keeps cached names resolving for 24h.
- Scale deploy/coredns to 3 replicas with required pod anti-affinity on
hostname via null_resource (hashicorp/kubernetes v3 dropped the _patch
resources); readiness gate asserts state post-apply.
- PDB coredns with minAvailable=2.
**Observability (WS G)**
- Fix DNSQuerySpike — rewrite to compare against
avg_over_time(dns_anomaly_total_queries[1h] offset 15m); previous
dns_anomaly_avg_queries was computed from a per-pod /tmp file so always
equalled the current value (alert could never fire).
- New: DNSQueryRateDropped, TechnitiumZoneSyncFailed,
TechnitiumZoneSyncStale, TechnitiumZoneCountMismatch,
CoreDNSForwardFailureRate.
**Post-apply readiness gate (WS H)**
- null_resource.technitium_readiness_gate runs at end of apply:
kubectl rollout status on all 3 deployments (180s), per-pod
/api/stats/get probe, zone-count parity across the 3 instances.
Fails the apply on any check fail. Override: -var skip_readiness=true.
**Docs (WS I)**
- docs/architecture/dns.md: CoreDNS Corefile hardening, new alerts table,
zone-sync metrics reference, why DNSQuerySpike was broken.
- docs/runbooks/technitium-apply.md (new): what the gate checks, failure
modes, emergency override.
Out of scope for this commit (see beads follow-ups):
- WS C: NodeLocal DNSCache (code-2k6)
- WS D: pfSense Unbound replaces dnsmasq (code-k0d)
- WS E: Kea multi-IP DHCP + TSIG (code-o6j)
- WS F: static-client DNS fixes (code-dw8)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 14:53:41 +00:00
forward . 10 . 0 . 20 . 1 8 . 8 . 8 . 8 1 . 1 . 1 . 1 {
policy sequential
health_check 5 s
max_fails 2
}
2026-03-17 21:42:16 +00:00
cache {
success 10000 300 6
denial 10000 300 60
2026-04-19 15:18:43 +00:00
serve_stale 86400 s
2026-03-17 21:42:16 +00:00
}
loop
reload
loadbalance
}
viktorbarzin . lan : 53 {
#log
errors
template ANY ANY viktorbarzin . lan {
match " .* \ ..* \ .viktorbarzin \ .lan \ . $ "
rcode NXDOMAIN
fallthrough
}
[dns] DNS reliability & hardening — Technitium + CoreDNS + alerts + readiness gate
Workstreams A, B, G, H, I of the DNS reliability plan (code-q2e).
Follow-ups for C, D, E, F filed as code-2k6, code-k0d, code-o6j, code-dw8.
**Technitium (WS A)**
- Primary deployment: add Kyverno lifecycle ignore_changes on dns_config
(secondary/tertiary already had it) — eliminates per-apply ndots drift.
- All 3 instances: raise memory request+limit from 512Mi to 1Gi (primary
was restarting near the ceiling; CPU limits stay off per cluster policy).
- zone-sync CronJob: parse API responses, push status/failures/last-run and
per-instance zone_count gauges to Pushgateway, fail the job on any
create error (was silently passing).
**CoreDNS (WS B)**
- Corefile: add policy sequential + health_check 5s + max_fails 2 on root
forward, health_check on viktorbarzin.lan forward, serve_stale
3600s/86400s on both cache blocks — pfSense flap no longer takes the
cluster down; upstream outage keeps cached names resolving for 24h.
- Scale deploy/coredns to 3 replicas with required pod anti-affinity on
hostname via null_resource (hashicorp/kubernetes v3 dropped the _patch
resources); readiness gate asserts state post-apply.
- PDB coredns with minAvailable=2.
**Observability (WS G)**
- Fix DNSQuerySpike — rewrite to compare against
avg_over_time(dns_anomaly_total_queries[1h] offset 15m); previous
dns_anomaly_avg_queries was computed from a per-pod /tmp file so always
equalled the current value (alert could never fire).
- New: DNSQueryRateDropped, TechnitiumZoneSyncFailed,
TechnitiumZoneSyncStale, TechnitiumZoneCountMismatch,
CoreDNSForwardFailureRate.
**Post-apply readiness gate (WS H)**
- null_resource.technitium_readiness_gate runs at end of apply:
kubectl rollout status on all 3 deployments (180s), per-pod
/api/stats/get probe, zone-count parity across the 3 instances.
Fails the apply on any check fail. Override: -var skip_readiness=true.
**Docs (WS I)**
- docs/architecture/dns.md: CoreDNS Corefile hardening, new alerts table,
zone-sync metrics reference, why DNSQuerySpike was broken.
- docs/runbooks/technitium-apply.md (new): what the gate checks, failure
modes, emergency override.
Out of scope for this commit (see beads follow-ups):
- WS C: NodeLocal DNSCache (code-2k6)
- WS D: pfSense Unbound replaces dnsmasq (code-k0d)
- WS E: Kea multi-IP DHCP + TSIG (code-o6j)
- WS F: static-client DNS fixes (code-dw8)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 14:53:41 +00:00
forward . 10 . 96 . 0 . 53 {
health_check 5 s
max_fails 2
}
2026-03-17 21:42:16 +00:00
cache {
success 10000 300 6
denial 10000 300 60
2026-04-19 15:18:43 +00:00
serve_stale 86400 s
2026-03-17 21:42:16 +00:00
}
}
EOF
}
}
2026-04-14 08:18:59 +00:00
resource " kubernetes_persistent_volume_claim " " primary_config_encrypted " {
wait_until_bound = false
metadata {
name = " technitium-primary-config-encrypted "
namespace = kubernetes_namespace . technitium . metadata [ 0 ] . name
annotations = {
2026-05-10 19:56:16 +00:00
" resize.topolvm.io/threshold " = " 10% "
2026-04-14 08:18:59 +00:00
" resize.topolvm.io/increase " = " 100% "
" resize.topolvm.io/storage_limit " = " 5Gi "
}
}
spec {
access_modes = [ " ReadWriteOnce " ]
storage_class_name = " proxmox-lvm-encrypted "
resources {
requests = {
storage = " 2Gi "
}
}
}
2026-03-17 21:42:16 +00:00
}
resource " kubernetes_deployment " " technitium " {
# resource "kubernetes_daemonset" "technitium" {
metadata {
name = " technitium "
namespace = kubernetes_namespace . technitium . metadata [ 0 ] . name
labels = {
app = " technitium "
tier = var . tier
}
}
spec {
strategy {
feat(storage): migrate 38 NFS PVCs to proxmox-lvm (Wave 2)
Add proxmox-lvm PVCs with pvc-autoresizer annotations for all
remaining single-pod app data services. Deployments updated to
use new block storage PVCs. Old NFS modules retained for rollback.
Services: affine, changedetection, diun, excalidraw, f1-stream,
hackmd, isponsorblocktv, matrix, n8n, send, grampsweb, health,
onlyoffice, owntracks, paperless-ngx, privatebin, resume,
speedtest, stirling-pdf, tandoor, rybbit (clickhouse), tor-proxy
(torrserver), whisper+piper, frigate (config), ollama (ui),
servarr (prowlarr/listenarr/qbittorrent), aiostreams, freshrss
(extensions), meshcentral (data+files), openclaw (data+home+
openlobster), technitium, mailserver (data+roundcube html+enigma),
dbaas (pgadmin).
Strategy set to Recreate where needed for RWO volumes.
2026-04-04 19:25:12 +03:00
type = " Recreate "
2026-03-17 21:42:16 +00:00
}
# replicas = 1
selector {
match_labels = {
app = " technitium "
}
}
template {
metadata {
annotations = {
feat: pin ~28 images to specific versions, enable DIUN monitoring, add app-stacks pipeline
Pin third-party images from :latest to current stable versions:
- Platform: cloudflared, technitium, snmp-exporter, pve-exporter,
headscale, shadowsocks, xray
- Apps: paperless-ngx, linkwarden, wealthfolio, speedtest, synapse,
n8n, prowlarr, qbittorrent, lidarr, rybbit, ollama, immichframe,
cyberchef, networking-toolbox, echo, coturn, shlink, affine
Enable DIUN annotations on all pinned deployments with per-image
tag patterns. Add Woodpecker app-stacks pipeline for selective
terragrunt apply on changed app stacks.
2026-04-06 14:27:13 +03:00
" diun.enable " = " true "
" diun.include_tags " = " ^ \\ d+ \\ . \\ d+ \\ . \\ d+ $ "
2026-03-17 21:42:16 +00:00
}
labels = {
app = " technitium "
" dns-server " = " true "
}
}
spec {
affinity {
# Spread DNS pods across nodes for HA
pod_anti_affinity {
required_during_scheduling_ignored_during_execution {
label_selector {
match_expressions {
key = " dns-server "
operator = " In "
values = [ " true " ]
}
}
topology_key = " kubernetes.io/hostname "
}
}
}
container {
feat: pin ~28 images to specific versions, enable DIUN monitoring, add app-stacks pipeline
Pin third-party images from :latest to current stable versions:
- Platform: cloudflared, technitium, snmp-exporter, pve-exporter,
headscale, shadowsocks, xray
- Apps: paperless-ngx, linkwarden, wealthfolio, speedtest, synapse,
n8n, prowlarr, qbittorrent, lidarr, rybbit, ollama, immichframe,
cyberchef, networking-toolbox, echo, coturn, shlink, affine
Enable DIUN annotations on all pinned deployments with per-image
tag patterns. Add Woodpecker app-stacks pipeline for selective
terragrunt apply on changed app stacks.
2026-04-06 14:27:13 +03:00
image = " technitium/dns-server:14.3.0 "
2026-03-17 21:42:16 +00:00
name = " technitium "
resources {
requests = {
[dns] DNS reliability & hardening — Technitium + CoreDNS + alerts + readiness gate
Workstreams A, B, G, H, I of the DNS reliability plan (code-q2e).
Follow-ups for C, D, E, F filed as code-2k6, code-k0d, code-o6j, code-dw8.
**Technitium (WS A)**
- Primary deployment: add Kyverno lifecycle ignore_changes on dns_config
(secondary/tertiary already had it) — eliminates per-apply ndots drift.
- All 3 instances: raise memory request+limit from 512Mi to 1Gi (primary
was restarting near the ceiling; CPU limits stay off per cluster policy).
- zone-sync CronJob: parse API responses, push status/failures/last-run and
per-instance zone_count gauges to Pushgateway, fail the job on any
create error (was silently passing).
**CoreDNS (WS B)**
- Corefile: add policy sequential + health_check 5s + max_fails 2 on root
forward, health_check on viktorbarzin.lan forward, serve_stale
3600s/86400s on both cache blocks — pfSense flap no longer takes the
cluster down; upstream outage keeps cached names resolving for 24h.
- Scale deploy/coredns to 3 replicas with required pod anti-affinity on
hostname via null_resource (hashicorp/kubernetes v3 dropped the _patch
resources); readiness gate asserts state post-apply.
- PDB coredns with minAvailable=2.
**Observability (WS G)**
- Fix DNSQuerySpike — rewrite to compare against
avg_over_time(dns_anomaly_total_queries[1h] offset 15m); previous
dns_anomaly_avg_queries was computed from a per-pod /tmp file so always
equalled the current value (alert could never fire).
- New: DNSQueryRateDropped, TechnitiumZoneSyncFailed,
TechnitiumZoneSyncStale, TechnitiumZoneCountMismatch,
CoreDNSForwardFailureRate.
**Post-apply readiness gate (WS H)**
- null_resource.technitium_readiness_gate runs at end of apply:
kubectl rollout status on all 3 deployments (180s), per-pod
/api/stats/get probe, zone-count parity across the 3 instances.
Fails the apply on any check fail. Override: -var skip_readiness=true.
**Docs (WS I)**
- docs/architecture/dns.md: CoreDNS Corefile hardening, new alerts table,
zone-sync metrics reference, why DNSQuerySpike was broken.
- docs/runbooks/technitium-apply.md (new): what the gate checks, failure
modes, emergency override.
Out of scope for this commit (see beads follow-ups):
- WS C: NodeLocal DNSCache (code-2k6)
- WS D: pfSense Unbound replaces dnsmasq (code-k0d)
- WS E: Kea multi-IP DHCP + TSIG (code-o6j)
- WS F: static-client DNS fixes (code-dw8)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 14:53:41 +00:00
cpu = " 100m "
2026-04-19 15:08:04 +00:00
memory = " 2Gi "
2026-03-17 21:42:16 +00:00
}
limits = {
2026-04-19 15:08:04 +00:00
memory = " 2Gi "
2026-03-17 21:42:16 +00:00
}
}
port {
container_port = 5380
}
port {
container_port = 53
}
port {
container_port = 80
}
liveness_probe {
tcp_socket {
port = 53
}
initial_delay_seconds = 10
period_seconds = 10
}
readiness_probe {
tcp_socket {
port = 53
}
initial_delay_seconds = 5
period_seconds = 5
}
volume_mount {
mount_path = " /etc/dns "
name = " nfs-config "
}
volume_mount {
mount_path = " /etc/tls/ "
name = " tls-cert "
}
}
volume {
name = " nfs-config "
persistent_volume_claim {
2026-04-14 08:18:59 +00:00
claim_name = kubernetes_persistent_volume_claim . primary_config_encrypted . metadata [ 0 ] . name
2026-03-17 21:42:16 +00:00
}
}
volume {
name = " tls-cert "
secret {
secret_name = var . tls_secret_name
}
}
dns_config {
option {
name = " ndots "
value = " 2 "
}
}
}
}
}
[dns] DNS reliability & hardening — Technitium + CoreDNS + alerts + readiness gate
Workstreams A, B, G, H, I of the DNS reliability plan (code-q2e).
Follow-ups for C, D, E, F filed as code-2k6, code-k0d, code-o6j, code-dw8.
**Technitium (WS A)**
- Primary deployment: add Kyverno lifecycle ignore_changes on dns_config
(secondary/tertiary already had it) — eliminates per-apply ndots drift.
- All 3 instances: raise memory request+limit from 512Mi to 1Gi (primary
was restarting near the ceiling; CPU limits stay off per cluster policy).
- zone-sync CronJob: parse API responses, push status/failures/last-run and
per-instance zone_count gauges to Pushgateway, fail the job on any
create error (was silently passing).
**CoreDNS (WS B)**
- Corefile: add policy sequential + health_check 5s + max_fails 2 on root
forward, health_check on viktorbarzin.lan forward, serve_stale
3600s/86400s on both cache blocks — pfSense flap no longer takes the
cluster down; upstream outage keeps cached names resolving for 24h.
- Scale deploy/coredns to 3 replicas with required pod anti-affinity on
hostname via null_resource (hashicorp/kubernetes v3 dropped the _patch
resources); readiness gate asserts state post-apply.
- PDB coredns with minAvailable=2.
**Observability (WS G)**
- Fix DNSQuerySpike — rewrite to compare against
avg_over_time(dns_anomaly_total_queries[1h] offset 15m); previous
dns_anomaly_avg_queries was computed from a per-pod /tmp file so always
equalled the current value (alert could never fire).
- New: DNSQueryRateDropped, TechnitiumZoneSyncFailed,
TechnitiumZoneSyncStale, TechnitiumZoneCountMismatch,
CoreDNSForwardFailureRate.
**Post-apply readiness gate (WS H)**
- null_resource.technitium_readiness_gate runs at end of apply:
kubectl rollout status on all 3 deployments (180s), per-pod
/api/stats/get probe, zone-count parity across the 3 instances.
Fails the apply on any check fail. Override: -var skip_readiness=true.
**Docs (WS I)**
- docs/architecture/dns.md: CoreDNS Corefile hardening, new alerts table,
zone-sync metrics reference, why DNSQuerySpike was broken.
- docs/runbooks/technitium-apply.md (new): what the gate checks, failure
modes, emergency override.
Out of scope for this commit (see beads follow-ups):
- WS C: NodeLocal DNSCache (code-2k6)
- WS D: pfSense Unbound replaces dnsmasq (code-k0d)
- WS E: Kea multi-IP DHCP + TSIG (code-o6j)
- WS F: static-client DNS fixes (code-dw8)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 14:53:41 +00:00
lifecycle {
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
ignore_changes = [ spec [ 0 ] . template [ 0 ] . spec [ 0 ] . dns_config ]
}
2026-03-17 21:42:16 +00:00
}
resource " kubernetes_service " " technitium-web " {
metadata {
name = " technitium-web "
namespace = kubernetes_namespace . technitium . metadata [ 0 ] . name
labels = {
" app " = " technitium "
}
# annotations = {
# "metallb.universe.tf/allow-shared-ip" : "shared"
# }
}
spec {
# type = "LoadBalancer"
# external_traffic_policy = "Cluster"
selector = {
app = " technitium "
}
port {
name = " technitium-dns "
port = " 5380 "
protocol = " TCP "
}
port {
name = " technitium-doh "
port = " 80 "
protocol = " TCP "
}
}
}
resource " kubernetes_service " " technitium-dns " {
metadata {
name = " technitium-dns "
namespace = kubernetes_namespace . technitium . metadata [ 0 ] . name
labels = {
" app " = " technitium "
}
consolidate MetalLB IPs: 5 → 1 (10.0.20.200)
Migrate all 11 LoadBalancer services to share 10.0.20.200:
- Update annotations: metallb.universe.tf → metallb.io
- Pin all services to 10.0.20.200 with allow-shared-ip: shared
- Standardize externalTrafficPolicy to Cluster (required for IP sharing)
- Remove redundant port 80 (roundcube) from mailserver LB
- Update CoreDNS forward: 10.0.20.204 → 10.0.20.200
- Update cloudflared tunnel target: 10.0.20.202 → 10.0.20.200
Services consolidated: coturn, headscale, kms, qbittorrent, shadowsocks,
torrserver, wireguard, mailserver, traefik, xray, technitium
2026-03-24 18:35:43 +02:00
annotations = {
2026-04-06 11:54:45 +03:00
" metallb.io/loadBalancerIPs " = " 10.0.20.201 "
consolidate MetalLB IPs: 5 → 1 (10.0.20.200)
Migrate all 11 LoadBalancer services to share 10.0.20.200:
- Update annotations: metallb.universe.tf → metallb.io
- Pin all services to 10.0.20.200 with allow-shared-ip: shared
- Standardize externalTrafficPolicy to Cluster (required for IP sharing)
- Remove redundant port 80 (roundcube) from mailserver LB
- Update CoreDNS forward: 10.0.20.204 → 10.0.20.200
- Update cloudflared tunnel target: 10.0.20.202 → 10.0.20.200
Services consolidated: coturn, headscale, kms, qbittorrent, shadowsocks,
torrserver, wireguard, mailserver, traefik, xray, technitium
2026-03-24 18:35:43 +02:00
}
2026-03-17 21:42:16 +00:00
}
spec {
type = " LoadBalancer "
port {
2026-04-06 11:54:45 +03:00
name = " dns-udp "
2026-03-17 21:42:16 +00:00
port = 53
protocol = " UDP "
}
2026-04-06 11:54:45 +03:00
port {
name = " dns-tcp "
port = 53
protocol = " TCP "
}
external_traffic_policy = " Local "
2026-03-17 21:42:16 +00:00
selector = {
" dns-server " = " true "
}
}
}
2026-04-06 11:54:45 +03:00
# Fixed ClusterIP for CoreDNS forwarding — bypasses MetalLB entirely.
# IP 10.96.0.53 is pinned so it survives Service recreation.
resource " kubernetes_service " " technitium_dns_internal " {
metadata {
name = " technitium-dns-internal "
namespace = kubernetes_namespace . technitium . metadata [ 0 ] . name
labels = {
app = " technitium "
}
}
spec {
type = " ClusterIP "
cluster_ip = " 10.96.0.53 "
selector = {
" dns-server " = " true "
}
port {
name = " dns-udp "
port = 53
protocol = " UDP "
}
port {
name = " dns-tcp "
port = 53
protocol = " TCP "
}
}
}
2026-03-17 21:42:16 +00:00
module " ingress " {
source = " ../../../../modules/kubernetes/ingress_factory "
ingress_factory: replace `protected` bool with `auth` enum + audit pass across 100 stacks
Phase 3+4 of default-deny ingress plan. Replaces the `protected = bool` (default
false → unprotected) variable in `modules/kubernetes/ingress_factory` with
`auth = string` enum (default "required" → fail-closed). Touches every
ingress_factory caller so the audit decision is recorded explicitly in code.
ingress_factory (Phase 3):
- `auth = "required"`: standard Authentik forward-auth (the legacy
`protected = true` semantic).
- `auth = "public"`: forward-auth via the new `authentik-forward-auth-public`
middleware → dedicated public outpost → guest auto-bind. Logged-in users
keep their real identity.
- `auth = "none"`: no Authentik middleware. For Anubis-fronted content, native
client APIs (Git, /v2/, WebDAV), webhook receivers, the Authentik outpost
itself.
- `effective_anti_ai` default flips ON only when `auth = "none"` (auth-gated
ingresses don't need anti-AI noise; the auth flow already discourages bots).
Audit pass (Phase 4) across 96 ingress_factory call sites:
- 49 explicit `protected = true` → `auth = "required"`
- 8 explicit `protected = false` → `auth = "none"` (5) or `auth = "public"` (3)
- 64 previously-default (no protected line) → `auth = "required"` ADDED, then
reviewed individually:
* 9 Anubis-fronted (blog, www, kms, travel, f1, cyberchef, jsoncrack,
homepage, wrongmove UI, privatebin) → `auth = "none"`
* 22 native-client / programmatic surfaces (Forgejo Git+/v2/, webhook
handler, claude-memory MCP, Nextcloud WebDAV, Matrix, Vault CLI/OIDC,
xray VPN, ntfy, woodpecker webhooks, n8n triggers, ntfy push, dawarich
location ingestion, immich frame kiosk, headscale CP, send anonymous
drops, rybbit beacon, vaultwarden API, Authentik UI itself + outposts) →
`auth = "none"`
* Remaining ~33 → `auth = "required"` confirmed (admin tools, internal
UIs, services without app-level auth)
- Smoke-test promotions to `auth = "public"`: fire-planner public UI,
k8s-portal API, insta2spotify callback.
Three call sites in wrapper modules (`stacks/freedify/factory/`,
`stacks/reverse-proxy/modules/reverse_proxy/`) keep their internal `protected`
bool — they translate to `auth` internally, out of scope for this rename.
Behavior change: previously-default ingresses now fail closed (require
Authentik login) unless explicitly flipped to `auth = "none"` or
`auth = "public"`. This is the audit goal — no more accidentally-unprotected
surfaces. Sites that were intentionally public (Anubis content, native APIs,
webhooks) are now explicitly recorded as `auth = "none"`.
Drive-by: `modules/create-vm/main.tf` picked up cosmetic alignment via
`terraform fmt -recursive` during the audit. Behavior-neutral.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 18:53:49 +00:00
auth = " required "
2026-04-16 13:45:04 +00:00
dns_type = " proxied "
2026-03-17 21:42:16 +00:00
namespace = kubernetes_namespace . technitium . metadata [ 0 ] . name
name = " technitium "
tls_secret_name = var . tls_secret_name
port = 5380
service_name = " technitium-web "
extra_annotations = {
" gethomepage.dev/enabled " = " true "
" gethomepage.dev/description " = " Internal DNS Server and Recursive Resolver "
" gethomepage.dev/group " = " Infrastructure "
" gethomepage.dev/icon " : " technitium.png "
" gethomepage.dev/name " = " Technitium "
" gethomepage.dev/widget.type " = " technitium "
" gethomepage.dev/widget.url " = " http://technitium-web.technitium.svc.cluster.local:5380 "
" gethomepage.dev/widget.key " = var . homepage_token
" gethomepage.dev/widget.range " = " LastWeek "
" gethomepage.dev/widget.fields " = " [ \ " totalQueries \ " , \ " totalCached \ " , \ " totalBlocked \ " , \ " totalRecursive \ " ] "
" gethomepage.dev/pod-selector " = " "
}
}
2026-04-17 18:55:52 +00:00
# DoH ingress removed — dns.viktorbarzin.me was externally unreachable and unused.
# DNS is served on UDP/TCP port 53 via the LoadBalancer service (10.0.20.201).
# module "ingress-doh" {
# source = "../../../../modules/kubernetes/ingress_factory"
# namespace = kubernetes_namespace.technitium.metadata[0].name
# name = "technitium-doh"
# tls_secret_name = var.tls_secret_name
# host = "dns"
# service_name = "technitium-web"
# }
2026-03-17 21:42:16 +00:00
2026-04-06 13:00:49 +03:00
# ExternalSecret for Technitium MySQL password (Vault auto-rotation)
resource " kubernetes_manifest " " external_secret " {
manifest = {
apiVersion = " external-secrets.io/v1beta1 "
kind = " ExternalSecret "
metadata = {
name = " technitium-db-creds "
namespace = kubernetes_namespace . technitium . metadata [ 0 ] . name
}
spec = {
refreshInterval = " 15m "
secretStoreRef = {
name = " vault-database "
kind = " ClusterSecretStore "
}
target = {
name = " technitium-db-creds "
}
data = [ {
secretKey = " db_password "
remoteRef = {
key = " static-creds/mysql-technitium "
property = " password "
}
} ]
}
}
depends_on = [ kubernetes_namespace . technitium ]
}
data " kubernetes_secret " " technitium_db_creds " {
metadata {
name = " technitium-db-creds "
namespace = kubernetes_namespace . technitium . metadata [ 0 ] . name
}
depends_on = [ kubernetes_manifest . external_secret ]
}
2026-04-16 13:45:04 +00:00
# Grafana datasource for Technitium DNS query logs in PostgreSQL
2026-03-17 21:42:16 +00:00
resource " kubernetes_config_map " " grafana_technitium_datasource " {
metadata {
name = " grafana-technitium-datasource "
namespace = " monitoring "
labels = {
grafana_datasource = " 1 "
}
}
data = {
" technitium-datasource.yaml " = yamlencode ( {
apiVersion = 1
datasources = [ {
2026-04-16 13:45:04 +00:00
name = " Technitium PostgreSQL "
type = " postgres "
2026-03-17 21:42:16 +00:00
access = " proxy "
2026-04-16 13:45:04 +00:00
url = " ${ var . postgresql_host } :5432 "
2026-03-17 21:42:16 +00:00
database = " technitium "
user = " technitium "
2026-04-16 13:45:04 +00:00
uid = " technitium-pg "
jsonData = {
sslmode = " disable "
postgresVersion = 1600
timescaledb = false
}
2026-03-17 21:42:16 +00:00
secureJsonData = {
2026-04-06 13:00:49 +03:00
password = data . kubernetes_secret . technitium_db_creds . data [ " db_password " ]
2026-03-17 21:42:16 +00:00
}
} ]
} )
}
}
# Grafana dashboard for Technitium DNS query logs
resource " kubernetes_config_map " " grafana_technitium_dashboard " {
metadata {
name = " grafana-technitium-dashboard "
namespace = " monitoring "
labels = {
grafana_dashboard = " 1 "
}
feat: organize Grafana dashboards into folders
Enable sidecar folderAnnotation + foldersFromFilesStructure to group
26 dashboards into 5 managed folders:
- Cluster (6): k8s health, API server, nodes, pods, kube-state-metrics
- Networking (6): CoreDNS, Technitium, Headscale, ingress, network traffic
- Hardware (5): node-exporter, proxmox, iDRAC, UPS, NVIDIA GPU
- Operations (4): backup health, registry, audit logs, Loki
- Applications (2): realestate-crawler, qBittorrent
Dashboard-to-folder mapping defined in grafana.tf locals block.
External stacks (headscale, technitium) annotated individually.
2026-03-28 16:23:49 +02:00
annotations = {
grafana_folder = " Networking "
}
2026-03-17 21:42:16 +00:00
}
data = {
" technitium-dns.json " = file ( " ${ path . module } /dashboards/technitium-dns.json " )
}
}
2026-04-06 13:00:49 +03:00
# CronJob to sync Vault-rotated MySQL password into Technitium's app config
resource " kubernetes_cron_job_v1 " " technitium_password_sync " {
metadata {
name = " technitium-password-sync "
namespace = kubernetes_namespace . technitium . metadata [ 0 ] . name
}
spec {
schedule = " 0 */6 * * * "
successful_jobs_history_limit = 1
failed_jobs_history_limit = 3
job_template {
metadata { }
spec {
template {
metadata { }
spec {
container {
name = " sync "
image = " curlimages/curl:latest "
resources {
requests = {
cpu = " 10m "
memory = " 32Mi "
}
limits = {
memory = " 32Mi "
}
}
env {
name = " DB_PASSWORD "
value_from {
secret_key_ref {
name = " technitium-db-creds "
key = " db_password "
}
}
}
env {
name = " TECH_USER "
value = var . technitium_username
}
env {
name = " TECH_PASS "
value = var . technitium_password
}
command = [ " /bin/sh " , " -c " , < < - EOT
set - e
TOKEN =$ $ ( curl - sf " http://technitium-web:5380/api/user/login?user= $ $ TECH_USER&pass= $ $ TECH_PASS " | grep - o ' " token " : " [^ " ] * " ' | cut -d' " ' - f4 )
if [ - z " $ $ TOKEN " ] ; then echo " Login failed " ; exit 1 ; fi
truenas deprecation: migrate all non-immich storage to proxmox NFS
- Migrate 7 backup CronJobs to Proxmox host NFS (192.168.1.127)
(etcd, mysql, postgresql, nextcloud, redis, vaultwarden, plotting-book)
- Migrate headscale backup, ebook2audiobook, osm_routing to Proxmox NFS
- Migrate servarr (lidarr, readarr, soulseek) NFS refs to Proxmox
- Remove 79 orphaned TrueNAS NFS module declarations from 49 stacks
- Delete stacks/platform/modules/ (27 dead module copies, 65MB)
- Update nfs-truenas StorageClass to point to Proxmox (192.168.1.127)
- Remove iscsi DNS record from config.tfvars
- Fix woodpecker persistence config and alertmanager PV
Only Immich (8 PVCs, ~1.4TB) remains on TrueNAS.
2026-04-12 14:35:39 +01:00
2026-04-17 08:20:55 +00:00
# Uninstall MySQL + SQLite query log plugins if present.
# These must be REMOVED, not just disabled — Technitium re-enables
# disabled plugins on pod restart, causing 46+ GB/day of writes.
# Only PostgreSQL query logging should remain.
APPS =$ $ ( curl - sf " http://technitium-web:5380/api/apps/list?token= $ $ TOKEN " )
if echo " $ $ APPS " | grep - q ' Query Logs ( MySQL ) ' ; then
curl - sf - X POST " http://technitium-web:5380/api/apps/uninstall?token= $ $ TOKEN&name=Query%20Logs%20(MySQL) "
echo " MySQL query log plugin UNINSTALLED "
else
echo " MySQL query log plugin already absent "
fi
if echo " $ $ APPS " | grep - q ' Query Logs ( Sqlite ) ' ; then
curl - sf - X POST " http://technitium-web:5380/api/apps/uninstall?token= $ $ TOKEN&name=Query%20Logs%20(Sqlite) "
echo " SQLite query log plugin UNINSTALLED "
else
echo " SQLite query log plugin already absent "
fi
truenas deprecation: migrate all non-immich storage to proxmox NFS
- Migrate 7 backup CronJobs to Proxmox host NFS (192.168.1.127)
(etcd, mysql, postgresql, nextcloud, redis, vaultwarden, plotting-book)
- Migrate headscale backup, ebook2audiobook, osm_routing to Proxmox NFS
- Migrate servarr (lidarr, readarr, soulseek) NFS refs to Proxmox
- Remove 79 orphaned TrueNAS NFS module declarations from 49 stacks
- Delete stacks/platform/modules/ (27 dead module copies, 65MB)
- Update nfs-truenas StorageClass to point to Proxmox (192.168.1.127)
- Remove iscsi DNS record from config.tfvars
- Fix woodpecker persistence config and alertmanager PV
Only Immich (8 PVCs, ~1.4TB) remains on TrueNAS.
2026-04-12 14:35:39 +01:00
2026-04-17 08:20:55 +00:00
# Ensure PG plugin is loaded
if ! echo " $ $ APPS " | grep - q ' Query Logs ( Postgres ) ' ; then
2026-04-15 15:12:32 +00:00
echo " WARNING: PG plugin not loaded — reinstall manually via Technitium UI "
truenas deprecation: migrate all non-immich storage to proxmox NFS
- Migrate 7 backup CronJobs to Proxmox host NFS (192.168.1.127)
(etcd, mysql, postgresql, nextcloud, redis, vaultwarden, plotting-book)
- Migrate headscale backup, ebook2audiobook, osm_routing to Proxmox NFS
- Migrate servarr (lidarr, readarr, soulseek) NFS refs to Proxmox
- Remove 79 orphaned TrueNAS NFS module declarations from 49 stacks
- Delete stacks/platform/modules/ (27 dead module copies, 65MB)
- Update nfs-truenas StorageClass to point to Proxmox (192.168.1.127)
- Remove iscsi DNS record from config.tfvars
- Fix woodpecker persistence config and alertmanager PV
Only Immich (8 PVCs, ~1.4TB) remains on TrueNAS.
2026-04-12 14:35:39 +01:00
fi
2026-04-17 08:20:55 +00:00
# Configure PG query logging (updates password from Vault rotation)
truenas deprecation: migrate all non-immich storage to proxmox NFS
- Migrate 7 backup CronJobs to Proxmox host NFS (192.168.1.127)
(etcd, mysql, postgresql, nextcloud, redis, vaultwarden, plotting-book)
- Migrate headscale backup, ebook2audiobook, osm_routing to Proxmox NFS
- Migrate servarr (lidarr, readarr, soulseek) NFS refs to Proxmox
- Remove 79 orphaned TrueNAS NFS module declarations from 49 stacks
- Delete stacks/platform/modules/ (27 dead module copies, 65MB)
- Update nfs-truenas StorageClass to point to Proxmox (192.168.1.127)
- Remove iscsi DNS record from config.tfvars
- Fix woodpecker persistence config and alertmanager PV
Only Immich (8 PVCs, ~1.4TB) remains on TrueNAS.
2026-04-12 14:35:39 +01:00
PG_CONFIG =" { \ " enableLogging \ " :true, \ " maxQueueSize \ " :1000000, \ " maxLogDays \ " :90, \ " maxLogRecords \ " :0, \ " data baseName \ " : \ " technitium \ " , \ " connectionString \ " : \ " Host =$ { var . postgresql_host } ; Port =5432 ; Username =technitium ; Password =$ $ DB_PASSWORD ; \ " } "
curl - sf - X POST " http://technitium-web:5380/api/apps/config/set?token= $ $ TOKEN " - - data -urlencode " name=Query Logs (Postgres) " - - data -urlencode " config= $ $ PG_CONFIG "
2026-04-16 13:45:04 +00:00
echo " PG logging configured on primary "
2026-04-17 08:20:55 +00:00
# Uninstall MySQL/SQLite on secondary and tertiary instances too
2026-04-16 13:45:04 +00:00
for INST in http : //technitium-secondary-web:5380 http://technitium-tertiary-web:5380; do
echo " Configuring $ $ INST "
R_TOKEN =$ $ ( curl - sf " $ $ INST/api/user/login?user= $ $ TECH_USER&pass= $ $ TECH_PASS " | grep - o ' " token " : " [^ " ] * " ' | cut -d' " ' - f4 )
if [ - z " $ $ R_TOKEN " ] ; then echo " Login failed for $ $ INST, skipping " ; continue ; fi
2026-04-17 08:20:55 +00:00
R_APPS =$ $ ( curl - sf " $ $ INST/api/apps/list?token= $ $ R_TOKEN " )
echo " $ $ R_APPS " | grep - q ' Query Logs ( MySQL ) ' && curl - sf - X POST " $ $ INST/api/apps/uninstall?token= $ $ R_TOKEN&name=Query%20Logs%20(MySQL) " && echo " MySQL uninstalled on $ $ INST "
echo " $ $ R_APPS " | grep - q ' Query Logs ( Sqlite ) ' && curl - sf - X POST " $ $ INST/api/apps/uninstall?token= $ $ R_TOKEN&name=Query%20Logs%20(Sqlite) " && echo " SQLite uninstalled on $ $ INST "
2026-04-16 13:45:04 +00:00
done
echo " Password sync complete "
2026-04-06 13:00:49 +03:00
EOT
]
}
restart_policy = " OnFailure "
}
}
}
}
}
}
2026-04-08 18:43:15 +00:00
# CronJob to configure Split Horizon AddressTranslation on all Technitium instances.
# Translates 176.12.22.76 (public IP) → 10.0.20.200 (Traefik LB) in DNS responses
# for 192.168.1.x clients, fixing hairpin NAT on the TP-Link router.
# Also configures DNS Rebinding Protection to allow viktorbarzin.me to return private IPs
# (otherwise the translated 10.0.20.200 gets stripped as a rebinding attack).
resource " kubernetes_cron_job_v1 " " technitium_split_horizon_sync " {
metadata {
name = " technitium-split-horizon-sync "
namespace = kubernetes_namespace . technitium . metadata [ 0 ] . name
}
spec {
schedule = " 15 */6 * * * "
successful_jobs_history_limit = 1
failed_jobs_history_limit = 3
job_template {
metadata { }
spec {
template {
metadata { }
spec {
container {
name = " sync "
image = " curlimages/curl:latest "
resources {
requests = {
cpu = " 10m "
memory = " 32Mi "
}
limits = {
memory = " 32Mi "
}
}
env {
name = " TECH_USER "
value = var . technitium_username
}
env {
name = " TECH_PASS "
value = var . technitium_password
}
command = [ " /bin/sh " , " -c " , < < - EOT
set - e
SPLIT_CONFIG =' { " networks " : { } , " enableAddressTranslation " : true , " domainGroupMap " : { } , " networkGroupMap " : { " 192.168.1.0/24 " : " sofia-lan " } , " groups " : [ { " name " : " sofia-lan " , " enabled " : true , " translateReverseLookups " : false , " externalToInternalTranslation " : { " 176.12.22.76 " : " 10.0.20.200 " } } ] } '
REBINDING_CONFIG =' { " enableProtection " : true , " bypassNetworks " : [ ] , " privateNetworks " : [ " 10.0.0.0/8 " , " 127.0.0.0/8 " , " 172.16.0.0/12 " , " 192.168.0.0/16 " , " 169.254.0.0/16 " , " fc00::/7 " , " fe80::/10 " ] , " privateDomains " : [ " home.arpa " , " viktorbarzin.me " ] } '
SPLIT_URL =" https://download.technitium.com/dns/apps/SplitHorizonApp-v10.zip "
REBINDING_URL =" https://download.technitium.com/dns/apps/DnsRebindingProtectionApp-v4.zip "
INSTANCES =" http://technitium-web:5380 http://technitium-secondary-web:5380 http://technitium-tertiary-web:5380 "
for INST in $ $ INSTANCES ; do
echo " === Configuring $ $ INST === "
TOKEN =$ $ ( curl - sf " $ $ INST/api/user/login?user= $ $ TECH_USER&pass= $ $ TECH_PASS " | grep - o ' " token " : " [^ " ] * " ' | cut -d' " ' - f4 )
if [ - z " $ $ TOKEN " ] ; then echo " Login failed for $ $ INST, skipping " ; continue ; fi
curl - sf - X POST " $ $ INST/api/apps/downloadAndInstall?token= $ $ TOKEN&name=Split%20Horizon&url= $ $ SPLIT_URL " | | true
curl - sf - X POST " $ $ INST/api/apps/downloadAndInstall?token= $ $ TOKEN&name=DNS%20Rebinding%20Protection&url= $ $ REBINDING_URL " | | true
curl - sf - X POST " $ $ INST/api/apps/config/set?token= $ $ TOKEN " - - data -urlencode " name=Split Horizon " - - data -urlencode " config= $ $ SPLIT_CONFIG "
curl - sf - X POST " $ $ INST/api/apps/config/set?token= $ $ TOKEN " - - data -urlencode " name=DNS Rebinding Protection " - - data -urlencode " config= $ $ REBINDING_CONFIG "
echo " Done with $ $ INST "
done
echo " Split Horizon sync complete "
EOT
]
}
restart_policy = " OnFailure "
}
}
}
}
}
}
truenas deprecation: migrate all non-immich storage to proxmox NFS
- Migrate 7 backup CronJobs to Proxmox host NFS (192.168.1.127)
(etcd, mysql, postgresql, nextcloud, redis, vaultwarden, plotting-book)
- Migrate headscale backup, ebook2audiobook, osm_routing to Proxmox NFS
- Migrate servarr (lidarr, readarr, soulseek) NFS refs to Proxmox
- Remove 79 orphaned TrueNAS NFS module declarations from 49 stacks
- Delete stacks/platform/modules/ (27 dead module copies, 65MB)
- Update nfs-truenas StorageClass to point to Proxmox (192.168.1.127)
- Remove iscsi DNS record from config.tfvars
- Fix woodpecker persistence config and alertmanager PV
Only Immich (8 PVCs, ~1.4TB) remains on TrueNAS.
2026-04-12 14:35:39 +01:00
# CronJob to apply DNS performance optimizations:
# 1. Set minimum cache TTL to 60s (avoids frequent re-queries for short-TTL domains like headscale's 18s)
# 2. Create emrsn.org stub zone → NXDOMAIN (avoids forwarding 27K+ daily corporate domain queries to Cloudflare)
resource " kubernetes_cron_job_v1 " " technitium_dns_optimization " {
metadata {
name = " technitium-dns-optimization "
namespace = kubernetes_namespace . technitium . metadata [ 0 ] . name
}
spec {
schedule = " 30 */6 * * * "
successful_jobs_history_limit = 1
failed_jobs_history_limit = 3
job_template {
metadata { }
spec {
template {
metadata { }
spec {
container {
name = " sync "
image = " curlimages/curl:latest "
resources {
requests = {
cpu = " 10m "
memory = " 32Mi "
}
limits = {
memory = " 32Mi "
}
}
env {
name = " TECH_USER "
value = var . technitium_username
}
env {
name = " TECH_PASS "
value = var . technitium_password
}
command = [ " /bin/sh " , " -c " , < < - EOT
set - e
TOKEN =$ $ ( curl - sf " http://technitium-web:5380/api/user/login?user= $ $ TECH_USER&pass= $ $ TECH_PASS " | grep - o ' " token " : " [^ " ] * " ' | cut -d' " ' - f4 )
if [ - z " $ $ TOKEN " ] ; then echo " Login failed " ; exit 1 ; fi
# 1. Ensure minimum cache TTL is 60s (reduces re-queries for short-TTL domains)
curl - sf - X POST " http://technitium-web:5380/api/settings/set?token= $ $ TOKEN&cacheMinimumRecordTtl=60 "
echo " Cache minimum TTL set to 60s "
# 2. Stub zone for emrsn.org corporate domains
# Returns NXDOMAIN immediately instead of forwarding to Cloudflare upstream
curl - sf " http://technitium-web:5380/api/zones/create?token= $ $ TOKEN&domain=emrsn.org&type=Primary " | | true
curl - sf " http://technitium-web:5380/api/zones/options/set?token= $ $ TOKEN&zone=emrsn.org&zoneTransfer=Allow " | | true
echo " emrsn.org stub zone configured "
echo " DNS optimization sync complete "
EOT
]
}
restart_policy = " OnFailure "
}
}
}
}
}
}