Reference

Helm values

Every option of the Kubernetes chart, generated from the chart's own values.yaml and its comments, so it can't drift from the chart.

  • The whole file, with its comments, for the version you install:
    helm show values oci://ghcr.io/paimonsoror/charts/telltale > values.yaml
    # or one version: --version X.Y.Z (builds from main: --devel, or --version X.Y.Z-edge.N)
  • The file on GitHub: deploy/helm/telltale/values.yaml.
  • Keep only what you change in your own file and install with -f my-values.yaml. The chart checks values against its schema, so a mistyped value fails the install with the key's path.
  • TelltaleDNS's own settings go in config: as TOML; every key is in the configuration reference.

Sections: cluster · resolvers · image · kubernetes · service · encrypted · ingress · persistence · resources · auth · serviceMonitor · prometheusRule · grafanaDashboard · networkPolicy · podSecurityContext · securityContext

Top level

KeyDefaultMeaning
modeallInOneShape. - allInOne: one StatefulSet (resolver, API, UI, query log). - scaled (CLU-009): that StatefulSet becomes the cluster's controller (primary, with the volume), plus resolvers.replicas resolver pods. The pods join the controller automatically with a generated Secret, take its configuration and lists, ship their query log to it, and come and go freely (ephemeral members). DNS is spread over every pod; the UI and API stay on the controller. A node outside Kubernetes (a Pi) can join too: see cluster.advertise. - daemonSet (host-network resolvers) comes later.
imagePullSecrets[]
nameOverride""
fullnameOverride""
config
[node]
# workers = 0  # auto: CPU limit or cores
Extra telltale.toml, merged after the settings the chart controls (listeners, data_dir, metrics and API addresses). See docs/configuration.md.
hostNetworkfalseBind DNS on the node's network directly (port 53): real client IPs and MAC addresses, no NAT, no LoadBalancer needed. Binding 53 on the host needs root with only NET_BIND_SERVICE (all other capabilities dropped). On k3s this is fine (CoreDNS listens on cluster IPs only); stop systemd-resolved's stub listener on the node first.
dnsPort5353Container port for DNS when not on the host network (the Service maps 53 to it).
secretMounts[]Secrets mounted read-only at /etc/telltale-secrets/<name>, e.g. an OIDC client secret (point client_secret_file at /etc/telltale-secrets/oidc/client_secret). optional: true lets the pod start without the Secret.
  - name: oidc
    secretName: telltale-oidc
    optional: true
extraEnv[]
podAnnotations{}
podLabels{}
nodeSelector{}
tolerations[]
affinity{}
priorityClassName""
terminationGracePeriodSeconds30Draining: readiness turns off first, then TCP and in-flight upstream queries finish.
drainDelaySeconds5On shutdown, pods keep answering this long after turning not ready, so the Service stops sending them queries first (no lost lookups during rollouts). Keep it well under terminationGracePeriodSeconds.

cluster

Clustering (mode: scaled).

KeyDefaultMeaning
sitek8sSite label for these pods (the Cluster page groups resolver pods by site).
port9443The cluster port inside the pods (mTLS between nodes).
service.typeClusterIPClusterIP for pods only. LoadBalancer (or NodePort) lets a node outside Kubernetes join.
service.annotations{}
advertise[]Extra URLs for the controller's cluster port that nodes outside Kubernetes dial, e.g. ["https://192.168.5.100:9443"] behind a LoadBalancer. In-cluster URLs are added for you.
bootstrapSecretThe shared join secret. Empty: the chart creates one (kept across upgrades). With Argo CD (no lookup), create the Secret yourself and name it here.
bootstrapSecret.existingSecret""A Secret of your own instead of the generated one (48 random characters). Use at least 32 random characters: anyone who knows it can join as a resolver pod.
bootstrapSecret.keybootstrap
rolloutStaged rollouts (mode: scaled): a configuration change reaches the canary nodes (and the controller, which runs it at once) first, and every other node after bakeSeconds if nothing fails; a failure pins the cluster to the version before. Empty (the default): changes reach every node at once. Entries are node IDs, site:<name>, or ephemeral (the resolver pods). With [ephemeral], the pods take a change at once under the guard's watch and nodes outside Kubernetes (a Raspberry Pi that joined) wait out the bake. Also editable in the web UI: Settings → Cluster → Staged rollouts.
rollout.canaries[]
rollout.bakeSeconds300How long a change bakes on the canaries before everyone gets it (at least 5).
rollout.servfailPct5Fail when the canaries' SERVFAIL share during the bake is over max(the share before + 2 points, this) percent (with at least 50 answers).
rollout.requireTrafficfalseKeep baking until the canaries answered 50 queries (or an hour), then judge.
rollout.history20Versions kept for pinning back to (their files stay on the controller's volume).

resolvers

KeyDefaultMeaning
replicas2
ephemeralTtlSeconds600Seconds the controller keeps a resolver pod it no longer hears from.
resources.requests.cpu50m
resources.requests.memory64Mi
resources.limits.cpu"1"
resources.limits.memory256Mi
podAnnotations{}
nodeSelector{}
tolerations[]
affinity{}E.g. spread the pods: podAntiAffinity on app.kubernetes.io/component: resolver

image

KeyDefaultMeaning
repositoryghcr.io/paimonsoror/telltale
tag""Default: the chart's appVersion.
pullPolicyIfNotPresent

kubernetes

Kubernetes workloads as devices: the controller (or the allInOne pod) watches pods and namespaces, read-only, and names each pod address by its workload (media/deploy/jellyfin); a Deployment's replicas are one device across rollouts. Pods are seen as themselves only when they ask TelltaleDNS directly: point the cluster's DNS at the DNS Service (service.dns.clusterIP, then the kubelet's cluster DNS; see NOTES and docs/running.md), or give a workload dnsConfig.nameservers. TelltaleDNS sends the cluster domain (and the pod and service reverse zones) on to CoreDNS. Don't set [kubernetes] in config as well: the chart writes it.

KeyDefaultMeaning
enabledfalse
clusterDomaincluster.localThe cluster's DNS domain.
clusterDns""CoreDNS's Service address (required when enabled): kubectl -n kube-system get svc kube-dns -o jsonpath='{.spec.clusterIP}'
podCidr""Optional: the pod and service networks (IPv4 CIDRs, e.g. 10.42.0.0/16 and 10.43.0.0/16 on k3s); their reverse zones (PTR) go to CoreDNS too, rounded to whole octets.
serviceCidr""
namespaces[]Namespaces to watch (empty: all), and namespaces never watched.
excludeNamespaces[]
defaultGroup""The group of a workload no label or rule places (empty: its network's group, or default).
map[]Groups by namespace glob (and optionally kind and name), first match wins after the telltaledns.io/group labels: [{namespace: "media*", group: "iot"}].
rbac.createtrueA ServiceAccount, and a ClusterRole that can only get/list/watch pods and namespaces.
rbac.serviceAccountName""Use an existing ServiceAccount instead (it needs those rights).

service

KeyDefaultMeaning
dns.typeLoadBalancerOne Service with both UDP and TCP 53.
dns.port53
dns.clusterIP""A fixed cluster address for the Service, so the kubelet's cluster DNS setting survives reinstalls (e.g. 10.43.0.53 inside k3s's 10.43.0.0/16). Empty: assigned.
dns.externalTrafficPolicyLocalLocal keeps client source IPs (per-device filtering and the query log need them). Cluster masks them behind node IPs: NOTES.txt warns.
dns.loadBalancerIP""
dns.loadBalancerClass""
dns.annotations{}E.g. metallb.universe.tf/loadBalancerIPs, io.cilium/lb-ipam-ips, kube-vip.io/loadbalancerIPs
dns.alsoServeOne address for everything: the DNS Service also carries the UI/API port and (scaled) the cluster port. Only the controller has those ports, so only it gets that traffic. Use this instead of giving the api or cluster Service the DNS Service's IP: with externalTrafficPolicy Local, MetalLB and Cilium share an IP only between Services that select the same pods, and in mode: scaled they don't (the DNS Service would lose its IP). See docs/running.md, "Scaling in Kubernetes".
dns.alsoServe.apifalse
dns.alsoServe.clusterfalse
api.typeClusterIPThe web UI and REST API. ClusterIP plus ingress, or LoadBalancer on the LAN.
api.port8053
api.externalTrafficPolicyLocal
api.loadBalancerIP""
api.annotations{}
metrics.port9153

encrypted

Encrypted DNS for devices: DNS over TLS (853) and DNS over HTTPS (443) on the same DNS Service. Needs a certificate: an existing kubernetes.io/tls Secret, or one from cert-manager. A wildcard name (*.dns.example.com) lets devices identify themselves (kids-tablet.dns.example.com → client ID "kids-tablet"). The certificate is re-read when cert-manager renews it; no restart.

KeyDefaultMeaning
dot.enabledfalse
dot.port853
doh.enabledfalse
doh.port443
doh.path/dns-query
doh.http3falseAlso serve DoH over HTTP/3 (QUIC, UDP on the same port); HTTP/2 answers advertise it with Alt-Svc.
doqDNS over QUIC (RFC 9250) on UDP 853: Android and iOS apps and some resolvers prefer it. Uses the same certificate.
doq.enabledfalse
doq.port853
proxyProtocolfalseAccept PROXY protocol v2 on TCP, DoT, and DoH (only when a balancer in front sends it: every connection must then start with the header).
tls.secretName""An existing Secret with tls.crt and tls.key. Ignored when certManager.enabled.
tls.certManager.enabledfalse
tls.certManager.issuerRef.name""
tls.certManager.issuerRef.kindClusterIssuer
tls.certManager.dnsNames[]E.g. [dns.example.com, "*.dns.example.com"]

ingress

The UI and API behind an Ingress (TLS). Also set [api] trusted_proxies in config to the ingress's source network so sign-in lockouts and the audit log see client addresses.

KeyDefaultMeaning
enabledfalse
className""
host""
annotations{}E.g. with cert-manager: {cert-manager.io/cluster-issuer: letsencrypt}
tls.secretName""

persistence

KeyDefaultMeaning
enabledtrueFilter snapshots, downloaded lists, the query log, rollups, users (state.db).
size2Gi
storageClass""
accessModes[ReadWriteOnce]
existingClaim""Use this PersistentVolumeClaim instead of creating one (size, storageClass, and accessModes are then ignored): for moving an existing install's data to this chart.

resources

KeyDefaultMeaning
requests.cpu100m
requests.memory96Mi
limits.cpu"2"
limits.memory512MiSteady state ~35 MiB with typical lists; compiling large lists peaks near 110 MiB.

auth

KeyDefaultMeaning
bootstrapAdminCreate the first admin from a Secret instead of the setup token. Keys: username and password or password-hash (an Argon2id hash from telltale auth hash-password).
bootstrapAdmin.existingSecret""
bootstrapAdmin.usernameKeyusername
bootstrapAdmin.passwordKeypassword
bootstrapAdmin.passwordHashKeypassword-hash

serviceMonitor

KeyDefaultMeaning
enabledfalse
interval30s
scrapeTimeout10s
labels{}

prometheusRule

Default alerts: down, upstreams down, SERVFAIL rate, stale lists, dropped telemetry.

KeyDefaultMeaning
enabledfalse
labels{}
sloSLO recording rules (telltale:slo_*_errors:ratio_rate<window>) and multi-window burn-rate alerts over every node. Keep these equal to the nodes' [slo] settings, which the web UI and the API use; latencyMs must be a bucket bound of telltale_query_duration_seconds (1, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000).
slo.enabledtrue
slo.availabilityTarget99.9
slo.latencyTarget99
slo.latencyMs250

grafanaDashboard

The Grafana dashboard as a ConfigMap for the Grafana sidecar.

KeyDefaultMeaning
enabledfalse
labels.grafana_dashboard"1"
annotations{}

networkPolicy

KeyDefaultMeaning
enabledfalse
dnsFrom[]CIDRs allowed to query DNS / reach the UI+API / scrape metrics. Empty: anyone.
apiFrom[]
metricsFrom[]

podSecurityContext

KeyDefaultMeaning
fsGroup65532
fsGroupChangePolicyOnRootMismatch
seccompProfile.typeRuntimeDefault

securityContext

KeyDefaultMeaning
runAsUser65532
runAsGroup65532
runAsNonRoottrue
readOnlyRootFilesystemtrue
allowPrivilegeEscalationfalse
capabilities.drop[ALL]