Security¶
Security considerations for the openKMS project.
Authentication¶
OPENKMS_AUTH_MODE=oidc(default): External OpenID Connect IdP – OAuth2 Authorization Code + PKCE in the SPA (oidc-client-ts). Backend trusts JWTs via issuer discovery (jwks_uri,issuerclaim).OPENKMS_AUTH_MODE=local: Users and bcrypt password hashes in PostgreSQL; backend-issued HS256 JWTs (OPENKMS_SECRET_KEY); optional HTTP Basic foropenkms-cli(OPENKMS_CLI_BASIC_*). Use TLS in production; Basic over plain HTTP is only for trusted dev networks.GET /api/auth/public-config(unauthenticated): Returnsauth_modeandallow_signuponly—no secrets and no VLM or other infrastructure hints—so clients pick the correct login flow and match the deployed mode (local authenticator vs central IdP).- Backend accepts:
Authorization: Bearer <JWT>for API requests- Session cookie (from
POST /sync-sessionafter browser login) - In local mode:
Authorization: Basicfor CLI (validated against env; minted service JWT internally) - OIDC JWTs validated via IdP JWKS; local JWTs validated with shared secret.
- Route protection:
/(home) is public for guests (static marketing content). All other routes underMainLayoutrequire authentication;/loginand/signupare outside that shell. For signed-in users who are not JWTadminand do not hold theallkey, the SPA also enforces frontend route patterns: after loadingGET /api/auth/permission-catalog, the pathname must match the union offrontend_route_patternsfor the user’s resolved keys, except always-allowed paths/and/profile. If the catalog request fails, the gate degrades to allow navigation (operators should fix API reachability). - Strict API patterns (optional):
OPENKMS_ENFORCE_PERMISSION_PATTERNS_STRICT(defaultfalse). Whentrue, after authentication every/api/...request must match catalog rules; the user must hold at least one of the permission keys tied for the best-matching rule tier when several rows share the same pattern specificity (e.g.console:object_typesandontology:writeon the same POST path), or holdall, or JWT realmadmin, or service subjectlocal-cli. Bypass / no-pattern paths (still subject to normalrequire_authwhere applicable):GET /api/auth/public-config,POST /api/auth/register,POST /api/auth/login;GET|HEAD /api/auth/me,GET /api/auth/permission-catalog,POST /api/auth/logout,GET|HEAD /api/feature-toggles;OPTIONSon any path;GET /openapi.json,GET /docs,GET /redoc; non-/apiroutes (e.g./health). Unmatched paths return 403 with a message to extend the catalog or turn strict mode off. Compiled rules are cached (OPENKMS_PERMISSION_PATTERN_CACHE_TTL_SECONDS, default60); adminPATCH/POST/DELETEon/api/admin/security-permissionsinvalidates that compiled-rule cache and the in-processGET /api/auth/permission-catalogresponse cache (seeOPENKMS_PERMISSION_CATALOG_CACHE_SECONDSin architecture / backend.env.example). Alembic revisiona2b3c4d5e6f7backfills default patterns for allOPERATION_KEY_HINTSkeys. - Console: Entry requires any
console:*operation permission fromGET /api/auth/me, or JWT realm roleadmin(OIDC). Individual console pages require specific keys (for exampleconsole:users,console:data_sources). Local users receive permissions fromuser_security_roles→security_role_permissions. OIDC non-admin users: JWTrealm_access.rolesnames are matched tosecurity_roles.name; the union of those roles’security_role_permissionsis used. Permission definitions live insecurity_permissions; empty databases getallfrom the seed migration, and Alembica2b3c4d5e6f7inserts/updates default pattern rows for everyoperation_key_hintkey. Admins may add or edit keys via/api/admin/security-permissionsor the Console, guided byGET /api/admin/permission-reference.is_admin(local) or realm roleadmin(OIDC) receives every defined permission key for bootstrap. - First-time admin (least-privilege catalog):
GET /api/admin/permission-referenceincludesoperation_key_hints—canonical operation strings aligned with backendrequire_permissionchecks, each with label, description, and category. The Console Permissions page can bulk-add missing hinted keys (createssecurity_permissionsrows with empty route/API pattern lists; operators fill patterns from the same reference). Dismissing the in-page Getting started panel is stored in the browser (openkms_permissions_onboarding_dismissed). Console overview shows a one-line nudge when the catalog still has onlyalland that flag is not set (alongside cards for console-only tools: permissions, data security, data sources, users & toggles, settings). For OIDC delegation, create roles whosesecurity_roles.namematches IdP realm role names, then assign catalog keys in the role matrix. - Operation permissions: Stable keys are listed by
GET /api/auth/permission-catalog(each entry includes human text plus frontend route and backend API path patterns for documentation).allgrants full access. Backend checks userequire_permissionorrequire_any_permissionwhere wired; holdingallsatisfies any key (JWT realmadminandlocal-clibypass). Console Permissions edits roles in PostgreSQL for local auth; some catalog keys are policy targets for UI and API enforcement alongside data scopes. - Group data scopes (local enforcement):
OPENKMS_ENFORCE_GROUP_DATA_SCOPES(defaultfalse). WhentrueandOPENKMS_AUTH_MODE=local, non-admin users who belong to at least one access group are filtered on channels/documents, knowledge bases, evaluation datasets, datasets (console), object types, link types, and object/link instance APIs under those types. Visibility is the union of (1) legacy allow lists selected per group and (2) data resources attached to the group: named rows with aresource_kindand JSONattributesusing whitelisted keys only (no free-form SQL). Document resources may useanchor_channel_idand/ormetadata.<key>(JSONB containment) and/orchannel_id. Knowledge-base resources useanchor_knowledge_base_idorkb_id/namein attributes; dataset/evaluation/object/link kinds use the corresponding*_idkey in attributes. Users with no group membership are not filtered (legacy behavior). JWTadminbypasses filters. Console (any auth mode): operators withconsole:groupsmay CRUD access groups, data resources, and group scopes (legacy ID lists + data resource attachments). User ↔ access group membership is maintained only in local auth (PUT /api/admin/groups/{id}/membersreturns 403 in OIDC;GETreturns an empty list). OIDC: data-scope enforcement is not applied in this phase (IdP group sync is future work). - Object Explorer (
POST /api/ontology/explore): Arbitrary read-only Cypher is not rewritten for group scope; trusted operators should treat it as a power-user path. Prefer restricting access via console permissions and deployment policy.
Credentials and Secrets¶
Do Not Expose in CLI¶
AWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEYare never CLI parameters.- They are read only from environment variables (e.g. via
.envwith python-dotenv). - Avoid passing secrets on the command line; they may appear in process lists and shell history.
Environment Variables¶
| Variable | Scope | Purpose |
|---|---|---|
OPENKMS_OIDC_CLIENT_SECRET |
Backend | OAuth2 confidential client secret (backend code flow, oidc mode) |
OPENKMS_SECRET_KEY |
Backend | Session cookie signing and local JWT signing |
OPENKMS_CLI_BASIC_PASSWORD |
Backend + CLI | Local mode CLI Basic secret (protect like a password) |
AWS_ACCESS_KEY_ID |
Backend, CLI | S3/MinIO access |
AWS_SECRET_ACCESS_KEY |
Backend, CLI | S3/MinIO secret |
OPENKMS_DATABASE_PASSWORD |
Backend | PostgreSQL |
OPENKMS_ENFORCE_GROUP_DATA_SCOPES |
Backend | When true (with local auth), apply access-group allow lists to data APIs; default false until groups are configured |
OPENKMS_ENFORCE_PERMISSION_PATTERNS_STRICT |
Backend | When true, authenticated /api/* must match catalog backend_api_patterns and user must hold the owning key; default false |
OPENKMS_PERMISSION_PATTERN_CACHE_TTL_SECONDS |
Backend | In-memory TTL for compiled pattern rules loaded from security_permissions |
Keep .env out of version control (use .env.example as a template).
Obtaining an API token¶
Examples in the docs use Authorization: Bearer $TOKEN. The backend accepts a Bearer JWT on every /api/* route when Authorization is present; otherwise it falls back to the session cookie (see below). How $TOKEN is obtained depends on OPENKMS_AUTH_MODE:
- Local mode —
POST /api/auth/loginwith JSON{ "login", "password" }returnsaccess_token(HS256, signed withOPENKMS_SECRET_KEY) plususer. That token is the same shape the SPA stores after login.
Prefer not embedding the password in the shell command (it can appear in shell history and, on shared hosts, process listings). Read credentials from the environment and build JSON with jq, or use a secrets manager / CI secret store:
TOKEN=$(curl -sS -X POST "${API%/}/api/auth/login" \
-H 'Content-Type: application/json' \
-d "$(jq -n --arg login "$OPENKMS_LOGIN" --arg password "$OPENKMS_LOGIN_PASSWORD" \
'{login:$login,password:$password}')" \
| jq -er '.access_token // empty')
If TOKEN is empty, inspect the response body for detail (wrong password, wrong mode, etc.). Default lifetime is OPENKMS_LOCAL_JWT_EXP_HOURS hours (168 by default) — long-lived tokens are convenient for dev scripts but increase blast radius if leaked.
- OIDC mode — the SPA obtains an access token from the IdP (Authorization Code + PKCE), then
POST /api/auth/sync-sessionwithAuthorization: Bearer <IdP access token>stores that same token in the session cookie so credentialed browser requests work without repeating the Bearer header.
For scripts and curl, pass the IdP access token as Authorization: Bearer …. The backend verifies it with the IdP JWKS (_verify_oidc_jwt in auth.py). Operation permissions for non-admin users come from realm_access.roles on the JWT matched against security_roles.name in PostgreSQL — so the token must be one that carries those roles (typically a user access token), not merely “any” client-credentials token unless your IdP is configured to attach the same role claims.
OPENKMS_OIDC_SERVICE_CLIENT_ID is used for service-only API paths (for example some internal model defaults); it is not a general substitute for a human user token on arbitrary /api/... routes unless you deliberately mirror roles onto that client in the IdP.
-
CLI / scripts in local mode — set
OPENKMS_CLI_BASIC_USERandOPENKMS_CLI_BASIC_PASSWORDand sendAuthorization: Basic …. The backend mints an internallocal-cliservice JWT (sub=local-cli). Use only on trusted networks; Basic over plain HTTP is dev-only. -
Personal API keys — signed-in users can create keys under Settings (
/settings, user menu → Settings) viaPOST /api/auth/api-keys. The plaintext value looks likeokms.{uuid}.{secret}and is shown once. Send it asAuthorization: Bearer …on/api/*routes; the backend resolves the same owner as your account and applies the usual permission checks. Local mode: permissions followuser_security_rolesas for a login JWT. OIDC mode: the key stores a snapshot of your IdP realm roles from creation time; if your roles change later, create a new key to pick up the new mapping (or revoke and recreate). Keys can be listed and soft-revoked viaGET/DELETE /api/auth/api-keys.
In all cases, GET /api/auth/me confirms the token (or session) is accepted and lists resolved permission keys.
Hardening backlog: token lifetime, script ergonomics, and machine-auth patterns are tracked in Technical debt: API tokens and machine authentication.
Storage¶
- S3/MinIO: Document files are stored under
{file_hash}/with presigned URLs for access. - PostgreSQL: Metadata, channels, and user-related data.
- Ensure S3 bucket policies and CORS are configured correctly for your deployment.
API Security¶
- All
/api/*endpoints require authentication. - Document file URLs are validated: backend checks that the requested path belongs to the document before redirecting to storage.
- VLM server URL is internal; avoid exposing it directly to untrusted clients.
Production Checklist¶
- Use strong
OPENKMS_SECRET_KEY(e.g. 32+ random bytes). - Configure the OIDC IdP with proper redirect URIs for production (no wildcards unless intended). For local mode, set
OPENKMS_ALLOW_SIGNUP=falseif you do not want public registration. - Use HTTPS for frontend and backend in production.
- Restrict database and S3 access to trusted networks where possible.
- Review IdP realm roles and client scopes (oidc mode).
- Keep dependencies up to date (
pip install -U,npm audit).
Reporting Vulnerabilities¶
If you discover a security vulnerability, please report it privately rather than opening a public issue.