Running locally over HTTPS

Why

Browsers (notably Safari) cache an HSTS rule that forces https://localhost, and they won't load a plain-HTTP site or trust an untrusted certificate. Running the stack over plain HTTP also means the app's Secure auth cookie is dropped, so login bounces. Rather than hand-edit the source to weaken cookie/OIDC security (which causes a divergent working tree), we run the local stack the same way production does: behind a TLS-terminating Caddy reverse proxy, with a locally-trusted certificate.

Behind HTTPS-terminating Caddy the app sees Request.IsHttps == true (via the already-committed UseForwardedHeaders middleware), so the normal production cookie and OIDC defaults just work.

One command

./scripts/dev-https.sh

This will (idempotently):

  1. install mkcert if needed (brew install mkcert nss),
  2. run mkcert -install — a one-time step that adds a local Certificate Authority to your login keychain (it may prompt for your password). This is what makes the cert trusted; it's the only manual step, and only the first time,
  3. generate certs/local.pem / certs/local-key.pem (git-ignored),
  4. build and start the stack behind Caddy.

Then open https://localhost:11401 — no certificate warning, no HSTS dead-end.

URL What
https://localhost:11401 App admin UI — admin@riptide.local / Admin@2026!
https://localhost:11403 Keycloak — admin / admin
http://localhost:11402 REST API (X-Api-Key) — unchanged, for SDK apps / the KMS stack

Stop with ./scripts/dev-https.sh down.

Local login is password-based (admin@riptide.local, above). Keycloak runs in the stack and the OIDC flow is wired correctly for HTTPS (issuer/redirect over https://localhost:11403), but no "Sign in with SSO" button appears on the login page by default: the login page lists only database IdentityProvider rows, and the local Keycloak is registered via config, not seeded as a DB row. Local SSO isn't needed for development — SSO is demoed against Azure Entra ID. If you do want the local button, add a Keycloak provider under Identity Providers in the admin UI (Authority http://keycloak:8080/realms/riptide, client application-manager, secret riptide-keycloak-secret).

How it works

  Browser ──HTTPS──► Caddy (trusted mkcert cert)
                       ├─ https://localhost:11401  ─► application-manager:11401  (Web)
                       │                            └─ /api/*  ─► application-manager:11402 (API)
                       └─ https://localhost:11403  ─► keycloak:8080              (Keycloak)

  App ──internal HTTP──► keycloak:8080   (OIDC discovery / token / jwks back-channel)
  • Files: docker-compose.https.yml (overrides the base compose), scripts/caddy/local.caddy, scripts/dev-https.sh.
  • OIDC issuer is HTTPS, back-channel is internal HTTP. The browser-facing Keycloak issuer is https://localhost:11403 (KC_HOSTNAME_URL), so SSO redirects and token iss are HTTPS. The app still reaches Keycloak's discovery/token/jwks endpoints over the trusted Docker network at http://keycloak:8080 — no split-horizon DNS and no need to trust the cert inside the container. That internal-HTTP authority is allowed via IdentityProviders__0__RequireHttpsMetadata=false (a per-provider config option; the default stays true, so production is unaffected).
  • No source edits: the cookie SecurePolicy and OIDC RequireHttpsMetadata defaults are the committed production values.

Caveats

  • Requires Docker Compose v2.24+ (the override uses the !reset merge tag to drop the base-published ports that Caddy takes over).
  • Existing Keycloak volume: --import-realm only seeds a fresh keycloak-data volume. If you ran the stack before this change, the realm's redirect URIs won't include the HTTPS origin. Refresh with docker compose -f docker-compose.yml -f docker-compose.https.yml down -v (wipes Keycloak data and re-imports) or add https://localhost:11401/* to the application-manager client in the Keycloak admin console.
  • Playwright/headless Chromium doesn't read the mkcert CA from your keychain; pass ignoreHTTPSErrors: true for automated tests. Your normal Safari/Chrome do trust it.

Pulling the published image instead of building

After this lands on development, CI publishes ghcr.io/riptide-solutions/application-manager:development. To run that image over HTTPS instead of building locally, docker pull it and point the application-manager service's image: at it (dropping build:), then use the same docker-compose.https.yml override + scripts/dev-https.sh.

Alternative: single URL

This setup uses two trusted origins (app :11401, Keycloak :11403). A single-origin variant — Keycloak under https://localhost:11401/idp via KC_HTTP_RELATIVE_PATH — is possible but needs Caddy path-routing and is more fragile; the two-origin model is preferred for local dev.