feat(cozystack-controller): extract application CA into a key-free tenant Secret - #3407
Conversation
…nant Secret Every catalog engine issues a per-release CA, but the object holding ca.crt usually holds a private key beside it (cert-manager's tls.key, CloudNativePG's ca.key), so granting a tenant read on it hands over key material too. Add a controller that publishes the trust anchor as one canonical, key-free Secret per release, "<release>.tenant-ca", holding only ca.crt. A chart declares the anchor by rendering a namespaced TenantProjection sentinel (internal.cozystack.io/v1alpha1) that names the source Secret and key. The controller rebuilds ca.crt from the parsed certificate DER rather than copying the source, refuses any value carrying private key material, and owns the projection by owner reference so a foreign Secret at the same name is never overwritten. No tenant RBAC grants any verb on internal.cozystack.io; a ValidatingAdmissionPolicy pins writes to helm-controller as defence in depth. The controller does not grant tenant read access — the lineage webhook does, from the ApplicationDefinition's spec.secrets. This side stamps the engine-agnostic internal.cozystack.io/tenant-ca label and forces a re-admission when those selectors change. Assisted-By: Claude <[email protected]> Signed-off-by: Aleksei Sviridkin <[email protected]>
CloudNativePG stores its self-signed CA in <release>-ca alongside the private key and offers no way to label it, so a tenant cannot be handed the trust anchor without also being handed the key. Render a TenantProjection that lifts ca.crt alone into the key-free <release>.tenant-ca Secret, and select that label from the postgres tenant-resource definition so the projection is served through the tenantsecrets API. Document the new retrieval path in the chart README. Assisted-By: Claude <[email protected]> Signed-off-by: Aleksei Sviridkin <[email protected]>
Assert the live controller publishes <release>.tenant-ca carrying only ca.crt, that its certificate matches CloudNativePG's CA while the private key never crosses over, and that the anchor is reachable through the tenantsecrets API while the key-bearing source Secret is not. Assisted-By: Claude <[email protected]> Signed-off-by: Aleksei Sviridkin <[email protected]>
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
📝 WalkthroughWalkthroughAdds a ChangesTenant CA projection
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant TenantProjection
participant CAReconciler
participant SourceSecret
participant TenantSecretAPI
TenantProjection->>CAReconciler: Declare CACert source
CAReconciler->>SourceSecret: Read ca.crt
CAReconciler->>CAReconciler: Validate certificate-only payload
CAReconciler->>TenantSecretAPI: Publish labeled tenant-ca Secret
TenantSecretAPI-->>TenantProjection: Serve certificate trust anchor
Possibly related issues
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 `@hack/e2e-chainsaw/postgres/chainsaw-test.yaml`:
- Around line 162-249: Add timeout: 2m to the script step containing the
validation commands, alongside its existing content and check fields, so the
multiple kubectl, openssl, and tenantsecret operations can complete reliably.
🪄 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: 8116482a-ca2a-4e19-8a48-71aaefffe80c
📒 Files selected for processing (16)
api/internalapi/v1alpha1/groupversion_info.goapi/internalapi/v1alpha1/tenantprojection_types.goapi/internalapi/v1alpha1/zz_generated.deepcopy.gocmd/cozystack-controller/main.gocmd/cozystack-controller/main_test.gohack/e2e-chainsaw/postgres/chainsaw-test.yamlhack/update-codegen.shinternal/controller/cacert/reconciler.gointernal/controller/cacert/reconciler_test.gopackages/apps/postgres/README.mdpackages/apps/postgres/templates/tenant-projection.yamlpackages/apps/postgres/tests/tenant_projection_test.yamlpackages/system/cozystack-controller/definitions/internal.cozystack.io_tenantprojections.yamlpackages/system/cozystack-controller/templates/rbac.yamlpackages/system/cozystack-controller/tests/rbac_test.yamlpackages/system/postgres-rd/cozyrds/postgres.yaml
| - script: | ||
| content: | | ||
| set -eu | ||
|
|
||
| # Exactly one key, and it is ca.crt. Any second key is a leak in the | ||
| # making: the source this was lifted from carries a private key. | ||
| keys=$(kubectl -n "$NAMESPACE" get secret postgres-test.tenant-ca \ | ||
| -o go-template='{{range $k, $v := .data}}{{$k}}{{"\n"}}{{end}}' | sort | tr '\n' ' ') | ||
| if [ "$keys" != "ca.crt " ]; then | ||
| echo "projection must hold exactly ca.crt, holds: $keys" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| # It lifted the right certificate: the projection's ca.crt is CNPG's | ||
| # ca.crt. This is what pins "{{ .release }}-ca" to reality — if CNPG ever | ||
| # renames its CA Secret, the controller finds nothing and this fails. | ||
| # | ||
| # Compare the parsed certificates, not the raw PEM. The controller | ||
| # re-encodes ca.crt from the DER it parsed (certificateChainPEM), so a PEM | ||
| # header line or a column-width change at the source would redden a | ||
| # byte-for-byte comparison of a correct projection. The certificate | ||
| # fingerprint is identity-stable across any re-encoding. | ||
| src=$(kubectl -n "$NAMESPACE" get secret postgres-test-ca -o jsonpath='{.data.ca\.crt}' | base64 -d) | ||
| proj=$(kubectl -n "$NAMESPACE" get secret postgres-test.tenant-ca -o jsonpath='{.data.ca\.crt}' | base64 -d) | ||
| if [ -z "$src" ]; then | ||
| echo "CNPG's postgres-test-ca has no ca.crt — the declared source name or key is wrong" >&2 | ||
| exit 1 | ||
| fi | ||
| src_fp=$(echo "$src" | openssl x509 -noout -fingerprint -sha256) | ||
| proj_fp=$(echo "$proj" | openssl x509 -noout -fingerprint -sha256) | ||
| if [ "$src_fp" != "$proj_fp" ]; then | ||
| echo "projection does not carry CNPG's CA certificate" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| # And the point of the whole exercise: the source carries the CA | ||
| # PRIVATE KEY, the projection must not. Asserting the source really | ||
| # does hold ca.key keeps this honest — if CNPG ever stopped storing it | ||
| # there, this check would otherwise pass while proving nothing. | ||
| if ! kubectl -n "$NAMESPACE" get secret postgres-test-ca \ | ||
| -o go-template='{{if .data}}{{range $k, $v := .data}}{{$k}}{{"\n"}}{{end}}{{end}}' | grep -qx 'ca.key'; then | ||
| echo "CNPG's CA Secret no longer carries ca.key — this check has stopped proving anything" >&2 | ||
| exit 1 | ||
| fi | ||
| if kubectl -n "$NAMESPACE" get secret postgres-test.tenant-ca \ | ||
| -o go-template='{{if .data}}{{range $k, $v := .data}}{{$k}}{{"\n"}}{{end}}{{end}}' | grep -qx 'ca.key'; then | ||
| echo "projection leaked the CA private key" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| # Belt and braces on the bytes themselves: no PEM key header of any | ||
| # flavour survived into the tenant-readable object. | ||
| if kubectl -n "$NAMESPACE" get secret postgres-test.tenant-ca \ | ||
| -o jsonpath='{.data.ca\.crt}' | base64 -d | grep -qi 'BEGIN .*PRIVATE KEY'; then | ||
| echo "projection carries PEM private key material" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| # The delivery path the README actually tells a tenant to use, run as | ||
| # the command the README prints. Everything above reads the raw Secret, | ||
| # which only an admin can do — it proves the projection's SHAPE and | ||
| # nothing about whether a tenant can reach it. | ||
| # | ||
| # core.cozystack.io/tenantsecrets serves only objects labelled | ||
| # tenantresource=true, and that label is the lineage webhook's verdict, | ||
| # computed from this app's ApplicationDefinition selectors. So this | ||
| # query 404s unless the whole chain held: the definition selects | ||
| # tenant-ca, the webhook admitted the projection, and the verdict came | ||
| # out positive. It is the one assertion that exercises the registry | ||
| # rather than inspecting its input. | ||
| anchor=$(kubectl -n "$NAMESPACE" get tenantsecret postgres-test.tenant-ca \ | ||
| -o jsonpath='{.data.ca\.crt}' | base64 -d) | ||
| if ! echo "$anchor" | grep -q 'BEGIN CERTIFICATE'; then | ||
| echo "the trust anchor is not served through core.cozystack.io/tenantsecrets" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| # And the negative that gives the positive its meaning: the registry | ||
| # must NOT serve CNPG's key-bearing CA. It carries no tenant-ca label, | ||
| # so the definition does not select it and the webhook marks it | ||
| # tenantresource=false. If this ever starts resolving, the label path | ||
| # has stopped gating on the platform's verdict. | ||
| if kubectl -n "$NAMESPACE" get tenantsecret postgres-test-ca >/dev/null 2>&1; then | ||
| echo "CNPG's key-bearing CA is being served as a tenant secret" >&2 | ||
| exit 1 | ||
| fi | ||
| check: | ||
| ($error == null): true |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the relevant section with line numbers.
sed -n '130,260p' hack/e2e-chainsaw/postgres/chainsaw-test.yaml
# Find other timeout usages in this file for comparison.
rg -n "timeout:" hack/e2e-chainsaw/postgres/chainsaw-test.yaml
# Look for Chainsaw docs or config references in the repo.
rg -n "exec-timeout|script timeout|timeout:" -g '*.md' -g '*.yaml' -g '*.yml' -g '*.json' .Repository: cozystack/cozystack
Length of output: 2026
🌐 Web query:
Chainsaw default exec timeout script timeout 5s documentation
💡 Result:
In Chainsaw, the default timeout for the exec operation (which includes script operations) is indeed 5 seconds [1][2][3]. You can manage this timeout at various levels of precedence: 1. Command-Line Flags: You can override the default using the --exec-timeout flag when running the chainsaw test command [1][2][3]. This takes the highest precedence [4]. 2. Configuration File: You can set a global exec timeout in a Chainsaw configuration file under the timeouts section [1][2]: apiVersion: chainsaw.kyverno.io/v1alpha2 kind: Configuration spec: timeouts: exec: 45s 3. Test/Operation Level: You can also specify timeouts at the individual test, step, or operation level within your test definitions to override global or command-line settings for specific tasks [5]. For script operations specifically, the exec timeout applies to the execution of the command or script [1][6]. If your script requires more than 5 seconds, you should increase this limit using one of the methods above [1][2].
Citations:
- 1: https://kyverno.github.io/chainsaw/main/configuration/options/timeouts/
- 2: https://kyverno.github.io/chainsaw/0.2.3/configuration/options/timeouts/
- 3: https://kyverno.github.io/chainsaw/0.2.3/reference/commands/chainsaw_test/
- 4: https://kyverno.github.io/chainsaw/0.2.15/configuration/flags/
- 5: https://kyverno.github.io/chainsaw/0.2.3/quick-start/timeouts/
- 6: https://kyverno.github.io/chainsaw/main/operations/script/
Add timeout: 2m to this script step. Chainsaw’s default exec/script timeout is 5s, and this step does several kubectl, openssl, and tenantsecret calls; make the timeout explicit to avoid CI flakes.
🤖 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 `@hack/e2e-chainsaw/postgres/chainsaw-test.yaml` around lines 162 - 249, Add
timeout: 2m to the script step containing the validation commands, alongside its
existing content and check fields, so the multiple kubectl, openssl, and
tenantsecret operations can complete reliably.
Timofei Larkin (lllamnyp)
left a comment
There was a problem hiding this comment.
Approved, but I strongly feel that api/internalapi should be renamed to api/internal.
…ame (#3408) ## What this PR does Ships `cozy-lib.tls.caCertSecret`, a render-time helper that emits a key-free trust-anchor Secret carrying only `ca.crt`. It converges the object name on `<release>.tenant-ca`, requires the whole value to be certificate blocks so a stray private-key header or trailing bytes are rejected, and coerces numeric scalars before the guard runs so a numeric value cannot slip past. No chart calls it yet. It is the render-time producer half of the trust-anchor contract; the controller half is #3407, now merged. The doc-comment on the helper shows the one-line call a chart uses, and says outright there is no caller yet, so nobody reads it as wired when it is not. Split out from #3299. Comments trimmed on review. ```release-note fix(cozy-lib): the CA trust-anchor helper emits the canonical `<release>.tenant-ca` name and rejects non-certificate input. ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Updated generated TLS CA Secret naming to use the `<release>.tenant-ca` convention. - Added/ensured a tenant CA label is set alongside the existing tenant resource label. - Continued support for custom labels (caller labels still applied). - **Bug Fixes** - Strengthened CA certificate PEM validation to be fail-closed: requires non-empty, complete `BEGIN/END CERTIFICATE` blocks only. - Rejects private-key material and other malformed or improperly structured PEM input, including unexpected extra content. - **Tests** - Updated TLS CA certificate fixtures and assertions to cover additional negative cases and refined error expectations. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…3463) ## What this PR does Two documentation defects that both landed with the CA trust-anchor work in #3407. The Postgres README told a tenant to fetch `<release>.tenant-ca`. The release name carries the `postgres-` prefix the application definition applies, so the object is `postgres-<name>.tenant-ca`, and the postgres chainsaw test shows exactly that for a resource named `test`. A tenant following the README got `NotFound`. I confirmed the real name on a live cluster while converting another engine onto the same contract. The second is a comment in the controller and one in its RBAC chart, both promising a companion `ValidatingAdmissionPolicy` that pins sentinel writes to helm-controller. No such policy ships. It was proposed in #3410 and rejected: authorizing by `request.userInfo.username` duplicates RBAC in a more brittle form, and the RBAC boundary already holds because no tenant role has any verb on `internal.cozystack.io`. The comments now say that instead of pointing at a guard that does not exist. ### Downstream repositories Documentation only, no API or chart behaviour changes. `cozystack/website` regenerates the Postgres reference page from this README on the next stable tag, so no manual follow-up is needed there. - [x] No downstream repository is affected by this change - [ ] [cozystack/website](https://github.com/cozystack/website) - follow-up: ### Release note ```release-note docs(postgres): the tenant CA trust anchor is `postgres-<name>.tenant-ca`, not `<release>.tenant-ca`. ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Documentation** * Updated PostgreSQL TLS setup instructions with the correct tenant CA Secret name and retrieval command. * Clarified that tenant-accessible Secrets contain only the CA certificate, while the original Secret contains the private key. * Clarified CA sentinel access controls, including tenant isolation and helm-controller-managed behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## What this PR does Converges qdrant onto the key-free CA trust-anchor contract (#2814), the same way #3340 does it for nats. The chart declares a `TenantProjection` sentinel naming its cert-manager CA Secret. The CA-extraction controller reads that declaration and publishes a key-free copy, `ca.crt` and nothing else, as `<release>.tenant-ca`. The qdrant ResourceDefinition selects that projection by label so it shows up in the tenant's secret list. No key-bearing Secret gains a tenant-visible label. The sentinel is gated on the chart's TLS condition. Engines whose operator mints a CA unconditionally do not need that, but qdrant only has a CA while TLS is on, and an ungated sentinel would sit `Ready=False`/`SourceNotFound` on every plaintext release. This was held while the CA-extraction controller was still open. That controller landed in #3407, so the sentinel is honoured as soon as this merges and the hold is gone. Tests pin the invariants that otherwise fail silently. The sentinel carries no `ownerReferences`, so the lineage walk falls through to the Flux release label. Neither key-bearing Certificate stamps a tenant-visible label on its Secret. The two objects that decide which Secrets a tenant may actually read, the dashboard Role with its binding and the ResourceDefinition, are pinned whole rather than checked for known-bad names: a rule granting secrets with no `resourceNames` is unrestricted rather than narrow, and a label selector can reach a key-bearing Secret while every name in the file stays correct, so neither adds a name for a probe to notice. Part of #2814. ### Follow-ups What this PR does not do, and where each belongs instead. - nats converges the same contract in #3340 and has no `dashboard-resourcemap_test.yaml`, so the dashboard Role pin belongs there too. Tracked in #3701 along with postgres, which already carries the label selector with no guard behind it. - The qdrant e2e fixture runs plaintext (`hack/e2e-chainsaw/qdrant/qdrant.yaml` sets `external: false`), so nothing in CI walks the path from the sentinel to `tenantsecrets`. postgres has a `verify-tenant-ca-projection` step to copy. - `hack/check-qdrant-rd-secrets.bats` is engine-agnostic apart from two literals, the chart path and the expected Secret set. The third copy of it is the point to parameterise over `packages/system/*-rd/cozyrds/` instead of copying again. ### Downstream repositories Walked the trigger map in `docs/agents/contributing.md` against the diff, file by file. Nothing needs a manual follow-up, but not for the reason a values-only walk would give: the website reference page for an app is regenerated from that package's `README.md`, and this PR does change it. `qdrant` is in the app list hardcoded in the website `Makefile`, so the docs bot picks the new section up on the next stable tag and there is nothing to open by hand. The Terraform provider's schema is hand-written against `values.schema.json`, which is untouched, and the provider offers no typed access to the new projection, the same position it holds for every other Secret it does not model. No package is added, renamed or removed, and the `cozyrds` file is edited in place rather than renamed, so the ccp dependency-contract anchor still resolves. User-facing documentation for consuming the trust anchor ships with the chart README, matching #3340. - [x] No downstream repository is affected by this change - [ ] [cozystack/website](https://github.com/cozystack/website) - follow-up: - [ ] [cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack) - follow-up: - [ ] [cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack) - follow-up: - [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up: - [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up: - [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) - follow-up: - [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) - follow-up: - [ ] [cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server) - follow-up: - [ ] [cozystack/external-apps-example](https://github.com/cozystack/external-apps-example) - follow-up: - [ ] [cozystack/examples](https://github.com/cozystack/examples) - follow-up: ### Release note ```release-note feat(qdrant): expose the CA certificate to tenants as a key-free `<release>.tenant-ca` Secret ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Tenant environments can securely access the Qdrant TLS CA certificate without receiving the private key. * TLS CA access is automatically enabled when TLS or external access is configured. * **Documentation** * Expanded TLS guidance explains certificate validation and provides commands for retrieving the tenant CA certificate. * **Tests** * Added coverage for TLS projections, secret visibility, dashboard resources, and secure fail-closed secret exposure. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
The repository convention asks for a decision record when an accepted design changes course, and this revision changes it twice. 0001 records that the trust anchor is declared by a namespaced TenantProjection sentinel and published as <release>.tenant-ca. It was decided in the rework of cozystack/cozystack#3299 that merged as cozystack/cozystack#3407, with #3408 and #3411 completing it. The record states the argument against the label mechanism in its own voice, lists the rejected alternatives with where each was argued, and names the two assumptions the dotted name rests on together with the mitigation: a Secret already sitting at the canonical name is refused, never adopted. 0002 records that mongodb sources the anchor from its leaf Secret rather than from the operator's CA Secret. The three reasons are verified against the pinned PSMDB operator v1.22.0: the self-signed fallback never writes the CA Secret, and a CA rotation merges old and new CA into the leaf's ca.crt while the CA Secret carries only the current one. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <[email protected]>
The repository convention asks for a decision record when an accepted design changes course, and this revision changes it twice. 0001 records that the trust anchor is declared by a namespaced TenantProjection sentinel and published as <release>.tenant-ca. It was decided in the rework of cozystack/cozystack#3299 that merged as cozystack/cozystack#3407, with #3408 and #3411 completing it. The record states the argument against the label mechanism in its own voice, lists the rejected alternatives with where each was argued, and names the two assumptions the dotted name rests on together with the mitigation: a Secret already sitting at the canonical name is refused, never adopted. 0002 records that mongodb sources the anchor from its leaf Secret rather than from the operator's CA Secret. The three reasons are verified against the pinned PSMDB operator v1.22.0: the self-signed fallback never writes the CA Secret, and a CA rotation merges old and new CA into the leaf's ca.crt while the CA Secret carries only the current one. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <[email protected]>
## What this PR does Converges qdrant onto the key-free CA trust-anchor contract (#2814), the same way #3340 does it for nats. The chart declares a `TenantProjection` sentinel naming its cert-manager CA Secret. The CA-extraction controller reads that declaration and publishes a key-free copy, `ca.crt` and nothing else, as `<release>.tenant-ca`. The qdrant ResourceDefinition selects that projection by label so it shows up in the tenant's secret list. No key-bearing Secret gains a tenant-visible label. The sentinel is gated on the chart's TLS condition. Engines whose operator mints a CA unconditionally do not need that, but qdrant only has a CA while TLS is on, and an ungated sentinel would sit `Ready=False`/`SourceNotFound` on every plaintext release. This was held while the CA-extraction controller was still open. That controller landed in #3407, so the sentinel is honoured as soon as this merges and the hold is gone. Tests pin the invariants that otherwise fail silently. The sentinel carries no `ownerReferences`, so the lineage walk falls through to the Flux release label. Neither key-bearing Certificate stamps a tenant-visible label on its Secret. The two objects that decide which Secrets a tenant may actually read, the dashboard Role with its binding and the ResourceDefinition, are pinned whole rather than checked for known-bad names: a rule granting secrets with no `resourceNames` is unrestricted rather than narrow, and a label selector can reach a key-bearing Secret while every name in the file stays correct, so neither adds a name for a probe to notice. Part of #2814. ### Follow-ups What this PR does not do, and where each belongs instead. - nats converges the same contract in #3340 and has no `dashboard-resourcemap_test.yaml`, so the dashboard Role pin belongs there too. Tracked in #3701 along with postgres, which already carries the label selector with no guard behind it. - The qdrant e2e fixture runs plaintext (`hack/e2e-chainsaw/qdrant/qdrant.yaml` sets `external: false`), so nothing in CI walks the path from the sentinel to `tenantsecrets`. postgres has a `verify-tenant-ca-projection` step to copy. - `hack/check-qdrant-rd-secrets.bats` is engine-agnostic apart from two literals, the chart path and the expected Secret set. The third copy of it is the point to parameterise over `packages/system/*-rd/cozyrds/` instead of copying again. ### Downstream repositories Walked the trigger map in `docs/agents/contributing.md` against the diff, file by file. Nothing needs a manual follow-up, but not for the reason a values-only walk would give: the website reference page for an app is regenerated from that package's `README.md`, and this PR does change it. `qdrant` is in the app list hardcoded in the website `Makefile`, so the docs bot picks the new section up on the next stable tag and there is nothing to open by hand. The Terraform provider's schema is hand-written against `values.schema.json`, which is untouched, and the provider offers no typed access to the new projection, the same position it holds for every other Secret it does not model. No package is added, renamed or removed, and the `cozyrds` file is edited in place rather than renamed, so the ccp dependency-contract anchor still resolves. User-facing documentation for consuming the trust anchor ships with the chart README, matching #3340. - [x] No downstream repository is affected by this change - [ ] [cozystack/website](https://github.com/cozystack/website) - follow-up: - [ ] [cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack) - follow-up: - [ ] [cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack) - follow-up: - [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up: - [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up: - [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) - follow-up: - [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) - follow-up: - [ ] [cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server) - follow-up: - [ ] [cozystack/external-apps-example](https://github.com/cozystack/external-apps-example) - follow-up: - [ ] [cozystack/examples](https://github.com/cozystack/examples) - follow-up: ### Release note ```release-note feat(qdrant): expose the CA certificate to tenants as a key-free `<release>.tenant-ca` Secret ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Tenant environments can securely access the Qdrant TLS CA certificate without receiving the private key. * TLS CA access is automatically enabled when TLS or external access is configured. * **Documentation** * Expanded TLS guidance explains certificate validation and provides commands for retrieving the tenant CA certificate. * **Tests** * Added coverage for TLS projections, secret visibility, dashboard resources, and secure fail-closed secret exposure. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
What this PR does
Adds an engine-agnostic controller that publishes a key-free
<release>.tenant-caSecret (onlyca.crt) for a managed application, so a tenant can verify the application's TLS without ever seeing a private key. The source is declared, not guessed: a chart renders a namespacedTenantProjectionsentinel (groupinternal.cozystack.io) naming the CA Secret to lift; the controller watches the sentinel, extractsca.crt, and writes the projection owner-referenced to the sentinel, so garbage collection is native. The projection carries theinternal.cozystack.io/tenant-calabel the lineage webhook turns into tenant visibility. Postgres is the first consumer.The trust boundary is RBAC: no tenant role grants any verb on
internal.cozystack.io, so a tenant cannot forge a sentinel. A companionValidatingAdmissionPolicythat pins the writer to helm-controller ships separately as defence in depth.This is the controller-and-CRD slice, split out from #3299 for focused review. It is the only piece required for the feature to work; the render-time helper, the writer policy, the capability-gate, and the release-prefix validation land as their own PRs.
Downstream repositories
Walked the trigger map against the diff. This adds an internal CRD and controller and a sentinel to the Postgres chart; no app is added or renamed and no
values.schema.jsonchanges, so the typed provider and Ansible are unaffected. The one to confirm is the Postgres README edit — if the website mirrors it, it needs a refresh.Release note
Summary by CodeRabbit
New Features
Documentation
Bug Fixes