Skip to main content

Security conventions for Gentian apps

This template implements the application-layer slice of gentian-os/docs/security/design.md. Platform MAC (namespaces, default-deny egress, mesh) is enforced by Gentian OS; apps must not weaken it.


Mandatory requirements (every app)​

These are non-negotiable for any Gentian first-party app (catalog or kernel). CI and catalog review should fail if any are missing.

Authentication​

#RequirementHow
M1Validate OIDC bearer tokens on every user-facing API routeDepends(get_current_user) on all /api/v1/* handlers that return or mutate tenant/user data
M2AUTH_DISABLED only in local devMust be false in cluster; chart sets it from auth.disabled: false
M3Validate JWT issuer, signature, and expiryUse gentian_app_runtime.auth — do not roll custom token parsing
M4Scope operations to the authenticated tenantRead TENANT_ID / claims; never trust client-supplied tenant IDs without matching the token

Public unauthenticated endpoints are limited to /healthz and /readyz only.

Secrets​

#RequirementHow
M5Pattern A — no secrets in Git or Helm valuesChart uses existingSecret + envFrom; credentials come from ESO/OpenBao
M6No secrets in ConfigMaps, env literals, or AppProfileOnly valueMapping key names in profile; values injected at deploy time
M7No logging of tokens, passwords, or connection stringsRedact Authorization headers in logs

API surface​

#RequirementHow
M8Expose /healthz (liveness) and /readyz (readiness)Unauthenticated; wired to Deployment probes
M9Explicit CORS in productionBACKEND_CORS_ORIGINS set to the app’s HTTPS origin(s) — never * on tenant apps
M10HTTPS only at the edgeApp listens on HTTP inside the cluster; TLS terminates at Envoy Gateway

Workload hardening (chart)​

#RequirementHow
M11Run as non-rootpodSecurity.runAsNonRoot: true, runAsUser: 65532
M12Drop all capabilities, no privilege escalationcontainerSecurityContext in chart templates
M13Read-only root filesystemWritable paths only via emptyDir (e.g. /tmp)
M14No privileged containers or hostPathAdmission will reject; do not request exceptions in chart
M15Pin container image tagsSemver or digest in chart/values.yaml — no floating latest in production

Edge routing​

#RequirementHow
M16Gateway API HTTPRoute onlychart/templates/httproute.yaml; no nginx Ingress, no in-pod reverse proxy
M17Split API and static web at the gateway/api, /healthz, /readyz → API Service; / → web Service

Catalog declaration (catalog apps only)​

Integration contracts are how Gentian declares what an app offers and what it can use from other apps. This is already in the AppProfile CRD — not a separate consumes / publishes block.

Security architecture termAppProfile CRD fieldMeaning
publishes (manifest)spec.providesContract names this app provides (e.g. project-management)
consumes (manifest)spec.optionalIntegrationsContract names this app may consume, with optional capabilities

Contract schemas live in gentian-apps/contracts/ (when present). The profile only references contract names that match those definitions.

Three layers — do not confuse them:

LayerWhatWho authorsToday
Declarationprovides + optionalIntegrations on AppProfileCatalog / app developerImplemented (CRD)
WiringIntegrationBinding — credentials + OIDC between installed appsPlatform operator (auto when peers match)Implemented
Grant (ReBAC)Future AppGrant — tenant-approved subset + OpenFGA tuplesTenant admin at installPlanned (Stage 2 in security architecture)

kernelRequirements (OIDC, Postgres, S3, …) is separate — it declares kernel services, not cross-app integration contracts.

#RequirementHow
M18Declare kernel needs honestlykernelRequirements — only OIDC/DB/storage/mail the app actually uses
M19Declare integration contracts honestlyprovides / optionalIntegrations — only contracts the app implements or consumes; see existing profiles under gentian-apps/profiles/
M20Never implement tenant grants in app codeCross-app access is wired by IntegrationBinding (today) and AppGrant + OpenFGA (future) — apps declare, they do not grant

Kernel-only repos (e.g. gentian-ui) skip M18–M20; they deploy via ApplicationSet, not AppProfile.


Mandatory when applicable​

Enable these when the condition is true — not optional laziness.

ConditionRequirement
App calls Kubernetes or Gentian APIsM21: Dedicated ServiceAccount + Role/RoleBinding (or ClusterRole if cross-namespace) with minimum verbs/resources; rbac.create: true
App exposes admin or destructive operationsM22: OpenFGA / AuthZEN Check before the handler runs (via require_permission() once PDP is wired)
App runs AI agents or workflowsM23: Dedicated Keycloak client per agent instance — never reuse human credentials or a shared service account
App agents act on behalf of a userM24: RFC 8693 token exchange with act / may_act; rights derived through user ceiling (§2.3)
App calls external URLsM25: Declare endpoints; rely on platform egress allowlist — do not disable NetworkPolicy
App stores tenant/user dataM26: Row-level tenant isolation in queries; no cross-tenant reads even if ReBAC is not wired yet
Production deployM27: CPU/memory requests and limits on api and web containers

Mandatory once platform Stage 2 is live​

When OpenFGA is available in the cluster, these become mandatory for all apps (not just admin apps):

#Requirement
S1Call OpenFGA Check (AuthZEN API) on every mutation and sensitive read
S2Effective access = AppProfile contract declaration ∩ tenant grant (AppGrant, future) ∩ user ceiling ∩ ABAC
S3Agent routes validate delegation tuple + task TTL before acting

Until then, M1–M4 + M26 are the minimum authorization bar.


Where this template implements each requirement​

AreaPath
JWT validation (M1–M4)gentian_app_runtime.auth, gentian_app_runtime.tenant
Log redaction (M7)backend/app/core/logging_middleware.py
Health probes (M8)backend/app/api/routes/health.py
CORS (M9)backend/app/core/config.py, chart/values.yaml
Origin check on POST/PUT/PATCH/DELETE (forged cross-site requests)gentian_app_runtime.origin_check — the rule and what it leaves open are in its docstring; CSRF_TRUSTED_ORIGINS allow-lists further origins, empty by default
OpenFGA PEP stub (M22, S1)backend/app/core/authz.py, backend/app/core/openfga_client.py
Tenant DB scoping (M26)backend/app/db/session.py
Resource limits (M27)chart/values.yaml, chart/values-production.yaml.example
Frontend OIDC + bearer (M1)frontend/src/auth/, frontend/src/api/client.ts
Protected routesfrontend/src/auth/RequireAuth.tsx, frontend/src/router.tsx

Platform-provided — apps must NOT reimplement or weaken​

These are not app author responsibilities. Do not duplicate or override them in the chart unless platform docs explicitly require an app-specific supplement.

ControlOwner
Per-tenant namespace isolationGentian OS / Crossplane
Default-deny egress NetworkPolicy (Cilium)Gentian OS
Tenant Gateway + wildcard TLS (cert-manager)Gentian OS operator
frame-ancestors / CSP for portal iframe embeddingGentian OS gateway policy
Admission control (no privileged pods, image policy)Kyverno / Gatekeeper
Keycloak realm, OIDC clients, token exchange infraKernel Keycloak config
OpenBao paths + ESO ExternalSecretsPlatform provisioning
HTTPRoute hostnames from AppProfile ingress.subDomainOperator (tenant apps)

Checklist before merge / release​

[ ] M1–M4 OIDC on all data routes; tenant scoping
[ ] M5–M7 Pattern A secrets; no leakage in logs
[ ] M8–M10 Health probes; CORS; no in-app TLS termination
[ ] M11–M15 Pod hardening; pinned images
[ ] M16–M17 HTTPRoute; no nginx
[ ] M18–M20 AppProfile honest declaration (catalog apps)
[ ] M21+ Conditional items reviewed
[ ] M27 Resource limits set for production