8. Crosscutting concepts¶
These concepts apply to several building blocks at once. When you change one of them, check every component that relies on it.
Security and identity¶
Kionga recognizes four kinds of credential. The gateway resolves them in this order
(platform/go/internal/auth/authenticator.go, platform/go/internal/auth/token.go):
| Credential | Presented as | Principal |
|---|---|---|
| Personal API key | Authorization: Bearer nxs_... |
Role user limited to the key's services and projects; the key's scope can never exceed the creator's grants. Stored only as a SHA-256 digest. |
| Internal service token | Authorization: Bearer <MLAIOPS_INTERNAL_TOKEN> |
internal-service with role service; compared in constant time |
| OIDC bearer token | Authorization: Bearer <JWT> |
Subject, email, groups and roles from the verified token |
| Browser session | kionga_local_session (local account) or kionga_session (single sign-on) cookie |
The signed-in subject |
Only the Bearer scheme counts as a credential. Other Authorization schemes, such
as the token header Jupyter sends through the workspace proxy, are ignored so the
browser session decides.
Single sign-on. The gateway is an OIDC relying party. It uses the authorization
code flow with PKCE (S256), a state cookie and an ID-token nonce, verifies tokens
against the issuer's JWKS (RSA keys, cached for 10 minutes), and checks issuer and
audience. KIONGA_SSO_SINGLE_TENANT=true assigns the configured tenant to tokens that
carry none (Dex cannot add a tenant claim); a token that names a different tenant is
rejected. Roles come from the token's roles claim when it has one. Otherwise
identity-provider groups map to roles through KIONGA_SSO_GROUP_ROLES, and a group
whose name equals a human role (admin, operator, engineer, user, viewer)
maps to that role directly; no group maps to service. A first sign-in with no profile follows the
sign-up policy (KIONGA_SIGNUP: approval, open or off). See
decision 0006 and the
sign-in sequence.
Local accounts. Without an issuer, local accounts are the development default.
Next to single sign-on they are break-glass access and require
KIONGA_LOCAL_LOGIN=true plus an explicit MLAIOPS_LOCAL_PASSWORD. Passwords for
provisioned local accounts are bcrypt hashes; verification of an unknown account still
runs a bcrypt comparison so timing does not reveal which accounts exist. After five
failures for one account, each attempt waits a doubling delay capped at one minute.
The throttle is per account, not per address, because every request behind the TLS
edge shares the proxy's address.
Browser hardening. Session cookies are HttpOnly and SameSite=Lax, and
Secure over HTTPS. Every response carries X-Content-Type-Options: nosniff and
Referrer-Policy: strict-origin-when-cross-origin; every response outside
/workspaces/ also carries X-Frame-Options: SAMEORIGIN. CORS reflects
MLAIOPS_ALLOWED_ORIGIN, which production requires. Return-to targets pass through
safeReturnTo, so a login cannot redirect off-site.
Workspace origins. On Kubernetes with KIONGA_WORKSPACE_BASE_DOMAIN, each
workspace is served from its own host under that domain. WorkspaceHostRouter keeps
those requests away from the console and auth routes. Production requires the
workspace domain to differ from the console host but share its registrable site, so
workspace cookies stay host-only. A separate cluster-internal listener on port 8087
accepts only verified workspace identities and is never exposed through the Ingress.
Editorial identity. The engineering blog uses its own memberships and its own
kionga_editorial session on top of the platform identity. Platform roles neither
grant nor deny editorial access, and service identities and API keys never reach it
(decision 0002).
Supply chain. Vendored browser libraries are loaded with integrity
hashes that a UI test recomputes. Images are built on distroless static bases for Go
services and published with provenance and an SBOM. CI fails on reachable
vulnerabilities, static-analysis findings and leaked secrets
(CI gates).
Authorization, RBAC and IAM policies¶
Authorization has two layers (platform/go/internal/httpapi/authorize.go).
- Coarse gate.
auth.Allowedruns in the RBAC middleware on every/api/and/workspaces/request, in every deployment profile. It decides from the role, the provisioned services and the request path whether the caller may use that API surface at all. A normaluserreaches only the services an administrator assigned;/api/v1/admin/requiresadminoroperator;servicemay only read and write the reporting paths.auth.Permissionsderives the console's control gates from the same function, so the UI cannot drift from enforcement. - Policy evaluation. For the covered actions, the handler evaluates the caller's
role baseline (a managed policy equivalent to the role, scoped to assigned projects)
plus every policy attached to the caller or their groups. The evaluator in
platform/go/internal/policyis pure: default deny, an explicit deny anywhere wins, otherwise any allow allows. Statements match actions andkionga:resource patterns and can carry IP CIDR, UTC hours, resource tag and resource profile conditions. A403names the deciding statement.
Policy evaluation currently covers pipelines, projects, workspace launch, features,
user provisioning, connections and IAM administration. Models, agents, functions and
storage rely on the role gate plus project assignment: their handlers return 404 for
an object outside the caller's projects. The blog:* actions in the catalog are not
enforced; the blog uses editorial memberships. Administration of policies has
anti-escalation, self-lockout and last-administrator protection.
The role and action reference is in the product documentation: permissions and users, roles and policies.
Multi-tenancy and isolation¶
Kionga isolates at three levels.
| Level | Mechanism | Limit |
|---|---|---|
| Tenant | One tenant per gateway process (MLAIOPS_TENANT, default default). Every connection sets app.tenant_id; every table has a tenant_id column and a forced row-level security policy on it. |
PostgreSQL superusers and roles with BYPASSRLS skip row-level security, so the gateway must connect as an ordinary role for the policy to isolate anything |
| Project | Resources belong to a project. Normal users see their owned and assigned projects; reads outside them return 404, and the SSE digest counts only visible objects. |
Administrators and operators see everything in the tenant |
| Workload | On Kubernetes, one KiongaWorkspace per access grant with its own PVC, credentials and resource limits, in a namespace with a ResourceQuota and default-deny network policies; agents get their own Deployments |
Compose shares one workbench and one agent runtime; it is not a multi-user boundary |
Quota admission. For normal users the gateway checks requests against the
administrator's grant before persisting desired state: project templates against
service, profile and GPU grants; the widest concurrent layer of a pipeline against
CPU, memory and GPU; functions against CPU, memory and count quotas; and every owned
agent at maximum replicas, including 100m CPU and 128Mi memory per trace sidecar.
GPU types must be valid, domain-qualified Kubernetes extended-resource names that
match the grant. Administrators and operators bypass these limits explicitly. Details
are in backend contracts.
Configuration and secrets¶
Every Go service calls runtimeconfig.Load at startup
(platform/go/internal/runtimeconfig/config.go, copied into serving and
trace-proxy):
KIONGA_CONFIG_FILEnames a JSON object of non-secret string values. A key that names a secret (*PASSWORD*,*SECRET*,*TOKEN*exceptOIDC_TOKEN_URL,*API_KEY*, any*_FILE, or a listed secret name) is rejected. Values never override an environment variable that is already set.- For each known secret name,
<NAME>_FILEpoints at a file holding the value. Setting both the variable and the file is an error; an empty file is an error; files are capped at 64 KiB.
The gateway then runs ValidateGateway. With KIONGA_ENVIRONMENT=production it
refuses to start unless:
- the database, OIDC, internal token, credential key and allowed origin are all set;
- every OIDC URL and the allowed origin are HTTPS without user information;
OIDC_REDIRECT_URLis the allowed origin's host plus/auth/callback;DATABASE_URLusessslmode=verify-full;OIDC_CLIENT_SECRETandMLAIOPS_INTERNAL_TOKENare at least 24 characters and do not containlocalorchange-me;- a break-glass password, when enabled, is at least 16 characters and not a development default;
KIONGA_CREDENTIAL_KEYdecodes to 32 bytes (it seals connected-account tokens with AES-GCM);- workspace domains, when set, satisfy the isolation rules above.
Locally, deploy/.env supplies values to Compose and every development default is
visible in deploy/compose.yaml. On a server and on Kubernetes, secrets arrive as
files (vm/up.py, or the chart's /run/secrets mount with <NAME>_FILE).
Observability¶
| Signal | Where it comes from | Where it goes |
|---|---|---|
| Audit events | Every mutation, in the same transaction as the change | audit_events table; GET /api/v1/audit; the mlaiops.audit.operations topic |
| Structured logs | Pipeline runner streaming, platform step events, Kubernetes Job events | log_entries (row-level security, cursor pagination, retention by KIONGA_LOG_RETENTION_DAYS); the Logs page and an SSE tail; optional export to Elasticsearch or OpenSearch |
| Secret redaction | platform/go/internal/redact masks known credential patterns and the exact values of secret environment variables |
Applied before logs are stored or exported |
| Gateway metrics | kionga_gateway_requests_total and kionga_gateway_request_duration_seconds, by bounded route category |
Separate listener on METRICS_PORT |
| Platform metrics | Metrics collector (pipeline outcomes, agent usage, real-time statistics, component health) | Prometheus; ServiceMonitors in the Helm chart |
| LLM traces | Trace proxy event per call; agent runtime session report with measured tokens and cost; Langfuse callbacks | mlaiops.llm.traces; agent sessions and traces in the gateway; Langfuse |
| Component health | Bounded active checks of connections and engines | /api/v1/components, the Platform page |
| Live updates | GET /api/v1/events sends a per-caller digest of visible state every three seconds |
The console reloads the active view only when the digest changes |
Health endpoints: the gateway exposes /api/v1/health (health) and /api/v1/ready
(readiness, used by Caddy and Kubernetes); the Go services expose /healthz.
Error handling and fail-closed behavior¶
- Startup. Unsafe or incomplete production configuration stops the process with a message that names the setting.
- API errors. Handlers answer with
{"error": "<code>", "message": "<text>"}. Store errors map to404 not_found,409 conflictor422 validation_error; authorization failures to401or403; objects outside the caller's projects to404. - Engine failures. When an engine rejects work, the control plane records the
failure on the resource: a pipeline step fails with the engine's message, a model
deployment is marked
failedand returns502 serving_failed, an unready agent returns503 agent_not_readybefore anything is forwarded. An not configured optional engine reportsconfigured: falseinstead of an error. - Bounded waits. Outbound calls carry explicit timeouts: agent readiness three seconds, list probes 1.5 seconds within a two-second budget, agent turns 120 seconds, serving deployment 90 seconds, predictions 30 seconds.
- Best-effort observability. Trace and session reporting never fails the operation it describes.
Persistence and the Kafka outbox¶
PostgreSQL is the system of record (decision 0005).
Resources are JSON documents in platform_resources, keyed by tenant, kind and ID.
audit_events and outbox_events are append-only. The migrations in
platform/go/internal/store/migrations/ are embedded in the binary and all run at
every start under a PostgreSQL advisory lock, so they must stay idempotent.
A mutation calls writeTx, which in one transaction:
- upserts the resource;
- inserts the audit event;
- inserts an outbox row for
mlaiops.audit.operations; - for lifecycle actions, inserts a command row for the matching topic
(
mlaiops.pipeline.commands,mlaiops.model.commands,mlaiops.agent.commands,mlaiops.tool.commands,mlaiops.connection.commands,mlaiops.workspace.commands).
Either everything commits or nothing does, so there is no published event without a stored change and no change without its audit record.
The outbox worker runs inside the gateway when KAFKA_REST_URL is set. Every
second it reads up to 100 unpublished rows in sequence order, publishes each through
the Kafka REST proxy, and marks it published. A row that fails increments attempts,
stores last_error, stays pending for the next pass, and does not block later rows.
Delivery is therefore at least once and not strictly ordered across failures.
The integration worker consumes the command topics on Kubernetes through an explicit Kafka REST consumer with auto-commit off. It dispatches a polled batch in order, retries a failed record up to five times with exponential backoff from 250 ms capped at two seconds, and commits the highest processed offset plus one per partition only after the whole batch succeeds. Commit retries do not redispatch. If dispatch or commit still fails, the worker exits before polling again and restarts from the last committed offset. Dispatch is an ID-keyed create-or-update of the matching custom resource, so redelivery is harmless.
Without DATABASE_URL the gateway uses a local JSON file (MLAIOPS_DATA_PATH, default
data/platform.json). That mode has no outbox and no row-level security and exists
only for lightweight development and tests.
Leases built on the same store keep singletons safe across gateway replicas: the pipeline scheduler and the editorial publisher each hold a row-locked lease document, and each schedule slot is claimed with a create-only document before its run starts.