Skip to main content

PowerDNS Operator: The Last Layer a Self-Hosted PaaS Still Outsources

9 min readDora NodaDora Noda
Share
On this page

A self-hosted PaaS on a Cluster-API fleet already owns its compute (Cluster API reconciling Hetzner nodes), its TLS (cert-manager and Let's Encrypt issuing per-tenant certs), and its builds (buildpacks turning a git push into an image). The one layer that still routes through a vendor's REST API on every tenant custom-domain signup is DNS. PowerDNS Operator closes that gap — it turns DNS zones and records into Kubernetes Custom Resources, reconciled the same way Cluster API reconciles a machine. Here's what it actually buys, in YAML, and what it doesn't.

The Zone-Ownership Problem external-dns Doesn't Solve

external-dns is the tool most Kubernetes platforms already reach for when a Service or Ingress needs a DNS record. It's provider-agnostic across 40+ backends, and for a single-tenant cluster pointed at one domain, that's plenty. But external-dns has a hard assumption baked into its design: the zone it's writing records into already exists, fully configured, before it ever runs. It adds and removes records inside a zone. It never creates the zone itself.

That assumption breaks the moment a self-hosted PaaS needs to provision a new zone per tenant — or hand a tenant's own namespace the ability to manage records inside a zone the platform owns — without a human running a pdnsutil create-zone command or clicking through a DNS provider's dashboard first.

PowerDNS Operator, an Apache-2.0 project maintained by Orange Open Source, is built around exactly that gap. It defines four Custom Resource Definitions:

CRDScopePurpose
ClusterZoneCluster-wideA DNS zone available across all namespaces
ZoneNamespace-scopedA DNS zone owned by one namespace
ClusterRRsetCluster-wideA DNS record set visible from any namespace
RRsetNamespace-scopedA DNS record set scoped to its own namespace

Both zones and records get the cluster/namespace split. That's the detail external-dns has no equivalent for: PowerDNS Operator can create and destroy the zone itself, declaratively, the same way a Machine resource creates and destroys a VM under Cluster API.

What the Reconciliation Actually Looks Like

A ClusterZone for the platform's own domain looks like this:

yaml
apiVersion: dns.cav.enablers.ob/v1alpha2
kind: ClusterZone
metadata:
  name: onbex.co
spec:
  kind: Native
  nameservers:
    - ns1.onbex.co
    - ns2.onbex.co

Apply it, and the operator calls the PowerDNS Authoritative Server's API to create the zone. A tenant subdomain becomes an RRset referencing that zone:

yaml
apiVersion: dns.cav.enablers.ob/v1alpha2
kind: RRset
metadata:
  name: web-acme-corp
  namespace: tenant-acme-corp
spec:
  type: A
  ttl: 300
  name: acme-corp
  records:
    - 203.0.113.42
  zoneRef:
    name: onbex.co
    kind: ClusterZone

Delete either resource and the operator deletes the matching zone or record over the PowerDNS API. That's the whole model: Kubernetes events in, PowerDNS API calls out. It's worth being precise about what "reconciled" means here, because it's narrower than Cluster API's own reconciliation loop. Cluster API continuously checks a Machine's actual state against its spec and corrects drift on a control loop, whether or not anything changed the Machine directly. PowerDNS Operator's FAQ is explicit that it doesn't do this: it "only reconciles on Kubernetes events (create, update, delete)" and does "not periodically check for drift between Kubernetes resources and PowerDNS." Edit a record through the PowerDNS API or an admin UI directly, and the operator has no mechanism to notice or correct it — the two states just silently diverge until the next Kubernetes-side change touches that resource. A fleet that leans on Cluster API's continuous reconciliation for compute is adopting a materially weaker consistency guarantee for DNS.

The Actual Payoff: Tenant Self-Service Without a Shared Credential

The namespace/cluster split is what turns this into a real multi-tenant primitive, not just a YAML wrapper around zone creation. The operator ships built-in RBAC roles — viewer (read-only) and editor (namespace-scoped write) — so a platform team can grant a tenant's own namespace permission to create RRset resources under the platform's ClusterZone, without ever handing that tenant a PowerDNS API key.

yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: tenant-dns-editor
  namespace: tenant-acme-corp
subjects:
  - kind: ServiceAccount
    name: acme-corp-deploy-bot
    namespace: tenant-acme-corp
roleRef:
  kind: ClusterRole
  name: powerdns-operator-rrset-editor-role
  apiGroup: rbac.authorization.k8s.io

Compare that to the alternative most platforms actually run today: a hand-rolled controller or CI script holding one long-lived API credential per tenant against Route53, Cloudflare, or a registrar's REST API, because tenants need to self-serve a subdomain and nobody wants to build a bespoke DNS-record-request queue. That credential is a standing, high-blast-radius secret — a leak exposes the whole zone, not just the one record a tenant is entitled to touch. Kubernetes RBAC already answers "can this namespace write this resource," which is the same question a per-tenant DNS credential exists to answer, just answered with infrastructure the platform already trusts instead of a second credential system bolted on beside it.

What This Doesn't Buy You

The honest costs sit right next to the win, and none of them are hidden in fine print.

One PowerDNS server per operator instance. The project's own FAQ states it plainly: "The operator is designed to manage a single PowerDNS server. For multiple servers, deploy separate operator instances in different clusters." Running PowerDNS itself in a highly available configuration — multiple authoritative servers behind a shared MySQL or PostgreSQL backend — is a normal, well-documented PowerDNS deployment pattern. But that's HA for the DNS server, not multi-server support in the operator. A fleet with more than one PowerDNS deployment needs an operator instance per deployment, coordinated by hand.

No drift detection, as covered above — a real operational gap next to Cluster API's continuous reconciliation of compute.

No anycast, and that's the gap that actually matters for public-facing resolution. Cloudflare's authoritative DNS answers queries from 310+ points of presence across 120+ countries at an average response time around 11ms, and that geographic spread is also the platform's DDoS absorption layer — an attack has to overwhelm all of it, not one target. Route53 backs a 100% availability SLA off four anycast IP addresses engineered for exactly that resilience. A couple of PowerDNS instances running on owned Hetzner boxes, however well they reconcile Kubernetes resources, are answering queries from wherever those boxes physically sit. Matching Cloudflare or Route53's resolution latency and DDoS resilience means building actual anycast infrastructure — BGP sessions, multiple physical PoPs, upstream DDoS scrubbing — which is a networking project an order of magnitude bigger than standing up PowerDNS itself.

Mitigating the Single-Server Limit With Catalog Zones

The single-PowerDNS-server limit is real, but it's narrower than it sounds, because PowerDNS itself already has a native answer to "keep more than one authoritative server in sync" that sits underneath the operator rather than inside it. PowerDNS supports Catalog Zones (RFC 9432): a primary server publishes the list of zones it hosts as a special zone, and any number of secondary servers configured to consume that catalog automatically provision and remove zones to match — no per-zone configuration on the secondaries, no manual pdnsutil calls when a new tenant zone appears.

That's exactly the shape PowerDNS Operator's Zone spec assumes: its kind field accepts Native, Master, Slave, Producer, or Consumer, and the catalog field lets a zone declare which catalog it belongs to. The practical pattern: point the operator at one PowerDNS instance running as the catalog producer, and let one or more secondary PowerDNS servers consume that catalog and replicate every zone and record the operator creates — without the operator ever needing to know the secondaries exist. That solves for redundancy against a single box failing. It still doesn't solve for anycast-grade geographic distribution or DDoS absorption; a secondary consuming a catalog zone is still one more server sitting at one more IP address, not a globally announced anycast prefix.

Where It Plugs Into the Certificate Automation Already in Place

None of this requires touching the ACME side of a self-hosted PaaS's stack. cert-manager's DNS-01 challenge type works against any DNS backend that has a webhook solver, and community-maintained cert-manager webhooks for PowerDNS already exist (aboron/webhook-powerdns, among others) — they write the validation TXT record through the same PowerDNS API the operator itself calls. Adopting PowerDNS Operator for tenant record management doesn't change how a platform's TLS automation talks to DNS; it changes who — or what — is allowed to create the records cert-manager validates against in the first place.

The Verdict: Own the API, Not the Edge

The right split is narrower than "self-host everything" or "call a vendor API for everything." PowerDNS Operator is worth adopting as the declarative source of truth for tenant custom-domain records — it removes the standing per-tenant credential problem, gives Kubernetes RBAC (which the platform already operates and audits) authority over who can create what record, and matches the same apply-a-manifest workflow Cluster API already trained tenants and operators to expect. That's a real, concrete win over a hand-rolled Route53 or Cloudflare API integration, and it costs one more stateful service on hardware the fleet already owns.

What it isn't is a reason to stop delegating the zone's public-facing nameservers to real anycast infrastructure if query latency and DDoS resilience for tenant-facing traffic actually matter — most self-hosted platforms serving public tenant domains still want NS records pointed at a provider built for that job, with PowerDNS Operator managing the records that provider serves via zone transfer, rather than a couple of owned boxes answering the world's DNS queries directly. Skip the operator entirely only if there's no capacity to run and monitor PowerDNS's own uptime at all — at that point, a third-party DNS API is still the pragmatic default, standing credential and all.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.

Sources

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex