Files
cozystack/docs/security/threat-model.md
Aleksei Sviridkin 800bb0369d feat(redis)!: refuse external access without TLS at admission
A Redis release with external: true and TLS off publishes a plaintext
endpoint outside the cluster, and nothing stopped a tenant from asking
for one. The chart never infers TLS from external, because enabling it
moves Redis to a TLS-only listener, so external: true without an
explicit tls.enabled: true is exactly that case. A
ValidatingAdmissionPolicy on the Redis kind now refuses it with a
message naming the release and a remedy that works.

TLS is fixed when a Redis instance is created: the operator's
RedisFailover schema refuses a change, but the Redis API accepts it.
A spec's tls.enabled is therefore a request, not the instance's state,
and cannot prove on UPDATE that an instance has TLS; trusting it would
let a tenant flip it, expose the instance, and remove it again. So
external access can be enabled only at creation, where tls.enabled
does decide the instance, and any UPDATE turning it on is refused.

The RedisFailover schema does not exist before the first reconcile, so
a release admitted as external with TLS could have TLS dropped before
the instance is created. tls.enabled is therefore frozen while
external access stays on. Withdrawing external access in the same edit
lifts the freeze, which leaves a tenant a way to unblock an instance
whose TLS was flipped earlier, and the callers trusted by the tenant
host policy are exempt so an administrator can repair one in place.
Other edits to an instance that is already external pass, so releases
exposed in plaintext before the policy keep working.

Postgres is left out on purpose. CNPG pins ssl=on, and its default
pg_hba accepts plaintext from any client that asks, whatever the
chart's tls.enabled says; that field only decides whether the external
hostname goes into the certificate. A policy keyed on it would close
nothing.

The decisions are tested by compiling the shipped template with the
apiserver's own CEL compiler, since a structural chart test cannot
show what the policy admits.

BREAKING CHANGE: creating a Redis with external: true and without
tls.enabled: true, which applied before, is now refused, including a
GitOps re-apply of such a manifest into a fresh cluster. Turning
external on for an existing Redis is refused as well, so exposing an
instance means creating a new one with TLS, and tls.enabled can no
longer be changed on an instance with external access.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-10-03 01:42:57 +03:00

28 KiB

Cozystack Threat Model

This document is a design-level threat model for Cozystack. It describes the actors in the system, the actions that cross trust boundaries, and the security guarantees the platform intends (and does not intend) to provide. It is organized around trust boundaries, following the CNCF TAG-Security "Actors / Actions" model, and is intended both as internal security documentation and as the basis for the project's CNCF self-assessment.

Scope: this is a design-level analysis of the Cozystack platform (the management cluster and its multi-tenancy model). It is not a code audit and does not enumerate implementation-specific bugs or misconfigurations. All architectural claims cite source files in this repository so the model can be re-verified as the code evolves.

Background

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/).

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.

Multitenancy model

Cozystack combines hard and soft multitenancy:

  • 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.

Trust boundaries

The actors below are decomposed by trust boundary, not by component: two components are distinct actors only when compromise of one does not automatically compromise the other. Cozystack has five trust tiers.

  • T0 — Root of trust. The platform / cluster administrator (system:masters), who owns the management cluster, the platform GitOps repository, Talos machine configuration, and the Keycloak realm. Nothing sits above T0. A compromised T0 is explicitly out of the adversary model (see Non-goals and SECURITY.md).
  • T1 — Platform control plane (cluster-admin-equivalent). The GitOps delivery layer (Flux source-, helm-, and kustomize-controllers plus flux-shard-operator), the cozystack-api aggregated API server's own ServiceAccount, and the system operators (the Cozystack controllers, plus Kamaji, KubeVirt, cert-manager, external-secrets, keycloak-operator, Piraeus/LINSTOR, Cluster API providers, and others). These components hold broad, cluster-admin-equivalent privileges. They enforce tenant boundaries but are not themselves sandboxed from one another or from the host.
  • T2 — Tenant boundary. Tenant administrators and tenant users, scoped to a tenant namespace subtree. This is the primary isolation unit. Tenancy is hierarchical: a parent tenant's ServiceAccounts are bound into every descendant namespace, but child ServiceAccounts are not bound upward, so ancestor→descendant trust is one-way.
  • T3 — Workload boundary. Tenant workloads, managed-service pods, KubeVirt virtual machines, and tenant Kubernetes clusters (each a dedicated Kamaji-hosted control plane with Talos worker nodes running as VMs). These run tenant-controlled code with no cluster-API rights beyond their own ServiceAccounts.
  • T4 — Untrusted. External end users and the unauthenticated internet. They reach published ingress/gateway hostnames and the UI front doors (when OIDC is enabled, the Keycloak login and dashboard oauth2-proxy; by default, OIDC-disabled, the dashboard token-proxy) — and, when a tenant opts into external exposure (e.g. external: true on apps such as VPN or MariaDB), the raw LoadBalancer L4 endpoints those apps publish, which sit outside the Gateway/OIDC path.

Actors

Actor Trust tier Privileges Isolation mechanism (separated from neighbours by)
Platform / cluster admin (system:masters) T0 Full control of the management cluster; owns GitOps, Talos config, Keycloak realm; may set/change a tenant's spec.host. Root of trust; nothing isolates it (trusted by design).
GitOps / Flux delivery layer T1 Reconciles HelmReleases/Kustomizations into arbitrary privileged manifests; effectively cluster-admin. Tenants cannot create HelmReleases/Kustomizations (RBAC); not tenant-network-reachable.
cozystack-api aggregated API server (ServiceAccount) T1 CRUD HelmReleases, Secrets, and CiliumNetworkPolicies cluster-wide; reads Namespaces/RBAC/quotas/nodes. Acts as the policy-enforcing gateway that converts tenant CRs to HelmReleases. A distinct, network-exposed attack surface from Flux; constrains tenants via fixed chart ref + name/quota/host validation + admission.
System operators / controllers T1 Domain-scoped cluster privileges (provision control planes, mint certs/secrets, program CNI). Distinct ServiceAccounts, but all cluster-admin-adjacent; isolated from tenants only by RBAC and network policy.
Tenant administrator (OIDC groups <tenant>-admin, <tenant>-super-admin) T2 CRUD on apps.cozystack.io application kinds and sdn.cozystack.io/securitygroups within its namespace subtree; read on pods/services/ingress plus delete on a fixed set (pods/services/endpoints/events); no create/update of arbitrary core resources. (The ClusterRole also lists persistentvolumes, but that grant is inert under the namespace-scoped RoleBinding, since PVs are cluster-scoped.) The <tenant>-super-admin group holds the apps.cozystack.io/* wildcard (and so can create child Tenants); the <tenant>-admin group has an enumerated allowlist that excludes some kinds (e.g. tenants, monitoring, etcd, ingress, external-dns, bootbox). Namespace-scoped RoleBindings + admission policies + quota; no access to HelmReleases, arbitrary core writes, or other tenants.
Tenant user (OIDC groups <tenant>-view, <tenant>-use) T2 Read-only view of Application CRs, pods, and services, plus operational verbs only (VM console/vnc/portforward, start/stop/restart). Cannot create Application CRs. Strictly dominated by tenant-admin; cannot escalate to create/delete, so it is a distinct actor.
Parent tenant vs child (nested) tenant T2 Parent ServiceAccounts are bound into descendant namespaces; child ServiceAccounts are not bound upward. Quota is hierarchically capped. Ancestor→descendant only; compromising a parent compromises its children, not the reverse.
Tenant workloads / managed services T3 Run tenant-controlled code; no cluster-API rights beyond their ServiceAccount. Namespace-scoped RBAC/identity isolate their API objects. On the network, Cilium egress confines each tenant pod to its own tenant subtree, platform system services, and world (out-of-cluster), so unrelated tenants cannot reach each other over the pod network — enforced on the egress side (tenant-pod ingress is open to cluster; see Non-goals for residual caveats).
Tenant Kubernetes clusters T3 Own kube-apiserver (a Kamaji hosted control plane running as pods on the management cluster) and Talos worker nodes running as hardware-virtualized KubeVirt VMs. Worker nodes carry a VM/kernel boundary (do not share a kernel with the host or other tenants). The Kamaji control-plane pods run on the management cluster and are isolated by namespace/RBAC/NetworkPolicy, not by virtualization.
External end user / unauthenticated internet T4 Reaches published gateway hostnames, the UI front door (Keycloak + dashboard oauth2-proxy when OIDC is enabled; a token-proxy by default), and any raw LoadBalancer L4 endpoints a tenant opts to expose (external: true). OIDC authentication (when enabled) and hostname admission policies for the gateway path; Cilium world-entity rules. Tenant-published LoadBalancer services are reachable directly and are not behind the hostname policies or OIDC.

Actions

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.
Create an application (e.g. apps.cozystack.io/Postgres) Tenant admin (T2) → cozystack-api (T1) → Flux (T1) → workload (T3) Application CR spec (Helm values only) RBAC allows apps.cozystack.io/* create in the tenant namespace (delegated to kube-apiserver via system:auth-delegator); cozystack-api validates the name, rejects _-prefixed keys, and runs the admission chain; the emitted HelmRelease uses a server-fixed chart reference. Managed service provisioned from a vetted chart; the tenant never touched a HelmRelease or chart.
Create a tenant / nested tenant (apps.cozystack.io/Tenant) Tenant super-admin (T2) → cozystack-api → Flux → new namespace (T2) Tenant CR (spec.host, quotas, feature toggles) Namespace-name checks and a best-effort hierarchical quota cap (a declaration-time snapshot: a child's declared quota is checked against the parent's remaining budget; concurrent writes can briefly overshoot, with the tenant-quota controller as the runtime backstop); the cozystack-tenant-host-policy admission policy allows only trusted callers to set/change spec.host. New tenant namespace with RBAC bindings, NetworkPolicies, Keycloak groups, quota, and gateway inheritance.
Publish a hostname (Gateway / HTTPRoute / TLSRoute / GRPCRoute / Ingress) Tenant Application spec (T2) → Flux / controllers (T1) → core kube-apiserver Rendered Gateway listener, Route, and Ingress hostnames; namespace namespace.cozystack.io/host label Tenants have no RBAC to write Gateways, Routes, Ingresses, or namespace labels; these are rendered by Flux/controllers from the tenant's Application spec. The hostname VAPs then validate the rendered hostnames against the namespace host label as defense-in-depth (so neither a tenant-authored spec nor a buggy chart escapes the apex). The apex itself is platform-set: cozystack-tenant-host-policy restricts Tenant.spec.host to trusted callers and the namespace-host-label policy keeps the label immutable. The Gateway path requires strict in-apex hostnames; the default legacy-Ingress path (cozystack-ingress-hostname-policy) additionally allows external custom domains (outside the platform root apex) while denying platform-apex claims outside the tenant's own sub-apex. Route/namespace/Ingress policies are fail-closed; the Gateway-listener policy is fail-open when the label is absent (see Admission enforcement points). A tenant cannot publish or hijack another tenant's platform-apex hostname, provided its namespace carries the platform-set host label.
Create a network policy (sdn.cozystack.io/SecurityGroup) Tenant admin (T2) → cozystack-api SecurityGroup spec RBAC on securitygroups; cozystack-api projects the spec into a CiliumNetworkPolicy under its own ServiceAccount; tenants cannot write raw cilium.io objects. CNI policy programmed on the tenant's behalf.
Delete a platform-critical object Any actor → core kube-apiserver DELETE request The cozystack-no-delete-guardrail policy denies DELETE on objects labelled platform.cozystack.io/no-delete=true (e.g. the tenant-root namespace and its HelmRelease). It is operational guidance, not adversarial defense: an actor able to update the object can remove the label and then delete (tenants have no access to these roots; only T0/T1 do). Accidental teardown of platform roots is blocked.
Tenant-to-tenant / tenant-to-platform traffic Workload (T3) → workload (T3) / system East-west packets Cilium enforces policy at both endpoints, so a flow needs the source's egress and the destination's ingress to allow it. Every tenant pod's egress is confined to its own tenant subtree (namespaces labelled tenant.cozystack.io/<tenant>), platform system services (DNS, apiserver, ingress, dashboard, Keycloak, CDI), and world (out-of-cluster). Ingress is permissively open to world and cluster. Cross-tenant pod-to-pod traffic is denied — an unrelated tenant has no egress path to another tenant's pod, even though that pod's ingress would accept it. Within a tenant subtree, parent→descendant traffic is allowed (descendants carry ancestor namespace labels); child→parent traffic is denied except for specific ancestor services (vminsert, etcd, ingress). See Non-goals for the ingress-side and LoadBalancer caveats.
Distribute platform config to tenants Platform (T1) → tenant namespace (T2) cozystack-values Secret (cluster/namespace host, OIDC toggles, gateway references) Rendered by the platform charts and merged into every tenant HelmRelease as valuesFrom. Tenants inherit platform config (host apex, etc.) but cannot forge it.

Goals

Cozystack intends to provide the following security guarantees:

  • 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/GRPCRoute 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/GRPCRoute, 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.
  • Hierarchical resource-quota enforcement (best-effort). At declaration time a child tenant's declared quota is checked against its parent's remaining budget (pkg/registry/apps/application/quota.go). The check is a non-transactional admission snapshot: concurrent tenant writes can briefly overshoot the parent budget, and the tenant-quota controller is the runtime backstop.
  • Strong isolation for tenant Kubernetes and VM workloads via Talos worker nodes running as KubeVirt VMs (hardware virtualization). The tenant control plane itself is a Kamaji hosted control plane running as pods on the management cluster, isolated by namespace/RBAC/network rather than by virtualization.
  • Central, token-based authentication when OIDC is enabled (opt-in; authentication.oidc.enabled is off by default): Keycloak/OIDC with group-to-role mapping into kube-apiserver RBAC and UI access mediated by oauth2-proxy. In the default OIDC-disabled mode the dashboard is fronted by a token-proxy instead.
  • Defense in depth at admission. ValidatingAdmissionPolicy is enforced at two points — the core kube-apiserver for core and Gateway objects, and inside cozystack-api for aggregated apps.cozystack.io kinds — and most tenancy policies are fail-closed (the Gateway-listener policy is the documented exception; see Admission enforcement points).
  • Accidental-deletion protection for platform-critical roots via the no-delete guardrail policy. This is operational guidance, not adversarial defense: a privileged (T0/T1) actor can remove the label and delete.
  • Trustworthy platform PKI: the aggregated-API serving certificate and tenant TLS are issued and rotated by cert-manager, with CA injection into the APIService.

Non-goals

The following are explicitly out of scope for the platform's security guarantees:

  • Defending against a compromised cluster administrator, node root, or infrastructure administrator. system:masters and the cozystack/Flux service accounts are trusted callers by design (SECURITY.md).
  • Treating the T1 platform control plane as a sandbox. cozystack-api's own ServiceAccount, Flux, and the system operators hold cluster-admin-equivalent power; they enforce tenant boundaries but are not isolated from each other or from the host.
  • Upstream-only vulnerabilities that Cozystack packaging and defaults do not introduce or worsen.
  • Ingress-side isolation of tenant pods. Tenant-pod ingress is open to the world and cluster entities, so a tenant pod is reachable by any in-cluster peer whose own egress permits it (platform components) and by a workload not governed by Cozystack's tenant egress policies (e.g. a privileged or system pod). Cross-tenant isolation is enforced on the egress side, not on tenant-pod ingress.
  • Blocking cross-tenant reachability through externally-exposed services. If a tenant publishes a LoadBalancer with an external IP, another tenant can reach it via world egress.
  • Network isolation between tenants on a hosted cluster without Cilium. On the isp-hosted and isp-hosted-slim variants the CNI belongs to the host, and the tenant chart creates its Cilium policies only if the host serves the Cilium policy API (packages/apps/tenant/templates/_helpers.tpl, helper tenant.ciliumPolicies). Without it, tenants get no network isolation from Cozystack, and isolating them is up to the host.
  • Noisy-neighbour and side-channel guarantees for co-located tenant pods beyond namespace scoping (and, for tenant Kubernetes/VMs, virtualization). Co-located pods are not claimed to be side-channel isolated.
  • Acting as an egress firewall. Tenant workloads are allowed egress to world by default (packages/apps/tenant/templates/networkpolicy.yaml).
  • Supporting pre-release builds and end-of-life versions (SECURITY.md).

Primary trust-boundary invariant

The single most load-bearing isolation control is the combination of two facts:

  1. The tenant ClusterRoles (packages/system/cozystack-basics/templates/clusterroles.yaml) grant no helm.toolkit.fluxcd.io access and no create/update of arbitrary core resources — only CRUD on apps.cozystack.io application kinds (super-admin holds the * wildcard; admin an enumerated allowlist) and sdn.cozystack.io/securitygroups, read access to pods/services/ingresses, and (for tenant-admins) delete on a fixed set of core objects (pods/services/endpoints/events; the persistentvolumes entry in the ClusterRole is inert under the namespace-scoped RoleBinding). Tenant isolation is enforced by the absence of privileged grants in RBAC, not by an admission policy that whitelists apps.cozystack.io/* writes.
  2. The Application-to-HelmRelease conversion (pkg/registry/apps/application/rest.go) takes the chart reference, release-name prefix, and valuesFrom from server-side configuration; the tenant controls only the Helm values (app.Spec).

If a future ApplicationDefinition, RBAC change, or API change ever let a tenant influence the chart reference, valuesFrom, or write HelmReleases directly, the T2→T1 isolation would collapse. Changes touching tenant RBAC or the Application REST layer should be reviewed against this invariant.

Admission enforcement points

Cozystack has two admission enforcement points, and the distinction matters when reasoning about which policies protect which resources:

  • The core kube-apiserver evaluates ValidatingAdmissionPolicy for resources it serves directly — Namespaces, Gateways, HTTPRoutes/TLSRoutes/GRPCRoutes, Ingresses, and the no-delete guardrail.
  • cozystack-api itself evaluates the admission chain (validating webhooks and ValidatingAdmissionPolicy) for aggregated apps.cozystack.io resources, because those are served by the aggregated API server and never reach the core apiserver's admission chain. The cozystack-tenant-host-policy on apps.cozystack.io/tenants is therefore enforced inside cozystack-api. So is cozystack-database-external-tls-policy, which refuses a redises object that asks for a plaintext endpoint outside the cluster: external: true without tls.enabled: true on create, any update that turns external access on, since an existing instance's spec does not prove its TLS state, and any change of tls.enabled by a tenant while external access stays on (the callers trusted by cozystack-tenant-host-policy are exempt, to repair such an instance), since before the first reconcile nothing else stops TLS being dropped. A release already external and plaintext before the policy existed stays editable.

Two hostname-policy details are worth stating precisely. The HTTPRoute/TLSRoute/GRPCRoute policies (route-hostname-policy.yaml) and the legacy-Ingress policy (ingress-hostname-policy.yaml) are fail-closed: a route or Ingress in a tenant namespace missing namespace.cozystack.io/host is denied, so an absent label cannot open those layers (the route policies also deny a route that declares no hostnames, since such a route inherits the hostnames of the listener it attaches to rather than matching nothing; the Ingress policy likewise denies hostless rules and spec.defaultBackend, and permits external custom domains outside the platform root apex). The namespace host-label policy is an immutability guard — it stops an untrusted caller from scrubbing or changing an already-set label, but it does not itself reject a namespace that never carried one. The Gateway-listener policy (gateway-hostname-policy.yaml) applies the same subdomain rule but its CEL short-circuits to allow when the host label is empty, so it is permissive (fail-open) for a tenant namespace that lacks the label. In normal operation the platform sets that label at tenant creation and the namespace host-label policy keeps it immutable, closing the gap in practice — but the Gateway-side guarantee is conditional on the label being present, not unconditional.

Authentication and authorization for aggregated resources are delegated to the core kube-apiserver via system:auth-delegator (TokenReview / SubjectAccessReview), so RBAC decisions remain centralized even though admission runs in cozystack-api.

References

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.
  • 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, database-external-tls-policy.yaml, packages/core/platform/templates/deletion-protection.yaml.
  • Tenant provisioning and isolation: packages/apps/tenant/templates/ (tenant.yaml, namespace.yaml, networkpolicy.yaml), pkg/registry/apps/application/quota.go.
  • UI OIDC front door (oauth2-proxy): packages/system/dashboard/templates/gatekeeper.yaml, packages/system/linstor-gui/templates/gatekeeper.yaml.
  • Reporting and supported versions: SECURITY.md.