mirror of
https://github.com/cozystack/cozystack.git
synced 2026-11-09 05:25:14 +09:00
## What this PR does Part of #2814. Part of #2811. This is the foundational ("keystone") piece of WS3: a canonical, reusable trust-anchor object that lets a tenant obtain `ca.crt` to verify a per-app TLS endpoint **without** read access to any object that also carries a private key. It unblocks the per-app TLS series (WS4/WS5) by giving every chart one agreed, key-free delivery shape instead of each chart inventing its own. ### The problem (verified against current `main`) Per-app TLS issues a per-release, self-signed CA. The only objects that hold `ca.crt` today also hold private keys: - the cert-manager CA Secret `<release>-ca` carries the CA private key (`tls.key`) — full trust-chain compromise if leaked (`packages/apps/nats/templates/certmanager.yaml`, the `isCA` Certificate); - the cert-manager leaf Secret `<release>-tls` carries the server private key (`tls.key`) plus `ca.crt` (same file, the leaf Certificate); - CNPG surfaces `ca.crt` only inside its user-credentials Secret (`packages/apps/postgres/templates/db.yaml`, the operator-managed TLS block). So any RBAC path that hands a tenant `ca.crt` by granting read on one of those Secrets also hands over a private key. On `main` today the cert-manager apps grant tenants no access to those Secrets at all, so a tenant currently cannot obtain `ca.crt` to verify the server — the gap this object closes safely. ### Design decision: key-free Opaque object delivered through `tenantsecrets`, not trust-manager I evaluated the two candidate delivery mechanisms. **Rejected — trust-manager.** A trust-manager `Bundle` reads its sources only from trust-manager's single configured "trust namespace"; its `namespaceSelector` governs only where the bundle is *distributed to*, not where it is *sourced from*. A per-release self-signed CA lives in the tenant namespace, so trust-manager cannot use it as a source, and projecting one shared CA to all tenants would defeat per-release isolation. I re-verified this against current upstream docs (cert-manager.io trust-manager) and the open per-namespace-trust-bundle request (cert-manager/trust-manager#131, which is target-side only). The prior analysis still holds. **Chosen — a `ca.crt`-only Opaque Secret surfaced through the existing tenant-secret API.** This generalizes the reference pattern from the redis work (#2729), where the operator publishes a CA-only Opaque Secret and RBAC is scoped to it instead of to the CA-private-key Secret. The new `cozy-lib.tls.caCertSecret` helper renders that object once, identically, for every chart: - `type: Opaque`, a single `ca.crt` data key, no `tls.key`/`tls.crt`; - label `internal.cozystack.io/tenantresource: "true"`; - fails closed on empty input and refuses any PEM that contains private key material. ### How the RBAC wiring works (no new roles needed) The label is the entire mechanism, and it routes through RBAC that already exists: - `pkg/registry/core/tenantsecret/rest.go` surfaces, under the virtual resource `core.cozystack.io/tenantsecrets`, exactly the namespace Secrets bearing `internal.cozystack.io/tenantresource=true` (`buildTenantSelector`, lines 199-213; label constants `pkg/apis/core/v1alpha1/tenantresource_types.go:3-4`; full `Data` copied through by `secretToTenant`, lines 84-103). - `packages/system/cozystack-basics/templates/clusterroles.yaml` grants `get/list/watch` on `core.cozystack.io/tenantsecrets` to tenant ServiceAccounts via `cozy:tenant:base` (lines 44-47) and to `use`/`admin`/`super-admin` subjects via `cozy:tenant:use:base` (lines 179-180). None of this touches raw core `secrets`, so attaching the label to a key-free object exposes the trust anchor and nothing else. - `internal/backupcontroller/credentials_projector.go:128-133` relies on the same rule in reverse — it deliberately *omits* the label so its key-bearing projection is not promoted to a `TenantSecret`. This helper is the positive counterpart: a key-free object that is safe to promote. `view` deliberately does not receive the trust anchor through this path: `cozy:tenant:view:base` grants only `core.cozystack.io/options` (lines 120-126), and `tenantsecrets` also includes credential Secrets, so granting it to a read-only role would leak passwords. The trust anchor reaches `use` and above plus tenant ServiceAccounts — the same access level at which connection credentials are already surfaced (the per-release dashboard Role binds at `use`, e.g. `packages/apps/postgres/templates/dashboard-resourcemap.yaml` via `cozy-lib.rbac.subjectsForTenantAndAccessLevel`). A chart that must show `ca.crt` at `view` level in the dashboard can additionally name the key-free Secret in its own per-release Role; that is a per-app detail, not a base-role change. ### Why a library helper and not a controller here Populating `ca.crt` stays the responsibility of whatever owns the PKI — the app operator (as the redis fork does) or a cert-manager chain resolved at the chart level. The helper is intentionally pure and value-driven so it renders deterministically and is the single shape every per-app PR converges on. A shared key-stripping projection controller is a reasonable future generalization but is out of scope for this foundational object. ### Convergence plan for the per-app TLS series (follow-up, not this PR) I did not modify any of the open per-app branches. Each should adopt the canonical object as follows: - redis #2729 — already publishes an operator-side CA-only Opaque Secret and already drops the grant on the CA-private-key Secret; converging means emitting it through the helper / carrying the `tenantresource` label so delivery matches the rest of the catalog. - nats, qdrant (merged, cert-manager) — emit the canonical object via the helper and withhold `<release>-ca` / `<release>-tls` from tenants. - mariadb #2680, opensearch #2682, rabbitmq #2683, mongodb #2692 — same: emit the canonical object, never grant a Secret that contains `tls.key`. ### Tests `helm-unittest` in the cozy-lib test chart (`make -C packages/tests/cozy-lib-tests test`) covers: the rendered object is `Opaque` with `ca.crt` and no `tls.key`/`tls.crt`; the `tenantresource` label is present; caller labels/annotations merge while the security label always wins; the helper fails closed on empty `caCert`; and it refuses a PEM that carries private key material. ### Release note ```release-note NONE ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * Added a Helm helper to generate CA-only Kubernetes TLS trust-anchor secrets, including input validation (rejects private key material, requires a properly formatted certificate, and ensures required metadata). * **Tests** * Introduced a dedicated test suite covering successful rendering, metadata and content assertions, tenant label enforcement, label/annotation merging, and detailed failure cases for invalid inputs and incorrect arguments. <!-- end of auto-generated comment: release notes by coderabbit.ai -->