Control catalog
One row per control objective: the principle it serves (security/principles.md), the mechanism that implements it, and the query that produces its evidence. This is the system description an ISO 27001 Statement of Applicability and a SOC 2 report map to. A row changes only when the code does; design.md §3.0 holds the finer-grained list of mechanisms.
An evidence query is what an auditor runs, or is shown, to pull the records of an arbitrary week.
| # | Control objective | Principle | Mechanism | Evidence query |
|---|---|---|---|---|
| K1 | Changes reach a cluster only through a reviewed, CI-checked pull request | 9 | Rulesets on main and develop of gentian-os, gentian-ui and gentian-apps: pull request, thread resolution, required status checks, no bypass actors; release tags immutable; the organization requires a second factor of every member and defaults members to read; CODEOWNERS in each repository; third-party Actions pinned by commit | gh api repos/gentian-org/<repo>/rulesets; gh api orgs/gentian-org (two_factor_requirement_enabled, default_repository_permission); gh pr list --state merged with each PR's checks |
| K2 | Every change to declared state names the person and the relation that allowed it | 7, 9 | The director commits to the deployments repository with a Gentian-Authz: trailer and an OpenPGP signature; Argo CD verifies through AppProject.spec.sourceIntegrity | git log --show-signature on the deployments repository; argocd app get showing the signature policy |
| K3 | Images are built, signed and attested by CI | 9 | Immutable image tags published by digest; every image signed keyless with the building workflow's identity and carrying an SBOM and a provenance attestation; the signature is written in cosign's legacy form (the sha256-<digest>.sig tag), which is the form Kyverno 1.13 reads; Kyverno checks the signature on ghcr.io/gentian-org/* in tenant namespaces in Audit mode, so a failure is reported and not refused; profiles pin bundles by digest | cosign verify --certificate-oidc-issuer https://token.actions.githubusercontent.com --certificate-identity-regexp 'https://github.com/gentian-org/' <image>; kubectl get policyreport -A; kubectl get pods -o jsonpath='{..imageID}' |
| K4 | Third-party code is scanned and known vulnerabilities are reported and updated | 9 | CodeQL default setup; Dependabot alerts, security updates and weekly grouped version updates for every ecosystem, against develop, the default branch; Actions and base images pinned by commit or digest; builds and tests install from the lockfiles (uv sync --locked, npm ci); govulncheck, pip-audit, npm audit and Trivy in CI, reporting without failing the run | gh api repos/gentian-org/<repo>/code-scanning/alerts; gh api repos/gentian-org/<repo>/dependabot/alerts with fixed_at; the audit jobs' logs |
| K5 | Images are scanned before release | 9 | Trivy on every published image, high and critical, in the publish jobs, reporting without blocking | The Scan the image step of each publish run; gh run view <id> --log |
| K6 | Identity is a verified claim set from a known issuer | 1 | Keycloak realm per tenant and a kernel realm; RS256 verification against the issuer's keys in every service; the gateway's OIDC session in front of every authMode: oidc route | Realm export; kubectl get securitypolicy -A |
| K7 | Every request to a protected route is decided by policy, and the decision fails closed | 2, 3 | OpenFGA model (authz/model/v1/model.fga) with CI tests; Check in bouncer, director, custodian, usher and registrar; failOpen: false at the gateway | CI run of the authorization policy tests; the model's tests |
| K8 | Administrators enroll a second factor | 1 | A sign-in flow in every realm requires a second factor of anyone in a group marked gentianRequiresMFA, and enrolls them if they have none | Realm export: browserFlow and the group attribute; Keycloak: the account's credentials of type otp |
| K9 | Privileged credentials are few and kept in the vault, and external ones are rotated | 5, 8 | OpenBao holds kernel credentials; the custodian rotates external ones; automated callers sign in to Keycloak as gentian-operator; one OpenFGA key per kernel service | OpenBao audit of the paths; rotation dates |
| K10 | Break-glass access is time-bound and recorded | 5, 7 | breakGlass role holder on the Cluster claim, with a reason and a required expiry, dropped by the operator at that time | The claim's commits; break-glass audit records from the director |
| K11 | Changes to a person's identity and rights are recorded with who asked and what allowed it | 7 | Registrar record, 400-day retention (internal/registrar/record); Keycloak user and admin events on in tenant realms | select * from actions where at between … on the registrar database; Keycloak events API |
| K12 | Who holds privileged access is reviewed on an interval and the review is recorded | 5 | kubectl gentian platform-roles review: the members of the admin and breakGlass groups and the role holders the Cluster claim names, confirmed by a person who may audit the cluster; the claim keeps spec.platformAccess.accessReview (who, when, the next date, a quarter by default) and the director answers reviewDue once it has passed; a tenant's own administrators are the tenant's to review | git log -p -- clusters/<cluster>/kernel/claims/cluster.yaml on the deployments repository, each review commit listing what was reviewed; kubectl gentian platform-roles holders for whether one is due |
| K13 | Tenants are isolated from each other and from the kernel | 6 | Namespace per tenant with default-deny ingress and egress; Kyverno pod-security policies in Enforce; LimitRange; per-tenant OpenBao store and policy; ingress NetworkPolicies on the kernel namespaces, on by default (KERNEL_NETWORK_POLICIES unset means on), generated into kernel/security/network-policies/ | kubectl get networkpolicy -n <tenant>; kubectl get networkpolicy -n kernel-control; kubectl get clusterpolicy; bao policy read <tenant> |
| K14 | Secrets are held in a vault and reach a workload only through its tenant's store | 8 | OpenBao with transit auto-unseal; External Secrets Operator with one store, role and policy per tenant | kubectl get externalsecret -A; OpenBao policies |
| K15 | Secrets are encrypted at rest | 8 | Pre-flight reads the API server's encryption flag where it is visible and otherwise the Cluster claim's controlPlane.secretsAtRest with its evidence; scripts/tools/rewrite-secrets.sh re-encrypts Secrets written before the provider was switched on | The claim's controlPlane block and the commit that set it; the pre-flight line; on a self-hosted control plane, etcdctl get on one Secret key showing the provider prefix |
| K16 | Data in transit is encrypted | 6 | cert-manager with Let's Encrypt at the edge; internal CA from OpenBao PKI; kernel Postgres clients require TLS; Argo CD serves TLS checked by the Gateway; MariaDB offers TLS | Certificate inventory; openssl s_client against each public host |
| K17 | Tenant data is backed up, encrypted and restorable | 8 | BackupPolicy, TenantExport, TenantRestore: age-encrypted bundles, signed manifests, retention tiers | kubectl get tenantexport -A; the record of a restore |
| K18 | Security-relevant events are written and forwarded to a retaining destination | 7 | One JSON envelope per decision and change from the bouncer and the director, the registrar's identity record, Keycloak's user and admin events as JSON with a thirty-day expiry, and the gateway's JSON access log, joined by the request id (audit-events.md); the forwarder ships them to the destination the Cluster claim names (spec.logForwarding); retention and immutability are the destination's; the Kubernetes API audit log is the provider's, since the platform does not own the control plane and configures no audit policy of its own | The destination's retention and lock settings and its signed manifests; one request id followed through the bouncer's, the director's and the gateway's records |
| K19 | Availability and capacity are monitored and alerted on | — | spec.monitoring.enabled installs kube-prometheus-stack with the platform's rules (audit-events.md §5) | Alertmanager's alert history; the rules in kernel-observability |
| K20 | Deleting a tenant removes its data and leaves a record | 8 | App purge, tenant retire and delete with purge, mailbox archive or delete | Director change log entry for the deletion |
| K22 | A vulnerability can be reported privately and is answered within a stated time | — | SECURITY.md in each repository; GitHub private vulnerability reporting enabled | gh api repos/gentian-org/<repo>/private-vulnerability-reporting |
| K23 | The documentation describes the controls as they are | — | This catalog; design.md §3.0; the principles | Reviewed at each release |
| K24 | The platform's own interfaces verify the person's token themselves and refuse to run open | 1, 3 | The desktop's and the administration console's backends verify RS256 against the issuer's keys with audience and expiry tests; the ComponentProfiles the platform ships inject issuer and audience from the vault, set ENVIRONMENT: production and expose every route with authMode: oidc, so the gateway's session and the bouncer (failOpen: false) stand in front of each backend and the tenant NetworkPolicy admits only the gateway; a backend refuses to start with AUTH_DISABLED in production, so the anonymous path exists only in a local environment | kubectl get componentprofile desktop admin-console -o yaml (the environment and the exposures); the backend's startup log; the tests in gentian-ui that assert the anonymous path needs the opt-out |