Skip to content

docs(security): add design-level threat model - #3217

Merged
Aleksei Sviridkin (lexfrei) merged 1 commit into
mainfrom
docs/security-threat-model
Jul 6, 2026
Merged

Aleksei Sviridkin (lexfrei) merged 1 commit into
mainfrom
docs/security-threat-model

Conversation

@lexfrei

@lexfrei Aleksei Sviridkin (lexfrei) commented Jul 6, 2026 •

Copy link
Copy Markdown
Contributor

What this PR does

Adds docs/security/threat-model.md, a design-level threat model for Cozystack. It documents the actors, the actions that cross trust boundaries, and the security goals and non-goals, organized by trust boundary (T0–T4) following the CNCF TAG-Security Actors/Actions model.

It records the usage and multitenancy model (managed-services backend vs direct dashboard/API access; combined hard multitenancy for tenant VMs and Kubernetes clusters, soft multitenancy for managed data services), the primary trust-boundary invariant (tenant RBAC grants no HelmRelease or arbitrary core writes; the aggregated API server fixes the chart reference server-side), and the two admission enforcement points (core kube-apiserver vs cozystack-api). Hostname integrity covers both the Gateway API path and the default legacy-Ingress path (cozystack-ingress-hostname-policy, #3200). Every architectural claim cites a source file so the model can be re-verified as the code evolves.

This is the in-repo companion to the project's CNCF TAG-Security self-assessment, which references it.

Screenshots

N/A — documentation only.

Release note

docs(security): add a design-level threat model (docs/security/threat-model.md)

Summary by CodeRabbit

  • Documentation
    • Added a security threat model for the platform.
    • Clarified system boundaries, trust tiers, and the main actors involved.
    • Documented key security goals, non-goals, and important enforcement points.
    • Described expected behavior for tenant actions, hostname handling, and cross-boundary access.

Document the platform's actors, boundary-crossing actions, and security
goals/non-goals, organized by trust boundary (T0-T4) per the CNCF
TAG-Security Actors/Actions model. Records the primary trust-boundary
invariant (tenant RBAC grants no HelmRelease/core writes; the aggregated
API server fixes the chart reference server-side) and the two admission
enforcement points (core kube-apiserver vs cozystack-api). Every claim
cites a source file so the model can be re-verified as the code evolves.

Assisted-By: Claude <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a formal design-level threat model for the Cozystack platform. It serves as a foundational security document that maps out trust boundaries, defines actor roles, and outlines the platform's security guarantees and non-goals. By linking architectural claims directly to source code, this documentation provides a verifiable reference for security assessments and future development, ensuring that the project's security posture remains consistent as the codebase evolves.

Highlights

  • Documentation: Added a comprehensive design-level threat model for Cozystack in docs/security/threat-model.md.
  • Security Architecture: Documented trust boundaries (T0-T4), actor privileges, and security goals following the CNCF TAG-Security model.
  • Invariant Tracking: Explicitly defined the primary trust-boundary invariant regarding Application-to-HelmRelease conversion to guide future development.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment Gemini (@gemini-code-assist) Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on Gemini (@gemini-code-assist) comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@dosubot dosubot Bot added kind/documentation Categorizes issue or PR as related to documentation security Security-related issues and features labels Jul 6, 2026
@coderabbitai

coderabbitai Bot commented Jul 6, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a new documentation file, docs/security/threat-model.md, describing a design-level security threat model for Cozystack. It covers system architecture, multitenancy isolation, trust tiers, actor privileges, security goals/non-goals, trust-boundary invariants, and admission enforcement details.

Changes

Threat Model Documentation

Layer / File(s) Summary
Architecture and multitenancy overview
docs/security/threat-model.md
Introduces the document's scope, architecture framing, usage modes, and hard/soft isolation model with trust tiers T0–T4.
Actor catalog and action matrix
docs/security/threat-model.md
Details actor privileges by trust tier and provides an actions matrix for authentication, tenant lifecycle events, and cross-tenant traffic behavior.
Security goals and non-goals
docs/security/threat-model.md
States isolation, provisioning, and authentication goals, alongside explicit non-goals and excluded threat scenarios.
Trust invariants and enforcement points
docs/security/threat-model.md
Documents the core trust-boundary invariant, admission enforcement points, hostname-policy fail modes, and re-verification references.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding a design-level security threat model under docs/security.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch docs/security-threat-model

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a comprehensive design-level threat model for Cozystack in docs/security/threat-model.md, detailing system actors, trust boundaries, actions, and security goals. The review feedback points out a few inaccuracies and opportunities for improvement in the document: correcting the description of the tenant ServiceAccount RBAC binding mechanism to accurately reflect the codebase, fixing typos in the file path of ApplicationDefinition types, and fully specifying the paths for the admission policy files to ensure clarity and consistency.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.


| Action | Actors (from → to) | Data crossing boundary | Authorization / validation checks | Result |
| --- | --- | --- | --- | --- |
| Authenticate | End user (T4) → Keycloak → kube-apiserver / dashboard | OIDC token with `groups` and `preferred_username` claims | When OIDC is enabled (`authentication.oidc.enabled`, **off by default**): kube-apiserver OIDC verification; dashboard and LINSTOR GUI fronted by oauth2-proxy (the `gatekeeper` Deployment); tenant kubeconfig uses `kubectl oidc-login`. In the default OIDC-disabled mode the dashboard uses a `token-proxy` and the LINSTOR GUI is not externally exposed. | In OIDC mode, an authenticated identity whose `groups` map to RBAC subjects (the `<tenant>-view/use/admin/super-admin` roles). In the default mode the `token-proxy` forwards a Kubernetes-issued ServiceAccount JWT as the bearer token, so authorization is whatever that ServiceAccount's RBAC grants. The tenant ServiceAccount is bound (via `tenant.yaml`) into the tenant's view/use/admin **and super-admin** RoleBindings, so a holder of the tenant SA token has super-admin-level access over the tenant subtree — equivalent to the `<tenant>-super-admin` OIDC group, not merely the base `cozy:tenant` role. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

In packages/apps/tenant/templates/tenant.yaml, the tenant ServiceAccount is bound to the cozy:tenant RoleBinding (which references the cozy:tenant ClusterRole), rather than being bound into the individual view/use/admin/super-admin RoleBindings. The cozy:tenant ClusterRole aggregates cozy:tenant:base, which grants the apps.cozystack.io/* wildcard, thereby giving the ServiceAccount super-admin-level access over the tenant subtree. Let's update this description to reflect the actual RBAC binding mechanism accurately.

Suggested change:

| Authenticate | End user (T4) → Keycloak → kube-apiserver / dashboard | OIDC token with `groups` and `preferred_username` claims | When OIDC is enabled (`authentication.oidc.enabled`, **off by default**): kube-apiserver OIDC verification; dashboard and LINSTOR GUI fronted by oauth2-proxy (the `gatekeeper` Deployment); tenant kubeconfig uses `kubectl oidc-login`. In the default OIDC-disabled mode the dashboard uses a `token-proxy` and the LINSTOR GUI is not externally exposed. | In OIDC mode, an authenticated identity whose `groups` map to RBAC subjects (the `<tenant>-view/use/admin/super-admin` roles). In the default mode the `token-proxy` forwards a Kubernetes-issued ServiceAccount JWT as the bearer token, so authorization is whatever that ServiceAccount's RBAC grants. The tenant ServiceAccount is bound (via `tenant.yaml`) to the `cozy:tenant` RoleBinding, which references the `cozy:tenant` ClusterRole. Because `cozy:tenant` aggregates `cozy:tenant:base` (which grants the `apps.cozystack.io/*` wildcard), a holder of the tenant SA token has super-admin-level access over the tenant subtree — equivalent to the `<tenant>-super-admin` OIDC group, not merely a restricted base role. |


Cozystack is delivered as a **management (root) Kubernetes cluster** onto which tenants, managed services, virtual machines, and tenant Kubernetes clusters are layered. The linchpin of the security model is the aggregated API server **`cozystack-api`** (`cmd/cozystack-api`, `pkg/apiserver`, `pkg/registry`). Tenants never write privileged Kubernetes objects (HelmRelease, Deployment, Secret, RBAC) directly. Instead they write thin, virtual `apps.cozystack.io/*` **Application** custom resources, and `cozystack-api` translates each one 1:1 into a Flux `HelmRelease` whose chart reference is fixed server-side.

`cozystack-api` serves three aggregated API groups (`pkg/apiserver/apiserver.go`): `core.cozystack.io/v1alpha1` (`tenantnamespaces`, `tenantsecrets`, `tenantmodules`, `options`), `sdn.cozystack.io/v1alpha1` (`securitygroups`), and `apps.cozystack.io/v1alpha1` (the per-tenant Application kinds). The set of Application kinds is registered **dynamically** at startup from `ApplicationDefinition` custom resources (`pkg/cmd/server/start.go`, `api/v1alpha1/applicationdefinitions_types.go`); when an `ApplicationDefinition` changes, `internal/controller/applicationdefinition_controller.go` rolls the `cozystack-api` Deployment so the new kinds are served. The server is registered with the main kube-apiserver as an `APIService` over TLS whose CA is minted by cert-manager (`packages/system/cozystack-api/templates/`).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

The file name for the ApplicationDefinition types is singular (applicationdefinition_types.go), not plural (applicationdefinitions_types.go). Let's correct this path for accuracy.

Suggested change:

`cozystack-api` serves three aggregated API groups (`pkg/apiserver/apiserver.go`): `core.cozystack.io/v1alpha1` (`tenantnamespaces`, `tenantsecrets`, `tenantmodules`, `options`), `sdn.cozystack.io/v1alpha1` (`securitygroups`), and `apps.cozystack.io/v1alpha1` (the per-tenant Application kinds). The set of Application kinds is registered **dynamically** at startup from `ApplicationDefinition` custom resources (`pkg/cmd/server/start.go`, `api/v1alpha1/applicationdefinition_types.go`); when an `ApplicationDefinition` changes, `internal/controller/applicationdefinition_controller.go` rolls the `cozystack-api` Deployment so the new kinds are served. The server is registered with the main kube-apiserver as an `APIService` over TLS whose CA is minted by cert-manager (`packages/system/cozystack-api/templates/`).

The claims in this document can be re-verified against these sources:

- Aggregated API server: `cmd/cozystack-api/`, `pkg/apiserver/apiserver.go`, `pkg/registry/apps/application/rest.go`, `pkg/config/config.go`.
- Dynamic Application kinds: `pkg/cmd/server/start.go`, `api/v1alpha1/applicationdefinitions_types.go`, `internal/controller/applicationdefinition_controller.go`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

Correct the plural applicationdefinitions_types.go to singular applicationdefinition_types.go to match the actual file name in the repository.

Suggested change:

- Dynamic Application kinds: `pkg/cmd/server/start.go`, `api/v1alpha1/applicationdefinition_types.go`, `internal/controller/applicationdefinition_controller.go`.

- Aggregated API server: `cmd/cozystack-api/`, `pkg/apiserver/apiserver.go`, `pkg/registry/apps/application/rest.go`, `pkg/config/config.go`.
- Dynamic Application kinds: `pkg/cmd/server/start.go`, `api/v1alpha1/applicationdefinitions_types.go`, `internal/controller/applicationdefinition_controller.go`.
- Tenant RBAC: `packages/system/cozystack-basics/templates/clusterroles.yaml`.
- Admission policies: `packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml`, `route-hostname-policy.yaml`, `ingress-hostname-policy.yaml`, `packages/core/platform/templates/deletion-protection.yaml`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

For clarity and consistency, fully specify the paths for route-hostname-policy.yaml and ingress-hostname-policy.yaml as they are located under packages/system/cozystack-basics/templates/.

Suggested change:

- Admission policies: `packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml`, `packages/system/cozystack-basics/templates/route-hostname-policy.yaml`, `packages/system/cozystack-basics/templates/ingress-hostname-policy.yaml`, `packages/core/platform/templates/deletion-protection.yaml`.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/security/threat-model.md`:
- Line 15: The direct-access description in the threat model is using the
resource API group name as if it were the authorization boundary. Update the
wording in the usage modes section to reference the tenant RBAC subject/role
names that actually control access in the dashboard and Cozystack API, and keep
the managed-services backend sentence unchanged. Use the existing “Usage modes”
text as the target location and replace the mention of apps.cozystack.io with
the correct RBAC subject names from the model.
- Line 69: Update the “Hostname and tenancy integrity” section in
threat-model.md so the fail-closed statement no longer includes the
namespace-host-label policy; keep the fail-closed guarantee limited to the
Tenant-CR host, HTTPRoute/TLSRoute, and Ingress policies, and describe the
namespace-host-label policy separately as only preventing mutation of an
existing label while relying on provisioning to set namespace.cozystack.io/host.
Use the existing policy names and “Admission enforcement points” language to
make the distinction explicit.
- Around line 21-22: The threat-model wording in the multitenancy section
conflates KubeVirt VM workloads with the Kamaji tenant-cluster control plane;
update the text around the hard multitenancy description to separate these
boundaries clearly. In the sentence describing tenant-controlled code, keep the
VM/worker-node isolation tied to the hardware-virtualization boundary, and
describe Kamaji control-plane components as pods running in the management
cluster rather than as tenant namespace pods. Use the existing “Hard
multitenancy for tenant-controlled code” and “Soft multitenancy for managed data
services” sections to preserve the distinction.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 03b1f2de-a50c-4010-9776-9996748ad7c1

📥 Commits

Reviewing files that changed from the base of the PR and between 7b65cf2 and 286f260.

📒 Files selected for processing (1)
  • docs/security/threat-model.md


Application storage is a **virtual REST backed by HelmReleases**, not etcd (`pkg/registry/apps/application/rest.go`). On create/update the tenant supplies only `app.Spec` (the Helm values) plus labels and annotations; the chart reference, release-name prefix, and the platform `cozystack-values` Secret mounted as `valuesFrom` all come from server-side configuration, not from the tenant (`pkg/registry/apps/application/rest.go` around the HelmRelease construction; `pkg/config/config.go`). Values keys beginning with `_` are reserved and rejected.

**Usage modes.** Cozystack is deployed in two ways. As a **managed-services backend** (roughly half of deployments), an external provisioning system authenticates with its own ServiceAccount and creates the tenant resources it needs — no OIDC involved. Used **directly**, a human is granted OIDC access to the dashboard and the Cozystack API and operates it, constrained by RBAC to the `apps.cozystack.io` group within their tenant.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use the RBAC subject names, not the API group name.

apps.cozystack.io is the resource API group, not an authorization group. This sentence should point to the tenant RBAC roles that actually gate direct access.

Suggested wording
- Used directly, a human is granted OIDC access to the dashboard and the Cozystack API and operates it, constrained by RBAC to the `apps.cozystack.io` group within their tenant.
+ Used directly, a human is granted OIDC access to the dashboard and the Cozystack API and operates it, constrained by the tenant's RBAC roles over `apps.cozystack.io` resources.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
**Usage modes.** Cozystack is deployed in two ways. As a **managed-services backend** (roughly half of deployments), an external provisioning system authenticates with its own ServiceAccount and creates the tenant resources it needs — no OIDC involved. Used **directly**, a human is granted OIDC access to the dashboard and the Cozystack API and operates it, constrained by RBAC to the `apps.cozystack.io` group within their tenant.
Used directly, a human is granted OIDC access to the dashboard and the Cozystack API and operates it, constrained by the tenant's RBAC roles over `apps.cozystack.io` resources.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/security/threat-model.md` at line 15, The direct-access description in
the threat model is using the resource API group name as if it were the
authorization boundary. Update the wording in the usage modes section to
reference the tenant RBAC subject/role names that actually control access in the
dashboard and Cozystack API, and keep the managed-services backend sentence
unchanged. Use the existing “Usage modes” text as the target location and
replace the mention of apps.cozystack.io with the correct RBAC subject names
from the model.

Comment on lines +21 to +22
- **Hard multitenancy for tenant-controlled code.** Anything in which a tenant can run arbitrary code — VMs and tenant Kubernetes clusters — is isolated at the hardware-virtualization / dedicated-control-plane boundary, and the tenant has full access inside it. Those VMs and tenant-cluster nodes are themselves ordinary, unprivileged pods in the tenant namespace: governed by Pod Security Standards, denied access to the management-cluster API, and subject to the tenant network and admission policies.
- **Soft multitenancy for managed data services.** Databases and queues run in the management cluster but expose only a fixed, validated set of parameters and never let a tenant execute arbitrary code (unsafe options are disabled). A tenant therefore cannot compromise the management cluster by swapping a container image or enabling a dangerous setting.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Separate the VM boundary from the Kamaji boundary.

This sentence conflates KubeVirt VM workloads with the tenant-cluster control plane. The worker nodes are VMs, but the Kamaji control-plane components are pods on the management cluster, so this wording blurs the actual isolation boundary.

Suggested wording
- Those VMs and tenant-cluster nodes are themselves ordinary, unprivileged pods in the tenant namespace: governed by Pod Security Standards, denied access to the management-cluster API, and subject to the tenant network and admission policies.
+ KubeVirt VM workloads are represented by pods in the tenant namespace, while tenant-cluster control planes run as Kamaji pods on the management cluster and worker nodes run as VMs; each layer is isolated by its own RBAC, network, and virtualization boundary.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- **Hard multitenancy for tenant-controlled code.** Anything in which a tenant can run arbitrary code — VMs and tenant Kubernetes clusters — is isolated at the hardware-virtualization / dedicated-control-plane boundary, and the tenant has full access inside it. Those VMs and tenant-cluster nodes are themselves ordinary, unprivileged pods in the tenant namespace: governed by Pod Security Standards, denied access to the management-cluster API, and subject to the tenant network and admission policies.
- **Soft multitenancy for managed data services.** Databases and queues run in the management cluster but expose only a fixed, validated set of parameters and never let a tenant execute arbitrary code (unsafe options are disabled). A tenant therefore cannot compromise the management cluster by swapping a container image or enabling a dangerous setting.
- **Hard multitenancy for tenant-controlled code.** Anything in which a tenant can run arbitrary code — VMs and tenant Kubernetes clusters — is isolated at the hardware-virtualization / dedicated-control-plane boundary, and the tenant has full access inside it. KubeVirt VM workloads are represented by pods in the tenant namespace, while tenant-cluster control planes run as Kamaji pods on the management cluster and worker nodes run as VMs; each layer is isolated by its own RBAC, network, and virtualization boundary.
- **Soft multitenancy for managed data services.** Databases and queues run in the management cluster but expose only a fixed, validated set of parameters and never let a tenant execute arbitrary code (unsafe options are disabled). A tenant therefore cannot compromise the management cluster by swapping a container image or enabling a dangerous setting.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/security/threat-model.md` around lines 21 - 22, The threat-model wording
in the multitenancy section conflates KubeVirt VM workloads with the Kamaji
tenant-cluster control plane; update the text around the hard multitenancy
description to separate these boundaries clearly. In the sentence describing
tenant-controlled code, keep the VM/worker-node isolation tied to the
hardware-virtualization boundary, and describe Kamaji control-plane components
as pods running in the management cluster rather than as tenant namespace pods.
Use the existing “Hard multitenancy for tenant-controlled code” and “Soft
multitenancy for managed data services” sections to preserve the distinction.

- **Tenant isolation (API / identity layer).** A tenant cannot read, modify, or delete an *unrelated* tenant's Application CRs, workloads, or secrets, and a descendant tenant cannot reach its ancestors. Parent→child access is intentional and not a violation: an ancestor's ServiceAccounts and OIDC groups are bound into descendant namespaces (one-way), so a parent tenant can act within its children. Isolation is enforced by namespace-scoped RBAC (`packages/system/cozystack-basics/templates/clusterroles.yaml`) and that one-way hierarchical binding (`packages/apps/tenant/templates/tenant.yaml`, helper `cozy-lib.rbac.subjectsForTenantAndAccessLevel`).
- **Network isolation between unrelated tenants.** An unrelated tenant's pods cannot reach a tenant's pods over the cluster pod network: each tenant pod's Cilium egress is confined to its own tenant subtree, platform system services, and `world` (`packages/apps/tenant/templates/networkpolicy.yaml`), and Cilium requires the source's egress to allow a flow — so there is no egress path between unrelated subtrees. Isolation is enforced on the egress side; tenant-pod ingress is permissively open (see Non-goals for the caveats this creates).
- **Constrained self-service provisioning.** Tenants may instantiate only vetted `ApplicationDefinition` kinds with **server-fixed chart references** and cannot run arbitrary Helm charts, create HelmReleases, or write raw privileged manifests.
- **Hostname and tenancy integrity.** Tenants cannot claim or hijack a hostname under the platform apex that belongs to another tenant, on both dataplanes. On the Gateway API path, Gateway-listener and HTTPRoute/TLSRoute hostnames must be within the tenant's own apex (`namespace.cozystack.io/host`). On the default legacy-Ingress path, `cozystack-ingress-hostname-policy` allows a hostname within the tenant's own apex or entirely outside the platform root apex (`_cluster.root-host`) — an external custom domain, e.g. a tenant Kubernetes cluster's Proxied ingress — and denies any hostname under the platform root apex but outside the tenant's own sub-apex, plus hostless rules and `spec.defaultBackend` catch-alls. The Tenant-CR host, namespace-host-label, HTTPRoute/TLSRoute, and Ingress policies are fail-closed; the Gateway-listener policy is permissive when the host label is absent and so relies on that label being present and immutable (see Admission enforcement points). Policies: `packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml`, `route-hostname-policy.yaml`, `ingress-hostname-policy.yaml`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Don't mark the namespace-host-label policy as fail-closed.

Line 106 says this policy only blocks mutation of an already-set label; a never-labeled namespace still depends on provisioning to populate namespace.cozystack.io/host. Reword this goal so the fail-closed guarantee only covers the policies that actually reject missing labels.

Suggested wording
- The Tenant-CR host, namespace-host-label, HTTPRoute/TLSRoute, and Ingress policies are fail-closed; the Gateway-listener policy is permissive when the host label is absent and so relies on that label being present and immutable (see Admission enforcement points).
+ The Tenant-CR host, HTTPRoute/TLSRoute, and Ingress policies are fail-closed; the namespace-host-label policy is an immutability guard, and the Gateway-listener policy is permissive when the host label is absent, so both rely on provisioning to set `namespace.cozystack.io/host` before tenant traffic is admitted.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- **Hostname and tenancy integrity.** Tenants cannot claim or hijack a hostname under the platform apex that belongs to another tenant, on both dataplanes. On the Gateway API path, Gateway-listener and HTTPRoute/TLSRoute hostnames must be within the tenant's own apex (`namespace.cozystack.io/host`). On the default legacy-Ingress path, `cozystack-ingress-hostname-policy` allows a hostname within the tenant's own apex or entirely outside the platform root apex (`_cluster.root-host`) — an external custom domain, e.g. a tenant Kubernetes cluster's Proxied ingress — and denies any hostname under the platform root apex but outside the tenant's own sub-apex, plus hostless rules and `spec.defaultBackend` catch-alls. The Tenant-CR host, namespace-host-label, HTTPRoute/TLSRoute, and Ingress policies are fail-closed; the Gateway-listener policy is permissive when the host label is absent and so relies on that label being present and immutable (see Admission enforcement points). Policies: `packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml`, `route-hostname-policy.yaml`, `ingress-hostname-policy.yaml`.
- **Hostname and tenancy integrity.** Tenants cannot claim or hijack a hostname under the platform apex that belongs to another tenant, on both dataplanes. On the Gateway API path, Gateway-listener and HTTPRoute/TLSRoute hostnames must be within the tenant's own apex (`namespace.cozystack.io/host`). On the default legacy-Ingress path, `cozystack-ingress-hostname-policy` allows a hostname within the tenant's own apex or entirely outside the platform root apex (`_cluster.root-host`) — an external custom domain, e.g. a tenant Kubernetes cluster's Proxied ingress — and denies any hostname under the platform root apex but outside the tenant's own sub-apex, plus hostless rules and `spec.defaultBackend` catch-alls. The Tenant-CR host, HTTPRoute/TLSRoute, and Ingress policies are fail-closed; the namespace-host-label policy is an immutability guard, and the Gateway-listener policy is permissive when the host label is absent, so both rely on provisioning to set `namespace.cozystack.io/host` before tenant traffic is admitted. Policies: `packages/system/cozystack-basics/templates/gateway-hostname-policy.yaml`, `route-hostname-policy.yaml`, `ingress-hostname-policy.yaml`.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/security/threat-model.md` at line 69, Update the “Hostname and tenancy
integrity” section in threat-model.md so the fail-closed statement no longer
includes the namespace-host-label policy; keep the fail-closed guarantee limited
to the Tenant-CR host, HTTPRoute/TLSRoute, and Ingress policies, and describe
the namespace-host-label policy separately as only preventing mutation of an
existing label while relying on provisioning to set namespace.cozystack.io/host.
Use the existing policy names and “Admission enforcement points” language to
make the distinction explicit.

@github-actions github-actions Bot added area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review size/L This PR changes 100-499 lines, ignoring generated files labels Jul 6, 2026
@lexfrei
Aleksei Sviridkin (lexfrei) merged commit 3bf8fec into main Jul 6, 2026
20 checks passed
@lexfrei
Aleksei Sviridkin (lexfrei) deleted the docs/security-threat-model branch July 6, 2026 15:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review kind/documentation Categorizes issue or PR as related to documentation security Security-related issues and features size/L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant