klimax is now marina. Same tool, new name since v0.3. Existing install? Migrate with marina migrate →
Documentation menu

Local DNS names

Every LoadBalancer Service gets a hostname — <service>.<namespace>.<cluster>.demo.internal — that resolves from your Mac, from the VM and from every pod. No domain to buy, no DNS provider account, no /etc/hosts edits — and HTTPS your browser trusts, from marina's own CA. Both are on by default.

Try it

$ marina up
$ marina cluster create dev
$ kubectl create deploy web --image=nginx
$ kubectl expose deploy web --port 80 --type LoadBalancer
$ curl http://web.default.dev.demo.internal/
<title>Welcome to nginx!</title>
$ marina dns list
NAME                               IP
web.default.dev.demo.internal    172.30.1.1

The first marina up asks for your password once, to write /etc/resolver/demo.internal. Every later run skips it.

What gets a name

ResourceName
LoadBalancer Service<service>.<namespace>.<cluster>.demo.internal, automatically
LoadBalancer Service with a hostname annotationThe annotated name, in addition to the automatic one
IngressNone by default. With network.dns.ingress: true: each host under *.<cluster>.demo.internal, plus <ingress>.<namespace>.<cluster>.demo.internal
ClusterIP, headless, NodePort ServicesNone — their addresses are not reachable from the Mac

A custom name goes in an annotation, and must sit under the cluster's own zone:

apiVersion: v1
kind: Service
metadata:
  name: keycloak
  annotations:
    external-dns.kubernetes.io/hostname: sso.dev.demo.internal
spec:
  type: LoadBalancer
⚠

The annotation prefix is external-dns.kubernetes.io/. The older external-dns.alpha.kubernetes.io/ prefix that most examples online use is ignored without a warning.

The names work between clusters too: a pod in staging can call web.default.dev.demo.internal, because every cluster shares the kind network.

Fleet-wide names

Members of a fleet can also publish under the fleet's zone: gateway.lab.demo.internal instead of gateway.lab-east.demo.internal, so clients do not need to know which cluster serves it. The first member to publish a name owns it. Which to choose: the Hostname playbook.

How it works

IN THE VM — ON THE KIND NETWORK ExternalDNS one per cluster watches LB Services etcd 172.30.255.52 holds the records CoreDNS 172.30.255.53 serves demo.internal WRITES READS Your Mac curl web.default .dev.demo.internal /etc/resolver demo.internal → 172.30.255.53 Pods cluster CoreDNS forwards the zone via the host route answer: the MetalLB VIP (e.g. 172.30.1.1) — the connection then goes straight to it
Records flow left to right along the top; lookups come up from below.
  • marina up runs the etcd and CoreDNS containers at fixed addresses on the kind network, and writes the resolver file on the Mac.
  • marina cluster create (and fleets) install ExternalDNS in the new cluster, and add the zone to that cluster's CoreDNS so pods resolve it too.
  • marina cluster delete removes the cluster's records.

Two properties make it low-maintenance:

  • The resolver file never goes stale. Its nameserver is on the kind network, which the host route already reaches and keeps reaching when the VM's lima0 address changes. That is why it needs sudo only once.
  • Clusters cannot overwrite each other. Each cluster's ExternalDNS may only write under <cluster>.demo.internal. Deleting a Service removes its name within about 15 seconds.

Clusters created before v0.2.0

Clusters that existed before you upgraded keep working, but have no names. Add them to the zone:

$ marina up                           # starts the DNS containers
$ marina dns attach dev staging        # installs ExternalDNS + the CoreDNS forward

attach restarts each cluster's CoreDNS, which interrupts in-cluster DNS for a few seconds. It is safe to re-run.

Turning it off, or changing the domain

network:
  dns:
    enabled: true              # false: the next marina up removes the containers, rules and resolver file
    domain: "demo.internal"  # names: <service>.<namespace>.<cluster>.<domain>

Both are applied by marina up — no destroy needed. After changing the domain, run marina dns attach on your clusters so their ExternalDNS writes the new one. marina refuses a bare top-level domain and anything under .local, which is reserved for multicast DNS and resolves in some tools but not others.

Why .internal

ICANN reserved .internal for private networks in 2024. It will never exist on the public internet, so no registered domain can ever clash with your names. marina uses a subdomain, demo.internal, rather than all of .internal: corporate networks and GCP (metadata.google.internal) already use other names under it, and marina should not capture those.

HTTPS

No public certificate authority issues certificates for .internal, so marina runs its own — on by default (network.dns.tls), with nothing to install: no mkcert, no homepki, no cert-manager.

TierMay issue forWhere
Root CA.demo.internal only~/.marina/pki/demo.internal/ on the Mac — its key never leaves it — and trusted in the System keychain
Intermediate, one per cluster.<cluster>.demo.internal onlyOn the Mac; given to cert-manager when the cluster runs it
Wildcard certificate, one per cluster*.<cluster>.demo.internalSecret default/marina-wildcard-tls in the cluster
Intermediate + wildcard, one per fleet.<fleet>.demo.internal onlySecret default/marina-fleet-wildcard-tls in every member

The first marina up creates the root and asks for your password once to trust it. Each marina cluster create then issues that cluster's certificates. Serve the wildcard Secret and browsers accept it with no warning:

volumes:
  - name: tls
    secret:
      secretName: marina-wildcard-tls   # tls.crt, tls.key, ca.crt
$ curl https://shop.dev.demo.internal/        # no -k, no --cacert

That holds for Safari, Chrome, macOS curl (Apple's and Homebrew's both defer to the keychain), Go programs and Apple's python3. Tools that ship their own CA bundle — Java, a Homebrew Python, the pods in your clusters — need the root added separately (marina ca cert prints it); Firefox reads the keychain only when security.enterprise_roots.enabled is on.

The Secret lives in default; copy it next to a workload or an Ingress in another namespace with marina ca secret dev -n <namespace>. tls.crt carries the intermediate after the leaf, so clients that trust only the root can build the chain.

What the wildcard covers

A wildcard covers exactly one label:

NameCovered by *.dev.demo.internal?
Annotated names and Ingress hosts: shop.dev.demo.internalyes
Automatic names: web.default.dev.demo.internalno — two labels under the zone

For automatic names, either give the Service a name directly under the cluster zone with the annotation, use cert-manager (below), or flatten the automatic names with network.dns.nameTemplate: "{{.Name}}-{{.Namespace}}".

With cert-manager

marina never installs cert-manager — your own install would collide with it. When the cluster already runs it, marina ca attach adds a marina-ca ClusterIssuer backed by the cluster's intermediate, so any name in the cluster's zone can get its own certificate:

$ marina ca attach dev
✓ dev: *.dev.demo.internal in default/marina-wildcard-tls (expires 2027-09-26); ClusterIssuer marina-ca ready
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: web-tls
spec:
  secretName: web-tls
  dnsNames: [web.default.dev.demo.internal]
  issuerRef: {name: marina-ca, kind: ClusterIssuer}

Or annotate an Ingress with cert-manager.io/cluster-issuer: marina-ca.

What the name constraints protect

Trusting a root in the System keychain normally means trusting it for every site. This one cannot be used that way: the constraints are enforced by the client. A certificate for github.com signed with a cluster's intermediate — which anyone who can read the cert-manager Secret could do — is refused:

$ curl https://github.com/ --resolve github.com:443:172.30.1.2
curl: (60) SSL certificate problem: permitted subtree violation

Go, macOS, Chrome and Firefox enforce it the same way. A cluster's intermediate cannot even sign for another cluster's zone.

Pods need the root too

The keychain covers the Mac. A pod calling https://shop.dev.demo.internal fails with unable to get local issuer certificate until it trusts the root — as with any private CA. marina puts it in the ConfigMap default/marina-root-ca; mount it and point the client at it:

volumes:
  - name: marina-ca
    configMap: {name: marina-root-ca}   # key: ca.crt

In another namespace: marina ca cert | kubectl create configmap marina-root-ca -n <namespace> --from-file=ca.crt=/dev/stdin. The nodes trust the root already, so image pulls from a registry served on a demo.internal name work.

Renewal and cleanup

  • The wildcard is valid for a year. marina ca attach <cluster> re-issues it within 30 days of expiry; marina doctor and marina ca status show the dates.
  • marina cluster delete removes the cluster's intermediate and wildcard. marina destroy keeps the root, like the registry cache.
  • Turning network.dns.tls off leaves the root and its trust in place; marina ca untrust removes the trust explicitly.

Troubleshooting

SymptomCause and fix
Nothing resolvesRun marina doctor. Its dns check sends a real query from the Mac and names the first part of the chain that fails — container, iptables exemption, route or resolver file. Its dns-system-resolver check then resolves a probe name the way apps do, through the macOS resolver.
A brand-new Service does not resolve on the MacExternalDNS syncs every 15 seconds. If you looked the name up before it was published, macOS keeps that "no such name" answer for about 75 seconds, whatever the zone says. Wait for marina dns list to show it before the first lookup, or run sudo killall -HUP mDNSResponder.
The browser warns about the certificateThe root is not trusted yet: marina ca status, then marina ca trust. Or the name is outside the wildcard — see What the wildcard covers. Certificates issued by v0.2.2–v0.2.3 are rejected by macOS: marina ca attach <cluster> re-issues them.
dig web.default.dev.demo.internal finds nothing, but curl worksdig never reads /etc/resolver. Use dig @172.30.255.53 <name> or dscacheutil -q host -a name <name>.
One tool fails where others workSome clients do their own DNS: Go programs built with the pure-Go resolver, and Chrome when Secure DNS uses a custom provider. curl, Safari, Chrome's default setting, kubectl and helm all work.
dig @172.30.255.53 <name> answers, but curl and every browser say the name does not existAn MDM encrypted-DNS profile (com.apple.dnsSettings.managed, pushed by Kandji, Jamf, Intune…) sends every lookup to its DoH or DoT server, so /etc/resolver is never read. scutil --dns still lists the resolver as reachable. marina doctor names the profile (check dns-system-resolver). You cannot override a managed profile: ask your MDM admin to exempt the zone with an OnDemandRules rule (Action: EvaluateConnection, DomainAction: NeverConnect, Domains: [demo.internal]).
An existing cluster has no namesIt was created before the feature was on: marina dns attach <cluster>.

Behind a corporate proxy, the zone is added to the no_proxy list marina computes, so these names never go to the proxy.

See also