Declarative configuration for my UniFi network, managed as
native Kubernetes resources with
Crossplane and the
provider-upjet-unifi
provider.
This repo is the desired state of the network (the WireGuard VPN client,
traffic routes, local DNS records, …), expressed as Crossplane Managed
Resources (Client, TrafficRoute, Record, …). It is reconciled
continuously on the platform: a
Flux Kustomization pulls this repo as a GitRepository and applies the
resources into the unifi namespace, and provider-upjet-unifi (installed as a
Crossplane provider package) reconciles each one against the UniFi controller
API. There is no Terraform and no separate state store — the Managed Resource
is the state, with its observed status in .status.atProvider.
this repo (Crossplane MRs)
│ Flux GitRepository + Kustomization → namespace: unifi
▼
provider-upjet-unifi (Crossplane, crossplane-system) ──► UniFi Controller API
Never let the first reconcile create or destroy live network config. A Managed Resource whose external object already exists must adopt it, not create a duplicate. To bring an existing object under management:
- Write the Managed Resource to match what already exists on the controller.
- Add the annotation
crossplane.io/external-name: <unifi-id>so Crossplane binds to the live object instead of creating a new one. - (Optional, safest) start it with
spec.managementPolicies: ["Observe"]so the first reconcile only reads the object; confirm.status.atProvidermatches, then widen to the default["*"]to manage it.
For a genuinely new object the network does not have yet (like everything shipped here today), no annotation is needed — Crossplane creates it. See the runbook for the step-by-step procedure and how to find a live object's id.
API-key auth (UniFi Controller ≥ 9.0.108). Use a dedicated service account with a
Limited Admin, Local Access Only role. This repo is public and holds no
secrets: the controller credentials live in the platform's secret store
(OpenBao) and are surfaced to Crossplane as a ProviderConfig whose secretRef
points at a Secret produced by an External Secret — never commit them here.
| Credential | Meaning |
|---|---|
api_url |
Controller base URL, without the /api path |
api_key |
API key (sensitive) |
site |
Site to manage (default default) |
allow_insecure |
Skip TLS verify — only for a self-signed cert |
The WireGuard VPN client additionally references two keys from a Secret in the
unifi namespace (cluster-wireguard): the gateway's own private key
(sensitive) and the Talos server's public key. Both are seeded by the
platform; see the runbook.
kustomize build . # render the Managed Resources
kustomize build . | kubeconform -strict -ignore-missing-schemas -summaryCRD-schema validation against the provider's own CRDs (in
provider-upjet-unifi
under package/crds/) happens authoritatively server-side at apply time, and can
be run locally by pointing kubeconform at those schemas. CI (.github/workflows/ci.yaml)
runs kustomize build + kubeconform and aggregates them into the single
required CI - Required Checks status.
This repo is the steady-state Crossplane model (it replaced an interim
OpenTofu + tofu-controller setup). Next steps are tracked in the
monorepo issues — e.g. a
cross-resource reference so a TrafficRoute can point at a Client by name
instead of a post-create network id, and bringing more of the network
(VLANs/WLANs/firewall) under management adopt-first.