Skip to content

Architecture

HelixIAM ships as two container images that are deployed and scaled independently: the server and the admin console. They are separate products: scaling one never scales the other.

Component Image What it does Replicas
Server (helix-iam-server) ghcr.io/melmarzgioui/helixiam/helix-iam-server The OAuth2 / OIDC / SAML 2.0 endpoints, the sign-in, registration and MFA pages, the admin REST API, and the end-user account console (/realms/{realm}/account, rendered by the server). Owns one PostgreSQL database directly; no message broker n, stateless across replicas: scale with sign-in traffic (2 for high availability, 3+ for several tenants)
Admin console (helix-dashboard) ghcr.io/melmarzgioui/helixiam/helix-dashboard The operator UI: a static React app served by nginx, which reverse-proxies /admin, sign-in and logout, the OIDC endpoints and the account API to the server, so the browser stays on one origin. Holds no state and no database connection Its own count, typically 1–2: operators, not end users. Can be internal-only

Both images are cosign-signed, with an SPDX SBOM attested to each image (cosign verify-attestation --type spdxjson). Platform: from 1.1.0-rc.12 the images are linux/amd64 only; arm64 images are planned to return (earlier releases published both). Both are delivered to licensed customers from KubeDNA B.V.'s registry. The console image is first published with the release after v1.1.0-rc.1.

flowchart LR
  user([End users and relying parties]) -->|"login.example.nl (no /admin, no master realm)"| ing1[Public ingress]
  ops([Operators]) -->|beheer.example.nl| ing2[Internal ingress]
  ing1 --> srv
  ing2 --> con
  subgraph cluster[Kubernetes namespace]
    con["Admin console<br/>helix-dashboard (nginx + SPA)<br/>console.replicaCount: 1-2"]
    srv["Server<br/>helix-iam-server<br/>replicaCount: n (2, 3, ...)<br/>OIDC / SAML / admin API / account console"]
    con -->|"/admin, sign-in, OIDC, account API"| srv
  end
  srv --> pg[(PostgreSQL)]
  srv -.->|optional| redis[(Redis)]

The same in plain text:

  end users ──► public ingress (login.example.nl) ──────────────► server pods × n ──► PostgreSQL
                                                                     ▲   (Redis optional)
  operators ──► internal ingress (beheer.example.nl) ──► console pods × 1-2
                                                         (nginx: SPA + proxy of /admin, sign-in, account API)

The server

helix-iam-server is the whole backend: it serves the OAuth2/OIDC endpoints (authorize, token, userinfo, JWKS, discovery, logout), the SAML 2.0 IdP, the sign-in pages and the realm-scoped admin REST API, and it owns the PostgreSQL database directly — no separate persistence tier, no cross-service calls.

  • Stateless across replicas. Browser sessions, authorization codes and tokens, SSO sessions, flow state and rate limits live in PostgreSQL (or Redis for the high-throughput tier), never in one pod's memory, so any replica can answer any request and replicas can be added or removed at any time. The Helm chart passes replicaCount to the server as HELIX_CLUSTER_EXPECTED_REPLICAS, and the server warns when shared state is configured in memory with more than one replica. See High availability.
  • Scales on its own. replicaCount, the PodDisruptionBudget and topologySpread in the chart apply to the server only.
  • PostgreSQL holds all durable state — users, clients, roles, keys, sessions, audit. The connection can use TLS (DB_SSLMODE). Redis optionally backs the session and token stores (Configuration). SMTP (or another configured provider) delivers notifications.
  • CSRF. Session-cookie writes echo the JavaScript-readable CSRF cookie in X-XSRF-TOKEN: __Host-XSRF-TOKEN in production (Secure, no Domain, so subdomains cannot plant one), XSRF-TOKEN on a plain-HTTP development server. It is always written at path /, so the console, /account.html and the realm URLs see the same token. Server-rendered forms carry _csrf instead. See Authenticating to the API.

The account consoles

There are two end-user account consoles:

  • The server-rendered account console at /realms/{realm}/account (profile and Edit details, password, MFA devices, sessions, linked identities, wallet), rendered by the server from its own templates, on the server's host. This is the account page users are sent to. The server also serves the account JSON API under /realms/{realm}/account/** (/profile, /editable-attributes, /credentials, /sessions, …).
  • A single-page account console in the admin console image. helix-dashboard also builds account.html; its nginx serves it at /account.html on the console's origin and proxies its calls to the account JSON API above. It only exists where the console is deployed.

Both edit the realm's self-editable profile attributes. See Account console.

The admin console

  • A separate image and a separate Deployment (Helm: console.enabled=true; off by default). Its own console.replicaCount, optional HPA (console.autoscaling), PodDisruptionBudget, topology spread, Service, Ingress on its own host and NetworkPolicy. See Install.
  • Same origin by proxy. The console's nginx forwards the admin API, sign-in and the OIDC endpoints to the server Service (console.apiTarget, by default the release's server Service), so the session cookie is never cross-site. Its origin is config.spBaseUrl (defaulted from the console Ingress host), which the server also uses as the origin of the built-in helix-console OIDC client (HELIX_CONSOLE_BASE_URL, redirect URI <origin>/console/callback).
  • Can be internal-only. Its NetworkPolicy admits only the ingress controller you name and lets it reach only DNS and the server pods; put its Ingress on an internal IngressClass or an IP allow-list.
  • Administration only on the admin host. With HELIX_ADMIN_ALLOWED_HOSTS (Helm config.adminAllowedHosts) set to the console host, the server answers the admin REST API (/admin/**, with /v3/api-docs/** and /swagger-ui/**) and the master realm (/realms/master/**; HELIX_ADMIN_RESTRICT_MASTER_REALM, default true) only on that host, and 404 on every other host. The host is the Host header, or X-Forwarded-Host from a trusted proxy only. Other realms are unaffected. Empty (the default) keeps the admin API on every allowed host.
  • Forwarded headers. The console's nginx passes the ingress's X-Forwarded-Proto/-Host/-Port on unchecked, and the server trusts the console pods as proxies. The console's ingress must therefore overwrite any client-sent X-Forwarded-* headers (do not enable ingress-nginx use-forwarded-headers or Traefik forwardedHeaders.insecure for it), and the console Service must never be exposed directly (no LoadBalancer or NodePort): otherwise a client could choose the host the server sees, including an admin host.
  • Runs locked down. nginx as uid 101, read-only root filesystem, all capabilities dropped, no service-account token. /healthz is answered by nginx itself, so a server outage does not take the console out of rotation.

Request flow (human login)

  1. An app redirects the browser to …/realms/<realm>/oauth2/authorize (OIDC) or POSTs a SAML AuthnRequest.
  2. The server runs the realm's authentication flow (password, MFA, …), reading/writing session state in Postgres/Redis.
  3. On success it issues an authorization code (OIDC) or SAML assertion; the app exchanges the code at the token endpoint for ID, access and refresh tokens signed by the realm's keys.

Request flow (machine, workload & agent token exchange)

Not every caller is a browser. The server also mints tokens for non-interactive callers with no session and no stored secret:

  1. A caller presents a credential — a client's client_credentials, a Kubernetes/CI workload JWT (Workload identity), or, for on-behalf-of delegation, a user subject_token + an agent actor_token (Agent authorization).
  2. The server verifies it against the configured external issuer's JWKS (workload) or the realm's own signing key (delegation), and enforces the agent's lifecycle gate (a suspended agent is denied here).
  3. It mints a short-lived realm-signed access token — carrying nhi: true for non-human callers, and an act claim naming the acting agent for delegated tokens. Endpoints include …/oauth2/token, …/workload-identity/token, and …/agent/delegation/token. Delegation with an actor_token runs only at …/agent/delegation/token; …/oauth2/token refuses actor_token with invalid_request.

See Observability for the metrics the server exposes and Backup & DR for what to back up (just the database).