Preskoči na sadržaj

TLS & Ingress

Source of truth

hns-platform/roles/portal-tls/ (cert collection + template), roles/cert-manager/ (ClusterIssuers), and roles/hns-service/templates/{ingress,ingress-internal,middleware,middleware-basicauth}.yml.j2. Update this page when those change.

Traefik (bundled with k3s) is the cluster edge. Every public host is served over one shared certificate, and the platform offers two ways to restrict a route: internal-only paths and HTTP basic-auth.

One shared multi-SAN certificate

After the per-service deploy loop, the portal-tls role reconciles a single Certificate named hns-portal-tls covering every service's ingress host:

portal-tls role (runs after the service loop):
  1. for each service in components.yml:
       load srv/<name>/.platform.yml
       append every deployments[].ingress.host → portal_hosts
  2. render one Certificate hns-portal-tls
       issuerRef: letsencrypt-prod (ClusterIssuer)
       dnsNames: portal_hosts | unique | sort
  3. kubectl apply

Every Ingress references secretName: hns-portal-tls,
so Traefik serves them all from the one cert.

cert-manager then issues/renews the cert (HTTP-01 challenge through Traefik) and is idempotent — it re-issues only when the dnsNames set changes or the cert nears expiry (30 days).

Locally the same secret comes from mkcert

On a local k3d cluster there is no cert-manager and no Let's Encrypt — make local-up fills the same hns-portal-tls secret with a per-developer mkcert wildcard cert. The ingress template is unchanged; only the secret's issuer differs. See Local development (k3d).

Why one cert instead of per-service

Per-Ingress certificates meant a full redeploy fired one Let's Encrypt order per subdomain and burned through rate limits. Collapsing to a single multi-SAN cert means one LE order per redeploy, and adding a subdomain is free.

Adding a subdomain

Declare ingress.host in the service's .platform.yml and redeploy. The wildcard DNS *.portal.hnst.dev3.wsagency.io already points at app-01, so no DNS record is needed; the portal-tls role picks up the new host, cert-manager adds the SAN, and Traefik serves it. The first request to a brand-new subdomain may take 1–2 minutes while the challenge completes.

cert-manager

  • Version: cert_manager_version: v1.20.2 (installed by roles/cert-manager/, idempotently — only re-fetched when absent or version-mismatched).
  • Issuers: two ClusterIssuers exist — letsencrypt-prod (default) and letsencrypt-staging. Both use the HTTP-01 challenge via the Traefik ingress class. To debug issuance without hitting prod rate limits, temporarily point certificate.yml.j2 at letsencrypt-staging.
  • Account email: cert_email in main.yml.

Public vs internal routes

The same Ingress template offers three modifiers, all declared under a deployment's ingress:

Modifier Behaviour
host Public Traefik route + TLS via the shared cert.
internal_paths Listed prefixes (e.g. /api/v1/internal) are served only in-cluster; Traefik returns 403 for them from outside. Rendered as a separate internal-deny Ingress + Middleware.
basic_auth HTTP basic-auth on the public host (Traefik Secret + Middleware). Empty = open.
websocket Sticky-cookie annotation so WebSocket upgrades stay pinned to one replica.

In-cluster traffic never traverses Traefik

internal_paths and basic_auth guard only the public ingress. Service-to-service calls go over ClusterIP DNS (see Service inventory) and bypass Traefik entirely — so, for example, the backend publishing to hns-ticketing-ntfy or the eventbus POSTing to /api/v1/internal/* is unaffected by either guard.

This is also why the backend fetches Keycloak JWKS over the in-cluster http://hns-idp:8080 rather than the public idp.{{ domain }}: the internal fetch needs no TLS, so token validation works even before the LE certificate is issued on a fresh composition. See Compositions & environments.


Last Updated: June 2026