Skip to content

feat(cozystack-controller): extract application CA into a key-free tenant Secret - #3407

Merged
Aleksei Sviridkin (lexfrei) merged 3 commits into
mainfrom
feat/ca-extraction-controller-v2
Jul 23, 2026
Merged

Aleksei Sviridkin (lexfrei) merged 3 commits into
mainfrom
feat/ca-extraction-controller-v2

Conversation

@lexfrei

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

Copy link
Copy Markdown
Contributor

What this PR does

Adds an engine-agnostic controller that publishes a key-free <release>.tenant-ca Secret (only ca.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 namespaced TenantProjection sentinel (group internal.cozystack.io) naming the CA Secret to lift; the controller watches the sentinel, extracts ca.crt, and writes the projection owner-referenced to the sentinel, so garbage collection is native. The projection carries the internal.cozystack.io/tenant-ca label 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 companion ValidatingAdmissionPolicy that 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.json changes, 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

feat(cozystack-controller): publish a key-free per-application CA trust anchor (`<release>.tenant-ca`) that tenants read to verify managed-application TLS, declared through a new `TenantProjection` CRD. Postgres is the first consumer.

Summary by CodeRabbit

  • New Features

    • Added tenant-facing CA certificate projections for PostgreSQL deployments.
    • Tenant secrets now provide only the CA certificate, excluding private keys.
    • Added support for configuring and monitoring tenant CA projections.
  • Documentation

    • Updated PostgreSQL TLS instructions to retrieve certificates through tenant secrets.
  • Bug Fixes

    • Improved protection against invalid certificates, secret collisions, and accidental exposure of private key material.
    • Added validation to ensure projected certificates remain synchronized with their sources.

…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]>
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@github-actions github-actions Bot added area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review kind/feature Categorizes issue or PR as related to a new feature size/XXL This PR changes 1000+ lines, ignoring generated files labels Jul 21, 2026
@coderabbitai

coderabbitai Bot commented Jul 21, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a TenantProjection internal API and reconciler that publishes certificate-only tenant CA Secrets. The controller gains dedicated metadata caching, scheme and RBAC wiring, while PostgreSQL templates, documentation, unit tests, and end-to-end checks adopt the projection.

Changes

Tenant CA projection

Layer / File(s) Summary
TenantProjection API contract
api/internalapi/v1alpha1/*, packages/system/cozystack-controller/definitions/*
Defines the TenantProjection API, generated deepcopy methods, validation schema, status conditions, and CRD metadata.
CA projection reconciliation
internal/controller/cacert/*
Reconciles projection sentinels, validates certificate-only output, manages ownership and collisions, tracks selector changes, and watches source resources.
Controller wiring and permissions
cmd/cozystack-controller/*, hack/update-codegen.sh, packages/system/cozystack-controller/templates/*, packages/system/cozystack-controller/tests/*
Registers the API, configures the dedicated CA metadata cache, includes internal APIs in code generation, and adds TenantProjection RBAC coverage.
PostgreSQL projection integration
packages/apps/postgres/*, packages/system/postgres-rd/cozyrds/postgres.yaml, hack/e2e-chainsaw/postgres/chainsaw-test.yaml
Adds the PostgreSQL projection manifest and tests, selects labeled tenant CA Secrets, updates retrieval documentation, and verifies certificate-only exposure end to end.

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
Loading

Possibly related issues

Possibly related PRs

  • cozystack/cozystack#3299 — Implements the closely related CA extraction reconciler logic extended here with the TenantProjection API.
  • cozystack/cozystack#3341 — Relies on the internal.cozystack.io/tenant-ca label and projected CA Secret contract added here.

Suggested labels: kind/api-change, area/api, area/tenant, area/testing

Suggested reviewers: kvaps

🚥 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 clearly matches the main change: extracting an application CA into a key-free tenant Secret.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ca-extraction-controller-v2

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.

@dosubot dosubot Bot added the area/platform Issues or PRs related to platform infrastructure (bundle, flux, talos, installer) label Jul 21, 2026

@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: 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

📥 Commits

Reviewing files that changed from the base of the PR and between aef9e20 and 99f04d9.

📒 Files selected for processing (16)
  • api/internalapi/v1alpha1/groupversion_info.go
  • api/internalapi/v1alpha1/tenantprojection_types.go
  • api/internalapi/v1alpha1/zz_generated.deepcopy.go
  • cmd/cozystack-controller/main.go
  • cmd/cozystack-controller/main_test.go
  • hack/e2e-chainsaw/postgres/chainsaw-test.yaml
  • hack/update-codegen.sh
  • internal/controller/cacert/reconciler.go
  • internal/controller/cacert/reconciler_test.go
  • packages/apps/postgres/README.md
  • packages/apps/postgres/templates/tenant-projection.yaml
  • packages/apps/postgres/tests/tenant_projection_test.yaml
  • packages/system/cozystack-controller/definitions/internal.cozystack.io_tenantprojections.yaml
  • packages/system/cozystack-controller/templates/rbac.yaml
  • packages/system/cozystack-controller/tests/rbac_test.yaml
  • packages/system/postgres-rd/cozyrds/postgres.yaml

Comment on lines +162 to +249
- 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

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.

🩺 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:


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.

@lllamnyp Timofei Larkin (lllamnyp) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approved, but I strongly feel that api/internalapi should be renamed to api/internal.

@lexfrei
Aleksei Sviridkin (lexfrei) merged commit 6ecb36f into main Jul 23, 2026
44 of 46 checks passed
@lexfrei
Aleksei Sviridkin (lexfrei) deleted the feat/ca-extraction-controller-v2 branch July 23, 2026 11:14
Aleksei Sviridkin (lexfrei) added a commit that referenced this pull request Jul 24, 2026
…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 -->
myasnikovdaniil added a commit that referenced this pull request Aug 17, 2026
…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 -->
Aleksei Sviridkin (lexfrei) added a commit that referenced this pull request Sep 1, 2026
## 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 -->
Aleksei Sviridkin (lexfrei) added a commit to cozystack/community that referenced this pull request Sep 3, 2026
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]>
Aleksei Sviridkin (lexfrei) added a commit to cozystack/community that referenced this pull request Sep 4, 2026
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]>
Andrei Kvapil (kvaps) pushed a commit that referenced this pull request Sep 7, 2026
## 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 -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/platform Issues or PRs related to platform infrastructure (bundle, flux, talos, installer) area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review kind/feature Categorizes issue or PR as related to a new feature size/XXL This PR changes 1000+ lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants