Files
home-network-k3s-impl/apps/wave-0/pihole.yaml
T
jakman85 b184a42729 merge: internal services stop advertising themselves in public DNS
Six exposure:internal services published A records into the public Cloudflare
zone answering 192.168.2.20, disclosing the internal subnet, the Gateway VIP
and a full service inventory, and forming a DNS-rebind primitive that is why
the SOHO router strips these answers in the first place.

The renderer now stamps external-dns.alpha.kubernetes.io/controller: none on
every HTTPRoute it renders for an internal service, derived from the exposure
field alone — so a service that omits exposure defaults to internal and is
likewise unpublishable. Established against the deployed v0.21.0's own source,
where the controller check runs before hostname resolution in both configured
sources, then proven live on a throwaway hostname including that adding the
annotation to an existing record triggers the DELETE.

Deletes exactly 12 records: the A and TXT pair for jackett, pihole,
qbittorrent, radarr, semaphore and sonarr. A 39-record recovery baseline is
committed alongside.

Safe to land before the DHCP change, contrary to my earlier caution: the
router returns NOTHING for all six names today, so these records serve nobody
on the LAN, while Pi-hole at 192.168.2.23 already answers for anyone pointed
at it.

The five exposure:public services are untouched. They remain unreachable
publicly, which is a separate problem: the legacy edge at 136.51.115.252
returns HTTP 403 for our hostnames and serves a placeholder certificate, so
nothing is reverse-proxying them today.
2026-09-18 20:50:43 -05:00

274 lines
6.9 KiB
YAML

---
# Namespace (trust zone; default-deny + policies apply)
apiVersion: v1
kind: Namespace
metadata:
name: pihole
labels:
homelab.keithjackson.dev/app-namespace: 'true'
gateway-access: shared
annotations:
homelab.keithjackson.dev/backup-policy: daily-offcluster
---
# Default-deny baseline (R-NET-01): deny all, allow only DNS
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
namespace: pihole
labels:
homelab.keithjackson.dev/baseline: default-deny
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
ingress: []
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
---
# PVC pihole-config (R-STATE)
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pihole-config
namespace: pihole
labels:
app.kubernetes.io/name: pihole
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn
resources:
requests:
storage: 2Gi
---
# Deployment (R-PLAT-02)
apiVersion: apps/v1
kind: Deployment
metadata:
name: pihole
namespace: pihole
labels:
app.kubernetes.io/name: pihole
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: pihole
template:
metadata:
labels:
app.kubernetes.io/name: pihole
homelab.keithjackson.dev/egress-class: internet
homelab.keithjackson.dev/auth-mode: forward-auth
annotations:
homelab.keithjackson.dev/auth-mode: forward-auth
homelab.keithjackson.dev/auth-groups: platform-admins
spec:
containers:
- name: pihole
image: pihole/pihole
ports:
- containerPort: 80
env:
- name: FTLCONF_dns_listeningMode
value: ALL
- name: FTLCONF_dns_upstreams
value: 8.8.8.8;8.8.4.4
- name: FTLCONF_dns_rateLimit_count
value: '0'
- name: FTLCONF_misc_dnsmasq_lines
value: address=/${BASE_DOMAIN}/${GATEWAY_VIP};address=/unifi.${BASE_DOMAIN}/${UNIFI_L4_VIP};address=/crafty-mc.${BASE_DOMAIN}/${CRAFTY_L4_VIP};address=/pihole-dns.${BASE_DOMAIN}/${PIHOLE_DNS_VIP}
volumeMounts:
- name: config
mountPath: /data/config
- name: oauth2-proxy
image: quay.io/oauth2-proxy/oauth2-proxy:v7.8.1
imagePullPolicy: IfNotPresent
args:
- --http-address=0.0.0.0:4180
- --reverse-proxy=true
- --provider=oidc
- --oidc-issuer-url=https://keycloak.${BASE_DOMAIN}/realms/homelab
- --upstream=http://127.0.0.1:80
- --email-domain=*
- --scope=openid email profile
- --oidc-groups-claim=groups
- --allowed-group=platform-admins
- --skip-provider-button=true
- --redirect-url=https://pihole.${BASE_DOMAIN}/oauth2/callback
- --cookie-domain=.${BASE_DOMAIN}
- --whitelist-domain=.${BASE_DOMAIN}
- --cookie-secure=true
- --pass-access-token=true
- --set-xauthrequest=true
env:
- name: OAUTH2_PROXY_CLIENT_ID
valueFrom:
secretKeyRef:
name: pihole-oauth2-proxy
key: client-id
- name: OAUTH2_PROXY_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: pihole-oauth2-proxy
key: client-secret
- name: OAUTH2_PROXY_COOKIE_SECRET
valueFrom:
secretKeyRef:
name: pihole-oauth2-proxy
key: cookie-secret
ports:
- name: http
containerPort: 4180
protocol: TCP
resources:
requests:
cpu: 25m
memory: 64Mi
limits:
memory: 128Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
livenessProbe:
httpGet:
path: /ping
port: http
initialDelaySeconds: 5
periodSeconds: 15
readinessProbe:
httpGet:
path: /ping
port: http
initialDelaySeconds: 5
periodSeconds: 15
volumes:
- name: config
persistentVolumeClaim:
claimName: pihole-config
---
# Service
apiVersion: v1
kind: Service
metadata:
name: pihole
namespace: pihole
labels:
app.kubernetes.io/name: pihole
spec:
selector:
app.kubernetes.io/name: pihole
ports:
- name: port-80
port: 80
targetPort: 80
- name: authproxy
port: 4180
targetPort: 4180
---
# CiliumNetworkPolicy gateway-ingress allow (R-NET-01/02): pihole-allow-gateway-ingress
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: pihole-allow-gateway-ingress
namespace: pihole
labels:
app.kubernetes.io/name: pihole
spec:
endpointSelector:
matchLabels:
app.kubernetes.io/name: pihole
ingress:
- fromEntities:
- ingress
toPorts:
- ports:
- port: '4180'
protocol: TCP
---
# CiliumNetworkPolicy world egress allow (egress class governs the path; R-NET-05): pihole-allow-internet-egress
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: pihole-allow-internet-egress
namespace: pihole
labels:
app.kubernetes.io/name: pihole
spec:
endpointSelector:
matchLabels:
app.kubernetes.io/name: pihole
egress:
- toEntities:
- world
- toEndpoints:
- matchLabels:
io.kubernetes.pod.namespace: keycloak
app: keycloak
---
# HTTPRoute pihole-80 (R-TLS/R-DNS)
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: pihole-80
namespace: pihole
labels:
app.kubernetes.io/name: pihole
annotations:
homelab.keithjackson.dev/auth-mode: forward-auth
homelab.keithjackson.dev/auth-groups: platform-admins
external-dns.alpha.kubernetes.io/controller: none
spec:
parentRefs:
- name: shared
namespace: gateway
sectionName: https
hostnames:
- pihole.${BASE_DOMAIN}
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: pihole
port: 4180
---
# PrometheusRule SLI alerts (R-OBS-01)
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: pihole-slis
namespace: pihole
labels:
release: kube-prometheus-stack
app.kubernetes.io/name: pihole
spec:
groups:
- name: pihole.slis
rules:
- alert: pihole-down
expr: kube_deployment_status_replicas_available{namespace="pihole", deployment="pihole"} == 0 or absent(kube_deployment_status_replicas_available{namespace="pihole", deployment="pihole"})
for: 5m
labels:
severity: critical
service: pihole
annotations:
summary: pihole is down
description: 'pihole: its Deployment has no available/ready replica for 5m, or the signal is gone entirely (SLI: up, via kube_deployment_status_replicas_available).'