ouroboros

module
v0.8.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 16, 2026 License: BSD-3-Clause

README

ouroboros

Go reimplementation of compumike/hairpin-proxy — fixes the hairpin-NAT problem for Kubernetes Ingress controllers configured with PROXY-protocol.

Why

When an external load balancer in front of an ingress-controller (typically ingress-nginx with use-proxy-protocol: true) prepends the PROXY-protocol header, internal traffic from in-cluster pods bypasses the LB and reaches the ingress-controller without the header. The connection is then rejected. Common offenders: cert-manager HTTP-01 challenges, internal https:// calls to your own public hostnames, healthchecks.

Do you need ouroboros at all? Since KEP-1860 (beta in Kubernetes 1.30, GA in 1.32) the kube-proxy can stop short-circuiting LoadBalancer IPs to the local Service when the cloud-controller-manager (CCM) sets status.loadBalancer.ingress[].ipMode: Proxy. With that flag set the LB always processes the connection — including its PROXY-protocol injection — and the hairpin path simply does not exist. A CNI that overrides kube-proxy must also honour the ipMode field — check your CNI's release notes for KEP-1860 support before relying on this fix. If your CCM and CNI both implement the contract you can remove ouroboros entirely; if only your CCM does, deploy on Kubernetes 1.30+ and check that the CNI agrees. ouroboros remains a workaround for the cluster topologies where that machinery is not available.

ouroboros fixes this with two cooperating components:

  1. A controller that watches Ingress (and optionally Gateway-API Gateway + HTTPRoute) and rewrites the kube-system/coredns ConfigMap so internal lookups for those hostnames resolve to a small in-cluster proxy.
  2. A TCP proxy that listens on 8080/8443, prepends the PROXY-protocol v1 header, and forwards to the real ingress-controller.

Both components ship as one binary, dispatched by subcommand.

Architecture

                        ┌────────────────────┐
                        │ Ingress / Gateway  │
                        │   (k8s API)        │
                        └────────┬───────────┘
                                 │ informers
                                 ▼
   ┌───────────────────────────────────────────┐
   │ ouroboros controller                      │
   │ - extracts hostnames                      │
   │ - reconciles CoreDNS ConfigMap (or hosts) │
   └───────────────────┬───────────────────────┘
                       │
                       ▼
       ┌───────────────────────────┐
       │ kube-system/coredns       │
       │  rewrite name foo.example │
       │    ouroboros-proxy....    │
       └───────────┬───────────────┘
                   │ DNS lookup from a pod
                   ▼
   ┌───────────────────────────────────────────┐
   │ ouroboros proxy   (Service ClusterIP)     │
   │ - accepts TCP                             │
   │ - prepends PROXY-protocol v1 header       │
   └───────────────────┬───────────────────────┘
                       │
                       ▼
              ingress-nginx-controller

Install

helm install ouroboros oci://ghcr.io/lexfrei/charts/ouroboros \
  --version 0.3.0 \
  --namespace ouroboros --create-namespace

Override the upstream backend if you don't run ingress-nginx:

helm install ouroboros oci://ghcr.io/lexfrei/charts/ouroboros \
  --namespace ouroboros --create-namespace \
  --set proxy.target.host=my-ingress.my-ns.svc.cluster.local

Enable Gateway-API support:

--set controller.gatewayApi.enabled=true

Modes

Mode Reconciler Use when
coredns mutates kube-system/coredns ConfigMap directly default — works for any pod that uses CoreDNS for DNS
coredns-import writes plugin-only rewrite name snippets into a separate ConfigMap (default kube-system/coredns-custom) that the operator's CoreDNS chart pulls in via an import /etc/coredns/custom/*.override directive — see the prereqs in the coredns-import mode section clusters where the main Corefile is owned by Helm/Flux and re-rendered (otherwise the inline block is overwritten on every reconcile)
etc-hosts writes /etc/hosts on each node (DaemonSet) for kubelet, container runtime, or anything bypassing CoreDNS
external-dns emits externaldns.k8s.io/v1alpha1.DNSEndpoint CRs (default) or annotated headless Services (externalDns.output=service, for instances that read --source=service with --annotation-prefix) managed clusters that block writes to kube-system/coredns (EKS Auto, GKE Autopilot, AKS); clusters with node-local-dns (the per-node cache bypasses CoreDNS for non-cluster.local queries — see caveat below); split-horizon DNS; multi-cluster published DNS

Switch via --set controller.mode=external-dns (or --set etcHosts.enabled=true for etc-hosts).

Cluster DNS domain

The chart is platform-agnostic by default. When controller.clusterDomain is empty (default), the chart omits --proxy-fqdn from the rendered controller args entirely and emits --proxy-service-name + --proxy-service-namespace instead. The controller binary then auto-detects the cluster's DNS domain at startup by parsing the search line in /etc/resolv.conf and composes the rewrite-target FQDN as <service>.<namespace>.svc.<auto-detected-domain>. at runtime. cozystack tenants on cozy.local, federations on k8s.example.com, RKE2/k3s clusters with custom domains, vanilla kubeadm on cluster.local — all work without operator override. The proxy binary follows the same pattern with --target-host / --target-service-name / --target-service-namespace for its backend.

Two paths exist:

  • Runtime composition (default, platform-agnostic): controller.clusterDomain empty. Chart emits --proxy-service-name + --proxy-service-namespace. Controller composes <service>.<namespace>.svc.<auto-detected-domain>. at startup. Same shape on the proxy: proxy.target.host empty, chart emits --target-service-name + --target-service-namespace, proxy composes the backend host. No cluster.local is ever baked into the rendered args.
  • Explicit override (legacy / non-Service backends): controller.clusterDomain set explicitly (e.g. --set controller.clusterDomain=cozy.local). Chart bakes --proxy-fqdn=*.svc.cozy.local. at templating time, ResolveProxyFQDN returns it unchanged, and the auto-detect becomes a sanity-check warning only. Same on the proxy: proxy.target.host set explicitly bakes --target-host= and skips composition. Use this when the backend is not a Kubernetes Service (external CDN, sidecar proxy, off-cluster ingress).

If auto-detect picks the wrong cluster domain (host-network pods see the host's /etc/resolv.conf, not the kubelet-injected one; corp-DNS suffixes can shadow the kubelet entry on dual-stack hosts), override at the binary level with --cluster-domain=<domain> or OUROBOROS_CONTROLLER_CLUSTER_DOMAIN=<domain>. Both shortcut the auto-detect entirely; the override is what reaches the startup log and gets used to compose the FQDN.

The detected (or overridden) cluster-domain is logged at startup so operators can sanity-check it against the resolved --proxy-fqdn=... line. When the explicit-override path is in use AND the chart-baked suffix does not match the detected domain, ClusterDomainMismatch fires a WARN at startup — operators with a deliberately mismatched FQDN (split-horizon DNS, custom CoreDNS rewrite chains) can ignore it; everyone else has caught a misconfiguration before it ships NXDOMAINs to production.

coredns mode (default)

coredns mode mutates the live kube-system/coredns Corefile in place between # === BEGIN ouroboros (do not edit by hand) === and # === END ouroboros === markers. The controller adds one rewrite name <host> ouroboros-proxy.<release-ns>.svc.<cluster-domain>. line per public hostname into the block on every reconcile, where <cluster-domain> is whichever value the runtime resolver landed on (chart-time bake from controller.clusterDomain if set, otherwise auto-detected from /etc/resolv.conf). Everything outside the markers is left untouched. CoreDNS' reload plugin (default in kubeadm-style Corefiles) picks the change up without a pod restart.

Use this mode on bare-kubeadm clusters where the Corefile is operator-owned and ouroboros is the only writer. Don't use it on clusters where the Corefile is rendered from Helm/Kustomize/Flux values — the chart owner re-templates data.Corefile on every reconcile, the markers vanish, ouroboros re-injects them, and you get a flap every reconcile cycle. For those clusters use coredns-import instead.

coredns-import mode

coredns mode patches the live kube-system/coredns Corefile between # === BEGIN ouroboros === markers. That works on bare-kubeadm clusters where ouroboros is the only writer, but on clusters where the Corefile is rendered from Helm/Kustomize/Flux values (cozystack, RKE2, k3s, EKS with coredns-custom, anyone running the coredns/helm-charts chart with custom extraConfig), the chart owner re-templates data.Corefile on every reconcile, the markers vanish, ouroboros re-injects them, and so on — a flap every reconcile cycle.

coredns-import side-steps the contention by writing plugin-only directives into a separate ConfigMap and never touching the main Corefile. CoreDNS picks the rewrites up via its import plugin.

This chart does not deploy CoreDNS. Two prerequisites must already be in place on the CoreDNS side before coredns-import works — set them up in whichever chart actually owns your CoreDNS:

  1. The import ConfigMap exists. Default name kube-system/coredns-custom (matches the de-facto k8s convention used by RKE2/k3s and the CoreDNS Helm chart). Empty data is fine; ouroboros writes its own key. Override via controller.corednsImport.{namespace,configmap} if your CoreDNS uses a different name.

  2. The Corefile contains an import directive that picks up the configured key, and the import ConfigMap is mounted into the CoreDNS Deployment at the matching path. Canonical wiring:

    .:53 {
        errors
        health
        ready
        kubernetes cluster.local in-addr.arpa ip6.arpa { ... }
        prometheus :9153
        forward . /etc/resolv.conf { ... }
        cache 30
        loop
        reload
        loadbalance
        import /etc/coredns/custom/*.override
    }
    

    plus a volume mount for the coredns-custom ConfigMap at /etc/coredns/custom/. The reload plugin is required so CoreDNS notices ouroboros writes without a pod restart (same caveat as coredns mode).

Distros that ship this wiring out of the box: RKE2, k3s, the coredns/helm-charts chart with extraConfig.import. EKS, GKE Autopilot, AKS — does not work, the managed CoreDNS is read-only; use external-dns mode instead.

Without prereqs (1) and (2), ouroboros writes the override key successfully, CoreDNS silently ignores it, and hairpin keeps failing with no log surfacing — the failure mode this section exists to prevent.

helm install ouroboros oci://ghcr.io/lexfrei/charts/ouroboros \
  --namespace ouroboros --create-namespace \
  --set controller.mode=coredns-import
Cleanup on uninstall (coredns and coredns-import modes)

Both coredns and coredns-import modes ship a pre-delete Helm hook that strips the rewrite block from the live ConfigMap before Helm deletes the proxy Service. Without this hook, helm uninstall removes the controller and proxy but leaves the rewrite snippet on the cluster pointing at a Service that is about to disappear — in-cluster DNS for hairpinned hostnames returns connection-refused until an operator runs the manual cleanup recipe by hand.

The hook quiesces the controller first (scales the Deployment to 0 and polls status.replicas via get until it reports 0/empty, with a 60-second cap) so its informers cannot fire during the hook window and re-add the rewrite the cleanup just stripped. The polling form keeps RBAC narrow to get on the named Deployment — kubectl rollout status would need list and watch on the resource type. Then:

  • coredns mode reads the ConfigMap with kubectl get -oyaml into /tmp/cm.yaml, validates that exactly one # === BEGIN ouroboros marker is present (refuses to act if zero or two+ — guards against in-band markers that would mis-aim the sed range), validates that a matching # === END ouroboros marker exists (refuses to truncate the Corefile to EOF if BEGIN is present without END), runs sed '/# === BEGIN ouroboros/,/# === END ouroboros/d' /tmp/cm.yaml > /tmp/cm-cleaned.yaml, and replays the cleaned manifest with kubectl replace --filename=/tmp/cm-cleaned.yaml. The whole read+validate+sed+replace cycle is wrapped in a 5-attempt retry loop — kubectl replace is a PUT and a mid-flight write from any other client returns 409, in which case the next attempt re-reads, re-validates, and re-applies. The temp-file shape (rather than a single shell pipe) keeps each phase debuggable from kubectl logs of the cleanup Pod and lets set -eux catch a partial-pipeline failure.
  • coredns-import mode runs kubectl patch cm <name> --type=merge --patch '{"data":{"<key>":null}}'. JSON merge-patch is conflict-free; only the configured key is nulled, leaving any other data keys (operator-owned *.override snippets, future additions) intact.

controller.coredns.cleanupOnUninstall and controller.corednsImport.cleanupOnUninstall both default to true. Set either to false if the ConfigMap data is owned by an external config-management system that you do not want a Helm hook reaching into during uninstall — at the cost of having to run the cleanup recipe by hand. Override the cleanup hook image (kubectl + sh + sed) via controller.cleanupHookImage.

Upgrade-time orphans. The hook fires only on helm uninstall. A helm upgrade that flips controller.mode between coredns and coredns-import (or any other mid-life mode switch) leaves the previously-managed key in the previously-managed ConfigMap untouched — the controller just stops re-rendering it. Operators changing modes mid-life must run the matching cleanup recipe by hand on the old ConfigMap before the upgrade lands.

external-dns mode

Set controller.mode=external-dns and ouroboros stops mutating CoreDNS. Instead it writes a DNSEndpoint per hostname per address family (one A record holding all IPv4 ClusterIPs of the proxy Service, one AAAA holding all IPv6 — dual-stack Services produce both) into the controller's namespace. An external-dns deployment configured with --source=crd picks them up and publishes to whichever DNS provider it manages.

helm install ouroboros oci://ghcr.io/lexfrei/charts/ouroboros \
  --namespace ouroboros --create-namespace \
  --set controller.mode=external-dns

externalDns.proxyService (default: the chart-rendered proxy Service) is auto-resolved to a ClusterIP at startup. Use externalDns.proxyIP to override; in that case the controller does not need a get on Services. Add provider-specific annotations such as external-dns.alpha.kubernetes.io/cloudflare-proxied: "false" via externalDns.annotations.

externalDns.cleanupOnUninstall (default true) renders a Helm post-delete hook that runs kubectl delete for ouroboros-owned records after the chart is uninstalled — DNSEndpoint CRs in output=crd, headless Services in output=service. It runs post-delete (not pre-delete) on purpose: at post-delete time the controller Deployment is already gone, so it cannot race-recreate anything we delete. external-dns then sees the records vanish via watch and drops upstream DNS without waiting for its TXT-registry sweep.

⚠️ Switching output between crd and service on an existing release leaves orphans. The reconciler only manages its own active output kind, and the post-delete cleanup hook fires only on helm uninstall (not on helm upgrade). If you flip externalDns.output, run the explicit cleanup once for the previous kind:

# Switching crd → service: clean up old DNSEndpoint CRs
kubectl --namespace <release-ns> delete dnsendpoints.externaldns.k8s.io \
  --selector='app.kubernetes.io/managed-by=ouroboros,ouroboros.lexfrei.tech/instance=<release-name>'

# Switching service → crd: clean up old Services
kubectl --namespace <release-ns> delete services \
  --selector='app.kubernetes.io/managed-by=ouroboros,ouroboros.lexfrei.tech/instance=<release-name>'

The controller probes the inactive output kind on startup and logs a Warn (with this command pre-rendered) when orphans are present. The probe is a best-effort safety net — it only succeeds when the controller's ServiceAccount has read access to the inactive kind. Chart-managed deployments deliberately minimise RBAC and grant verbs for the active kind only, so the probe silently 403s and the warning never fires; operators flipping output in a chart-managed release MUST still run the kubectl delete commands above. The probe surfaces orphans only in clusters where an operator has manually broadened the controller's RBAC (e.g. cluster-admin or a custom Role).

Empty-hosts mass-prune guard

If every Ingress / HTTPRoute disappears (or every hostname becomes a wildcard — controller filters wildcards before they reach the reconciler), the reconciler refuses to prune ouroboros-owned records and instead logs a Warn telling the operator to either uninstall the chart/manifests or restore at least one hostname. This protects against a single accidental commit silently wiping all published DNS — for example, an Ingress / HTTPRoute manifest rewrite that replaces every hostname with a wildcard, or a GitOps revert that drops every Ingress / HTTPRoute in one pass. The legitimate "remove ouroboros entirely" path stays through helm uninstall, which fires the post-delete cleanup hook — uninstall, not config drift, is the way to mass-delete.

external-dns output: crd vs service

Two ways for ouroboros to talk to external-dns. Pick the one that matches what your external-dns instance is configured to read.

externalDns.output Object emitted external-dns side requires When to use
crd (default) externaldns.k8s.io/v1alpha1.DNSEndpoint per hostname per address family --source=crd --crd-source-apiversion=externaldns.k8s.io/v1alpha1 --crd-source-kind=DNSEndpoint The cleanest path for instances that speak the CRD source. Tag with externalDns.labels for --label-filter separation.
service One annotated headless Service per hostname (<prefix>/hostname, <prefix>/target, <prefix>/ttl) --source=service --annotation-prefix=<prefix> Existing external-dns instances that consume Services and use --annotation-prefix to scope themselves. Common in homelab split-horizon (one public, one internal).

In service mode set externalDns.annotationPrefix to whatever your target external-dns instance uses (default is the upstream external-dns.alpha.kubernetes.io/):

helm install ouroboros oci://ghcr.io/lexfrei/charts/ouroboros \
  --namespace ouroboros --create-namespace \
  --set controller.mode=external-dns \
  --set externalDns.output=service \
  --set externalDns.annotationPrefix=internal-dns/

The receiving external-dns then needs --annotation-prefix=internal-dns/ for its config to match. Other instances reading external-dns.alpha.kubernetes.io/ ignore ouroboros's Services.

Service-mode keeps the chart-rendered Services lightweight (spec.clusterIP: None, no selector, no ports) — they exist purely as annotation carriers. Dual-stack targets are joined into a single <prefix>/target: 10.0.0.1,fd00::1 entry, and external-dns produces both A and AAAA records from the one Service.

Why not DNSRecordSet

DNSRecordSet is an evolving proposal in external-dns; not yet ratified, so adopting it would tie the chart to a moving target. DNSEndpoint is the documented stable CRD contract — every shipping provider supports it through --source=crd. The annotated-Service path uses upstream's stable Service source, so neither output ties ouroboros to an unstable interface.

Coverage caveat (both modes). Hostname extraction is asymmetric by design:

  • Ingress: only spec.tls[].hosts is read; plain HTTP-only Ingresses are ignored. The hairpin-NAT problem manifests for TLS-terminated PROXY-protocol traffic.
  • Gateway-API: Gateway.spec.listeners[].hostname and HTTPRoute.spec.hostnames are read regardless of protocol. Listeners are commonly paired (HTTP + HTTPS for redirect-to-TLS), and operators expect both to be hairpinned.

etc-hosts caveat. Each DaemonSet pod runs a full controller — Ingress/Gateway informers per node. On large clusters that is N replicated kube-apiserver watches producing identical results. Prefer coredns mode unless your nodes genuinely bypass cluster DNS.

node-local-dns caveat. Both coredns and coredns-import modes do NOT cover pods that resolve through node-local-dns. Pods on node-local-dns-equipped clusters query the per-node cache first; for non-cluster.local queries (which is exactly the hairpin case) node-local-dns forwards UPSTREAM and never sees the rewrite block ouroboros writes into CoreDNS (or the import ConfigMap CoreDNS pulls in). Hairpin silently fails for those pods. ouroboros logs a Warn at startup when it detects the kube-system/node-local-dns ConfigMap (in either mode). Override the lookup target via OUROBOROS_NODE_LOCAL_DNS_NAMESPACE / OUROBOROS_NODE_LOCAL_DNS_CONFIGMAP if your deployment uses a non-default location. The detection probe is best-effort: the chart's narrowed RBAC does not grant cluster-wide get on ConfigMaps, so on managed clusters that block the probe (Forbidden response) the warning never fires. If your cluster runs node-local-dns and you cannot extend ouroboros's RBAC, treat one of the workarounds below as mandatory regardless of whether the warning surfaces. Two reliable workarounds:

  • Switch controller.mode=external-dns — DNSEndpoint records flow through whatever provider/CCM the cluster uses, independent of the node-local cache.
  • Manually add the same rewrite name directives to the node-local-dns Corefile block(s) that handle external queries. ouroboros does not auto-mutate node-local-dns because its Corefile uses pillar templates and zone scopes that are gnarly to safely transform without per-cluster knowledge.

Multi-ingress-controller caveat. Clusters running two ingress controllers (one with PROXY-protocol, one without) need to scope ouroboros to the PROXY-protocol one — otherwise every hostname is rewritten and traffic for the other controller's hosts hits a 404. Set controller.ingressClass=<class> (and controller.gatewayClass=<class> for Gateway-API) to filter sources. Ingresses without an explicit spec.ingressClassName are dropped under the filter — they are ambiguous, and silently hairpinning them via the wrong controller is the failure mode this knob exists to prevent.

RBAC matrix (operator-facing)

The chart suppresses the Role belonging to the other modes — operators running external-dns mode never see kube-system manifests, which is the frequent ask from managed-cluster users.

Mode Cluster-scope reads Namespaced writes
coredns networking.k8s.io/ingresses (+ gateway.networking.k8s.io opt-in) kube-system: configmaps/coredns get,update,patch (controller); pre-delete hook (controller.coredns.cleanupOnUninstall: true, default): SA + Role[configmaps/coredns get,update] + RoleBinding in the Corefile namespace; release-ns Role[deployments/scale/<controller> get,patch,update + deployments/<controller> get] + RoleBinding to quiesce the controller (scale to 0, then poll status.replicas until empty/0) before the cleanup edit
coredns-import same controller.corednsImport.namespace (default kube-system): configmaps/<controller.corednsImport.configmap> (default coredns-custom) get,update,patch (controller). NOT granted access to the main kube-system/coredns ConfigMap. Pre-delete hook (controller.corednsImport.cleanupOnUninstall: true, default): SA + Role[configmaps/<configmap> get,patch] + RoleBinding in the import-CM namespace; release-ns Role[deployments/scale/<controller> get,patch,update + deployments/<controller> get] + RoleBinding to quiesce the controller
external-dns (output=crd) same externalDns.namespace (default release-ns): externaldns.k8s.io/dnsendpoints full CRUD; release-ns: named-Service get for ClusterIP auto-discovery; release-ns post-delete hook: SA + Role[dnsendpoints list,delete,deletecollection] + RoleBinding for cleanup-on-uninstall
external-dns (output=service) same externalDns.namespace (default release-ns): core services full CRUD (replaces dnsendpoints); release-ns: named-Service get for ClusterIP auto-discovery; release-ns post-delete hook: SA + Role[services list,delete,deletecollection] + RoleBinding for cleanup-on-uninstall
etc-hosts same (no extra Role; node-local file write via DaemonSet hostPath)

CoreDNS reload caveat. Both coredns and coredns-import modes assume CoreDNS' reload plugin is enabled (the default in kubeadm). If your Corefile lacks it, the writes ouroboros makes are not picked up until CoreDNS pods are restarted manually. ouroboros runs a startup probe that logs a Warn when reload is missing — but the probe is best-effort: in coredns-import mode the chart deliberately narrows the controller's RBAC away from the main kube-system/coredns ConfigMap, so the probe will silently 403 (downgraded to Debug) and the Warn never fires. Operators on coredns-import should verify reload manually using the kubectl one-liner below regardless of whether the Warn appears in the controller logs. Verify with:

kubectl --namespace kube-system get configmap coredns --output jsonpath='{.data.Corefile}' | grep -w reload

Verification

After install:

kubectl --namespace kube-system get configmap coredns --output jsonpath='{.data.Corefile}' | grep -A20 BEGIN.ouroboros

You should see a block like:

# === BEGIN ouroboros (do not edit by hand) ===
rewrite name foo.example.com ouroboros-proxy.ouroboros.svc.cluster.local.
# === END ouroboros ===

From any pod:

getent hosts foo.example.com
# returns the ouroboros-proxy ClusterIP, not the LoadBalancer IP

A curl https://foo.example.com/ from a pod will then see X-Forwarded-For populated correctly by the ingress-controller because the PROXY-protocol header reached it.

Configuration

Both subcommands accept flags and env vars (flags override env, env overrides defaults). Run with --help for the full list — the table below is the source-of-truth alphabetical reference.

ouroboros controller
Flag Env var Default Notes
--mode OUROBOROS_CONTROLLER_MODE coredns One of coredns, coredns-import, etc-hosts, external-dns.
--kubeconfig OUROBOROS_CONTROLLER_KUBECONFIG (empty = in-cluster) Path to a kubeconfig file.
--gateway-api OUROBOROS_CONTROLLER_GATEWAY_API false Watch Gateway/HTTPRoute in addition to Ingress.
--resync OUROBOROS_CONTROLLER_RESYNC 10m Informer resync period.
--coredns-namespace OUROBOROS_CONTROLLER_COREDNS_NAMESPACE kube-system CoreDNS ConfigMap namespace.
--coredns-configmap OUROBOROS_CONTROLLER_COREDNS_CONFIGMAP coredns CoreDNS ConfigMap name.
--coredns-key OUROBOROS_CONTROLLER_COREDNS_KEY Corefile Data key holding the Corefile.
--coredns-import-namespace OUROBOROS_CONTROLLER_COREDNS_IMPORT_NAMESPACE kube-system Namespace of the separate import ConfigMap (coredns-import mode).
--coredns-import-configmap OUROBOROS_CONTROLLER_COREDNS_IMPORT_CONFIGMAP coredns-custom Name of the import ConfigMap (coredns-import mode).
--coredns-import-key OUROBOROS_CONTROLLER_COREDNS_IMPORT_KEY ouroboros.override Data key inside the import ConfigMap holding the rewrite snippet.
--proxy-fqdn OUROBOROS_CONTROLLER_PROXY_FQDN (empty — use runtime composition via --proxy-service-name) Explicit rewrite-target FQDN. Must end with a trailing dot. When non-empty, takes precedence over --proxy-service-name+--proxy-service-namespace.
--proxy-service-name OUROBOROS_CONTROLLER_PROXY_SERVICE_NAME ouroboros-proxy Platform-agnostic alternative to --proxy-fqdn: when --proxy-fqdn is empty, the controller composes the FQDN at runtime as <svc>.<ns>.svc.<cluster-domain>. using this value, --proxy-service-namespace, and the auto-detected --cluster-domain.
--proxy-service-namespace OUROBOROS_CONTROLLER_PROXY_SERVICE_NAMESPACE (empty) Namespace of the in-cluster ouroboros-proxy Service. Set together with --proxy-service-name to enable runtime composition; the chart passes .Release.Namespace here.
--etc-hosts OUROBOROS_CONTROLLER_ETC_HOSTS /host/etc/hosts Path to host-mounted hosts file (etc-hosts mode).
--proxy-ip OUROBOROS_CONTROLLER_PROXY_IP (empty) Required for etc-hosts mode.
--external-dns-namespace OUROBOROS_CONTROLLER_EXTERNAL_DNS_NAMESPACE (release namespace) Where DNSEndpoint CRs are written (external-dns mode). Validated as RFC 1123 label only when explicitly set; the release-namespace fallback is already valid by definition.
--external-dns-record-ttl OUROBOROS_CONTROLLER_EXTERNAL_DNS_RECORD_TTL 60 Record TTL on each emitted DNSEndpoint, [1, 86400] seconds.
--external-dns-proxy-ip OUROBOROS_CONTROLLER_EXTERNAL_DNS_PROXY_IP (empty) Override target IP. Empty = discover via the named Service.
--external-dns-proxy-service OUROBOROS_CONTROLLER_EXTERNAL_DNS_PROXY_SERVICE ouroboros-proxy Service name resolved at startup to ClusterIP.
--external-dns-annotation (no env mapping; chart only) (none) Repeatable key=value annotations copied onto every emitted record (DNSEndpoint in output=crd, headless Service in output=service). Reserved keys are rejected at runtime.
--external-dns-label (no env mapping; chart only) (none) Repeatable key=value labels copied onto every emitted record (DNSEndpoint in output=crd, headless Service in output=service). Use case: multi-instance external-dns with --label-filter (e.g. dedicated internal-DNS instance) — the filter is applied to whichever kind ouroboros emits. Reserved keys (app.kubernetes.io/managed-by, ouroboros.lexfrei.tech/instance) are rejected.
--external-dns-output OUROBOROS_CONTROLLER_EXTERNAL_DNS_OUTPUT crd One of crd (DNSEndpoint CRs) or service (annotated headless Services).
--external-dns-annotation-prefix OUROBOROS_CONTROLLER_EXTERNAL_DNS_ANNOTATION_PREFIX external-dns.alpha.kubernetes.io/ Annotation prefix for output=service. Must end with /. Override to match your target external-dns instance's --annotation-prefix.
--ingress-class OUROBOROS_CONTROLLER_INGRESS_CLASS (empty) Filter Ingresses by spec.ingressClassName. Empty = all. Ingresses without an explicit class are dropped under the filter.
--gateway-class OUROBOROS_CONTROLLER_GATEWAY_CLASS (empty) Filter Gateways by spec.gatewayClassName (and HTTPRoutes attached to surviving Gateways). Empty = all.
--cluster-domain OUROBOROS_CONTROLLER_CLUSTER_DOMAIN (auto-detect from /etc/resolv.conf, fallback cluster.local) Kubernetes cluster DNS domain. The chart sets this from controller.clusterDomain; bare-binary deployments inherit auto-detection unless this flag/env overrides. The detected value is logged at startup and compared against --proxy-fqdn; a mismatch produces a WARN log line but does not block startup.
--log-level OUROBOROS_CONTROLLER_LOG_LEVEL info Slog handler verbosity: debug, info, warn, or error. Validated at startup (typos fail rather than silently downgrade). Set to debug to surface per-event informer activity (AddFunc/UpdateFunc/DeleteFunc) and reconcile pacing — useful when an Ingress-event-to-rewrite latency looks suspicious (e.g. tenant-apiserver behind konnectivity).
ouroboros proxy
Flag Env var Default
--listen-http OUROBOROS_PROXY_LISTEN_HTTP :8080
--listen-https OUROBOROS_PROXY_LISTEN_HTTPS :8443
--listen-health OUROBOROS_PROXY_LISTEN_HEALTH :8081
--target-host OUROBOROS_PROXY_TARGET_HOST (empty — use runtime composition via --target-service-name)
--target-service-name OUROBOROS_PROXY_TARGET_SERVICE_NAME (empty)
--target-service-namespace OUROBOROS_PROXY_TARGET_SERVICE_NAMESPACE (empty)
--cluster-domain OUROBOROS_PROXY_CLUSTER_DOMAIN (auto-detect from /etc/resolv.conf)
--target-http-port OUROBOROS_PROXY_TARGET_HTTP_PORT 80
--target-https-port OUROBOROS_PROXY_TARGET_HTTPS_PORT 443
--dial-timeout OUROBOROS_PROXY_DIAL_TIMEOUT 5s
--ready-timeout OUROBOROS_PROXY_READY_TIMEOUT 2s
--shutdown-grace OUROBOROS_PROXY_SHUTDOWN_GRACE 30s
Readiness diagnostics

/readyz (on --listen-health) dials the resolved backend and returns 200 when the dial succeeds, 503 otherwise. On a failure the proxy logs a single WARNbackend readiness check failed with the attempted backend address, a cause token (dns-nxdomain, dns-timeout, dns-error, connection-refused, timeout, or unreachable), and the raw dial error — so a misconfigured target (wrong --target-service-name / --target-service-namespace, or an unreachable address) is obvious from kubectl logs instead of leaving the pod silently NotReady.

The WARN fires once on the transition into the failing state, not on every probe, and a later recovery logs one INFO; an unchanged state stays silent so the few-second probe period never floods the log. The same cause is embedded in the 503 body (backend unreachable (<cause>): <error>), so it also surfaces from a manual curl /readyz.

Build

go build ./cmd/ouroboros
docker build --file Containerfile --tag ouroboros:dev .

Develop

go test -race -count=1 ./...
golangci-lint run ./...
helm lint   charts/ouroboros
helm unittest charts/ouroboros

License

BSD 3-Clause — see LICENSE.

Directories

Path Synopsis
cmd
ouroboros command
Package main is the entry point for the ouroboros binary, dispatching to the controller or proxy subcommand.
Package main is the entry point for the ouroboros binary, dispatching to the controller or proxy subcommand.
internal
config
Package config parses CLI flags and environment variables into typed configuration structs for the ouroboros controller and proxy.
Package config parses CLI flags and environment variables into typed configuration structs for the ouroboros controller and proxy.
controller
Package controller orchestrates Ingress and Gateway-API informers, extracting hostnames into a sorted, deduplicated set that downstream reconcilers (CoreDNS, /etc/hosts) write into the cluster.
Package controller orchestrates Ingress and Gateway-API informers, extracting hostnames into a sorted, deduplicated set that downstream reconcilers (CoreDNS, /etc/hosts) write into the cluster.
coredns
Package coredns mutates the CoreDNS Corefile so the in-cluster proxy receives DNS-rewritten traffic for ingress hostnames.
Package coredns mutates the CoreDNS Corefile so the in-cluster proxy receives DNS-rewritten traffic for ingress hostnames.
externaldns
Package externaldns produces externaldns.k8s.io/v1alpha1.DNSEndpoint objects for ouroboros's external-dns mode.
Package externaldns produces externaldns.k8s.io/v1alpha1.DNSEndpoint objects for ouroboros's external-dns mode.
hosts
Package hosts mutates a hosts(5) file so the in-cluster proxy receives traffic for ingress hostnames on nodes that bypass CoreDNS (kubelet, the container runtime, etc.).
Package hosts mutates a hosts(5) file so the in-cluster proxy receives traffic for ingress hostnames on nodes that bypass CoreDNS (kubelet, the container runtime, etc.).
k8s
Package k8s constructs Kubernetes and Gateway-API clientsets from either an in-cluster ServiceAccount or a kubeconfig file.
Package k8s constructs Kubernetes and Gateway-API clientsets from either an in-cluster ServiceAccount or a kubeconfig file.
proxy
Package proxy implements the in-cluster TCP proxy that injects PROXY-protocol v1 headers in front of forwarded connections.
Package proxy implements the in-cluster TCP proxy that injects PROXY-protocol v1 headers in front of forwarded connections.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL