FAQ¶
Common questions a maintainer or agent asks when working in this repository. For the longer-form design notes, follow the links into Architecture.
Where do secrets live and how do I add one?¶
Secrets never live in Git. They flow 1Password → ExternalSecret → Kubernetes Secret → workload. See Secret management for the full picture. To add one:
- For a per-app secret, add an
externalsecret.yamlto the app'sapp/directory: - Use
secretStoreRef.kind: ClusterSecretStore,name: onepassword-connect(reads theTalos1Password vault). - Reference the 1Password item by title, and have the app's
ks.yamldependsOnonepassword-connectinexternal-secrets. - For a cluster-wide value, decide which store it belongs in:
cluster-secrets: a 1Password item injected into every app'spostBuild.substituteFrom. Holds sensitive cluster-wide values such as${SECRET_DOMAIN},${SECRET_INTERNAL_DOMAIN}, and internal device DNS names.cluster-settings: a git-tracked ConfigMap incomponents/global-vars/for non-sensitive cluster-wide values. Anything in here is world-visible. It is currently empty (data: {}) but stays wired into every app's substitution.
How does Flux variable substitution work, and why must I escape $${VAR}?¶
The root Kustomization patches every child with postBuild.substituteFrom, so Flux replaces ${VAR}
tokens against cluster-secrets/cluster-settings at apply time.
- Undefined variables become the empty string. A typo silently blanks the value rather than erroring.
- Any literal
${VAR}you want to survive substitution (Grafana dashboard template variables, envsubst templates, shell snippets in ConfigMaps) must be escaped as$${VAR}so Flux passes through a literal${VAR}.
What is the difference between ${SECRET_DOMAIN} and ${SECRET_INTERNAL_DOMAIN}?¶
Both are substituted from cluster-secrets, but they are not an external/internal pair for app
hostnames:
${SECRET_DOMAIN}: the domain every app route uses. All routes follow${APP}.${SECRET_DOMAIN}; whether an app is LAN-only or publicly reachable is decided by which gateway the route attaches to (envoy-internalvsenvoy-external), not by the domain in its hostname.${SECRET_INTERNAL_DOMAIN}: kept for non-route uses only, such as device records for IPMI probe targets and the NAS S3 endpoint, plus the opnsense-dns domain filter. Do not add${SECRET_INTERNAL_DOMAIN}aliases to app routes. Each alias costs an OPNsense host-override record, and the record count has a hard ceiling above which publishing silently stops.
"Available under ${SECRET_DOMAIN}" for a home app therefore means internal DNS on the
envoy-internal gateway, not public exposure. See Networking and
Split DNS.
How do I force a reconcile?¶
Push your change, then walk the dependency chain top-down:
# 1. Reconcile the Git source
flux reconcile source git flux-system
# 2. Reconcile the dependency chain (if a Kustomization shows "not ready")
flux reconcile kustomization external-secrets -n external-secrets
flux reconcile kustomization onepassword-connect -n external-secrets
# 3. Reconcile the target app Kustomization
flux reconcile kustomization <app-name> -n <namespace>
# 4. Reconcile the HelmRelease
flux reconcile helmrelease <app-name> -n <namespace>
Many app Kustomizations dependsOn external-secrets/onepassword-connect, so reconcile that chain first if
apps report "dependency not ready". A ConfigMap content change does not roll pods, so follow up with
kubectl rollout restart deploy <app> -n <ns>.
Why is the repo public, and what must never be committed?¶
The repository is publicly readable, so anything committed is world-visible. Never commit:
- LAN IPs and internal CIDRs.
- Node and device hostnames, switch names, and MACs.
.lan/.internalhostnames and deployment topology.- Disk serials and vendor-specific device models that map the home network.
Use the ${SECRET_DOMAIN} / ${SECRET_INTERNAL_DOMAIN} placeholders and let Flux substitute the
real values at apply time. A CI guard (check_internal_identifiers.py, run by the security-scans
workflow) fails any pull request that introduces a LAN IP, node or device hostname, MAC, or
.lan/.internal hostname outside a small allowlist. Disk serials and device models are
deliberately not pattern-matched by the (public) script, so keeping those out is enforced by review
rather than CI. See Secret management.
Where do device addresses for monitoring live?¶
Not in ConfigMaps. Address and lookup tables (router backup inventories, SNMP/NUT targets, exporter
scrape lists) must be templated inside an ExternalSecret's target.template.data block and mounted
from the rendered Secret:
- Internal device DNS names and addresses live in the
cluster-secrets1Password item, not incluster-settings. - Do not render the table at boot via init + envsubst against a ConfigMap. That puts the template in Git. Template the file content inside the ExternalSecret and mount the rendered key directly.