Prerequisite for running more than one PiHole replica. A second replica with a fresh PVC would have come up serving DNS and blocking nothing, with a different admin password -- answers varying by which pod replied. pihole.toml's own ### CHANGED markers gave the exact inventory: four settings already came from env (declared), one came from the image, and exactly ONE operator-set value lived only on the PVC -- webserver.api.pwhash, a one-way hash nobody could recover. Separately the adlists were undeclared: one list and 72,234 gravity domains that nothing in this repo named. Declares both: - A SOPS secret feeding FTLCONF_webserver_api_password. webserver.api.password takes cleartext and FTL hashes it itself (verified: pihole setpassword IS setFTLConfigValue on that key), so the PVC's pwhash becomes derived state. - A level-triggered postStart hook that reconciles the declared adlist set into gravity.db -- adds, re-enables, and disables (never deletes) undeclared lists, running pihole -g only when something moved. A file would not work: v6's adlists.list import is migration-only, guarded on the DB not existing. The rendered hook was run in the live pod against throwaway copies of gravity.db, exercising all four paths. A fresh DB built from the image's own schema template has ZERO adlists, confirming this is load-bearing rather than cosmetic. Password kept rather than set empty: the pihole Service still publishes port 80 past the oauth2-proxy, closed only by policy, and default-deny has drifted here before. Multi-replica is phase 2 and deliberately not in this branch -- it needs a schema field, a StatefulSet (the config PVC is RWO), anti-affinity and a PDB. Adds a *.sops.yaml -- a sensitive path -- so approval-policy will block pending an operator /approve-override. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
73 lines
4.7 KiB
YAML
73 lines
4.7 KiB
YAML
# =================================================================================================
|
|
# PiHole WEB/API ADMIN PASSWORD — the one piece of PiHole state that was NOT declared anywhere.
|
|
# =================================================================================================
|
|
# WHY THIS FILE EXISTS. Measured on live prod 2026-10-04, `pihole.toml` marks every non-default
|
|
# setting with `### CHANGED`, and names the ones that came from the environment `### CHANGED (env)`.
|
|
# The complete list was:
|
|
#
|
|
# dns.upstreams ### CHANGED (env) <- declared, catalog/pihole.yaml
|
|
# dns.listeningMode ### CHANGED (env) <- declared, catalog/pihole.yaml
|
|
# dns.rateLimit.count ### CHANGED (env) <- declared, catalog/pihole.yaml
|
|
# misc.dnsmasq_lines ### CHANGED (env) <- declared, catalog/pihole.yaml
|
|
# files.macvendor ### CHANGED <- set by the image itself, container path
|
|
# webserver.api.pwhash ### CHANGED <- SET OUT OF BAND. Declared NOWHERE. This file.
|
|
#
|
|
# So exactly ONE operator-set value lived only on the Longhorn PVC: the admin password, as a
|
|
# one-way BALLOON-SHA256 hash. Nobody could recover it (a hash does not reverse), nothing in git
|
|
# produced it, and a rebuild from git would have come up with a DIFFERENT password — or none.
|
|
#
|
|
# THE REASON THIS BLOCKS HA, which is why it is fixed first. The next step is running more than one
|
|
# PiHole replica, and a replica with a fresh PVC would come up with its own unrelated password. The
|
|
# admin UI would then accept or reject the same credential depending on which pod answered. Declaring
|
|
# it here makes every replica, and every rebuild, identical by construction.
|
|
#
|
|
# MECHANISM: `webserver.api.password` is a real, settable FTL config key — verified in the running
|
|
# image, where `pihole setpassword` is itself just `setFTLConfigValue "webserver.api.password"` with
|
|
# the comment "pihole-FTL will automatically hash the password". So FTL takes CLEARTEXT on this key
|
|
# and writes the hash into `webserver.api.pwhash` itself. catalog/pihole.yaml therefore passes this
|
|
# value as `FTLCONF_webserver_api_password`, and the hash on the PVC becomes a derived artefact
|
|
# rather than the source of truth.
|
|
#
|
|
# DEFENCE IN DEPTH, NOT THE ONLY GATE. The admin UI is already behind the oauth2-proxy sidecar
|
|
# (`--allowed-group=platform-admins`), so this password is a SECOND gate. It is kept rather than set
|
|
# empty because the `pihole` Service still publishes port 80 (PiHole direct, bypassing the proxy on
|
|
# :4180) and only the CiliumNetworkPolicy closes it — and the default-deny policy in this repo has a
|
|
# documented drift history. An unauthenticated admin UI that can rewrite LAN DNS is not a good thing
|
|
# to leave resting on one policy staying correct. Narrowing that Service port is tracked follow-up.
|
|
#
|
|
# ROTATION: `sops` this file in your own terminal, change the value, commit. Flux re-applies the
|
|
# Secret, the pod restarts, FTL re-hashes. Do NOT `sops -d` it into a shared terminal or a log
|
|
# (CLAUDE.md section 6); read it in an editor. The value below was generated randomly at authoring
|
|
# time and has never been printed to a transcript.
|
|
apiVersion: v1
|
|
kind: Secret
|
|
metadata:
|
|
name: pihole-bootstrap
|
|
namespace: pihole
|
|
labels:
|
|
app.kubernetes.io/name: pihole
|
|
type: Opaque
|
|
stringData:
|
|
#ENC[AES256_GCM,data:TMOuRyJeb5IVn3lZPXY7lgh1KGxu7GY9+WIOLHpMB50JqzmM2tHIaWUWKTt1JtZoX/E5/Y7WCUTnfkJLhRBN2zV2MwWRBSLYVy5CMg7xV8PTLRZJYAVUYS5Trg==,iv:q+DpfT6PQfY5ZuNsmP7Ddl+PDLjSm89fJICAhYoW64k=,tag:axijaO3DaxIZgZ3LeK2hXA==,type:comment]
|
|
webui-password: ENC[AES256_GCM,data:7FcK/bUW79M46f/MlouAmATKbMnoUTNuMUjtqJ5vpU4=,iv:I8DBFB39uXZV42Ie2IPwSefmMgdKEtiDknfc9Tu7h5E=,tag:L423784UrDU3TvylpHc6KQ==,type:str]
|
|
sops:
|
|
kms: []
|
|
gcp_kms: []
|
|
azure_kv: []
|
|
hc_vault: []
|
|
age:
|
|
- recipient: age175l8mdpdfu7hq0aml3exs3gykarwjmuvr23sq56s3wjejnyk44wsp4mfzg
|
|
enc: |
|
|
-----BEGIN AGE ENCRYPTED FILE-----
|
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0MkRSNjZLZlpXYkcvaVJL
|
|
cjlqb2wrcWREZnZLdGpsTHNPWEpseHlRZnpjCnJVM1FDQ2M2dXlaS1JBZ2k2V1BP
|
|
Yi9QUDZSMzZsL09pTi93RTdjdGxtY2MKLS0tIDIrQTZIVWdhMGIxSCtlUFN2Mlho
|
|
SWVybld4VjJyaHRSKzZZU0xsUUV2SEEKbDYHV/qBZVspK7DpBW3vYjRMJ/HmAN+9
|
|
G9bOlNEp0jAtcNZEn6lpQ9ORgvmIrhJHoBQRLabdMWzNWSZsNn56tA==
|
|
-----END AGE ENCRYPTED FILE-----
|
|
lastmodified: "2026-10-04T06:01:27Z"
|
|
mac: ENC[AES256_GCM,data:rhTMTOwLOtHXHgqI5M2ZW/6w+UpLpxBlTk7NoN0wmGSqOcE+OGrXA1DhkZrJweyQT0RSNSSBm/6QMztLP81eepOzdp2rEkwvbQPK/l6K9g16Ww9c1IatS3NQsKEB8AoMDx7jBQ6exXYgggPx3MJsNYbE+WyloQ96U9KPdrecAM8=,iv:n34v2zX9D5xsaR1PFCH94mNMiuRfhUyI2lseUCoqiTA=,tag:9YmN9nIVLXfsGYIb0bUl2w==,type:str]
|
|
pgp: []
|
|
encrypted_regex: ^(data|stringData)$
|
|
version: 3.9.4
|