An OrbStack Kubernetes demo that serves private LAN applications through Traefik with publicly trusted Let's Encrypt certificates issued by cert-manager and Cloudflare DNS-01 validation.
The tracked manifests use the reserved example.com domain as a placeholder. The setup script asks for a domain at runtime and never writes it to tracked files.
The deployment provides:
- Host-based routing for two unprivileged NGINX frontends.
- Automatic HTTP-to-HTTPS redirects.
- A publicly trusted wildcard certificate without a public application endpoint.
- Private DNS resolution for clients on the same LAN.
- An optional Traefik dashboard protected by Basic Auth, rate limiting, and a private-address allowlist.
- Restricted pod security settings, namespace-scoped Traefik RBAC, and frontend network isolation.
Example endpoints:
https://frontend-1.demo.example.comhttps://frontend-2.demo.example.comhttps://traefik.demo.example.com/dashboard/when the dashboard is explicitly enabled
Application traffic remains on the local network:
Client device on the same LAN
-> private DNS resolver
-> macOS host LAN IP on ports 80/443
-> OrbStack LoadBalancer
-> Traefik
-> frontend Service
Certificate issuance uses a separate public DNS path:
cert-manager
-> Cloudflare API creates a temporary ACME TXT record
-> Let's Encrypt validates domain control
-> cert-manager stores the certificate in frontends-tls
-> Cloudflare TXT record is removed
Cloudflare validates DNS ownership but does not proxy application traffic. DNS-01 avoids exposing an HTTP challenge endpoint and supports wildcard certificates. Certificate Transparency logs permanently record the issued wildcard hostname, and the ACME TXT record is publicly queryable while validation is active.
The Cloudflare token is stored in the cert-manager namespace. Traefik uses namespace-scoped RBAC and watches only ingress-demo, so it cannot read that credential.
- macOS with OrbStack installed and Kubernetes enabled.
kubectlconfigured with theorbstackcontext.- Kubernetes 1.33 or later.
- Helm 3.9 or later, installed with
brew install helmif needed. curl,shasum, andhtpasswdfrom macOS;htpasswdis required only for the optional dashboard.- A DNS zone managed by Cloudflare.
- Permission to create a scoped Cloudflare API token.
- A router or local DNS server that supports private DNS records.
- A client device connected to the same non-isolated LAN.
orb start k8s
kubectl config use-context orbstack
kubectl get nodesThe node must report Ready. The scripts refuse to operate on any other kubectl context.
In OrbStack Settings > Kubernetes, enable Expose services to local network devices.
Assign the Mac a DHCP reservation so its LAN address remains stable. For Wi-Fi, retrieve the current address with:
ipconfig getifaddr en0Use the active Ethernet interface instead when applicable. Do not configure router port forwarding, Cloudflare Tunnel, or Cloudflare proxying.
Add both frontend records to the router or local DNS server. Add the dashboard record only if you plan to enable it. Replace the placeholder domain and IP address with your values:
frontend-1.demo.example.com -> 192.168.1.50
frontend-2.demo.example.com -> 192.168.1.50
traefik.demo.example.com -> 192.168.1.50 # Optional dashboard
Clients must use the private DNS resolver and remain on a non-isolated LAN. VPNs, iCloud Private Relay, and custom DNS services may bypass local DNS.
Verify private resolution:
dig +short frontend-1.demo.example.com
dig +short frontend-2.demo.example.comThese commands should return the Mac's LAN IP. Confirm that no public application address is published:
dig +short @1.1.1.1 A frontend-1.demo.example.com
dig +short @1.1.1.1 AAAA frontend-1.demo.example.com
dig +short @1.1.1.1 A frontend-2.demo.example.com
dig +short @1.1.1.1 AAAA frontend-2.demo.example.comThese commands should produce no output.
Create a custom token under Cloudflare > My Profile > API Tokens with these permissions:
Permissions:
Zone / DNS / Edit
Zone / Zone / Read
Zone Resources:
Include / Specific zone / example.com
Select your actual DNS zone instead of example.com. Use a scoped API token rather than the Global API Key, and never commit the token.
./scripts/setup.shEnter the base domain and Cloudflare token at the hidden prompts. The script:
- Downloads Traefik chart
41.0.2, its required CRDs, and cert-managerv1.21.0over TLS and verifies fixed SHA-256 checksums before use. - Runs Traefik
v3.7.8, cert-manager, and the frontend image by immutable multi-platform container digests. - Installs Traefik with namespace-scoped RBAC and only the CRDs required by this project.
- Uses an existing cert-manager installation without modifying or claiming it, or installs and labels its own instance.
- Stores the Cloudflare token in
cert-manager, separate from the ingress controller. - Deploys the frontends, network policy, certificate, and Traefik route.
- Refuses to modify namespaces, CRDs, issuers, or Secrets not owned by this project.
Initial certificate issuance can take several minutes. Setup is idempotent for the same domain.
For noninteractive domain selection, set BASE_DOMAIN. Secret values must be supplied in the environment if no project-owned Secret exists:
BASE_DOMAIN=your-domain.tld CLOUDFLARE_API_TOKEN="$CLOUDFLARE_API_TOKEN" ./scripts/setup.shThe Traefik API and dashboard are disabled by default. Enable them explicitly:
ENABLE_TRAEFIK_DASHBOARD=true ./scripts/setup.shSetup prompts for a username and a password of at least 12 characters. The route is HTTPS-only, preserves the LoadBalancer source address, accepts source addresses only from loopback, RFC 1918, or IPv6 ULA ranges, and applies a request rate limit. Rerunning setup without ENABLE_TRAEFIK_DASHBOARD=true disables the API and removes project-owned dashboard resources.
Check the deployed resources:
kubectl get pods,services,networkpolicy -n ingress-demo
kubectl get certificate,certificaterequest,order,challenge -n ingress-demo
kubectl get clusterissuer ingress-demo-letsencrypt-cloudflare
kubectl get ingressroute,middleware -n ingress-demoThe frontends Certificate should report READY=True. A completed Challenge normally disappears after issuance.
Verify redirects, TLS, and host-based routing from the Mac:
curl -I http://frontend-1.demo.example.com
curl https://frontend-1.demo.example.com
curl https://frontend-2.demo.example.comThe HTTP request should redirect permanently to HTTPS. The HTTPS requests should return the corresponding frontend messages. A browser on any device connected to the same LAN should trust the certificate without installing a private CA.
When enabled, open https://traefik.demo.example.com/; the root redirects to /dashboard/, where Basic Auth is required.
The unprivileged NGINX containers mount HTML from Kustomize-generated ConfigMaps. After editing either frontend, rerun setup with the same domain:
BASE_DOMAIN=your-domain.tld ./scripts/setup.sh
kubectl rollout status deployment/frontend-1 -n ingress-demo
kubectl rollout status deployment/frontend-2 -n ingress-demoKustomize updates the affected ConfigMap name, which triggers a rollout without requiring a container build.
Older revisions supported the now-retired ingress-nginx controller and created resources without ownership labels. Setup intentionally refuses to adopt those resources. Remove the old deployment first while the cluster is online:
# Older ingress-nginx deployment
./scripts/cleanup.sh --remove-legacy-ingress-nginx
# Older Traefik deployment
./scripts/cleanup.sh --remove-legacy-traefik --remove-traefik-crdsThe command requires typing ingress-demo before deletion. It recognizes a legacy installation only when the old demo namespace contains the expected controller annotation. The old cert-manager installation is preserved because it has no reliable ownership marker and is treated as externally managed.
Inspect certificate state, events, and logs:
kubectl describe certificate frontends -n ingress-demo
kubectl describe clusterissuer ingress-demo-letsencrypt-cloudflare
kubectl get certificate,certificaterequest,order,challenge -n ingress-demo
kubectl get events -n ingress-demo --sort-by=.lastTimestamp
kubectl logs -n cert-manager deployment/cert-manager --since=10m
kubectl logs -n traefik deployment/traefik --since=10mIf a client cannot connect, verify that OrbStack LAN exposure is enabled, the private DNS records use the Mac's current LAN IP, the client uses the private resolver, and client isolation is disabled. Temporarily disable VPN or privacy services that override DNS.
An externally managed cert-manager installation is not patched by setup. If DNS-01 self-checks resolve private split-horizon records instead of authoritative public DNS, configure that installation to use appropriate public recursive resolvers.
Run cleanup while the cluster is online so cert-manager can remove any active DNS challenge:
./scripts/cleanup.shCleanup displays its target and requires typing ingress-demo. Project-owned namespaces are deleted with all of their contents; other individual resources require this project's ownership marker. Legacy resources require a matching migration option. Traefik CRDs are preserved by default and can be removed only when no Traefik custom resources exist:
./scripts/cleanup.sh --remove-traefik-crdscert-manager is preserved by default even when setup originally installed it. Remove it only when no other workload depends on it:
./scripts/cleanup.sh --remove-cert-managerThe script still refuses cert-manager removal if cert-manager custom resources remain. It retains the cert-manager namespace without the project ownership marker to avoid cascading unrelated objects that may have been added later. A provenance annotation lets setup safely reuse that namespace only while no cert-manager components have appeared in it. For automation, --yes skips the typed confirmation but does not bypass ownership or dependency checks.
Afterward, remove the private DNS records and revoke the Cloudflare API token. Certificate Transparency entries are permanent and cannot be removed.
See SECURITY.md for vulnerability reporting. Automated checks scan committed content and history for secrets, validate Kubernetes manifests, and scan pinned container images and repository configuration for known vulnerabilities and misconfigurations.
Licensed under the MIT License.