# Running fancy-dns outside the cluster This is how to run a second resolver on a machine that is not a pod, for example the controller node, as a backup for the in-cluster one. It uses the same code and the same cluster objects (`DNSZone`, `ClientGroup`, `BlockList`, `AddressOverride`, `DomainRule`), so an edit made in the console reaches both resolvers and nobody keeps a second copy of the configuration. There are two binaries and you probably only want the first: | Binary | Role | On the controller node | |---|---|---| | `fancy-dns` | The resolver. Answers DNS, reads the cluster objects with a watch, never writes them. | **Yes**: this is the backup. | | `fancy-dns-console` | The web UI. The only thing that writes the objects, and it proxies the rest to a resolver. Not on the DNS path. | Optional. See [the console](#the-console-outside-the-cluster). | If you meant something else by "the client", say so; the rest of this file assumes the resolver. ## How it behaves when the cluster is gone This is the reason to run it, so know it first. | Situation | What the resolver does | |---|---| | Cluster reachable | Serves the cluster objects live and saves a snapshot of them to `/kube-snapshot.json` after every change. | | Cluster unreachable at start | Waits 30 seconds, then starts from the saved snapshot and keeps retrying in the background. `GET /api/status` lists `kubernetes: not synced, serving the last saved cluster configuration` under `components.degraded` (and `GET /api/healthz`, which needs no key, reports `degraded`). | | Cluster unreachable at start, **never synced before** | Starts with the YAML file only: no cluster zones, address overrides, groups or lists. `GET /api/status` says `not synced and no saved snapshot`. Start it once while the cluster is up, and keep its data directory. | | Cluster goes away while running | Keeps serving what it has and reconnects when the API server returns. | | The kubeconfig is missing or unreadable | **Refuses to start.** A resolver that cannot build a client at all is a configuration error, not a degraded cluster. Test the kubeconfig (below) before relying on it. | Because of the snapshot, the backup needs a data directory that survives reboots. That is also where the cache snapshot lives, so it is the same directory you would have anyway. Plain DNS to the backup does not depend on the console, on Valkey, or on the in-cluster resolver being up. ## 1. Give it read-only access to the objects Do not copy the cluster's admin kubeconfig onto a DNS server if you can avoid it: the resolver only ever needs to read five resource types in one namespace. `deploy/k8s/crds/rbac.yaml` already defines that Role (`fancy-dns-objects`). Bind a dedicated service account to it and give that account a long-lived token: ```yaml # kubectl apply -f this-file.yaml apiVersion: v1 kind: ServiceAccount metadata: {name: fancy-dns-backup, namespace: dns} --- apiVersion: v1 kind: Secret metadata: name: fancy-dns-backup-token namespace: dns annotations: {kubernetes.io/service-account.name: fancy-dns-backup} type: kubernetes.io/service-account-token --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: {name: fancy-dns-objects-backup, namespace: dns} subjects: [{kind: ServiceAccount, name: fancy-dns-backup, namespace: dns}] roleRef: {apiGroup: rbac.authorization.k8s.io, kind: Role, name: fancy-dns-objects} ``` Build a kubeconfig from it. Run this where you have admin `kubectl`, then copy the file to the node as `/etc/fancy-dns/kubeconfig`: ```sh SERVER=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}') CA=$(kubectl -n dns get secret fancy-dns-backup-token -o jsonpath='{.data.ca\.crt}') TOKEN=$(kubectl -n dns get secret fancy-dns-backup-token -o jsonpath='{.data.token}' | base64 -d) cat > fancy-dns.kubeconfig < +short ``` `components.degraded` should be empty. The two `dig`s tell you it resolves and that it has the cluster's data. Then simulate the outage you bought this for: block the API server from the node (or stop the resolver and start it with the cluster unreachable), and confirm the same names still answer and `components.degraded` now names the snapshot. Finally point clients at it as a **second** nameserver (DHCP option 6, or the router's secondary). It is a backup, not a load balancer: most stub resolvers only try the second server after the first times out. ## The console outside the cluster Optional. You only need it on the node if you want to edit while the in-cluster console is unavailable, and it can only save while the API server is reachable, since an edit is a write to the cluster. Reads of statistics come from whichever resolver you point it at. It needs a **write** Role, which is a separate account from the resolver's. `deploy/k8s/console/console.yaml` defines the Role `fancy-dns-console`; bind a second service account to it the same way as above (name it `fancy-dns-console-ext`, build `/etc/fancy-dns/console.kubeconfig`). `/etc/fancy-dns/console.yaml` (see `deploy/console.example.yaml`): ```yaml blocklist: {enabled: false} api: enabled: true listen: ["127.0.0.1:8081"] base_path: /api ui: true auth: mode: apikey api_keys: ["${FANCYDNS_CONSOLE_KEY}"] trust_loopback: false ``` `/etc/fancy-dns/console.env`: ```sh FANCYDNS_CONSOLE_KEY= FANCYDNS_RUNTIME_KEY= ``` Run it with a unit like the resolver's, minus the port-53 capability: ```ini ExecStart=/usr/local/bin/fancy-dns-console \ -config /etc/fancy-dns/console.yaml \ -runtime-url http://127.0.0.1:8080 \ -kubeconfig /etc/fancy-dns/console.kubeconfig \ -kube-namespace dns ``` Keep it on loopback and reach it with an SSH tunnel (`ssh -L 8081:127.0.0.1:8081 controller`) or an authenticated TLS reverse proxy. The UI can change DNS for every client you have; do not publish it with an API key on plain HTTP. ## Troubleshooting | Symptom | Likely cause | |---|---| | Service exits immediately: `no usable Kubernetes credentials` | `-kubeconfig` points at a file that is missing, unreadable by the `fancy-dns` user, or malformed. | | Starts, `components.degraded` says `not synced` | The API server is unreachable or the token is rejected (`kubectl --kubeconfig … get clientgroups`), or the CRDs are not installed. | | `…not synced and no saved snapshot` and cluster data missing | First start with the cluster down. Start it once with the cluster up. Also check the data directory is writable. | | `kubernetes: objects rejected` in `components.degraded` | An object failed validation (or this binary is older than the cluster's). The last good version stays in force. Which objects, and why: `jq .problems /var/lib/fancy-dns/kube-snapshot.json`. | | Cluster answers differ from the backup's | The upstream/forward-zone config differs (it is YAML, not an object), or the backup's cache holds older answers. `POST /api/cache/flush` on it. | | `bind: address already in use` | Something owns port 53; see [Port 53](#port-53). | | Console: `cluster: …forbidden` | It is using the read-only kubeconfig; it needs the write Role. |