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
| Resource | Name |
|---|---|
| LoadBalancer Service | <service>.<namespace>.<cluster>.demo.internal, automatically |
| LoadBalancer Service with a hostname annotation | The annotated name, in addition to the automatic one |
| Ingress | None by default. With network.dns.ingress: true: each host under *.<cluster>.demo.internal, plus <ingress>.<namespace>.<cluster>.demo.internal |
| ClusterIP, headless, NodePort Services | None — 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
marina upruns 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 deleteremoves 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
lima0address changes. That is why it needssudoonly 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.
| Tier | May issue for | Where |
|---|---|---|
| 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 only | On the Mac; given to cert-manager when the cluster runs it |
| Wildcard certificate, one per cluster | *.<cluster>.demo.internal | Secret default/marina-wildcard-tls in the cluster |
| Intermediate + wildcard, one per fleet | .<fleet>.demo.internal only | Secret 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:
| Name | Covered by *.dev.demo.internal? |
|---|---|
Annotated names and Ingress hosts: shop.dev.demo.internal | yes |
Automatic names: web.default.dev.demo.internal | no — 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 doctorandmarina ca statusshow the dates. marina cluster deleteremoves the cluster's intermediate and wildcard.marina destroykeeps the root, like the registry cache.- Turning
network.dns.tlsoff leaves the root and its trust in place;marina ca untrustremoves the trust explicitly.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
| Nothing resolves | Run 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 Mac | ExternalDNS 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 certificate | The 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 works | dig never reads /etc/resolver. Use dig @172.30.255.53 <name> or dscacheutil -q host -a name <name>. |
| One tool fails where others work | Some 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 exist | An 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 names | It 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
- Networking & L3 routing — the host route the lookups travel over.
- CLI reference → DNS —
marina dns listandattach. - CLI reference → CA —
marina ca status,attach,secret,trust. - Public hostnames with ExternalDNS — names under a domain you own, reachable from other machines.