Skip to main content

Platform operator identity

Platform Identity authenticates the people and automation that administer a Runku installation. It is independent from Application Identity: rk_pub_*, rk_sec_*, and rk_dev_* identify application or development clients and never authorize Management API access. Operator sessions use their own rk_at_v1_* access and rk_rt_v1_* refresh credentials.

Available management scope

The current compact distribution provides the following management scope:

  • PostgreSQL 16+ authoritative storage;
  • first-owner bootstrap with a server-generated, single-use rk_inv_v1_* code;
  • delegated invitations with installation, Project, or Environment scope, idempotent issuance operations, non-secret reconciliation, and revocation;
  • short-lived access tokens, rotating refresh tokens, session listing, and revocation;
  • optional external OIDC identity linking using the hardened discovery/JWKS verifier;
  • a versioned HTTP API and runku login invitation/OIDC-token flows;
  • native browser login through OIDC Authorization Code + PKCE and a loopback callback;
  • authenticated Workspace publication, Release validation, Channel promotion/rollback, status, and historical/streaming Operational Logs for one configured Product Environment;
  • transactional security audit records and process-local aggregate counters;
  • a tagged runku-server Linux binary/image with check, migrate, recover-bootstrap, serve, and version modes.

The compact runku-server distribution can compose Platform Identity with one initialized Product Environment selected by RUNKU_PRODUCT_ROOT. That profile exercises the real local repositories, artifact store, Gateway, runtime, background loops, release lifecycle, and logs behind authenticated remote management. Tagged releases publish its Linux GNU ARM64/x86_64 archives and non-root OCI image. It is not the distributed/HA installation package: multi-Environment orchestration, separated roles, backup windows, and mixed-version support remain governed by the production-readiness contract.

Trust and data flow

first server start ──writes protected invitation file──► initial owner
initial owner ──runku login + invitation──────────────► operator session
operator session ──creates scoped invitation─────────► another operator
external IdP ──signed user JWT──► Runku OIDC verifier ─► linked operator session
operator access token ──capability + scope check──────► Management operation

Runku never stores an invitation, access token, or refresh token in recoverable form. PostgreSQL stores domain-separated HMAC-SHA-256 digests. External OIDC sub values are transformed into an opaque keyed identifier before persistence. The Platform Identity pepper and OIDC subject pepper are separate 256-bit secrets; changing either one without a planned migration invalidates the corresponding credentials or identity links.

Prerequisites

For the compact Self-Hosted installation you need:

  • PostgreSQL 16 or newer reachable from the server process;
  • an absolute private state directory writable only by the server identity;
  • two independently generated 32-byte peppers when OIDC is enabled;
  • TLS at a trusted reverse proxy before exposing the Management listener beyond loopback;
  • optionally, one OIDC provider with an HTTPS issuer, asymmetric signing keys, exact audience, and a stable claim that distinguishes operator tokens.

The database role needs permission to connect, create the Platform Identity tables on first use, and read/write those tables. Do not share its password with application code or the CLI.

Configure and start the compact server

Generate the installation credential-verification pepper once and store it in a secret manager:

openssl rand -base64 32 | tr '+/' '-_' | tr -d '=\n'

The result is URL-safe base64 without padding. Supply it without placing it in a command-line argument:

export RUNKU_IDENTITY_DATABASE_URL='postgres://runku_management:REDACTED@postgres.example/runku_identity'
export RUNKU_PLATFORM_IDENTITY_PEPPER='REDACTED_URL_SAFE_BASE64_32_BYTES'
export RUNKU_STATE_DIRECTORY='/var/lib/runku'
export RUNKU_MANAGEMENT_LISTEN='127.0.0.1:3220'

runku-server check
runku-server migrate
runku-server serve

check parses and validates configuration without connecting to PostgreSQL or changing state. migrate connects, verifies PostgreSQL compatibility, and applies the checksum-protected schema. It is safe to repeat with the same binary and schema. serve also applies the same idempotent schema check before listening.

Configuration is strict:

VariableRequiredContract
RUNKU_IDENTITY_DATABASE_URLone sourcePlatform Identity PostgreSQL URL with host and one database path for operators, grants, sessions, invitations, and audit; sensitive
RUNKU_PLATFORM_IDENTITY_PEPPERone sourceURL-safe base64, exactly 32 decoded bytes; sensitive
RUNKU_IDENTITY_DATABASE_URL_FILEalternativepath to a file containing the same Identity URL; absolute, one-line, regular, non-symlinked
RUNKU_PLATFORM_IDENTITY_PEPPER_FILEalternativeabsolute one-line regular non-symlink file; mutually exclusive with direct pepper
RUNKU_STATE_DIRECTORYyesabsolute path other than /; holds bootstrap material
RUNKU_MANAGEMENT_LISTENnodefaults to 127.0.0.1:3220
RUNKU_MANAGEMENT_TLS_TERMINATEDnoexact true permits a non-loopback listener behind a trusted TLS boundary
RUNKU_PUBLIC_MANAGEMENT_URLnocanonical public HTTPS Management origin returned by login discovery; literal-loopback HTTP is local-only
RUNKU_PLATFORM_OIDC_CONFIGnoabsolute path to a strict JSON file, at most 64 KiB
RUNKU_PLATFORM_MANAGED_ENROLLMENT_TOKENpairedseparate internal gateway secret, at least 32 bytes; enables managed OIDC enrollment and the exact managed-grants S2S endpoint
RUNKU_PLATFORM_MANAGED_ENROLLMENT_TOKEN_FILEalternativeabsolute one-line regular non-symlink file containing the same separate gateway secret
RUNKU_PLATFORM_MANAGED_SOURCE_AUTHORITYpairedexact canonical HTTPS origin that owns every grant revision accepted with the managed token; configure together with that token
RUNKU_PRODUCT_ROOTnoabsolute initialized Product Environment root exposed by authenticated lifecycle routes
RUNKU_APPLICATION_LISTENpairedcompact-server application listener; requires Product root and explicit trusted TLS termination
RUNKU_APPLICATION_TLS_TERMINATEDpairedexact true asserts the operator-owned application ingress terminates TLS before the private listener
RUNKU_PLATFORM_DATABASE_URLnooptional Environment-scoped PostgreSQL URL for Function documents, indexes, outbox, and schedules; sensitive; requires Product root
RUNKU_PLATFORM_DATABASE_URL_FILEalternativepath to a file containing the same Function platform URL; absolute, one-line, regular, non-symlinked
RUNKU_PRODUCT_ALLOWED_ORIGINSnoup to 64 exact comma-separated browser origins; requires Product root
RUNKU_PRODUCT_AUTH_CONFIGnoProduct-root-relative JWT descriptor without parent traversal; requires Product root

RUNKU_IDENTITY_DATABASE_URL is the database connection string itself. Its _FILE alternative is not another database: its value is only an absolute filesystem path, and Runku reads the connection string from that file. Use exactly one of those two forms. The same rule applies to the optional Function platform database and to the pepper. Secret files are bounded to 64 KiB and one canonical line; missing, empty, oversized, symlinked, multiline, or conflicting inputs fail before a connection. Unknown OIDC fields, malformed secrets, unsafe database schemes, relative state/config paths, and a non-loopback plaintext listener fail before readiness. RUNKU_MANAGEMENT_TLS_TERMINATED=true is an assertion by the operator; Runku cannot verify the reverse proxy. Restrict the backend listener and configure exact trusted-proxy behavior at the deployment boundary. The same rule applies independently to the Product application listener: neither RUNKU_APPLICATION_LISTEN nor RUNKU_APPLICATION_TLS_TERMINATED=true is accepted alone, and the pair is invalid without RUNKU_PRODUCT_ROOT.

The Function platform database is independent of Platform Identity and has its own exact Environment binding, readiness, least-privilege credential, and coordinated recovery contract. See Environment-scoped Function platform PostgreSQL.

Version 0.4.4 still accepts the deprecated RUNKU_DATABASE_URL and RUNKU_PRODUCT_DATABASE_URL names, including their _FILE forms, for transition. Do not configure a canonical and legacy name for the same role at once; even equal values fail closed as SERVER_SECRET_CONFIGURATION_CONFLICT.

Enroll the initial owner

On the first successful serve, while the database contains no operator, the server creates one bootstrap invitation and writes it to:

$RUNKU_STATE_DIRECTORY/bootstrap/initial-owner.code

The file is created with mode 0600 on Unix. Startup is idempotent: if the pending bootstrap exists, the server requires the original file to remain present and never generates a second valid code. The bootstrap expires after 24 hours; the next start revokes it and atomically creates a fresh code while no operator exists.

If the protected file is lost before enrollment, stop the server, preserve the database/logs, and run the explicit local recovery operation with the same database, pepper, and state directory:

RUNKU_BOOTSTRAP_RECOVERY_CONFIRM='replace-lost-initial-owner-code' \
runku-server recover-bootstrap

Recovery atomically revokes every pending bootstrap, records bootstrap.recover in security audit, persists one replacement digest, and writes a new initial-owner.code. It fails permanently after the first operator exists. The confirmation is an accident-prevention phrase, not a credential; authority comes from administrative access to the server configuration, pepper, state directory, and PostgreSQL. Never expose this command through an HTTP endpoint or unattended startup flag.

Transfer the code through a protected local channel and read it into an environment variable. Do not paste it into a CLI argument or shared shell history:

export RUNKU_INITIAL_OWNER_CODE="$(tr -d '\r\n' </var/lib/runku/bootstrap/initial-owner.code)"
runku login \
--url https://runku.example.com \
--device operator-laptop \
--code-env RUNKU_INITIAL_OWNER_CODE
unset RUNKU_INITIAL_OWNER_CODE

For a server bound to literal loopback, http://127.0.0.1:3220 is accepted. Every other origin must use HTTPS. A successful exchange atomically creates the operator, owner grant, device session, and audit event, then consumes the invitation. The server removes the bootstrap file on the next startup after it observes completed initialization.

runku login stores one current server session in the platform user configuration directory:

PlatformDefault path
Linux$XDG_CONFIG_HOME/runku/credentials-v1.json or $HOME/.config/runku/credentials-v1.json
macOS$HOME/Library/Application Support/runku/credentials-v1.json
Windows%APPDATA%\runku\credentials-v1.json

RUNKU_CONFIG_HOME selects an absolute alternative directory, primarily for isolated automation. The file is 0600 on Unix and inherits the user's profile ACL on Windows. It contains bearer credentials, must never be committed or backed up unencrypted, and is currently a protected-file fallback rather than a native keychain integration. A second login replaces the single stored profile.

Session schema v2 records the authentication origin separately from the Management origin. Existing schema v1 profiles remain readable and are upgraded after the next successful refresh or login.

Invite another operator

Only a context that has operators:manage at the requested scope and every capability being delegated may create an invitation. This prevents a scoped owner from escalating another operator beyond the creator's own authority.

Until the invitation subcommand is added to the CLI, use the versioned API from a trusted operator tool. Read the current access token from an approved credential helper; the following variables are placeholders and must not be printed:

curl --fail-with-body \
--request POST \
--header "Authorization: Bearer $RUNKU_OPERATOR_ACCESS_TOKEN" \
--header "Idempotency-Key: $RUNKU_INVITATION_OPERATION_ID" \
--header 'Content-Type: application/json' \
--data '{
"operatorName": "release-operator",
"role": "developer",
"scope": {
"kind": "environment",
"projectId": "prj_00000000000000000000000001",
"environmentId": "env_00000000000000000000000002"
}
}' \
https://runku.example.com/v1/access/invitations

RUNKU_INVITATION_OPERATION_ID must be a newly generated canonical opn_* Operation ID retained by the caller as non-secret operation metadata. The first committed response is 201 and contains operationId, invitationId, scope, capabilities, timestamps, code, secretShownOnce: true, and replayed: false; it has Cache-Control: no-store, max-age=0. Deliver the code once. It expires after 30 minutes and cannot be recovered, replayed, or used after revocation.

An exact POST replay with the same operation and request returns 200, the same non-secret metadata, secretShownOnce: false, and replayed: true; it never includes code. Reusing an operation ID for different operator, scope, role/capabilities, or other issuance content returns 409 PLATFORM_INVITATION_OPERATION_REUSED.

After an uncertain response, reconcile before producing more bearer material:

curl --fail-with-body \
--header "Authorization: Bearer $RUNKU_OPERATOR_ACCESS_TOKEN" \
"https://runku.example.com/v1/access/invitation-operations/$RUNKU_INVITATION_OPERATION_ID"

404 proves no committed operation exists at the time of the authoritative read. 200 proves the operation committed and returns metadata but never the code. If the caller did not durably deliver the original code, revoke the unknown credential and issue a replacement with a new Operation ID:

curl --fail-with-body \
--request DELETE \
--header "Authorization: Bearer $RUNKU_OPERATOR_ACCESS_TOKEN" \
"https://runku.example.com/v1/access/invitations/$RUNKU_INVITATION_ID"

Deleting a pending or already revoked invitation returns 204, so the exact revocation is safe to repeat. A consumed invitation returns conflict because revoking its code cannot disable the operator/session created by consumption. Lookup and revocation reload current authority and require operators:manage at every stored invitation scope. Operation IDs are correlation identities, not credentials; knowing one never bypasses authorization.

For compatibility, a POST without Idempotency-Key retains the previous one-shot behavior. It is not reconcilable and must not be used by unattended automation or any workflow that could retry after losing the response.

Supported scope shapes are exact:

{"kind":"installation","projectId":null,"environmentId":null}
{"kind":"project","projectId":"prj_...","environmentId":null}
{"kind":"environment","projectId":"prj_...","environmentId":"env_..."}

Roles, capabilities, and scope

Roles are input conveniences. Runku expands them to explicit durable capabilities; authorization checks capabilities and scope, not the role label.

RoleCapabilities
ownerinstallation, Project, Environment, operator, Release, Channel, Function invocation, configuration, credential, Data Admin, log, usage, and backup management
operatorEnvironment, Release, Channel, Function invocation, configuration, credential, Data Admin, log, usage, and backup operations; no installation/operator ownership
developerread/publish Releases, promote Channels, invoke Functions, read configuration, read/write Data Admin, read credential metadata, read/follow logs
observerread Releases, configuration, Data Admin, credential metadata, logs, and usage

An installation grant contains every Project and Environment. A Project grant contains that Project and its Environments. An Environment grant contains only the exact Project/Environment pair. A Project or Environment grant never authorizes installation-wide work or a sibling resource.

Every lifecycle request reloads current grants. releases:publish protects publication and Release validation; channels:promote protects promotion and rollback; releases:read protects status; Function/schema catalogs; functions:invoke authorizes a human/BFF to attempt the canonical Product invocation path but never replaces its separately scoped Application identity; data:read protects logical reads; data:write protects logical writes; logs:read protects snapshots; and logs:follow protects streaming. The URL's Project and Environment are checked against both the grant and configured Product Environment before product state is accessed.

The Data Admin and Function invocation capabilities are additive source-line capabilities. Existing 0.4.5 durable grants are not backfilled or reinterpreted and therefore gain no document or Runner access after upgrade. Reissue or replace a grant deliberately to opt in. Role expansion only affects newly issued/reconciled grants; custom grants retain exactly their stored capability set.

Configure external OIDC

OIDC is optional. Invitation-only operation has no external IdP dependency. When enabled, create a strict JSON file readable by the server identity:

{
"providerId": "workforce-main",
"issuer": "https://identity.example.com/realms/operators",
"discoveryUrl": "https://identity.example.com/realms/operators/.well-known/openid-configuration",
"audience": "https://runku.example.com",
"allowedOrigins": ["https://identity.example.com"],
"discriminatorClaim": "runku_actor_type",
"discriminatorValue": "operator",
"algorithm": "RS256",
"requiredType": "JWT",
"subjectPepper": "REDACTED_URL_SAFE_BASE64_32_BYTES",
"nativeClient": {
"authorizationEndpoint": "https://identity.example.com/realms/operators/protocol/openid-connect/auth",
"tokenEndpoint": "https://identity.example.com/realms/operators/protocol/openid-connect/token",
"clientId": "runku-cli",
"scopes": ["openid", "profile"],
"resource": "https://runku.example.com"
}
}

Set its absolute path in RUNKU_PLATFORM_OIDC_CONFIG. The verifier requires:

  • exact HTTPS issuer and audience;
  • one selected asymmetric algorithm: RS256, PS256, ES256, or EdDSA;
  • optional exact JWT header typ;
  • exact discriminator claim/value so an unrelated token class is rejected;
  • bounded token lifetime and clock skew;
  • discovery and jwks_uri origins in the HTTPS allowlist;
  • no redirects, bounded bodies/timeouts, DNS/IP controls, cached last-known-good keys, and bounded unknown-kid refresh.

The IdP authenticates the human; Runku remains authoritative for grants, sessions, and resource scope. Self-Hosted first OIDC login must consume a Runku invitation so a verified external subject cannot self-enroll. A managed service can opt into the separate managed-enrollment gateway contract described below. The normal interactive flow is simply:

runku login

With no prior profile this queries https://api.runku.app/v1/auth/config. A self-hosted operator uses --url for the installation's authentication origin. The server advertises invitation, browser OIDC, and helper-token support plus an optional separate Management origin. If multiple human methods exist, the CLI asks which to use; --browser is an explicit choice, not a normal requirement. For first identity binding, provide the invitation through a protected environment variable:

RUNKU_OPERATOR_INVITATION='rk_inv_v1_...' runku login \
--url https://runku.example.com \
--device operator-laptop \
--browser \
--code-env RUNKU_OPERATOR_INVITATION

The CLI obtains public native-client settings from Runku, generates fresh state and a PKCE S256 verifier, binds an ephemeral 127.0.0.1 callback, opens the authorization URL, checks callback Host/state and any returned issuer, rejects duplicate security parameters, exchanges the code against the fixed token endpoint without a client secret, and sends the resulting external access token only to Runku's OIDC exchange. The browser sees success only after Runku validates that token, commits the operator session, and persists the local profile. --no-open prints the authorization URL to stderr for headless or controlled-browser automation while retaining those checks.

nativeClient.resource is optional. When present, the CLI sends the exact RFC 8707 resource indicator in both the authorization and token requests. Use it when the provider requires an explicit protected resource to issue a JWT access token with the configured Runku audience. The value must be an HTTPS URL (literal-loopback HTTP is conformance-only), and it must agree with the provider client/resource registration and top-level audience. Omitting it preserves the ordinary OIDC flow for providers that issue the required JWT without a resource indicator.

For workload identity, an approved helper, or deterministic protocol conformance, the external token can instead be supplied through an allowlisted environment variable:

RUNKU_OPERATOR_INVITATION='rk_inv_v1_...' \
RUNKU_EXTERNAL_OIDC_TOKEN='eyJ...' \
runku login \
--url https://runku.example.com \
--device operator-laptop \
--code-env RUNKU_OPERATOR_INVITATION \
--oidc-token-env RUNKU_EXTERNAL_OIDC_TOKEN

The invitation, external identity link, operator, grants, first session, and audit event commit in one transaction. Later logins for the same configured provider and subject omit --code-env:

RUNKU_EXTERNAL_OIDC_TOKEN='eyJ...' runku login \
--url https://runku.example.com \
--device operator-laptop \
--oidc-token-env RUNKU_EXTERNAL_OIDC_TOKEN

The external token is never written to Runku's credential file; only the resulting Runku access and rotating refresh session is stored. Runku does not retain the IdP password, authorization code, PKCE verifier, or external token.

Managed-service OIDC enrollment

A SaaS control plane that already owns user and tenant membership configures a high-entropy RUNKU_PLATFORM_MANAGED_ENROLLMENT_TOKEN together with one exact canonical HTTPS RUNKU_PLATFORM_MANAGED_SOURCE_AUTHORITY. It authenticates its private POST /v1/auth/oidc hop with runku-managed-enrollment: Bearer …. After verifying the OIDC bearer, Runku accepts a bounded managedEnrollment object containing operatorName, sourceRevision (u64, at least 1), and the complete Project-scoped role set owned by that configured source. First login creates the external identity, source-owned grants, session, authorization revision, and audit atomically.

The same trusted reconciler is available without an OIDC login at exact endpoint PUT /v1/auth/managed/operators/{operatorId}/grants. It accepts only the managed header—not an operator session—and the JSON body { "sourceRevision": 2, "grants": [...] }. A greater revision atomically replaces only this source's grant subset; an empty list revokes it. An equal revision with identical normalized content returns a successful replay. Equal revision with different content returns 409 PLATFORM_MANAGED_SOURCE_CONFLICT; a lower revision returns 409 PLATFORM_MANAGED_SOURCE_STALE. The stable response contains only applied, replayed, sourceRevision, and authorizationRevision.

Existing sessions remain valid, but every Management request and each logs follow iteration reloads current grants, so a committed downgrade or revocation takes effect immediately. The first trusted reconciliation for a managed OIDC operator adopts and replaces legacy pre-source grants. Invitation-only operators are unchanged unless the control plane explicitly targets their ID via the S2S endpoint. The gateway header is never a user credential and must be stripped from public input. A missing/incomplete token-authority pair, mismatched token, zero revision, or simultaneous invitation and managed enrollment fails closed. Ordinary Self-Hosted OIDC remains invitation-gated.

Changing providerId creates a distinct trust namespace. Rotating subjectPepper makes existing links unresolvable. Treat either as an identity migration requiring overlap or re-enrollment; do not edit database identity rows.

allowLoopbackHttp: true permits exactly one literal-loopback HTTP origin for discovery/JWKS. It exists only for local conformance, requires an HTTPS issuer identifier, and rejects localhost, remote HTTP, or multiple origins. Never use it in a networked deployment.

Choose and qualify an identity provider

Runku integrates with the OIDC protocol boundary, not with a Keycloak-specific API. Keycloak is present in this repository because it provides a convenient, disposable way to issue a real signed token and expose discovery and JWKS documents during a reproducible test. Its use in that test is not a recommendation, certification, support preference, or claim that it is the best fit for an installation.

Choose the identity system according to the installation's security, operations, compliance, and user-lifecycle needs. Examples worth evaluating include authentik, ZITADEL, and Dex as a federation layer, as well as an OIDC provider the organization already operates. These examples are illustrative, are not an equivalence matrix, and have not been certified by Runku. Product features, deployment models, and token behavior change independently; use each provider's current documentation and validate the exact deployed version.

Evaluate at least the following before selecting a provider:

AreaQuestions the installation must answer
Trust ownershipIs the IdP self-managed, managed, or federated, and who owns incidents, upgrades, and recovery?
OIDC contractDoes it publish stable discovery metadata, an HTTPS issuer, and an HTTPS jwks_uri whose values exactly match issued tokens?
Token profileCan it issue an asymmetrically signed JWT with Runku's exact audience and a dedicated operator discriminator claim?
Subject stabilityIs sub stable for the lifetime of the operator account, including rename, migration, and directory synchronization?
Human securityAre MFA, phishing-resistant authentication, enrollment recovery, lockout, and deprovisioning appropriate for platform operators?
Key operationsHow are signing keys generated, protected, rotated, overlapped, cached, audited, and recovered?
AvailabilityWhat happens to new logins during an IdP outage, and is invitation-only recovery documented and protected?
Organization constraintsDo audit retention, data residency, privacy, licensing, capacity, and support meet local requirements?
Client flowWhich approved browser, device, helper, or workload flow obtains the external token without exposing it in shell history?

Qualify the chosen provider in a non-production environment before enabling it:

  1. Record the deployed provider version and retrieve its discovery document over the same network path Runku will use.
  2. Create a dedicated Runku client/resource audience; do not reuse a token intended for another application.
  3. Add a dedicated operator discriminator claim/value and verify ordinary application tokens do not contain it.
  4. Pin the exact issuer, discovery URL, audience, allowed origins, algorithm, optional typ, and discriminator in RUNKU_PLATFORM_OIDC_CONFIG.
  5. Exercise first enrollment with an invitation, a later linked login without an invitation, /v1/auth/me, refresh, session listing, and revocation.
  6. Prove rejection of the wrong issuer, audience, discriminator, algorithm, signature, expired token, unknown key, redirect, and non-allowlisted JWKS origin.
  7. Rotate a signing key under the provider's normal procedure and verify both the intended overlap window and removal of the retired key.
  8. Document IdP outage, operator removal, compromised-account, subject-change, and Runku subjectPepper recovery procedures before production use.

Passing this qualification establishes compatibility only for that provider configuration and version in that installation. It does not transfer to another deployment or make the provider a Runku dependency. The normative external boundary is the OpenID Connect Discovery contract plus Runku's stricter verifier rules above.

Sessions and HTTP endpoints

Method and pathAuthenticationEffect and retry
GET /v1/auth/confignonereturns versioned methods and an optional canonical Management origin; never returns secrets
POST /v1/auth/exchangesingle-use invitation in JSON bodycreates operator/session atomically; do not replay after success
POST /v1/auth/oidcexternal bearer; first Self-Hosted link uses invitation, or a separately authenticated managed gateway supplies authoritative grantsverifies OIDC and creates/reconciles a Runku session
GET /v1/auth/oidc/confignonereturns exact issuer and public native-client endpoints/ID/scopes/optional RFC 8707 resource; never returns secrets
POST /v1/auth/refreshcurrent rk_rt_v1_* in JSON bodyatomically rotates both tokens; reconcile an uncertain response before retry
GET /v1/auth/merk_at_v1_* bearerreloads current operator and grants; safe to retry
GET /v1/auth/resourcesrk_at_v1_* bearer at the stored authentication originreturns at most 1,024 linkable Product Environments in a versioned response bounded to 1 MiB; safe to retry
GET /v1/auth/sessionsrk_at_v1_* bearerlists non-secret sessions owned by the operator; safe to retry
DELETE /v1/auth/sessions/{ops_*}rk_at_v1_* bearerrevokes own session; another operator requires installation operators:manage
POST /v1/access/invitationsrk_at_v1_* + delegated authoritywith Idempotency-Key: opn_*, atomically creates or replays one issuance; code appears only on create
GET /v1/access/invitation-operations/{opn_*}rk_at_v1_* + current operators:manage at stored scopereconciles non-secret status; safe to retry
DELETE /v1/access/invitations/{opi_*}rk_at_v1_* + current operators:manage at stored scopeidempotently revokes pending material; never reopens consumed identity
POST /v1/projects/{project}/environments/{environment}/workspace/publishreleases:publishbounded canonical package publication with explicit Workspace CAS
POST /v1/projects/{project}/environments/{environment}/releases/{release}releases:publishvalidates the candidate and makes it servable
PUT /v1/projects/{project}/environments/{environment}/channels/{channel}channels:promotepromotes through exact optional CAS
POST /v1/projects/{project}/environments/{environment}/channels/{channel}/rollbackchannels:promoterolls back through required exact-current CAS
GET /v1/projects/{project}/environments/{environment}/statusreleases:readreads a coherent Release/Channel snapshot
GET .../environments/{environment} and GET .../environment-operations/{opn_*}environments:readreads portable desired/observed configuration or reconciles an uncertain lifecycle operation
POST or PUT .../environments/{environment}environments:manage + Idempotency-Key: opn_*creates the exact configured scope or replaces its complete configuration using CAS
POST .../environments/{environment}/archive or /restoreenvironments:manage + Idempotency-Key: opn_*changes desired lifecycle through exact revision CAS; compact archive stops Product serving and restore restarts it when a Channel exists
GET .../serving-policy and GET .../serving-policy-operations/{opn_*}releases:readreads desired/observed rollout state or reconciles an uncertain operation
GET .../schemas/compatibilityreleases:readreads the shared canonical schema/index/Cron evidence for the desired serving set without source or customer data
PUT .../serving-policychannels:promote + Idempotency-Key: opn_*replaces the complete compatible desired policy using exact CAS and materializes it for environment:default serving
GET .../configuration and GET .../configuration/historyconfiguration:readreads variables, secret-reference metadata, revision, and value-free audit history; never secret values
PUT or DELETE .../configuration/{NAME}configuration:manage + Idempotency-Key: opn_*creates, updates, rotates, or deletes one exact name through global revision CAS
GET .../application-clients and credential lists/revealcredentials:readreads non-secret identity metadata or re-derives a verified publishable key
Application Client/credential create, rotate, revoke, and delete routescredentials:managemanages exact-scope Product credentials; confidential material is returned once
Bucket/key lists, bucket get, and storage-operation lookupstorage:readreads provider-independent logical storage metadata only
Bucket create/replace/archive and storage-key issue/rotate/revokestorage:manage + Idempotency-Key: opn_*applies exact-scope CAS/idempotency; Product key secrets appear once
GET .../crons?target=...cron:readreads code-owned declarations plus current durable activation without exposing leases
PUT .../crons/{name}/activationcron:activate + Idempotency-Key: opn_*enables/disables an exact immutable declaration using repository CAS; operator disable intent survives restart and manifest reconciliation
GET .../cron-operations/{opn_*}cron:readreconciles one successful non-secret activation result after uncertainty
GET .../scheduled?limit=...&after=...schedules:readreads bounded durable queue/history in stable ID order without worker identity
GET .../metricsusage:readreads a bounded fixed-name aggregate metric set; decimal strings preserve full counter precision and remain diagnostic only
GET .../instances/healthzenvironments:readreads sanitized Product dependency/runtime status without provider, cell, credential, or error details
GET .../functions and GET .../schema/tablesreleases:readreads a bounded catalog from one verified effective artifact
POST .../data/query and GET .../data/documents/{table}/{document}data:readreads logical documents through the canonical store and schema
POST .../data/documents/{table}data:write + Idempotency-Key: opn_*inserts a schema-valid document with deterministic identity
PUT or DELETE .../data/documents/{table}/{document}data:write + Idempotency-Key: opn_*applies exact OCC and supports safe identical replay
GET .../logslogs:readreads one bounded exact-scope page
GET .../logs/followlogs:followstreams NDJSON and rechecks the session/grant during the connection
GET /health/livenoneprocess liveness only
GET /health/readynonebounded authoritative PostgreSQL health

Ordinary JSON bodies are limited to 16 KiB; Data Admin document bodies are limited to 12 MiB so the canonical value envelope can reach its documented bound, and the publication route has the protocol's explicit manifest/artifact bound. Authorization headers are limited to 16 KiB, JSON rejects unknown fields, and semantic request concurrency is bounded. Data responses and secret-bearing responses use no-store. All Management API access reloads current grants; a stale token does not freeze old authorization indefinitely.

Default lifetimes are 10 minutes for access, 30 days for rotating refresh, 30 minutes for delegated invitations, and 24 hours for bootstrap. A successful refresh invalidates the prior refresh token. Each login creates an independently revocable ops_* device session.

Durable state, backup, and restore

Platform Identity schema v1 owns these PostgreSQL tables:

  • runku_platform_meta and runku_platform_migrations;
  • runku_operators and runku_operator_grants;
  • runku_operator_identities;
  • runku_operator_invitations;
  • runku_operator_sessions;
  • runku_platform_audit.

Schema v2 append-only adds runku_operator_invitation_operations, a revocation timestamp on delegated invitations, and operation/invitation correlation columns plus an index on security audit. The operation table stores only the canonical Operation ID, SHA-256 request fingerprint, explicit installation/Project/Environment scope, invitation ID, creator, and timestamp. Its check constraint rejects incomplete or mixed scope shapes. It never stores the invitation code or its raw secret.

Schema versions carry a checksum and fail closed if the recorded version is unknown or its expected definition differs. PostgreSQL transactions keep operator, grants, identity link, session, and audit changes atomic.

A recoverable backup must include the complete PostgreSQL database, the Platform Identity pepper, the OIDC subject pepper and configuration revision, and the pending bootstrap file if initialization is incomplete. Store database and secret backup material under separate access control but one coordinated recovery manifest. Restoring PostgreSQL without the original peppers preserves rows but invalidates every associated credential/link. Restoring peppers without the matching database can create unsafe identity assumptions and is unsupported.

After restore, start on loopback, verify /health/ready, authenticate a designated recovery operator, inspect sessions and grants, verify OIDC key retrieval, then admit management traffic. An older restore can resurrect a session or invitation that had later been revoked/consumed. As a conservative incident response, rotate the peppers or explicitly revoke affected sessions and pending invitations through the authenticated operator surface. Restore operation IDs with the matching invitation and audit rows; losing only the operation table removes safe create reconciliation and is not a valid partial restore.

Upgrade and rollback

Before upgrading:

  1. record the exact source commit and runku-server version;
  2. back up PostgreSQL and both peppers and verify the backup;
  3. run the new binary's check against its configuration;
  4. run migrate during the declared maintenance window;
  5. start on a restricted listener and verify liveness, readiness, invitation/session, and OIDC;
  6. admit traffic and retain the old binary only within the schema compatibility decision.

Schema v1 initialization, v2 invitation operations, and v3 source-owned managed grants are append-only. The v3 migration snapshots existing effective grants as unmanaged legacy ownership; only a later authenticated reconciliation adopts the explicitly targeted operator. There is no published mixed-version or downgrade window. After v3 is recorded, move forward; do not use an older server as an operational rollback. Never drop tables or change migration rows to force an older binary to start.

Failure handling

SignalMeaningSafe response
SERVER_CONFIGURATION_MISSINGa required environment variable is absent/emptyfix configuration; no durable change occurred
SERVER_DATABASE_URL_INVALIDthe Identity URL has an unsupported scheme, no host, or no database namecorrect the Identity secret source; no connection was attempted
SERVER_OIDC_CONFIG_INVALIDunsafe JSON, issuer/origin/algorithm/pepper policyreject startup; correct config without broadening trust
SERVER_MANAGED_SOURCE_AUTHORITY_INVALIDmanaged source is not one exact canonical HTTPS origincorrect the configured authority; never accept it from a request
SERVER_MANAGED_SOURCE_CONFIGURATION_INCOMPLETEmanaged token or source authority is configured without its pairconfigure both for managed reconciliation or remove both
SERVER_PLATFORM_DATABASE_UNAVAILABLEthe Identity database connect, version, schema, or migration check failedpreserve logs; verify dependency/schema before retry
SERVER_BOOTSTRAP_FILE_MISSINGdatabase has a pending bootstrap but protected file is absentstop; preserve evidence, then restore the matching set or run the explicit recovery operation
SERVER_BOOTSTRAP_RECOVERY_CONFIRMATION_INVALIDthe offline replacement phrase is missing or wrongverify the intended installation and rerun with the exact documented confirmation
SERVER_BOOTSTRAP_ALREADY_COMPLETErecovery was attempted after an operator existsuse an existing owner session or normal scoped operator invitation; bootstrap cannot reopen
SERVER_BOOTSTRAP_RECOVERY_RESULT_UNCERTAINreplacement may have committed before the client lost the resultrerun the same offline recovery; it safely revokes any unknown pending replacement and emits one new code
PLATFORM_AUTHENTICATION_FAILEDmalformed, wrong, expired, replayed, or revoked credentialreacquire/refresh; do not weaken authorization
PLATFORM_ACCESS_DENIEDvalid operator lacks capability at exact scopechange the grant deliberately; do not use an application key
PLATFORM_IDENTITY_RESULT_UNCERTAINcommit may have succeededreconcile session/invitation/audit before creating new secret material
PLATFORM_INVITATION_OPERATION_REUSEDone opn_* was presented with different issuance contentstop; retain both requests as evidence and allocate a new ID only for a deliberate new operation
PLATFORM_MANAGED_SOURCE_CONFLICTthe current managed revision was reused with different normalized grantsstop; reconcile control-plane state and never retry with changed bytes at that revision
PLATFORM_MANAGED_SOURCE_STALEa lower managed revision attempted rollbackread the control-plane revision, allocate a greater revision for a deliberate new state
PLATFORM_IDENTITY_STORAGE_CORRUPTschema/persisted invariant failedstop writes and restore/investigate; never edit rows ad hoc

SERVER_PLATFORM_DATABASE_UNAVAILABLE is an older stable error code: in this table it means the database selected by RUNKU_IDENTITY_DATABASE_URL, not the Function platform database. The code is preserved in 0.4.4 so existing monitoring does not silently break.

Do not log request bodies, Authorization headers, codes, tokens, peppers, DSNs, raw external subjects, or configuration file contents. Audit records intentionally retain IDs, operation, outcome, actor/subject, and time, not bearer material.

Installation acceptance

Before exposing Management access, validate the actual identity provider and proxy selected for the installation:

  1. enroll the first owner from the protected local invitation and prove the code cannot replay;
  2. create a narrowly scoped invitation, reconcile an uncertain issuance, enroll, and revoke;
  3. log in through OIDC Authorization Code + PKCE and verify issuer, audience, asymmetric algorithm, discovery/JWKS rotation, expiry, and linked subject;
  4. reject wrong issuer/audience/algorithm/key, expired token, token tampering, callback mismatch, login CSRF/mix-up, and a different Management origin;
  5. prove exact Installation/Project/Environment capability denial and live session revocation;
  6. exercise managed grant revisions/replay/stale/divergent updates when managed enrollment is used;
  7. restart and verify sessions/grants/audit persist without exposing tokens or raw external subjects;
  8. restore PostgreSQL plus matching peppers into an isolated installation and reconcile all sessions/invitations/grants before reopening access.

Repeat this qualification with the identity provider actually selected. A development-mode IdP or successful SaaS login does not validate the Self-Hosted provider, TLS, secret, recovery, or operator access policy.