From cbc34c699da095c48048b019ad48d57ef25903bc Mon Sep 17 00:00:00 2001 From: Oleksandr Berezovskyi Date: Sun, 1 Mar 2026 12:48:23 +0200 Subject: [PATCH] feat(k8s/pihole): add Pi-hole stack (deployment scaled to 0 for data migration) --- kubernetes/app/pihole/configmap-dnscrypt.yaml | 39 ++++++++++ kubernetes/app/pihole/deployment.yaml | 72 +++++++++++++++++++ kubernetes/app/pihole/ingress.yaml | 24 +++++++ kubernetes/app/pihole/namespace.yaml | 4 ++ kubernetes/app/pihole/networkpolicy.yaml | 31 ++++++++ kubernetes/app/pihole/pvc.yaml | 25 +++++++ kubernetes/app/pihole/secret.sops.yaml | 22 ++++++ kubernetes/app/pihole/service-dns.yaml | 20 ++++++ kubernetes/app/pihole/service.yaml | 12 ++++ kubernetes/config/cluster-vars.sops.yaml | 5 +- 10 files changed, 252 insertions(+), 2 deletions(-) create mode 100644 kubernetes/app/pihole/configmap-dnscrypt.yaml create mode 100644 kubernetes/app/pihole/deployment.yaml create mode 100644 kubernetes/app/pihole/ingress.yaml create mode 100644 kubernetes/app/pihole/namespace.yaml create mode 100644 kubernetes/app/pihole/networkpolicy.yaml create mode 100644 kubernetes/app/pihole/pvc.yaml create mode 100644 kubernetes/app/pihole/secret.sops.yaml create mode 100644 kubernetes/app/pihole/service-dns.yaml create mode 100644 kubernetes/app/pihole/service.yaml diff --git a/kubernetes/app/pihole/configmap-dnscrypt.yaml b/kubernetes/app/pihole/configmap-dnscrypt.yaml new file mode 100644 index 0000000..4f4734a --- /dev/null +++ b/kubernetes/app/pihole/configmap-dnscrypt.yaml @@ -0,0 +1,39 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: dnscrypt-config + namespace: pihole +data: + dnscrypt-proxy.toml: | + listen_addresses = ['127.0.0.1:5353'] + max_clients = 250 + ipv4_servers = true + ipv6_servers = false + block_ipv6 = true + dnscrypt_servers = true + doh_servers = true + require_dnssec = false + force_tcp = false + timeout = 5000 + keepalive = 30 + lb_strategy = 'p2' + cache = true + cache_size = 4096 + cache_min_ttl = 2400 + cache_max_ttl = 86400 + http3 = true + http3_probe = true + + fallback_resolvers = ['8.8.8.8:53', '1.1.1.1:53'] + ignore_system_dns = true + + server_names = [] + + [static] + # Cloudflare DoH + [static.'cloudflare'] + stamp = 'sdns://AgcAAAAAAAAABzEuMC4wLjEAEmRucy5jbG91ZGZsYXJlLmNvbQovZG5zLXF1ZXJ5' + + # Google DoH + [static.'google'] + stamp = 'sdns://AgUAAAAAAAAABzguOC44LjggsKKKE4EwvtIbNjGjagI2607EdKSVHowYZtyvD9iPrkkHOC44LjguOAovZG5zLXF1ZXJ5' diff --git a/kubernetes/app/pihole/deployment.yaml b/kubernetes/app/pihole/deployment.yaml new file mode 100644 index 0000000..ac919f8 --- /dev/null +++ b/kubernetes/app/pihole/deployment.yaml @@ -0,0 +1,72 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: pihole + namespace: pihole + labels: + app: pihole +spec: + replicas: 0 + strategy: + type: Recreate + selector: + matchLabels: + app: pihole + template: + metadata: + labels: + app: pihole + spec: + containers: + - name: pihole + image: pihole/pihole:2025.08.0 + env: + - name: TZ + value: Europe/Kyiv + - name: FTLCONF_webserver_api_password + valueFrom: + secretKeyRef: + name: pihole-credentials + key: WEBPASSWORD + - name: FTLCONF_dns_listeningMode + value: all + - name: FTLCONF_dns_upstreams + value: "127.0.0.1#5353" + - name: FTLCONF_misc_etc_dnsmasq_d + value: "true" + ports: + - containerPort: 53 + protocol: TCP + name: dns-tcp + - containerPort: 53 + protocol: UDP + name: dns-udp + - containerPort: 80 + protocol: TCP + name: http + volumeMounts: + - name: pihole-config + mountPath: /etc/pihole + - name: pihole-dnsmasq + mountPath: /etc/dnsmasq.d + + - name: dnscrypt-proxy + image: klutchell/dnscrypt-proxy:latest + env: + - name: TZ + value: Europe/Kyiv + volumeMounts: + - name: dnscrypt-config + mountPath: /config/dnscrypt-proxy.toml + subPath: dnscrypt-proxy.toml + + volumes: + - name: pihole-config + persistentVolumeClaim: + claimName: pihole-config + - name: pihole-dnsmasq + persistentVolumeClaim: + claimName: pihole-dnsmasq + - name: dnscrypt-config + configMap: + name: dnscrypt-config diff --git a/kubernetes/app/pihole/ingress.yaml b/kubernetes/app/pihole/ingress.yaml new file mode 100644 index 0000000..80356c6 --- /dev/null +++ b/kubernetes/app/pihole/ingress.yaml @@ -0,0 +1,24 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: pihole + namespace: pihole + annotations: + cert-manager.io/cluster-issuer: letsencrypt + traefik.ingress.kubernetes.io/router.middlewares: authelia-chain-authelia-authelia-auth@kubernetescrd +spec: + tls: + - hosts: + - ${PIHOLE_HOST} + secretName: pihole-tls + rules: + - host: ${PIHOLE_HOST} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: pihole + port: + number: 80 diff --git a/kubernetes/app/pihole/namespace.yaml b/kubernetes/app/pihole/namespace.yaml new file mode 100644 index 0000000..9693809 --- /dev/null +++ b/kubernetes/app/pihole/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: pihole diff --git a/kubernetes/app/pihole/networkpolicy.yaml b/kubernetes/app/pihole/networkpolicy.yaml new file mode 100644 index 0000000..31788c4 --- /dev/null +++ b/kubernetes/app/pihole/networkpolicy.yaml @@ -0,0 +1,31 @@ +# Note: NetworkPolicy applies to pod-level traffic via the cluster network. +# DNS traffic on port 53 arrives via hostNetwork and bypasses these policies. +# These policies govern cluster-internal traffic (e.g. Traefik → pihole web UI). +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: default-deny-ingress + namespace: pihole +spec: + podSelector: {} + policyTypes: + - Ingress +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-ingress-controller + namespace: pihole +spec: + podSelector: + matchLabels: + app: pihole + policyTypes: + - Ingress + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: traefik + ports: + - port: 80 diff --git a/kubernetes/app/pihole/pvc.yaml b/kubernetes/app/pihole/pvc.yaml new file mode 100644 index 0000000..45fcf72 --- /dev/null +++ b/kubernetes/app/pihole/pvc.yaml @@ -0,0 +1,25 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: pihole-config + namespace: pihole +spec: + accessModes: + - ReadWriteOnce + storageClassName: nfs-synology-ssd + resources: + requests: + storage: 2Gi +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: pihole-dnsmasq + namespace: pihole +spec: + accessModes: + - ReadWriteOnce + storageClassName: nfs-synology-ssd + resources: + requests: + storage: 1Gi diff --git a/kubernetes/app/pihole/secret.sops.yaml b/kubernetes/app/pihole/secret.sops.yaml new file mode 100644 index 0000000..36f2c3b --- /dev/null +++ b/kubernetes/app/pihole/secret.sops.yaml @@ -0,0 +1,22 @@ +apiVersion: v1 +kind: Secret +metadata: + name: pihole-credentials + namespace: pihole +stringData: + WEBPASSWORD: ENC[AES256_GCM,data:Sl3wOaQ=,iv:Dfxr5s97cMJHK0R+Ve6AzMFcLpl7ilV9Pq8RLSvQQHE=,tag:5u+8u1DBrU0Gazm/4JxIBQ==,type:str] +sops: + age: + - recipient: age1zffnskvuezntkk703a0pyxsd5m8vx2hm33dr47wdfy8mn4fdw4sqgw0jgc + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBRTE1tbzhLdkJpeXFOSHF3 + Nlk1Tm83UVdoZytuQ01yLzhJUlJPdVdEWkhzClJ6MkxvSVZmMkEwNWJYM0R3MGxP + NHhsVElPTlJZeEZIakZGSWpXYWcvV2MKLS0tIGZPbkxvTDBnNVVKUk5YZXZOQVJ1 + dVRld2ZKdmdUNDgwZmpjeElvSXdydE0KZItCnGh6eHKJYtC5n5JLwLCfOWblh0iN + 4Q1O8uUXyz9EzTgY7CSAGpHo7N9pLznFr3AnOE4b2T5enDDnSK/tSg== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2026-03-01T10:40:42Z" + mac: ENC[AES256_GCM,data:fEe5fEqgRtru1dZ/rVeDWIiT0sY4Or5z4AR8gzBDuBoF/lJQz86v32CoqhboMHl3EPdvZtteg2WBDU4LuwOHmhLf4qpvPX2KxL9rbe9ND3wP2PoEK/cMO7lqIKCVDOOz3OtdEcR7iT1XLr7wze1x4W9MjqLMqvcGplkRls77hlU=,iv:2JTEavm5ftNOU51dNG73RxpqahBNAsXlm+QHnlWt3Cc=,tag:YBmATtmbnsWGY3348Kz3/w==,type:str] + encrypted_regex: ^(data|stringData|email)$ + version: 3.12.1 diff --git a/kubernetes/app/pihole/service-dns.yaml b/kubernetes/app/pihole/service-dns.yaml new file mode 100644 index 0000000..29400f9 --- /dev/null +++ b/kubernetes/app/pihole/service-dns.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: Service +metadata: + name: pihole-dns + namespace: pihole + annotations: + metallb.io/loadBalancerIPs: 10.127.1.200 +spec: + type: LoadBalancer + selector: + app: pihole + ports: + - port: 53 + targetPort: 53 + protocol: TCP + name: dns-tcp + - port: 53 + targetPort: 53 + protocol: UDP + name: dns-udp diff --git a/kubernetes/app/pihole/service.yaml b/kubernetes/app/pihole/service.yaml new file mode 100644 index 0000000..d1f53bc --- /dev/null +++ b/kubernetes/app/pihole/service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: pihole + namespace: pihole +spec: + selector: + app: pihole + ports: + - port: 80 + targetPort: 80 + name: http diff --git a/kubernetes/config/cluster-vars.sops.yaml b/kubernetes/config/cluster-vars.sops.yaml index 5e1bfaa..c285567 100644 --- a/kubernetes/config/cluster-vars.sops.yaml +++ b/kubernetes/config/cluster-vars.sops.yaml @@ -19,6 +19,7 @@ stringData: ARCHMIRROR_MIRROR_URL: ENC[AES256_GCM,data:cIORJWshvr4fL/OqyvplXllcrMdh3UMrt11cBqwgS12O3wGBgyULJNDcP7c2,iv:8Efs43us8xlUvkafWf15K5wqBoJnYLmC50j094taoFs=,tag:6hV2emMunQ1jOteRCANRsA==,type:str] PODSYNC_HOST: ENC[AES256_GCM,data:MK+WWo8R2uS45U8suBDusOp922YqngM=,iv:7QfuVU6ICEmpNwtgpnXa2phwP0+0pcmv8w3CJSLwvrA=,tag:z6qizhm8fzzDZq/726kKsQ==,type:str] PODSYNC_NFS_PATH: ENC[AES256_GCM,data:O1ZHSOsmwe57nY0T42pHOHcc/aB9,iv:FS4Yb9F4mzrvKni0hg6HD22R83v3YoGlDAeEPBc4RzE=,tag:f+Wi8BOPIVod/8upGZmw5A==,type:str] + PIHOLE_HOST: ENC[AES256_GCM,data:LysbDrRB3nmYkuJrvnNTJ4mlN52w3Q==,iv:pp1wubLuF69IRFJtkLlPC1EeW/whQNLwXeOiNvFlsPI=,tag:rSgcD4HmzJsynuPwU4GB/Q==,type:str] sops: age: - recipient: age1zffnskvuezntkk703a0pyxsd5m8vx2hm33dr47wdfy8mn4fdw4sqgw0jgc @@ -30,7 +31,7 @@ sops: LzhUN3Z4cExIL1IyS3ZCNWh5aWpLbDgKQ7c3MmLykA00NaLoctKVDfJvPqTqh3Ia cDZJUc6jYJXOJYM6YYyZOYcCL2z8V2RpIfA9sPg8PB2eiipZxjk+Cg== -----END AGE ENCRYPTED FILE----- - lastmodified: "2026-02-28T20:47:12Z" - mac: ENC[AES256_GCM,data:c8pE3AixjxpDSGwnTYrhHRDDXFAAhHs4zaveies6/4feWUY1o+26Z0aWQssWQaQCR9V5mo831B400jMg4tudbJflRHE6VV0ah5eFh5+N7M5vnbxrWHCwGW3Y5bAUXAuaMFDgOO5fCi+iryCC8WZe6FxqZTMawWAcjMq93X55jbY=,iv:RWU3PTXd1XOdmGbr87LSqUud1Aak8VzXzjLLorh2UHc=,tag:rNWOmU/W0NfIupMV9mMfig==,type:str] + lastmodified: "2026-03-01T10:40:47Z" + mac: ENC[AES256_GCM,data:ilvEM/b6Dvl6FpScV4yZAFnhv6M3M+UQ+sFIUT0wN95N1ndN0uk0vcgSlyaxKMZIj0UjepTqgu2i+/88WKuOlkDWoyRjdXsTcQvaOyVM5UQWwilzCZjpRCuYE+4UVQM6DVvBTplkhaBZcDSrTpBnHFyTdEb9iysTt2Ki6UP7Dx8=,iv:4UJ/fjG2KUQVBZIFwgCReZlgdItCTkh47HLJvPP7HoI=,tag:WGRK1ulWu2FgxawKpq3BfA==,type:str] encrypted_regex: ^(data|stringData|email)$ version: 3.12.1