Skip to content

CLI Reference

bindy is a single binary with multiple subcommands, each running as an independent controller loop.

bindy <SUBCOMMAND> [OPTIONS]

Subcommands:
  bootstrap   Bootstrap bindy components into the cluster
  run         Run the main BIND9 DNS operator (all controllers)
  scout       Run the Scout controller (Ingress and Service → ARecord)
  completion  Output shell completion code for the specified shell
  version     Print the binary version (same output as --version)

Options:
  -V, --version  Print the binary version (matches the crate version, e.g. `bindy 0.6.0`)

bindy bootstrap

Applies Kubernetes resources via server-side apply. Every operation is idempotent — safe to run multiple times and safe to use in automation pipelines.

bindy bootstrap <SUBCOMMAND> [OPTIONS]

Subcommands:
  operator        Apply namespace, CRDs, RBAC, and the operator Deployment
  scout           Apply namespace, CRDs, scout RBAC, and the scout Deployment
  multi-cluster   (alias: mc) Create a child cluster's ServiceAccount on the bindy
                  cluster, optionally register it for its cluster names and zones,
                  and write its kubeconfig Secret to a private file

bindy bootstrap operator

Applies the following resources in order:

  1. Namespace (bindy-system by default)
  2. All 12 CRDs (bindy.firestoned.io/v1beta1)
  3. ServiceAccount/bindy
  4. ClusterRole/bindy-role — operator permissions
  5. ClusterRole/bindy-admin-role — admin/destructive permissions
  6. ClusterRoleBinding/bindy-rolebinding
  7. Deployment/bindy — the operator itself
bindy bootstrap operator [OPTIONS]

Options

Flag Default Description
--namespace <NS> bindy-system Namespace to install the operator into.
--version <TAG> Binary version (e.g. v0.5.0) Image tag for the operator Deployment. Use latest for debug builds.
--registry <REGISTRY> — Override the container registry. See Air-gapped environments.
--dry-run — Print all resources as YAML without connecting to the cluster.

Examples

# Standard install — image tag matches the binary version automatically
bindy bootstrap operator

# Pin to a specific version
bindy bootstrap operator --version v0.5.0

# Custom namespace
bindy bootstrap operator --namespace my-bindy

# Preview what would be applied without touching the cluster
bindy bootstrap operator --dry-run

# Air-gapped: pull from a private registry mirror
bindy bootstrap operator --registry harbor.corp.internal/bindy-mirror

# Air-gapped with explicit version
bindy bootstrap operator --registry harbor.corp.internal/bindy-mirror --version v0.5.0

bindy bootstrap scout

Applies the following resources in order:

  1. Namespace (bindy-system by default)
  2. All 12 CRDs — same set as the operator, so this is safe to run on a cluster that already has the operator installed
  3. ServiceAccount/bindy-scout
  4. ClusterRole/bindy-scout — cluster-wide Ingress watch and Secret read (for remote kubeconfig); DNSZone read is cluster-wide in same-cluster mode only
  5. ClusterRoleBinding/bindy-scout
  6. Role/bindy-scout-writer — ARecord write access in the target namespace
  7. RoleBinding/bindy-scout-writer

Multi-cluster mode

In multi-cluster mode, Scout validates DNSZones via the remote kubeconfig using a namespaced Role on the Queen Bee cluster (created by bindy bootstrap mc). No cluster-wide DNSZone access is required on the Queen Bee cluster.

  1. Deployment/bindy-scout — the scout controller
bindy bootstrap scout [OPTIONS]

Options

Flag Default Description
--namespace <NS> bindy-system Namespace to install Scout into. Scout also writes its ARecords there (BINDY_SCOUT_NAMESPACE).
--version <TAG> Binary version (e.g. v0.5.0) Image tag for the Scout Deployment.
--registry <REGISTRY> unset Override the container registry. See Air-gapped environments.
--dry-run off Print all resources as YAML without connecting to the cluster.
--cluster-name <NAME> default Logical name of this cluster, passed to Scout as --cluster-name and stamped on every ARecord as bindy.firestoned.io/source-cluster. Use a unique name per cluster.
--default-ips <IP[,IP]> unset Passed to Scout as --default-ips.
--default-zone <ZONE> unset Passed to Scout as --default-zone.
--remote-secret <NAME> unset Name of the kubeconfig Secret from bindy bootstrap mc; sets BINDY_SCOUT_REMOTE_SECRET so Scout writes to the bindy cluster.
--allowed-zones <ZONE[,ZONE]> unset Passed to Scout as --allowed-zones: the zones this cluster may publish into, each with its subzones. Recommended (ADR-0025).
--allowed-ip-ranges <CIDR[,CIDR]> unset Passed to Scout as --allowed-ip-ranges: the IPv4 ranges every published address must fall in. Recommended.

The allowlist values are checked by Scout when it starts, not by bootstrap: a malformed entry, or a --default-zone / --default-ips outside them, makes the Scout pod exit with an error naming the setting.

Examples

# Standard install
bindy bootstrap scout

# Preview resources without applying
bindy bootstrap scout --dry-run

# Air-gapped: pull from a private registry mirror
bindy bootstrap scout --registry harbor.corp.internal/bindy-mirror

# A child cluster writing to a bindy cluster, limited to its own zone and
# load-balancer range (the Secret comes from `bindy bootstrap mc`)
bindy bootstrap scout \
  --cluster-name cluster-a \
  --remote-secret bindy-scout-cluster-a-kubeconfig \
  --allowed-zones cluster-a.example.com \
  --allowed-ip-ranges 10.20.0.0/24

CRDs are always included

Both bootstrap operator and bootstrap scout apply the full set of CRDs. Running both commands on the same cluster is safe — the CRDs are applied via server-side apply and remain unchanged on subsequent runs.

bindy bootstrap multi-cluster (alias mc)

Run against the bindy (Queen Bee) cluster, once per child cluster. It creates the ServiceAccount a child cluster's Scout writes with, and writes that account's kubeconfig as a Secret manifest to a private file for you to apply on the child cluster.

Applies, in --namespace (all named after --service-account):

  1. ServiceAccount/<sa>
  2. Role/<sa>: arecords get, list, watch, create, update, patch, delete; dnszones get, list, watch
  3. RoleBinding/<sa>
  4. With --cluster-name and --allowed-zones: the keys <sa>.clusters and <sa>.zones in ConfigMap/bindy-scout-sources, under the field manager bindy-bootstrap-mc.<sa> (other accounts' keys are left alone)
  5. Secret/<sa>-token (a kubernetes.io/service-account-token Secret), then waits for the token

Then writes Secret/<sa>-kubeconfig (type bindy.firestoned.io/remote-kubeconfig) to --output, mode 0600. It is never printed to stdout.

bindy bootstrap mc [OPTIONS]

Options

Flag Default Description
--namespace <NS> bindy-system Namespace on the bindy cluster for the account, and where its Scout writes ARecords.
--service-account <NAME> bindy-scout-remote Account to create. Use one per child cluster, so each can be revoked and registered on its own.
--cluster-name <NAME[,NAME]> unset Cluster name(s) this account's Scout may stamp in source-cluster (its own --cluster-name). Requires --allowed-zones. List a second name while renaming a cluster.
--allowed-zones <ZONE[,ZONE]> unset Zones this account may publish into, each with its subzones. Requires --cluster-name. Lowercased, trailing dot dropped.
--server <URL> the KUBECONFIG server API server URL written into the kubeconfig, when the child cluster reaches the bindy cluster at another address.
-o, --output <PATH> <sa>-kubeconfig.yaml Where to write the Secret manifest (mode 0600; - is refused).
--insecure-skip-tls-verify off Allow a kubeconfig without a CA bundle. Local testing only.
--revoke (alias --delete) off Delete the account's RoleBinding, Role, Secrets and ServiceAccount, and remove its keys from bindy-scout-sources. Safe to repeat.

With --cluster-name and --allowed-zones the account is registered: the bindy-scout-source-binding admission policy then refuses any ARecord create, update or delete it makes outside those cluster names and zones (see Binding each cluster to its names and zones). Without them the command warns that the account is unconstrained.

Examples

# A registered account for child cluster cluster-a
bindy bootstrap mc \
  --service-account bindy-scout-cluster-a \
  --cluster-name cluster-a \
  --allowed-zones cluster-a.example.com \
  -o cluster-a.yaml
kubectl --context=cluster-a apply -f cluster-a.yaml && rm cluster-a.yaml

# Register an existing account without changing anything else. It re-applies
# the same ServiceAccount and RBAC and writes the kubeconfig file again (same
# token); the child cluster's Secret does not need re-applying.
bindy bootstrap mc --service-account bindy-scout-cluster-a \
  --cluster-name cluster-a --allowed-zones cluster-a.example.com -o register.yaml
rm register.yaml

# Renaming cluster-a to cluster-a2: allow both names during the switch
bindy bootstrap mc --service-account bindy-scout-cluster-a \
  --cluster-name cluster-a2,cluster-a --allowed-zones cluster-a.example.com -o register.yaml
rm register.yaml

# Remove the account and its registration
bindy bootstrap mc --revoke --service-account bindy-scout-cluster-a

Air-gapped environments

In environments without internet access, images must be mirrored to a private registry. Use --registry to point both subcommands at the mirror.

The --registry value replaces the default ghcr.io/firestoned prefix. The image name (bindy) and tag (--version) are kept:

--registry --version Resulting image
(not set) v0.5.0 ghcr.io/firestoned/bindy:v0.5.0
harbor.corp.internal/bindy-mirror v0.5.0 harbor.corp.internal/bindy-mirror/bindy:v0.5.0
registry.example.com latest registry.example.com/bindy:latest

Typical air-gapped workflow:

# 1. On an internet-connected machine, pull and re-tag the image
docker pull ghcr.io/firestoned/bindy:v0.5.0
docker tag  ghcr.io/firestoned/bindy:v0.5.0 harbor.corp.internal/bindy-mirror/bindy:v0.5.0
docker push harbor.corp.internal/bindy-mirror/bindy:v0.5.0

# 2. Copy the bindy binary to the air-gapped environment
# (scp, USB, artifact repository, etc.)

# 3. Bootstrap using the private registry
bindy bootstrap operator --registry harbor.corp.internal/bindy-mirror --version v0.5.0
bindy bootstrap scout    --registry harbor.corp.internal/bindy-mirror --version v0.5.0

Use --dry-run first to verify the image reference before applying:

bindy bootstrap operator --dry-run --registry harbor.corp.internal/bindy-mirror --version v0.5.0 \
  | grep "image:"

bindy run

Starts the main operator. Manages the full lifecycle of all BIND9 DNS custom resources: ClusterBind9Provider, Bind9Cluster, Bind9Instance, DNSZone, and all DNS record types.

All configuration is via environment variables. See Environment Variables for the full reference.

bindy run

What it starts

Controller Watches Manages
ClusterBind9Provider ClusterBind9Provider Bind9Cluster
Bind9Cluster Bind9Cluster Bind9Instance
Bind9Instance Bind9Instance, Deployment, Service, … DNS server pods
DNSZone DNSZone, all record types, Bind9Instance Zone files
ARecord ARecord A record in BIND9
AAAARecord AAAARecord AAAA record in BIND9
CNAMERecord CNAMERecord CNAME record in BIND9
MXRecord MXRecord MX record in BIND9
NSRecord NSRecord NS record in BIND9
TXTRecord TXTRecord TXT record in BIND9
SRVRecord SRVRecord SRV record in BIND9
CAARecord CAARecord CAA record in BIND9
PTRRecord PTRRecord PTR record in BIND9

Environment variables

Kubernetes client

Variable Default Description
BINDY_KUBE_QPS 20.0 Sustained API server request rate (queries per second), enforced client-side
BINDY_KUBE_BURST 30 Requests allowed to burst above the sustained rate
BINDY_KUBE_REQUEST_TIMEOUT_SECS 30 Deadline in seconds for each non-watch API request (watches exempt); a timed-out request is retried with backoff

Leader election

Variable Default Description
BINDY_ENABLE_LEADER_ELECTION true Set to false to disable. Only disable for local development.
BINDY_LEASE_NAME bindy-leader Name of the Kubernetes Lease object used for leader election.
BINDY_LEASE_NAMESPACE $POD_NAMESPACE or bindy-system Namespace where the Lease object lives.
BINDY_LEASE_DURATION_SECONDS 15 Lease duration. A new leader cannot be elected until this expires.
BINDY_LEASE_RENEW_DEADLINE_SECONDS 10 Time within which the leader must renew its lease.
BINDY_LEASE_RETRY_PERIOD_SECONDS 2 How often non-leaders attempt to acquire the lease.
POD_NAME — Leader election identity. Falls back to $HOSTNAME, then a random string. Set via Kubernetes downward API.
POD_NAMESPACE — Pod namespace. Injected by Kubernetes downward API. Used for lease namespace fallback.

Metrics

Variable Default Description
BINDY_METRICS_BIND_ADDRESS 0.0.0.0 Prometheus scrape endpoint bind address.
BINDY_METRICS_PORT 8080 Prometheus scrape endpoint port.
BINDY_METRICS_PATH /metrics HTTP path for Prometheus scraping.

Logging

Variable Default Description
RUST_LOG info Log level filter. Values: trace, debug, info, warn, error. Supports module-level filters, e.g. bindy=debug,kube=warn.
RUST_LOG_FORMAT text Log output format. text = compact human-readable; json = structured for log aggregators (Loki, ELK, Splunk).

bindy scout

Starts the Scout controller. Watches Ingress, LoadBalancer Service, and Gateway API HTTPRoute / GRPCRoute / TLSRoute / TCPRoute resources cluster-wide and publishes one ARecord per DNS name for opted-in resources. See Bindy Scout for the full conceptual guide.

bindy scout [OPTIONS]

Options

Flag Env var Default Description
--cluster-name <NAME> BINDY_SCOUT_CLUSTER_NAME none Required. Logical name of this cluster, stamped on all created ARecord labels as bindy.firestoned.io/source-cluster. Used to distinguish records from multiple workload clusters writing to the same bindy namespace.
--namespace <NS> BINDY_SCOUT_NAMESPACE bindy-system Namespace where ARecord CRs are created.
--default-zone <ZONE> BINDY_SCOUT_DEFAULT_ZONE unset Zone used when a source has no bindy.firestoned.io/zone annotation. Must be inside --allowed-zones when that is set.
--default-ips <IP[,IP]> BINDY_SCOUT_DEFAULT_IPS unset Addresses used when a source has no IP annotation and none is discovered. Must be inside --allowed-ip-ranges when that is set.
--gateway-service <class=target> BINDY_SCOUT_GATEWAY_SERVICES unset Repeatable gatewayClass to LoadBalancer Service(s) map for route IP resolution: class=namespace/name (one Service) or class=namespace/<label-selector> (every matching LoadBalancer Service, scoped to the route's Gateway by the GEP-1762 gateway.networking.k8s.io/gateway-name label when present; equality-based selectors only). ADR-0028.
--namespace-selector <SELECTOR> BINDY_SCOUT_NAMESPACE_SELECTOR unset (every namespace) Label selector a source's namespace must match. Recommended.
--allowed-zones <ZONE[,ZONE]> BINDY_SCOUT_ALLOWED_ZONES unset (every zone) Zones Scout may publish into, each with its subzones. A source naming another zone publishes nothing. * is refused. Recommended (ADR-0025).
--allowed-ip-ranges <CIDR[,CIDR]> BINDY_SCOUT_ALLOWED_IP_RANGES unset (every address) IPv4 ranges (a bare address means /32) every published address must fall in. A source with any address outside publishes nothing, except a route's addresses discovered from its Gateway, which are dropped one by one with a warning (ADR-0028). Recommended.
--remote-endpoint <URL> BINDY_SCOUT_REMOTE_ENDPOINT unset Bindy cluster API URL for the endpoint remote mode (ADR-0008). Requires --remote-token-file.
--remote-token-file <PATH> BINDY_SCOUT_REMOTE_TOKEN_FILE unset Bearer-token file minted by the bindy cluster.
--remote-ca-file <PATH> BINDY_SCOUT_REMOTE_CA_FILE unset (webpki roots) CA bundle for the remote endpoint.

CLI takes precedence

When both a CLI flag and the corresponding environment variable are set, the CLI flag wins.

Additional environment variables

Variable Default Description
POD_NAMESPACE default Scout's own namespace. Always excluded from Ingress watching. Set via Kubernetes downward API.
BINDY_SCOUT_EXCLUDE_NAMESPACES — Comma-separated list of namespaces to skip in addition to POD_NAMESPACE.
BINDY_SCOUT_REMOTE_SECRET unset Name of the kubeconfig Secret (from bindy bootstrap mc) for the kubeconfig remote mode.
BINDY_SCOUT_REMOTE_SECRET_NAMESPACE POD_NAMESPACE Namespace of that Secret.
RUST_LOG info Log level filter.
RUST_LOG_FORMAT text Log format (text or json).

Examples

# Minimal — cluster name required, everything else defaults
bindy scout --cluster-name prod

# Explicit namespace
bindy scout --cluster-name prod --namespace bindy-system

# Using environment variables only (typical Kubernetes deployment)
BINDY_SCOUT_CLUSTER_NAME=prod BINDY_SCOUT_NAMESPACE=bindy-system bindy scout

# Mix: cluster name from CLI flag, namespace from environment
BINDY_SCOUT_NAMESPACE=bindy-system bindy scout --cluster-name staging

# Debug logging
RUST_LOG=bindy=debug bindy scout --cluster-name dev

# Production: bounded to this cluster's zone and load-balancer range
bindy scout --cluster-name prod \
  --allowed-zones prod.example.com \
  --allowed-ip-ranges 10.20.0.0/24

Scout validates its whole configuration at startup and exits with an error naming the setting when a value is empty, malformed, or (for --default-zone and --default-ips) outside the allowlists. With neither allowlist set it starts, and logs a warning for each.

Ingress annotations

Scout reads the following annotations from every source kind (see the Annotations Reference for the full rules):

Annotation Required Description
bindy.firestoned.io/scout-enabled Yes ("true") Opts the resource in. The legacy bindy.firestoned.io/recordKind: "ARecord" is still accepted on Ingresses.
bindy.firestoned.io/zone Yes, unless --default-zone is set DNS zone that owns the hosts (e.g. example.com). Must be inside --allowed-zones when that is set.
bindy.firestoned.io/ip No Explicit IP override. Defaults to the discovered address. Must be inside --allowed-ip-ranges when that is set.
bindy.firestoned.io/ttl No TTL override in seconds.
bindy.firestoned.io/record-name No Record name override (@ for the apex). Required on TCPRoute.

bindy completion

Outputs shell completion code for the specified shell. No environment variables or Kubernetes access required.

bindy completion <SHELL>

Supported shells

Shell Value
Bash bash
Zsh zsh
Fish fish
PowerShell powershell

Installation

Add to ~/.bashrc:

source <(bindy completion bash)

Or for a system-wide install:

bindy completion bash > /etc/bash_completion.d/bindy

Add to ~/.zshrc:

source <(bindy completion zsh)

Or with oh-my-zsh:

bindy completion zsh > "${fpath[1]}/_bindy"
bindy completion fish | source

Or to persist across sessions:

bindy completion fish > ~/.config/fish/completions/bindy.fish

Add to $PROFILE:

bindy completion powershell | Out-String | Invoke-Expression