Skip to content

feat(mongodb): add TLS support via PSMDB operator-managed certificates - #2692

Merged
Aleksei Sviridkin (lexfrei) merged 19 commits into
mainfrom
feat/tls-mongodb
Sep 1, 2026
Merged

Aleksei Sviridkin (lexfrei) merged 19 commits into
mainfrom
feat/tls-mongodb

Conversation

@Arsolitt

@Arsolitt Arsolitt (Arsolitt) commented May 20, 2026 •

Copy link
Copy Markdown
Contributor

What this PR does

Enables operator-managed TLS for MongoDB and stops handing tenants any Secret that carries private key material.

  • Percona Server for MongoDB serves TLS unconditionally (mode: preferTLS), so this PR drops the misleading tls.enabled toggle rather than pretending the chart controls it.
  • The tenant no longer receives the operator's CA Secret (<release>-ca-cert, which holds ca.key) or the leaf (<release>-ssl). Both were previously reachable, and either one is a private key in tenant hands, the CA one is a full trust-chain compromise.
  • The chart declares a TenantProjection sentinel naming its CA source Secret (<release>-ssl, the operator-managed leaf, which carries ca.crt), and the ResourceDefinition selects the resulting key-free <release>.tenant-ca projection by the engine-agnostic internal.cozystack.io/tenant-ca label. The CA-extraction controller reads the sentinel and publishes the projection holding only ca.crt. The tenant gets what it needs to verify the server and nothing else.

Why the include also carries an exclude list

The trust-anchor selector matches by label with no name constraint, so any Secret in the namespace that acquired that label would be promoted with its data copied verbatim. The enumerable dangerous set is therefore excluded explicitly: the CA, both leaves, the rotation snapshots (-ssl-old, -ssl-internal-old, same server keys as the leaves, and unlike them they carry no owner reference, so an interrupted rotation can leave them behind after the release is gone), the system-user passwords, the at-rest encryption key, the keyfile, the object-storage credentials, and both forms of the operator's internal Secret.

That backstop is bounded, and the comment says so: matching is by exact name with no globs, so a Secret a future operator version introduces, or a transient object an issuer creates mid-renewal, stays in scope of the label-only include. The per-user Secrets cannot be enumerated at all, since their names carry the username. The primary defence remains that the controller stamps only its own projection; this closes the enumerable set, not the whole surface.

Release note

feat(mongodb): stop exposing TLS private key material to tenants and publish the CA as a key-free trust anchor

@coderabbitai

coderabbitai Bot commented May 20, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The MongoDB chart now configures Percona Operator-managed TLS with preferTLS, conditional external split horizons, and crVersion: 1.22.0. Secret projections, dashboard access, documentation, and Helm, Bats, and Chainsaw validation are updated accordingly.

Changes

MongoDB TLS and Secret Handling

Layer / File(s) Summary
MongoDB TLS and external host configuration
packages/apps/mongodb/templates/mongodb.yaml, packages/apps/mongodb/README.md
Configures operator-managed TLS, conditional per-replica splitHorizons, and documents TLS lifecycle, verification, sharding, and upgrade behavior.
Dashboard access and Helm validation
packages/apps/mongodb/templates/dashboard-resourcemap.yaml, packages/apps/mongodb/tests/*, packages/apps/mongodb/Makefile
Documents TLS secret access, tests TLS and split-horizon rendering across replica and sharding cases, and adds the Helm test target.
MongoDB RD secret projection rules
packages/system/mongodb-rd/cozyrds/mongodb.yaml, hack/check-mongodb-rd-secrets.bats
Projects the CA trust anchor, excludes key-bearing Secrets, preserves credentials, and validates selector and CRD-schema conditions.
MongoDB TLS end-to-end validation
hack/e2e-chainsaw/mongodb/chainsaw-test.yaml
Checks CA contents and ownership, dashboard Role restrictions, tenant-resource labels, and old TLS Secret cleanup.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

Suggested labels: area/testing

Suggested reviewers: myasnikovdaniil, androndo, sircthulhu, kvaps, lllamnyp

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: adding MongoDB TLS support through PSMDB operator-managed certificates.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/tls-mongodb

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.

@github-actions github-actions Bot added size/L This PR changes 100-499 lines, ignoring generated files 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 labels May 20, 2026
@Arsolitt Arsolitt (Arsolitt) self-assigned this May 20, 2026
@Arsolitt Arsolitt (Arsolitt) added do-not-merge/hold Indicates that a PR should not merge because someone has issued /hold area/database Issues or PRs related to managed databases (postgres, mariadb, redis, etcd, kafka, clickhouse) and removed area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review labels May 20, 2026
@github-actions github-actions Bot added the area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review label May 20, 2026

@lexfrei Aleksei Sviridkin (lexfrei) 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.

NOT LGTM — re-review on head 0d729e52. Two of my three prior blockers are resolved (the <release>-ca name mismatch is fixed, and the misleading toggle is gone — the latest commit drops the tri-state field entirely and makes operator-managed TLS unconditionally on, which is the honest design). One security blocker remains.

Business context: PSMDB always serves TLS once spec.secrets.ssl is set; the operator owns the cert-manager chain. This PR makes TLS unconditionally on (spec.tls.mode: preferTLS) and folds external hostnames into the leaf cert SANs via replsets[].splitHorizons (gated on PSMDB ≥ 1.22.0). There is no longer a toggle pretending to enable/disable TLS — good.

Blockers

B1: operator-managed TLS Secrets (carrying private keys) are exposed to tenant use access
Files: packages/apps/mongodb/templates/dashboard-resourcemap.yaml:21-22, packages/system/mongodb-rd/cozyrds/mongodb.yaml:40-41
Issue: both files grant tenant use subjects read on <release>-ca-cert and <release>-ssl. Both are cert-manager Certificate objects, so cert-manager writes each as a kubernetes.io/tls Secret containing tls.key: <release>-ssl is the leaf server/client private key; <release>-ca-cert is the CA private key (isCA: true).
Evidence: PSMDB issues both as Certificates; the dashboard Role grant at dashboard-resourcemap.yaml:21-22 bound to "use" (:37), and the cozyrds secrets.include at mongodb.yaml:40-41. RBAC is per-Secret and cannot scope to ca.crt alone.
Impact: any tenant principal at use can read the leaf private key (impersonate the server, MITM clients, decrypt non-PFS sessions) and the CA private key (mint any cert the cluster trusts — full TLS trust compromise). To verify a TLS connection a client needs only ca.crt, never tls.key.
Fix: remove <release>-ssl and <release>-ca-cert from both the dashboard Role and the cozyrds secrets.include; expose a ca.crt-only trust anchor instead (a private-key-stripped Secret/ConfigMap, e.g. via trust-manager). Update the dashboard-resourcemap test fixture (tests/dashboard-resourcemap_test.yaml:111-138), which currently asserts test-mongodb-ssl presence and locks in the insecure state. This is the same defect as the sibling mariadb/rabbitmq TLS PRs — a single shared fix would close it across the series.

Non-blocking follow-ups

  1. README documents that TLS is always on and that the secrets exist, but shows no tenant-facing CA-retrieval / verifying-connection example (tlsCAFile / mongodb://...tls=true). Once B1 lands on a ca.crt-only object, add a short retrieval + connection example.
  2. preferTLS (not requireTLS) means plaintext is still accepted on the wire; the README flags this as a deliberate interim state pending backup-CronJob TLS support, with NetworkPolicy as the stopgap. Acceptable, but the posture is "TLS available," not "TLS enforced."
  3. mongos/sharding SAN coverage is documented as possibly incomplete and untested on-cluster. Fine to defer; flagged.

@lexfrei Aleksei Sviridkin (lexfrei) removed the area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review label May 25, 2026
@Arsolitt

Copy link
Copy Markdown
Contributor Author

Parking in draft — blocked on PSMDB operator 1.22+ upgrade.

After verifying against PSMDB v1.21.1 source (pkg/apis/psmdb/v1/psmdb_defaults.go:86-127 and psmdb_types.go:1415-1430), the tls.enabled: false branch in this PR is a no-op in operator-managed mode: when the chart omits spec.tls and spec.secrets.ssl, the operator's Default() method sets spec.tls.mode = preferTLS and spec.secrets.ssl = <release>-ssl, and TLSEnabled() returns true. The server still terminates TLS in preferTLS mode using the operator's auto-generated cert chain — only the chart-side wiring differs from the "TLS on" branch.

The real value-add this PR was meant to deliver — external-DNS SAN injection — requires PSMDB 1.22+ for the splitHorizons field on replset members. Without that, the chart produces no observable improvement over operator defaults.

Outstanding work before this PR can land:

  • Bump PSMDB operator to 1.22+
  • Wire spec.replsets[].splitHorizons for external SAN injection on each pod's LoadBalancer endpoint
  • Fix the dashboard RBAC and cozyrds reference: the chart advertises <release>-ca, but PSMDB writes the CA Secret at <release>-ca-cert
  • Decide off-branch semantics in operator-managed mode (drop entirely vs. emit mode: disabled + unsafeFlags.tls: true for true plaintext)

All future commits will be squashed into a single feature commit when the operator bump lands.

@github-actions github-actions Bot added area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review size/XL This PR changes 500-999 lines, ignoring generated files and removed size/L This PR changes 100-499 lines, ignoring generated files labels May 27, 2026
@Arsolitt
Arsolitt (Arsolitt) marked this pull request as ready for review May 27, 2026 17:20
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request enables native TLS support for the managed MongoDB service by utilizing the PSMDB operator's built-in cert-manager integration. By upgrading the operator to version 1.22.0 and implementing a tri-state configuration for TLS, the changes allow for secure internal and external communication. The implementation ensures that external hostnames are correctly propagated to certificate SANs, improving verification for external clients while maintaining compatibility with existing backup workflows.

Highlights

  • TLS Support: Added native TLS support using the PSMDB operator's built-in cert-manager integration.
  • Operator Upgrade: Upgraded the Percona Server for MongoDB operator from 1.21.1 to 1.22.0.
  • Configuration: Introduced a tri-state tls.enabled field for flexible control over TLS activation.
  • External Access: Configured splitHorizons to include external hostnames in certificate SANs for secure external connectivity.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Ignored Files
  • Ignored by pattern: **/charts/** (9)
    • packages/system/mongodb-operator/charts/psmdb-operator/Chart.yaml
    • packages/system/mongodb-operator/charts/psmdb-operator/README.md
    • packages/system/mongodb-operator/charts/psmdb-operator/crds/crd.yaml
    • packages/system/mongodb-operator/charts/psmdb-operator/templates/NOTES.txt
    • packages/system/mongodb-operator/charts/psmdb-operator/templates/_helpers.tpl
    • packages/system/mongodb-operator/charts/psmdb-operator/templates/deployment.yaml
    • packages/system/mongodb-operator/charts/psmdb-operator/templates/role-binding.yaml
    • packages/system/mongodb-operator/charts/psmdb-operator/templates/role.yaml
    • packages/system/mongodb-operator/charts/psmdb-operator/values.yaml
  • Ignored by pattern: **/zz_generated.*.go (1)
    • api/apps/v1alpha1/mongodb/zz_generated.deepcopy.go
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment Gemini (@gemini-code-assist) Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on Gemini (@gemini-code-assist) comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

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

Code Review

This pull request introduces TLS support for the MongoDB application. It adds a tri-state TLS configuration option (tls.enabled) that can be explicitly enabled, disabled, or automatically resolved based on the external access setting. It updates the PerconaServerMongoDB CR to version 1.22.0, implements split-horizon DNS for external TLS, and updates the dashboard resource map, Helm values, and schemas accordingly. The review feedback points out a potential nil pointer evaluation error in the mongodb.tls.enabled template helper if .Values.tls is explicitly set to null or omitted, suggesting a defensive dictionary assignment guard.

Comment thread packages/apps/mongodb/templates/_tls.tpl Outdated

@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

🧹 Nitpick comments (1)
packages/apps/mongodb/tests/dashboard-resourcemap_test.yaml (1)

141-165: ⚡ Quick win

Add -ssl assertions to auto-on and override-off RBAC tests.

These cases currently validate only test-mongodb-ca-cert; they should also assert test-mongodb-ssl presence/absence to catch partial secret RBAC regressions.

Proposed test additions
   - it: Role includes TLS secrets when external=true (auto-on tri-state)
@@
     asserts:
       - contains:
           path: rules[1].resourceNames
           content: test-mongodb-ca-cert
         documentIndex: 0
+      - contains:
+          path: rules[1].resourceNames
+          content: test-mongodb-ssl
+        documentIndex: 0

   - it: Role excludes TLS secrets when external=true but tls.enabled=false (override)
@@
     asserts:
       - notContains:
           path: rules[1].resourceNames
           content: test-mongodb-ca-cert
         documentIndex: 0
+      - notContains:
+          path: rules[1].resourceNames
+          content: test-mongodb-ssl
+        documentIndex: 0
🤖 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 `@packages/apps/mongodb/tests/dashboard-resourcemap_test.yaml` around lines 141
- 165, Update the two RBAC test cases named "Role includes TLS secrets when
external=true (auto-on tri-state)" and "Role excludes TLS secrets when
external=true but tls.enabled=false (override)" to also assert the
presence/absence of the test-mongodb-ssl secret: for the first test add a
contains assertion with path rules[1].resourceNames and content test-mongodb-ssl
(documentIndex: 0); for the second test add a notContains assertion with the
same path/content/documentIndex to ensure both CA and SSL secrets are validated.
🤖 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 `@packages/apps/mongodb/README.md`:
- Around line 42-43: Remove the blank quoted separator line in the README
blockquote: locate the two adjacent quoted paragraphs in
packages/apps/mongodb/README.md and delete the empty line that creates an extra
">" line between them so the two quoted paragraphs form a single continuous
blockquote (no code changes, just remove the stray ">" line).

---

Nitpick comments:
In `@packages/apps/mongodb/tests/dashboard-resourcemap_test.yaml`:
- Around line 141-165: Update the two RBAC test cases named "Role includes TLS
secrets when external=true (auto-on tri-state)" and "Role excludes TLS secrets
when external=true but tls.enabled=false (override)" to also assert the
presence/absence of the test-mongodb-ssl secret: for the first test add a
contains assertion with path rules[1].resourceNames and content test-mongodb-ssl
(documentIndex: 0); for the second test add a notContains assertion with the
same path/content/documentIndex to ensure both CA and SSL secrets are validated.
🪄 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: 395bcf62-2f45-41a8-9a48-9e40c2122e93

📥 Commits

Reviewing files that changed from the base of the PR and between 628ebce and ce53e49.

📒 Files selected for processing (23)
  • api/apps/v1alpha1/mongodb/types.go
  • api/apps/v1alpha1/mongodb/zz_generated.deepcopy.go
  • packages/apps/mongodb/Makefile
  • packages/apps/mongodb/README.md
  • packages/apps/mongodb/templates/_tls.tpl
  • packages/apps/mongodb/templates/dashboard-resourcemap.yaml
  • packages/apps/mongodb/templates/mongodb.yaml
  • packages/apps/mongodb/tests/dashboard-resourcemap_test.yaml
  • packages/apps/mongodb/tests/mongodb_test.yaml
  • packages/apps/mongodb/tests/tls_test.yaml
  • packages/apps/mongodb/values.schema.json
  • packages/apps/mongodb/values.yaml
  • packages/system/mongodb-operator/charts/psmdb-operator/Chart.yaml
  • packages/system/mongodb-operator/charts/psmdb-operator/README.md
  • packages/system/mongodb-operator/charts/psmdb-operator/crds/crd.yaml
  • packages/system/mongodb-operator/charts/psmdb-operator/templates/NOTES.txt
  • packages/system/mongodb-operator/charts/psmdb-operator/templates/_helpers.tpl
  • packages/system/mongodb-operator/charts/psmdb-operator/templates/deployment.yaml
  • packages/system/mongodb-operator/charts/psmdb-operator/templates/role-binding.yaml
  • packages/system/mongodb-operator/charts/psmdb-operator/templates/role.yaml
  • packages/system/mongodb-operator/charts/psmdb-operator/values.yaml
  • packages/system/mongodb-rd/cozyrds/mongodb.yaml
  • packages/system/mongodb-rd/tests/cozyrds_test.yaml

Comment thread packages/apps/mongodb/README.md Outdated
@github-actions github-actions Bot added size/L This PR changes 100-499 lines, ignoring generated files and removed size/XL This PR changes 500-999 lines, ignoring generated files labels May 27, 2026
Arsolitt (Arsolitt) and others added 19 commits September 1, 2026 11:10
Address review feedback from lexfrei (B3): the tri-state tls.enabled field
could not actually disable TLS. The PSMDB operator defaults spec.tls to
preferTLS whenever it is unset, so tls.enabled: false rendered no spec.tls
while the operator still served TLS — a misleading no-op.

Remove the field and unconditionally render spec.secrets.ssl and
spec.tls.mode: preferTLS to match operator behaviour. splitHorizons SAN
injection now keys off external alone. Also drop the Makefile jq patch that
re-injected the tls schema property.

Assisted-By: Claude <[email protected]>
Signed-off-by: Arsolitt <[email protected]>
The tenant-facing paths handed over both Secrets the PSMDB operator creates
for TLS. <release>-ca-cert holds the CA private key, so read access is a full
trust-chain compromise; <release>-ssl holds the server private key, so read
access allows impersonation. A client verifying the endpoint needs ca.crt and
nothing else, so neither object belongs on a tenant-facing path.

Declare the trust anchor by name instead. The operator creates its CA Secret
itself and offers no way to label it, so the ApplicationDefinition names it and
the CA-extraction controller lifts ca.crt alone into the key-free projection.
The include selector matches that projection by label, and the dashboard Role
drops both key-bearing names without gaining the projection by name: a name
grant conveys whatever occupies the name, while the label path conveys only
what the platform vouched for.

Witness both delivery paths against a running cluster, since the two that
matter are separate. The Role is asserted as an exact grant with bounded rule
counts, because a rule with no resourceNames — or one granting every resource
— would otherwise pass an absence check while conveying every Secret in the
namespace. The label path is asserted as a negative on all three key-bearing
Secrets. A bats check pins the declaration's names and keys, which nothing else
covers: the field is pruned at admission until the controller ships, so drift
there would surface as an empty trust anchor with nothing red.

Drop the mongodb-rd unit test. That package has no test target, so the file
never ran, and what it asserted was the leak.

Assisted-By: Claude <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
With no external hostname to advertise — zero replicas, or a tenant host the
controller has not populated yet — the horizons range produced nothing and the
key was emitted with a null value. The CRD types it as an object without
nullable, so the API server rejected the whole PerconaServerMongoDB and the
HelmRelease with it. The values schema permits zero replicas, so this was
reachable. Guard on both and omit the key entirely.

Pin the surrounding invariants, which were asserted in comments and enforced
nowhere. The chart must render no cert-manager objects of its own, since the
operator owns the chain and anything rendered here would be orphaned while
looking like it participated. It must set neither of the two fields that
switch TLS off at defaulting time while the rendered mode still reads
preferTLS. And the external-access case now supplies a tenant host, so it
isolates the flag it names rather than passing on the host guard.

Correct the header comment, which fused two false claims: that setting
spec.secrets.ssl is what keeps TLS on, and that it is what engages the
cert-manager integration. The operator defaults that name unconditionally and
runs the cert-manager path regardless.

Assisted-By: Claude <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
The unit-test runner discovers charts by probing for a test target, and this
chart had none — so its suites had never run in CI.

Assisted-By: Claude <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
…impact

The TLS notes made several claims the operator's own behaviour contradicts.
The CA is issued for a year and renewed about a month early, not for a decade
with rotation disabled — so a bundle copied once stops verifying, and the
retrieval steps now say to re-read it. Sharded mongos SANs are complete,
because the operator emits them for every topology; the real gap is that no SAN
covers the single external LoadBalancer fronting mongos, so external hostname
verification is unsupported there. preferTLS is the operator's own default
rather than a posture adopted to accommodate backups, which run under the same
certificates. External access in replica set mode is one LoadBalancer per
member with driver-side primary discovery, not a single load-balanced
endpoint — the old description contradicted the external SAN note. And the
posture section named only one of its two inherited weaknesses: unverified
clients are accepted as well as plaintext ones.

The lifecycle section was wrong in both directions. The operator owns its TLS
Secrets and its cert-manager objects through the CR, so they are collected on
uninstall rather than left behind. The rotation snapshots are the opposite of
what was written: the operator deletes them itself once the merged CA has
landed, and reads the previous CA back out of them while a rotation is in
flight — so the advice to delete them by hand could destroy the only copy of
the old CA. Only an interrupted rotation strands them.

Document what an upgrade actually does to a release already running with
external access: the new hostnames become certificate SANs, the leaf is
reissued, and the replica set rolls. Gate the retrieval steps on the component
that publishes the trust anchor and on cert-manager, without which the declared
source does not exist and the command returns nothing.

Assisted-By: Claude <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
The uninstall section promised that no manual cleanup of the TLS Secrets is
needed, reversing the instruction it replaced. That promise rests on an
ownerReference the operator patches on at reconcile time rather than sets at
creation, and two paths skip the patch: a dry-run reconcile returns before it,
and a Secret already present under the same name with a mismatched
cert-manager.io/certificate-name annotation is counted toward success and
returned on unpatched. The deletion finalizer does not cover the gap — it
enumerates only the users, internal-<release>, internal-<release>-users and
encryption-key Secrets. So a Secret predating the release at one of these names
can stay unowned and survive uninstall, and for <release>-ca-cert that orphans
the CA private key.

State the reconcile-time mechanism and tell operators to confirm the CA Secret
is gone. Pin the outcome with a chainsaw step asserting the CR owns all three
TLS Secrets on a normal install, so the claim goes red if the operator stops
patching instead of rotting as prose.

The external-hostname note described a permanently empty _namespace.host as a
bootstrap window that self-heals. A tenant with no host of its own and no
ancestor supplying one resolves to an empty host for good, leaving external
TLS hostname verification silently unavailable. Name that case alongside the
transient one, and say why the render is not failed closed: it would break
existing hostless tenants already running external access over IP.

Assisted-By: Claude <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
Every assertion in the suite passed while checking nothing. containsDocument
without `any: true` asserts that EVERY rendered document matches, so negating
it asks whether "not every document is a Certificate" — true of any chart
rendering more than one object, and this one renders eight. A cert-manager
Certificate appended to templates/mongodb.yaml, the most plausible place for
one to appear, left the suite fully green.

Add `any: true` to each containsDocument so the negation means "no document
anywhere is of this kind". Unscoped assertions are ANDed across template
files, which is what makes the negatives correct: they go red if any file
renders the forbidden kind.

Add a positive control asserting the chart does render the PerconaServerMongoDB
CR through the same matcher shape, scoped to the one template that emits it.
The four negatives are all satisfied by a matcher that never matches, which is
exactly how this suite failed; the control fails if that returns.

Verified by injecting a Certificate into the chart: the suite now fails and
previously passed.

Assisted-By: Claude <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
…etime

The external-hostname note claimed the operator and the CR version were "both
shipped by this chart". Only crVersion is: the PSMDB operator is a separate
platform component with its own lifecycle, and the SAN fold is gated on both.
On a platform carrying an older operator nothing reports the unmet gate — the
chart still renders splitHorizons, the CR is still accepted and the cluster
still comes up healthy, while the external hostnames never enter the SAN list
and a verifying client fails the handshake. State which half the chart owns and
point at the operator version as the thing to check.

The leaf certificate lifetime was stated as a flat 90 days. It is an operator
default this chart deliberately does not pin: nothing in the tenant contract
keys off it, the operator reissues without client action, and pinning would
freeze the chart against upstream shortening it. Attribute the number to the
operator version and pin the field's absence, so "inherited rather than chosen"
stays falsifiable — the same treatment allowInvalidCertificates already gets.

Assisted-By: Claude <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
The CA retrieval command was documented without the other half: what to do
with the bundle once it is in hand. A tenant reading only the retrieval step
has the certificate and no worked example of a verified connection.

Pass the two TLS inputs as mongosh flags rather than folding tls=true into
the connection string, because the correct separator differs by topology --
the replica-set URI already carries a ?replicaSet=rs0 query and the sharded
one carries none, so a single copyable form would be wrong for one of them.

Call out that omitting the flags is silent. Under preferTLS the server
accepts the plaintext connection and nothing reports the downgrade, so a
working connection is not evidence that TLS was negotiated.

Signed-off-by: Aleksei Sviridkin <[email protected]>
… sizes

The upgrade-restart note said the replica set stays available through the
rollout. That holds at three members and fails below them, and the chart
permits both smaller sizes: replicas 1 and 2 render unsafeFlags.replsetSize
and are not excluded from the SAN change, so they gain horizons and roll on
upgrade exactly as a three-member release does. At one replica the rollout
takes the only mongod down; at two the write majority is still two, so
restarting either member stalls writes until it returns. An operator running
a single-replica external release would have read the old text and declined
to book a window.

Cover the two replicas case, which was untested, and assert horizons and the
sub-quorum marker together at both sizes, so gating horizons to sizes that
can absorb a restart turns the tests red and sends someone back to this
paragraph. The default-size case carries the pair as a positive control: the
marker proves nothing unless it is absent where a restart is survivable.

Signed-off-by: Aleksei Sviridkin <[email protected]>
A tenant verifying a MongoDB endpoint needs the CA certificate, and
every Secret that holds one also holds a private key: the operator's
<release>-ca-cert carries the CA key, the leaf <release>-ssl the server
key. Granting either would hand over the key along with the anchor.

Render a TenantProjection naming the source, so the CA-extraction
controller can publish the key-free <release>.tenant-ca, and select that
projection by label from the managed-service definition. The sentinel
carries no ownerReferences: it is resolved back to its release through
the label Flux stamps, and an owner reference short-circuits that walk
and silently costs the tenant its anchor.

The sentinel is ungated. The operator issues the certificate chain
whatever the chart asks for, so the source exists for every release and
there is no plaintext leg to skip.

The source is <release>-ssl rather than <release>-ca-cert. It is the
name this chart itself sets in spec.secrets.ssl, so the reference cannot
drift from the CR; the operator writes it on both its cert-manager path
and the self-signed fallback it uses when cert-manager is absent, where
no <release>-ca-cert exists at all; and it holds the merged old and new
CA through a rotation, where <release>-ca-cert holds only the new one
and would stop verifying pods still serving the previous leaf.

Withhold the key-bearing Secrets from the tenant explicitly as well. The
label selector matches on no name, so a single mis-stamp would promote a
private key; the exclude list is evaluated first and closes the
enumerable set.

Signed-off-by: Aleksei Sviridkin <[email protected]>
The existing steps only assert that key-bearing Secrets are NOT promoted
to the tenant, and every one of those is satisfied by an absent label —
they hold just as well if nothing was ever published. Add the positive:
the projection exists, carries exactly ca.crt, matches the source
certificate by fingerprint, and is reachable through the tenantsecrets
registry rather than only as a raw Secret.

Signed-off-by: Aleksei Sviridkin <[email protected]>
The cert-manager exclusion suite justified its `any: true` requirement
with a specific number of rendered documents. The argument only needs
"more than one"; the number was already tied to one value set and the
sentinel adds another document.

Signed-off-by: Aleksei Sviridkin <[email protected]>
The README said the sentinel would sit inert with nothing else
affected. The CRD is a template of the controller's own chart, so a
cluster without the controller has no TenantProjection kind and the
release fails to render at all. The caveat also guarded a state that
cannot occur, since the controller is a mandatory platform component —
drop it rather than restate it, matching the postgres chart, and keep
the paragraph explaining the choice of source Secret.

Enumerate all three key-bearing Secrets in the registry negative. Two of
the three were listed, which leaves the third free to acquire the label
and leak a key, the exact reasoning the step above it already spells
out.

Collect TenantProjection status on failure. The controller reports
SourceNotFound and its siblings there, and without it a failed
projection surfaces only as an absent Secret with no reason given.

Rename the source-shape step, which no longer looks at the CA Secret,
and cross-reference the two halves of the leaf Secret's name.

Signed-off-by: Aleksei Sviridkin <[email protected]>
The name-clash note claimed the shared TLS helper documents
<release>-ca-cert as the conventional key-free anchor. It documents the
opposite: the canonical name is <release>.tenant-ca, and it names this
engine as the reason, because MongoDB already claims <release>-ca-cert
with a key-bearing Secret. There is no clash to warn about and the chart
follows the convention rather than deviating from it. The paragraph
above already says <release>-ca-cert holds the CA private key, so drop
the note instead of inverting it.

Guard the ownerReference assertions against an absent list. length() of
null is a type error rather than a failed comparison, and the harness
returns it as an error, which aborts the poll instead of retrying — so
the timeout never applies while the operator is still patching the
reference on.

Restore the two-variable form of the fingerprint comparison. Inlining
both command substitutions into the test swallows a non-zero exit: with
no openssl on PATH both sides are empty, they compare equal, and the
check passes having compared nothing.

Signed-off-by: Aleksei Sviridkin <[email protected]>
The assertions withholding the key-bearing TLS Secrets from the tenant
Role addressed it as document 0 and, in two cases, asserted only with
notContains. Neither survives a rearrangement.

notContains passes when its path does not resolve, so a test built from
it alone goes green against the wrong document. Reversing the order of
the two documents in the template moved the RoleBinding into position 0,
and "does not grant the key-bearing TLS secrets when external is
enabled" and "does not grant the tenant-ca projection by name" both
stayed green while the Role granted <release>-ca-cert by name — the
Secret holding the CA private key.

Select the Role by kind, and pair every notContains with a positive
assertion on the same path so a rule reordering that moves rules[1]
reddens too. Indexing survives neither rearrangement; selecting survives
the first; the positive anchor catches the second.

Signed-off-by: Aleksei Sviridkin <[email protected]>
Every object this chart creates is named after the Helm release, not
after the MongoDB resource, so a MongoDB named catest yields
mongodb-catest.tenant-ca. The retrieval commands used a bare <release>
placeholder, which reads as the resource name and produces a command
that returns nothing. Spell the prefix out in the two commands meant to
be copied, and define the placeholder once for the prose.

Split the sentinel's shape test in two. The absence of ownerReferences
is the invariant — it breaks the walk from the projection back to the
release, and the tenant stops receiving the anchor. An explicit
namespace or a chart-added label breaks nothing by itself; grouping all
three under one name presented them as equally load-bearing.

Signed-off-by: Aleksei Sviridkin <[email protected]>

@myasnikovdaniil myasnikovdaniil 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.

LGTM on the code, please fix the description before merging. It describes an earlier revision of this branch.

  1. "Both were previously reachable", and the release note "stop exposing TLS private key material to tenants". Neither secret was reachable by a tenant before this branch. On main the cozyrds carries exclude: [] and an include naming only mongodb-{{ .name }}-credentials, and include is a whitelist (internal/lineagecontrollerwebhook/matcher.go:64-72), so a secret matching nothing never gets stamped and tenantsecrets serves only tenantresource=true. Dashboard Role on main grants <release>-credentials and nothing else, the diff to that file here is +13/-0 and adds a comment. The exposure existed in intermediate revisions of this branch, not in a release. Changelog generation reads PR bodies, so as written this ships as a claim that cozystack was handing tenants CA private keys.
  2. spec.caCert is gone, the chart renders a TenantProjection now and there is no caCert field in api/v1alpha1/applicationdefinitions_types.go.
  3. Merge is not blocked on #3299. It is closed, what it carried is on main, and this branch is current with main.

I ran helm unittest (130/130) and the new bats suite, and checked the jmespath guards in the new e2e steps against the rendered Role and against widened variants, each one discriminates. My own concern about sourcing the projection from <release>-ssl during CA rotation was wrong, the extraction controller takes a multi-cert bundle and re-encodes every block.

Not blocking, two things worth knowing. External replica set releases will roll on upgrade because the leaf is reissued with new SANs, README covers that including the outage below 3 replicas. And nothing publishes <release>-rs0-<i>.<host>, replsets[].expose.serviceAnnotations is a single map shared by all per-pod services so per-pod external-dns hostnames cannot be expressed through the operator, SANs stay inert until someone creates the records by hand. Worth putting that in the release note and not only the README.

postgres-rd still has exclude: [] with the same label-only include if you want to converge them later.

@lexfrei
Aleksei Sviridkin (lexfrei) merged commit c94a1da into main Sep 1, 2026
21 of 22 checks passed
@lexfrei
Aleksei Sviridkin (lexfrei) deleted the feat/tls-mongodb branch September 1, 2026 10:16
Andrei Kvapil (kvaps) pushed a commit that referenced this pull request Sep 7, 2026
#2692)

## What this PR does

Enables operator-managed TLS for MongoDB and stops handing tenants any
Secret that carries private key material.

- Percona Server for MongoDB serves TLS unconditionally (`mode:
preferTLS`), so this PR drops the misleading `tls.enabled` toggle rather
than pretending the chart controls it.
- The tenant no longer receives the operator's CA Secret
(`<release>-ca-cert`, which holds `ca.key`) or the leaf
(`<release>-ssl`). Both were previously reachable, and either one is a
private key in tenant hands, the CA one is a full trust-chain
compromise.
- The chart declares a `TenantProjection` sentinel naming its CA source
Secret (`<release>-ssl`, the operator-managed leaf, which carries
`ca.crt`), and the ResourceDefinition selects the resulting key-free
`<release>.tenant-ca` projection by the engine-agnostic
`internal.cozystack.io/tenant-ca` label. The CA-extraction controller
reads the sentinel and publishes the projection holding only `ca.crt`.
The tenant gets what it needs to verify the server and nothing else.

### Why the include also carries an exclude list

The trust-anchor selector matches by label with no name constraint, so
any Secret in the namespace that acquired that label would be promoted
with its data copied verbatim. The enumerable dangerous set is therefore
excluded explicitly: the CA, both leaves, the rotation snapshots
(`-ssl-old`, `-ssl-internal-old`, same server keys as the leaves, and
unlike them they carry no owner reference, so an interrupted rotation
can leave them behind after the release is gone), the system-user
passwords, the at-rest encryption key, the keyfile, the object-storage
credentials, and both forms of the operator's internal Secret.

That backstop is bounded, and the comment says so: matching is by exact
name with no globs, so a Secret a future operator version introduces, or
a transient object an issuer creates mid-renewal, stays in scope of the
label-only include. The per-user Secrets cannot be enumerated at all,
since their names carry the username. The primary defence remains that
the controller stamps only its own projection; this closes the
enumerable set, not the whole surface.

### Release note

```release-note
feat(mongodb): stop exposing TLS private key material to tenants and publish the CA as a key-free trust anchor
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/database Issues or PRs related to managed databases (postgres, mariadb, redis, etcd, kafka, clickhouse) do-not-merge/hold Indicates that a PR should not merge because someone has issued /hold 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.

3 participants