feat(qdrant): publish ca.crt-only trust anchor to tenants - #3341
feat(qdrant): publish ca.crt-only trust anchor to tenants#3341Aleksei Sviridkin (lexfrei) wants to merge 2 commits into
Conversation
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughQdrant now renders a TLS-gated tenant CA projection, selects the tenant CA Secret by label, documents CA retrieval, and adds regression tests for Secret visibility and dashboard RBAC output. ChangesQdrant tenant CA projection and access controls
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant QdrantChart
participant CertManager
participant TenantProjection
participant TenantSecrets
QdrantChart->>CertManager: Render CA and leaf Certificate resources
CertManager->>TenantSecrets: Create release CA and TLS Secrets
QdrantChart->>TenantProjection: Render projection when TLS is enabled
TenantProjection->>TenantSecrets: Project ca.crt from the release CA Secret
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 |
|
Closing — opened before the branch cleared its review gate. Will reopen once the gate is green. |
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
IvanHunters
left a comment
There was a problem hiding this comment.
Reviewed hermetically. LGTM with non-blocking notes.
Verified:
- The key-bearing CA Secret is stamped
internal.cozystack.io/publish-ca-cert, while the tenant-facing cozyrds selector matchesinternal.cozystack.io/tenant-ca. The leaf cert carries no publish labels. No TLS private key reaches the tenant tier. - Neither label has an in-tree consumer yet, so this leg is inert until the CA-extraction controller (#3299) lands.
- Upgrade: adding
secretTemplateto an existing CA Certificate is an in-place Secret-metadata patch, no reissuance. Fresh install renders identically plus two currently-unused labels.
Non-blocking:
- PR body omits the mandatory Downstream Repositories checklist; the diff is qdrant-only so most likely no downstream repo is affected, but that should be stated rather than omitted.
- The
release-notereads in present tense as if shipped, which is slightly ahead of the inert-until-#3299 reality. - Consider enforcing the hold-until-#3299 intent with a draft/blocking label rather than prose.
|
All three notes addressed. The Downstream Repositories checklist is now in the body, walked file by file rather than assumed: no package added or renamed, The hold is now the On the contract itself I re-checked against the constants in #3299 rather than by eye: |
myasnikovdaniil
left a comment
There was a problem hiding this comment.
NOT LGTM — the contract itself is correct and byte-for-byte identical to #3340's, but this PR ships the same security-shaped change with the key-leak guard missing. Reviewed as a pair with #3340, which is where the gap becomes visible.
Business context: same as #3340 — tenants with TLS enabled get no CA to verify the server against, and the only existing CA Secret carries the private key; this opts qdrant into the shared key-free trust-anchor contract.
A framing note that applies to both PRs: neither actually projects ca.crt. Neither creates a Secret. Each stamps the publish-ca-cert labels on the cert-manager-authored CA Secret and adds a matchLabels selector to the ResourceDefinition; the key-free guarantee lives in #3299's controller. What these PRs own is the inverse risk — stamping a tenant-visible label onto the key-bearing Secret. That is the thing that needs guarding, and it is the thing that is guarded here only partially.
Blocker
The suite stays green when the key-bearing CA Secret is marked tenant-readable
packages/apps/qdrant/tests/certmanager_test.yaml — by absence.
Evidence. I stamped internal.cozystack.io/tenant-ca: "true" onto the CA secretTemplate in packages/apps/qdrant/templates/certmanager.yaml:23-26 — the file this PR touches — and ran both suites:
qdrant → Tests: 62 passed, 62 total ← mutation NOT caught
nats → FAIL tests/certmanager_test.yaml
- key-bearing CA Secret is never marked as a tenant-readable trust anchor
Tests: 1 failed, 54 passed
This is not a hypothetical mutation. The RD selector added by this very PR (packages/system/qdrant-rd/cozyrds/qdrant.yaml:36-37) is matchLabels: internal.cozystack.io/tenant-ca: "true", and the CA Secret carries the CA private key under tls.key (isCA: true, certmanager.yaml:31). So a one-line edit to a file in this PR's own diff would grant every tenant subject read access to the CA private key — sufficient to issue certificates for anything — with CI fully green.
The existing tests here do cover the adjacent case: leaf certificate does NOT carry publish-ca-cert labels asserts notExists for publish-ca-cert on documentIndex 3. What is missing is the assertion that neither key-bearing cert carries tenant-ca or tenantresource — which is exactly the label the RD selector keys on. #3340 asserts both, on both documents (packages/apps/nats/tests/certmanager_test.yaml:167-190).
Fix is roughly 14 lines, ported from nats — see the inline suggestion.
Action items (non-blocking)
1. Port the RD/Role guard. hack/check-nats-rd-secrets.bats (107 lines) covers a layer helm-unittest structurally cannot: neither the cozyrds nor the dashboard Role is a chart template. qdrant has no equivalent, so deleting its selector or adding a key-bearing name to resourceNames is entirely unguarded. A hack/check-qdrant-rd-secrets.bats is picked up automatically by BATS_UNIT_FILES (Makefile:162). Use ${BATS_TEST_FILENAME:-$0} as nats does — that is the idiom that avoids the set -u abort trap that has bitten other suites here.
2. Quote the release label for parity. certmanager.yaml:26 — {{ .Release.Name }} versus nats's {{ .Release.Name | quote }}. Cosmetic only, and I confirmed it cannot bite: release prefixes are qdrant- (cozyrds:13), so the value always begins with letters and can never parse as a bare numeric or boolean scalar. Worth aligning anyway since the two files are otherwise identical.
3. Settle the docs question across the pair. This PR's body argues user-facing documentation belongs with the controller that creates it (#3299) rather than with a per-app label leg — a defensible position. But #3340, same author and same contract, adds 19 lines of tenant-facing README. Both cannot be right for one contract; worth picking one and applying it to both engines.
Contract consistency — the wire format is genuinely identical
To be clear about what is not wrong here, since the blocker is about guard strength rather than the contract:
| Aspect | #3340 nats | #3341 qdrant | Same |
|---|---|---|---|
| Source label | publish-ca-cert: "true" |
identical | yes |
| Release label value | {{ .Release.Name | quote }} |
unquoted | cosmetic |
| Stamped on | {{ .Release.Name }}-ca CA cert secretTemplate |
identical | yes |
| Leaf cert | unlabelled | unlabelled | yes |
| RD selector | matchLabels: tenant-ca: "true" |
identical | yes |
| RD name grants | nats-{{ .name }}-credentials |
qdrant-{{ .name }}-apikey |
engine-specific, correct |
| TLS-disabled render | fully gated | identical | yes |
| Key-leak negative tests | yes | no | blocker |
| RD/Role bats guard | 107 lines | none | action item 1 |
| Tenant-facing README | 19 lines | none | action item 3 |
All three wire constants match #3299 exactly (SourceLabel reconciler.go:229, SourceReleaseLabel :262, TenantCALabel :268), so tenants do get a consistent contract across both engines. The divergence is entirely in guard strength and documentation.
Root cause of the asymmetry is visible in the diff sizes and is worth naming, because it is not carelessness: #3340 was advertised at +34/-0 and is now +189/-0, having picked up a hardening pass under review. #3341 has the identical 6-line functional core at its originally-advertised +43/-0 — it simply never received that pass. Porting it across is what closes the gap.
Merge order
After #3299, and after the blocker above. Both labels have zero producers and consumers on main (git grep), so this is inert until the controller lands. #3299 is reviewed and ready; #3340 is ready as-is.
Dismissed
- Private-key leak via templating (a
rangeover.data, a whole-secret copy) — neither PR creates a Secret. The projection is built by #3299'sprojectionData, which returns exactlymap[string][]byte{"ca.crt": chain}rebuilt from parsed x509 blocks. Structurally single-key. - Empty or invalid Secret when TLS is disabled — the template is fully gated at
templates/certmanager.yaml:6; nothing renders. matchLabelsunsupported in RDspec.secrets.include— supported, and already used by bucket-rd, harbor-rd, kubernetes-rd, rabbitmq-rd and vm-instance-rd.- Missing helm-unittest
test:target — present; 62/62 pass locally.
| asserts: | ||
| - notExists: | ||
| path: spec.secretTemplate.labels["internal.cozystack.io/publish-ca-cert"] | ||
| documentIndex: 3 |
There was a problem hiding this comment.
This is where the key-leak guard is missing. The suite asserts the leaf carries no publish-ca-cert, but never asserts that either key-bearing cert lacks tenant-ca / tenantresource — which is the label the RD selector added by this PR actually keys on.
Mutation-proven: stamping internal.cozystack.io/tenant-ca: "true" onto the CA secretTemplate in templates/certmanager.yaml leaves this suite at 62 passed, 62 total, while the equivalent mutation on nats fails with "key-bearing CA Secret is never marked as a tenant-readable trust anchor".
Porting the two nats cases (packages/apps/nats/tests/certmanager_test.yaml:167-190) closes it:
| documentIndex: 3 | |
| documentIndex: 3 | |
| - it: key-bearing CA Secret is never marked as a tenant-readable trust anchor | |
| release: | |
| name: test-qdrant | |
| namespace: tenant-test | |
| set: | |
| tls: | |
| enabled: true | |
| _cluster: | |
| cluster-domain: cozy.local | |
| asserts: | |
| - notExists: | |
| path: spec.secretTemplate.labels["internal.cozystack.io/tenant-ca"] | |
| documentIndex: 1 | |
| - notExists: | |
| path: spec.secretTemplate.labels["internal.cozystack.io/tenantresource"] | |
| documentIndex: 1 | |
| - notExists: | |
| path: spec.secretTemplate.labels["internal.cozystack.io/tenant-ca"] | |
| documentIndex: 3 | |
| - notExists: | |
| path: spec.secretTemplate.labels["internal.cozystack.io/tenantresource"] | |
| documentIndex: 3 |
(documentIndex 1 is the CA cert, 3 the leaf — matching the indices already used in this file.)
There was a problem hiding this comment.
The blocker is closed, though not by the patch you suggested. The branch moved to the TenantProjection sentinel while your review was open, so certmanager.yaml is out of the diff entirely and there is no secretTemplate left on either Certificate to stamp.
The two key-leak cases are in certmanager_test.yaml regardless, selected by documentSelector on spec.secretName rather than by documentIndex, so reordering the template cannot slide a labelled Certificate out from under them.
Checked by mutation, not by reading. Stamping internal.cozystack.io/tenant-ca on the CA secretTemplate turns the suite red, so does stamping it on the leaf, and so does renaming the CA secretName — the selector fails loudly rather than matching nothing and passing green. Twelve mutations across the sentinel, the dashboard Role and the ResourceDefinition, no survivors.
4e6744e to
54d78a1
Compare
|
myasnikovdaniil Blocker closed. The branch changed under your review: the contract is the Key-leak cases are in On action item 1 you were half right. helm-unittest cannot see the cozyrds file, but Both are pinned whole instead of probed for known-bad names, since the variants that matter add no name to notice. A rule granting secrets with no Item 2 is moot, the sentinel rework dropped Two things out of scope here. The Role gap is not qdrant-specific, the same one-line edit works on #3340. And the qdrant e2e fixture runs plaintext, so the controller to tenantsecrets path is not exercised in CI; postgres has a |
472cc26 to
96ab7f9
Compare
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
96ab7f9 to
b2e1b21
Compare
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
2abd73c to
8aec254
Compare
A qdrant release with TLS on mints a self-signed CA whose Secret also holds the CA private key, so tenants had no way to verify the server without being handed a signing key. The chart now declares a TenantProjection sentinel naming that Secret. The CA-extraction controller reads it and publishes a key-free copy, ca.crt only, as <release>.tenant-ca; the qdrant ResourceDefinition selects that projection by label so it surfaces 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. Unlike engines whose operator mints a CA unconditionally, qdrant only has one while TLS is enabled, and an ungated sentinel would sit Ready=False/SourceNotFound on every plaintext release. Tests pin the two invariants that fail silently: the sentinel carries no ownerReferences, so the lineage walk falls through to the Flux release label, and neither key-bearing Certificate stamps a tenant-visible label on its Secret. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
8aec254 to
abf38ff
Compare
Two objects decide which Secrets a tenant may read: the dashboard Role with its binding, and the ResourceDefinition. Adding qdrant-<name>-ca to either hands every tenant subject the CA private key — enough to issue certificates for anything — and nothing failed when that happened. Each layer is pinned whole rather than checked for known-bad names, because the ways this breaks add no name to notice: a rule granting secrets with no resourceNames restricts nothing, and a label selector can reach a key-bearing Secret while every name stays correct. The files carry the reasoning, including what the pinning does not settle. The ResourceDefinition is checked against the rendered chart rather than the file on disk, since the template globs a directory and a second file there ships the same way. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
abf38ff to
6322b49
Compare
|
myasnikovdaniil Re-review, please. The branch moved to the Blocker: The merge order you set is satisfied: #3299 was superseded by #3407, which merged on 23 July, so the hold came off. Checked by mutation rather than by reading. Eleven mutations across both Certificates, the sentinel, the ResourceDefinition and the Role, no survivors: stamping either label on either key-bearing Certificate, dropping the label selector, granting the CA Secret by name, an include that restricts nothing, a second selector on a label cert-manager stamps on its own Secrets, an |
IvanHunters
left a comment
There was a problem hiding this comment.
LGTM.
Verified the trust anchor is ca.crt-only: the TenantProjection (<release>-ca, kind CACert, sourceKey ca.crt) matches the merged postgres reference template byte-for-byte, and the cacert controller refuses private-key material. Canonical name <release>.tenant-ca and the internal.cozystack.io/tenant-ca: "true" selector line up with the ADR. Dashboard RBAC coverage via helm-unittest over the tri-state TLS matrix is solid.
Nothing blocking.
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
TenantProjectionsentinel naming its cert-manager CA Secret. The CA-extraction controller reads that declaration and publishes a key-free copy,ca.crtand 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/SourceNotFoundon 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 noresourceNamesis 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.
dashboard-resourcemap_test.yaml, so the dashboard Role pin belongs there too. Tracked in postgres: the tenant-visible Secret surface has no guard while postgres-rd selects the trust anchor by label #3701 along with postgres, which already carries the label selector with no guard behind it.hack/e2e-chainsaw/qdrant/qdrant.yamlsetsexternal: false), so nothing in CI walks the path from the sentinel totenantsecrets. postgres has averify-tenant-ca-projectionstep to copy.hack/check-qdrant-rd-secrets.batsis engine-agnostic apart from two literals, the chart path and the expected Secret set. The third copy of it is the point to parameterise overpackages/system/*-rd/cozyrds/instead of copying again.Downstream repositories
Walked the trigger map in
docs/agents/contributing.mdagainst 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'sREADME.md, and this PR does change it.qdrantis in the app list hardcoded in the websiteMakefile, 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 againstvalues.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 thecozyrdsfile 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.
Release note
Summary by CodeRabbit
New Features
Documentation
Tests