Ingress, TLS & Cloudflare
Traffic into the cluster takes two very different paths depending on who's asking:
- Path A — Public HTTP(S) sites like
www.wecare.gr→ Cloudflare proxy → Hetzner Load Balancer → Traefik HTTP entry point → app. - Path B — Management URLs like
grafana-ecnv4-mgmt.ecommercen.com→ Cloudflare Tunnel (cloudflared) → in-cluster service. Gated by Cloudflare Access SSO.
Both paths start at Cloudflare. Public customers never touch the tunnel; operators never touch the Hetzner LB directly.
No external database port
There is no public path to the database. The wecare ERP connector runs as an in-cluster pod and reaches MariaDB via the internal app-db-maxscale.ecommercen-clients-wecare-infrastructure.svc.cluster.local:3306 Service — no external port, no Traefik TCP route, no LB listener. Earlier deployments exposed port 3306 externally for an out-of-cluster ERP client; that route was retired 2026-04-23 (see ecnv4_manifests#11). If a future tenant ever needs external SQL access, do not use port 3306 — pick a non-default port (13306, 33306, …) and pair it with a strict MiddlewareTCP ipAllowList. Default DB ports on a public LB get fingerprinted by scanners within minutes.
Path A — public traffic (customer browsing www.wecare.gr)
Public DNS records (www.wecare.gr, api.wecare.gr, and similar) are orange-cloud proxied through Cloudflare — they resolve to Cloudflare's anycast IPs, Cloudflare terminates TLS for the customer, and Cloudflare forwards to the origin (our Hetzner Cloud Load Balancer's public IP). The LB is an HCCM-managed Service of type LoadBalancer that fronts the Traefik pods; it distributes traffic across all cluster nodes and Traefik handles the in-cluster routing from there.
Path B — management URLs (operator visiting grafana)
Management hostnames live under *ecnv4-mgmt.ecommercen.com. Their DNS records are CNAMEs to the Cloudflare Tunnel address, not to a public IP. Cloudflare Access checks for a valid @advisable.com SSO session (or a Service Auth token for LLM clients), and the tunnel delivers the request to the cloudflared pod running inside the cluster. cloudflared looks at its ingress rules and sends the request directly to the target in-cluster Service — Grafana, ArgoCD server, Longhorn UI, Hubble UI, Keycloak admin, this docs site, etc.
No inbound ports are opened on the Hetzner side for any of this; the tunnel is initiated outbound from the cluster.
Management panels at a glance
Everything below is behind Cloudflare Access (@advisable.com SSO). Bookmark these — they're the day-to-day operator toolkit.
Cluster-wide (not tenant-scoped):
| Panel | URL | What it's for |
|---|---|---|
| ArgoCD | argocd-ecnv4-mgmt.ecommercen.com | GitOps sync state, app diffs, manual sync |
| Grafana | grafana-ecnv4-mgmt.ecommercen.com | Metrics + log dashboards |
| Traefik | traefik-ecnv4-mgmt.ecommercen.com | Live router state, middlewares, entry points |
| Longhorn | longhorn-ecnv4-mgmt.ecommercen.com | RWX volume state, replica placement, backups |
| Hubble | hubble-ecnv4-mgmt.ecommercen.com | Live network flow observability (Cilium) |
Per-tenant (one set per client — wecare shown):
| Panel | URL pattern | Wecare example |
|---|---|---|
| MaxScale GUI | <tool>-<tenant>-ecnv4-mgmt.ecommercen.com | maxscale-wecare-ecnv4-mgmt.ecommercen.com |
| RedisInsight | <tool>-<tenant>-ecnv4-mgmt.ecommercen.com | redisinsight-wecare-ecnv4-mgmt.ecommercen.com |
| phpMyAdmin | <tenant>-<tool>-ecnv4-mgmt.ecommercen.com | wecare-phpmyadmin-ecnv4-mgmt.ecommercen.com |
Naming inconsistency
Most per-tenant panels follow <tool>-<tenant>-... (maxscale-wecare, redisinsight-wecare) but phpMyAdmin uses <tenant>-<tool>-... (wecare-phpmyadmin). This is historical — when typing from memory, check both shapes if you get a 404.
All URLs above are defined in manifests_v1/app-constructs/cloudflared/configmap.yaml (the ingress rules) plus DNS CNAMEs in the Cloudflare dashboard and Access policies on the Cloudflare Access application.
Cloudflare — two distinct roles
- Proxy for public traffic. Orange-cloud DNS for
*.wecare.gr/*.ecommercen.comcustomer-facing hostnames. Cloudflare runs the CDN, WAF, DDoS mitigation, and terminates public TLS. Origin is the Hetzner LB's public IP. - Tunnel + Access for management traffic.
*ecnv4-mgmt.ecommercen.comhostnames CNAME into the tunnel. Cloudflare Access enforces SSO on the URL before any request reaches cloudflared.
Tunnel configuration (which hostnames route to which in-cluster services) lives in manifests_v1/app-constructs/cloudflared/ — declared in the repo, synced like any other app. DNS records themselves live in the Cloudflare dashboard, not in the repo.
Traefik — the in-cluster router (public path only)
Traefik is our Ingress controller and it sits on the public path only. When a public request arrives at a Traefik pod (via the Hetzner LB), Traefik:
- Looks at the
Host:header (e.g.www.wecare.gr). - Matches it against
IngressorIngressRouteresources. - Routes the request to the matching Service → backing pod.
Three resource kinds you'll see in the repo:
Ingress— the standard Kubernetes resource. We use it for simple hostname-based HTTP routing.IngressRoute— Traefik's HTTP CRD. More expressive:Host(\...`)` predicates, middleware, path rewriting, redirects.IngressRouteTCP— Traefik's TCP CRD. Nothing currently uses it (the MaxScale-on-3306 external route was retired 2026-04-23). If you ever reintroduce raw-TCP ingress, matching is viaHostSNI(...)rather thanHost()because TCP has no HTTP headers — and make absolutely sure the entry-point port is non-default if the service is a database (see the note in the Path A/B section above).
Each client's ingress rules live in their own manifests, e.g. manifests_v1/app-constructs/ecommercen-clients/wecare/adveshop4/prod/ingress*.yaml for HTTP.
Management URLs (Path B) don't normally go through Traefik — cloudflared talks directly to the target Service.
cert-manager — internal TLS
- Cloudflare handles the public TLS cert (browser ↔ Cloudflare) for both paths.
- Inside the cluster, traffic is also encrypted end-to-end. cert-manager issues Let's Encrypt certificates for internal services.
- We use the DNS01 challenge: cert-manager proves domain ownership by creating a TXT record in Cloudflare, which Cloudflare verifies via our Cloudflare API token.
- Renewal is automatic, happening at ~30 days before expiry. Cert-manager logs on failure; we have the
ExternalCertExpiringSoonalert as a safety net.
You almost never interact with cert-manager directly. If you see a Certificate CR in a manifest, cert-manager is handling it.
Adding a new public hostname for an existing client
Scenario: wecare wants to add shop.wecare.gr alongside the existing domains.
- Cloudflare dashboard: add a DNS record for
shop.wecare.gr. For a public site, use a proxied (orange-cloud) CNAME that points at an existing proxied hostname for the same origin — or an A record at the Hetzner LB's public IP. Either way, the orange cloud must be on. - Repo: add
shop.wecare.grto the relevant Ingress / IngressRoute resource (copy an existinghost:entry). - Repo: cert-manager will issue the internal cert automatically when the Ingress is applied.
- Commit + push.
- ArgoCD syncs the Ingress. Traefik picks up the new route within seconds.
Adding a new management (ecnv4-mgmt) hostname
Different path — Cloudflared tunnel, not Cloudflare proxy.
- Add an ingress rule for the new hostname in
manifests_v1/app-constructs/cloudflared/configmap.yaml, pointing at the in-cluster Service URL. - Add a CNAME in Cloudflare pointing at the tunnel address (
<uuid>.cfargotunnel.com). - Add the hostname to the Cloudflare Access application policy so SSO protects it.
- Commit + push. ArgoCD reloads cloudflared.
What to look at when a hostname doesn't work
Ask first: is this a public hostname or a management one? The debugging paths differ.
Public hostname (e.g. www.wecare.gr)
- Step 1: external probe — is it up from outside? See the Wecare External Probes Grafana dashboard.
- Step 2: DNS —
dig www.wecare.grshould resolve to Cloudflare anycast IPs. If it doesn't, the DNS record is wrong in Cloudflare. - Step 3: Hetzner LB — is the load balancer healthy? (Delegate to the hcloud-operator agent or check the Hetzner console.)
- Step 4: Traefik —
kubectl -n traefik get ingressroute -Aandkubectl -n traefik logs deploy/traefikto confirm the route exists and requests are arriving. - Step 5: backend — check app status on the target Service's pods.
Management hostname (e.g. grafana-ecnv4-mgmt.ecommercen.com)
- Step 1: can you even reach the Cloudflare login page? If no, DNS or tunnel is broken.
- Step 2: cloudflared tunnel health —
kubectl -n cloudflared logs deploy/cloudflared --tail=50. - Step 3: cloudflared ingress rules —
kubectl -n cloudflared get configmap cloudflared-config -o yamlshould list your hostname. - Step 4: target Service and pod — standard app status checks.