Skip to content

feat: manage the UniFi network declaratively via tofu-controller#2042

Merged
botantler-1[bot] merged 13 commits into
mainfrom
claude/unifi-tenant
Jun 19, 2026
Merged

feat: manage the UniFi network declaratively via tofu-controller#2042
botantler-1[bot] merged 13 commits into
mainfrom
claude/unifi-tenant

Conversation

@devantler

@devantler devantler commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

What & why

Manage the UniFi network declaratively as a platform tenant. The desired
state lives in the public repo
devantler-tech/unifi (plain OpenTofu
using filipowm/unifi);
this PR installs tofu-controller and wires a unifi tenant so the cluster
reconciles that repo against the controller API.

devantler-tech/unifi (public; OpenTofu, filipowm/unifi)
   │  Flux GitRepository (ns: unifi, no auth — public, no secrets)
   ▼
Terraform CR (tofu-controller, infra.contrib.fluxcd.io/v1alpha2)
   │  tf-runner pod runs OpenTofu; state in a Secret in the unifi namespace
   ▼  ↑ unifi-credentials ← ExternalSecret ← OpenBao (openbao ClusterSecretStore)
UniFi Controller API   (reached from Hetzner)

The repo is public and holds no secrets; the only sensitive value (the UniFi
API key) lives in OpenBao and is pulled in via an ExternalSecret — exactly
like external-dns's Cloudflare token. A real-CRD provider-upjet-unifi
(Crossplane) is the tracked steady-state follow-up. See docs/unifi-management.md.

Contents

  • infrastructure/controllers/tofu-controller/ — HelmRepository + HelmRelease
    (flux-iac chart 0.16.4, installs the Terraform CRD), registered in the
    controllers kustomization. Prod-only.
  • providers/hetzner/unifi/ — namespace (restricted PSS), tf-runner SA +
    namespaced RBAC, CiliumNetworkPolicy, GitRepository (public, no auth),
    Terraform CR, and an ExternalSecret pulling the API creds from OpenBao.
  • bases/infrastructure/vault-config/job.yaml — adds the infra-unifi-readonly
    OpenBao policy and attaches it to the external-secrets role.
  • clusters/prod/unifi-flux-kustomization.yaml — dedicated, isolated,
    prod-only Flux Kustomization (wait: false, dependsOn: infrastructure) so a
    UniFi stall never blocks app deploys (same rationale as overprovisioning).
  • docs/unifi-management.md.

Safety (observe-first)

  • Terraform CR ships approvePlan: ""plan only, never applies. Review
    the plan, import existing objects until it's a no-op, then approve a specific
    plan id (or flip to auto) once trusted.
  • destroyResourcesOnDeletion: false — deleting the CR never tears down the network.

Gate (maintainer) — one secret; Kustomization is red by design until done

Generate a Limited Admin, Local Access Only API key (UniFi OS ≥ 9.0.108) and seed
OpenBao:

bao kv put secret/infrastructure/unifi/controller \
  api_url=https://<controller> api_key=<api-key>

(api_url omits the /api path.) GitOps alternative: SOPS-encrypt in
variables-cluster + a PushSecret under providers/hetzner/infrastructure/vault-seed/.

Validate on the live cluster before promoting

  • Runner ↔ controller mTLS: SPIRE require-mutual-auth applies to the
    tofu-controller ↔ tf-runner gRPC (:30000). Cilium auto-issues SPIFFE identities,
    so the L3/L4 allow should suffice — confirm the runner connects.
  • Runner PSS: namespace enforces restricted; if the tf-runner image can't
    run non-root, rely on Kyverno securityContext mutation or relax to baseline.
  • HelmRelease values: confirm chart 0.16.4 value paths and Kyverno fit.
  • Egress: networkpolicy.yaml allows world:443,8443; tighten to the
    controller address + OpenTofu registry FQDNs once known.

Validation done

ksail workload validate --config ksail.prod.yaml324 files validated, pass
(incl. the Terraform CR and ExternalSecret). kubectl kustomize builds clean.

Companion PRs: scaffold + docs in devantler-tech/unifi
(#1); submodule registration
monorepo#1861;
provider-upjet-unifi tracking issue monorepo#1860.

🤖 Generated with Claude Code

Install tofu-controller (flux-iac, chart 0.16.4) as a prod-only controller and
wire a `unifi` tenant that reconciles the UniFi network from the new
devantler-tech/unifi repo (plain OpenTofu, filipowm/unifi provider).

- providers/hetzner/infrastructure/controllers/tofu-controller/: HelmRepository
  + HelmRelease (installs the Terraform infra.contrib.fluxcd.io CRD), registered
  in the controllers kustomization.
- providers/hetzner/unifi/: namespace, tf-runner SA + RBAC, CiliumNetworkPolicy,
  GitRepository (private repo, token auth), Terraform CR (v1alpha2), and the
  credentials/git-auth Secrets (values substituted from variables-cluster).
- clusters/prod/unifi-flux-kustomization.yaml: dedicated, isolated, prod-only
  Flux Kustomization (wait:false, dependsOn infrastructure-controllers) so a
  UniFi stall never blocks app deploys — same pattern as overprovisioning.
- docs/unifi-management.md.

Observe-first by design: Terraform CR ships approvePlan:"" (plan only, never
applies) and destroyResourcesOnDeletion:false. Validated with
`ksail workload validate --config ksail.prod.yaml` (325 files, pass).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The devantler-tech/unifi repo is now public and holds no secrets, so:

- GitRepository drops its secretRef (no clone auth needed); remove the
  unifi-git-auth Secret and the unifi_git_token gate entirely.
- Replace the SOPS/variables-cluster-substituted unifi-credentials Secret with
  an ExternalSecret that reads infrastructure/unifi/controller from OpenBao via
  the shared `openbao` ClusterSecretStore — mirroring external-dns's Cloudflare
  token (non-bootstrap-critical infra credential).
- Add the infra-unifi-readonly OpenBao policy and attach it to the
  external-secrets role (vault-config job).
- unifi Flux Kustomization now dependsOn `infrastructure` (the ClusterSecretStore
  is an infrastructure-layer resource) and drops the now-unneeded SOPS decryption
  + postBuild substitution.

Single remaining gate: `bao kv put secret/infrastructure/unifi/controller
api_url=... api_key=...`. Validated with `ksail workload validate` (324 files).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts:
#	k8s/bases/infrastructure/vault-config/job.yaml
@botantler-1 botantler-1 Bot enabled auto-merge June 14, 2026 19:21
@devantler devantler marked this pull request as draft June 14, 2026 19:21
auto-merge was automatically disabled June 14, 2026 19:21

Pull request was converted to draft

@devantler devantler marked this pull request as ready for review June 14, 2026 19:28
@botantler-1 botantler-1 Bot added this pull request to the merge queue Jun 15, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jun 15, 2026
Resolve the conflict by taking main's clusters/prod/kustomization.yaml (which
dropped overprovisioning), and per maintainer direction relocate the UniFi
tenant to follow the proven tenant pattern:

- Move k8s/providers/hetzner/unifi/ -> k8s/providers/hetzner/apps/unifi/ and
  register it in providers/hetzner/apps/kustomization.yaml, so the shared `apps`
  Flux Kustomization reconciles it like wedding-app/ascoachingogvaner.
- Delete the dedicated clusters/prod/unifi-flux-kustomization.yaml.
- Prod-only via the hetzner apps overlay (tofu-controller + the controller API
  are Hetzner-only; the Terraform CRD is absent on local/CI).
- Docs updated: apps-layer placement + seed the OpenBao secret before merge
  (the apps layer is wait:true).

Validated: `ksail workload validate --config ksail.prod.yaml` (327 files).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@devantler devantler marked this pull request as draft June 16, 2026 17:42
Replace the external-dns-style `bao kv put` seeding with the platform's GitOps
seeding pattern so OpenBao is reproducible from Git (and self-heals after a vault
re-init):

- seed-unifi PushSecret (providers/hetzner/infrastructure/vault-seed/) pushes
  unifi_api_url + unifi_api_key from variables-cluster to
  secret/infrastructure/unifi/controller — mirrors seed-hcloud, prod-only.
- variables-cluster-secret.enc.yaml ships SOPS-encrypted PLACEHOLDERS
  (unifi_api_url=https://unifi.example.invalid, unifi_api_key=PLACEHOLDER…) so the
  chain is healthy on merge; the maintainer sets the real key by editing that one
  SOPS file.
- vault-seed-write policy gains secret/data/infrastructure/unifi/* write.
- Docs + ExternalSecret comment updated to the declarative flow.

Validated: `ksail workload validate --config ksail.prod.yaml` (328 files).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
devantler and others added 2 commits June 18, 2026 22:15
Drop SOPS-based seeding in favour of the same pattern provider-upjet-github uses:
the vault-config Job seeds a placeholder at secret/infrastructure/unifi/controller
on first run (only if absent, so a re-run never clobbers the real value), and the
maintainer overwrites it in place via the OpenBao UI. OpenBao is backed up
(Raft → R2), so the manually-set value is durable.

- vault-config job.yaml: add the "1c" placeholder-seed block (mirrors the GitHub
  App "1b" block); drop the now-unneeded vault-seed-write infrastructure/unifi/*
  grant (the Job seeds with its own admin token).
- Revert SOPS seeding: remove unifi_api_url/unifi_api_key from
  variables-cluster-secret.enc.yaml; delete the seed-unifi PushSecret + its
  kustomization entry.
- Keep infra-unifi-readonly + the ExternalSecret reading via the openbao
  ClusterSecretStore (unifi is a plain infra ns, like external-dns).
- Docs + ExternalSecret comment updated to the Job-placeholder + UI-overwrite flow.

Also merges origin/main (brings in the merged provider-upjet-github work).
Validated: `ksail workload validate --config ksail.prod.yaml` (356 files).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mit)

Completes the previous commit (which only captured the seed-unifi.yaml deletion):

- vault-config job.yaml: add the "1c" UniFi placeholder-seed block (mirrors the
  GitHub App "1b" block, idempotent — only if absent); drop the now-unneeded
  vault-seed-write infrastructure/unifi/* grant.
- variables-cluster-secret.enc.yaml: remove the SOPS unifi_api_url/unifi_api_key.
- vault-seed kustomization: de-register seed-unifi.
- ExternalSecret comment + docs: Job-placeholder + OpenBao-UI overwrite flow.

Validated: `ksail workload validate --config ksail.prod.yaml` (356 files).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@devantler devantler marked this pull request as ready for review June 18, 2026 20:23
Signed-off-by: Nikolai Emil Damm <nikolaiemildamm@icloud.com>
@botantler-1 botantler-1 Bot enabled auto-merge June 18, 2026 21:45
@botantler-1 botantler-1 Bot added this pull request to the merge queue Jun 18, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jun 18, 2026
@devantler devantler marked this pull request as draft June 19, 2026 06:12
@devantler devantler marked this pull request as ready for review June 19, 2026 06:17
@botantler-1 botantler-1 Bot enabled auto-merge June 19, 2026 06:20
@botantler-1 botantler-1 Bot added this pull request to the merge queue Jun 19, 2026
Merged via the queue into main with commit bbad708 Jun 19, 2026
10 of 11 checks passed
@botantler-1 botantler-1 Bot deleted the claude/unifi-tenant branch June 19, 2026 06:47
@github-project-automation github-project-automation Bot moved this from 🫴 Ready to ✅ Done in 🌊 Project Board Jun 19, 2026
@botantler-1

botantler-1 Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 1.69.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@botantler-1 botantler-1 Bot added the released label Jun 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: ✅ Done

Development

Successfully merging this pull request may close these issues.

2 participants