Skip to content

feat(mariadb): add TLS support via cert-manager - #2680

Open
Arsolitt (Arsolitt) wants to merge 68 commits into
mainfrom
feat/tls-mariadb
Open

feat(mariadb): add TLS support via cert-manager#2680
Arsolitt (Arsolitt) wants to merge 68 commits into
mainfrom
feat/tls-mariadb

Conversation

@Arsolitt

@Arsolitt Arsolitt (Arsolitt) commented May 19, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Gives MariaDB a chart-managed certificate chain, makes TLS enforcement explicit, and hands the tenant a key-free trust anchor instead of a private key.

  • tls.issuer selects who issues the server certificate: operator (the CA mariadb-operator manages itself, the default and today's behaviour) or cert-manager (the instance gets its own CA, and this is what covers the external hostname). The field is named for the issuer rather than for on/off because TLS is served under both — unlike the similarly spelled field in some other charts, which really is a switch.
  • tls.required refuses plaintext connections. It is opt-in and defaults to false: enforcing TLS before the tenant can obtain the CA would be backwards, and a platform upgrade must not sever connections that work today.
  • Neither issuer is inferred from external. Existing instances keep the CA they have and change nothing on upgrade.
  • The tenant no longer receives <release>-ca-tls or <release>-tls — the CA private key and the server private key. It receives the operator's ca.crt-only bundle, which works today, and the ResourceDefinition additionally declares that bundle as the source for the CA-extraction controller.

Fixes along the way

  • The TLS backup path could never have worked: it passed --ssl-mode=VERIFY_IDENTITY, a MySQL-8 flag the MariaDB client rejects during argument parsing. Replaced with --ssl-ca + --ssl-verify-server-cert, and the test now asserts the dialect rather than the presence of a substring.
  • The leaf certificate carried clientAuth. Because REQUIRE X509 accepts any certificate signed by a trusted CA, the server's own key was usable as a client credential. Removed.
  • The CA and server private keys survived deletion of the instance: cert-manager runs with enableCertificateOwnerRef: false, so removing the Certificate left the Secrets behind. Both are now cleaned up by the existing post-delete hook.
  • The Secrets are labelled for the operator's watch predicate, without which a renewed leaf would not reach the pods until a full resync.

Why the include carries an exclude list

The trust-anchor selector matches by label with no name constraint, so the enumerable dangerous set is excluded explicitly — both issuers' private keys, the internal credentials, the backup keys, and the metrics configuration, which the operator writes unconditionally with credentials in plain text for a user holding SELECT on everything.

The backstop is bounded and the comment says so: matching is by exact name with no globs, so a Secret introduced by a future operator version, or a transient object created mid-renewal, stays in scope of the label-only include. The primary defence remains that the controller stamps only its own projection.

Known limitations

  1. Merge is blocked on feat(controller): engine-agnostic CA extraction for the tenant trust anchor #3299 — and the blocking is mechanical, not just procedural. The CRD on main does not carry spec.caCert, and under Flux's Server-Side Apply the API server rejects a field the schema does not declare (field not declared in schema); under the older client-side apply the same field was silently dropped, so installing this branch without feat(controller): engine-agnostic CA extraction for the tenant trust anchor #3299 fails outright. An earlier revision of this description claimed the field would be silently pruned; that was wrong. The tenant still gets a usable trust anchor once merged in order, through the operator's bundle.
  2. The deprecated backup CronJob cannot start at all, for reasons that predate this branch: its secretKeyRef names a Secret and key the chart never renders. That is filed separately as bug(apps/mariadb): deprecated backup CronJob cannot start — secretKeyRef names a Secret and key that do not exist #3347 and deliberately not fixed here, since repairing it would start scheduled dumps on production instances from a TLS change. The tests that pin the backup TLS flags therefore pin a path that does not currently execute — the suite header says so.
  3. The operator's clusterName is hardcoded, so on a cluster with a non-default domain the SANs and the name the operator dials diverge. Pre-existing, raised separately.
  4. No e2e coverage for the managed path. Unit coverage pins the rendered chain, the SAN set, the exclusion set and the guards; the end-to-end handshake is not asserted in CI.

On review coverage

Two independent review passes were run repeatedly against this branch — seven rounds, converging from a security finding, through comment accuracy, to guard strength, and finally to nothing new. A third, model-based pass reviewed the branch through 6cb1bd236 and its findings were fixed; it could not be run against the final revision because of an external account quota lasting until 25 July. The delta since that revision is substantial and includes runtime code, not only tests and docs. Recording this so the coverage is not overstated: if the quota returns before this is ready to merge, that pass is worth running again.

Release note

feat(mariadb): chart-managed TLS via cert-manager, opt-in enforcement, and the CA published to tenants as a key-free trust anchor

@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

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

Adds tri-state TLS support across CRD types and deepcopy, Helm values/schema/docs, a Helm tri-state helper, cert-manager CA/leaf templates, MariaDB spec and RBAC wiring, backup TLS wiring, extensive helm-unittest coverage, and ApplicationDefinition schema propagation.

Changes

MariaDB TLS Feature

Layer / File(s) Summary
CRD Contract & Deep-Copy Methods
api/apps/v1alpha1/mariadb/types.go, api/apps/v1alpha1/mariadb/zz_generated.deepcopy.go
Extends ConfigSpec with Tls field and adds exported TLS struct (Enabled *bool, Required bool) plus generated deepcopy methods handling the Enabled pointer.
Helm Values, Schema & Docs
packages/apps/mariadb/values.yaml, packages/apps/mariadb/values.schema.json, packages/apps/mariadb/README.md
Adds top-level tls values (enabled tri-state, required default true), updates values schema and README TLS parameters.
Tri-State Helper & Build Target
packages/apps/mariadb/templates/_tls.tpl, packages/apps/mariadb/Makefile
Implements mariadb.tls.enabled helper to normalize tri-state tls.enabled with .Values.external fallback; adds test Makefile target (helm unittest .).
Cert-Manager Certificate Provisioning
packages/apps/mariadb/templates/certmanager.yaml
Conditionally renders self-signed Issuer, CA Certificate, CA-backed Issuer, and leaf Certificate with SAN lists, ECDSA keys, and rotation policies when TLS is enabled.
MariaDB Spec & RBAC Configuration
packages/apps/mariadb/templates/mariadb.yaml, packages/apps/mariadb/templates/dashboard-resourcemap.yaml
Gates spec.tls rendering on helper output, sets spec.tls.enabled/required and secret refs, and conditionally adds TLS secret names to dashboard Role resourceNames.
Backup CronJob & Script TLS Wiring
packages/apps/mariadb/templates/backup-cronjob.yaml, packages/apps/mariadb/templates/backup-script.yaml
Backup CronJob mounts CA secret and backup script sets mysqldump SSL_OPTS (--ssl-mode=VERIFY_IDENTITY --ssl-ca=...) when TLS enabled; omitted otherwise.
Comprehensive TLS Test Suite
packages/apps/mariadb/tests/tls_test.yaml
Adds extensive helm-unittest coverage for tri-state logic, null handling, cert-manager outputs, SAN composition, EKU/usages, rotation policies, RBAC secret wiring, and backup TLS behavior.
Application Definition Schema Propagation
packages/system/mariadb-rd/cozyrds/mariadb.yaml
Adds spec.tls to ApplicationDefinition openAPISchema and updates keysOrder entries for spec.tls and spec.tls.required.

Sequence Diagram

sequenceDiagram
  participant Values
  participant Helper
  participant CertMgr
  participant MariaDB
  participant RBAC

  Values->>Helper: provide external and tls.enabled
  Helper->>CertMgr: emit tlsEnabled
  CertMgr->>CertMgr: create Issuer, CA, CA-Issuer, leaf Certificate
  CertMgr->>MariaDB: create TLS secrets (CA, leaf)
  MariaDB->>MariaDB: render spec.tls and secret refs
  Helper->>RBAC: tlsEnabled
  RBAC->>RBAC: include or exclude TLS secrets and mounts
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

size/XXL, area/platform

Suggested reviewers

  • kvaps
  • sircthulhu
  • IvanHunters
  • lllamnyp
  • myasnikovdaniil

🐰 I hopped through charts and schemas bright,
Tri-state flags now guide TLS right,
CA roots steady, leafs renewed on cue,
Backups and RBAC now know what to do,
Cozy DB snug beneath TLS night.

🚥 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 summarizes the main change: adding MariaDB TLS support via cert-manager.
✨ 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/tls-mariadb

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/XL This PR changes 500-999 lines, ignoring generated files area/database Issues or PRs related to managed databases (postgres, mariadb, redis, etcd, kafka, clickhouse) kind/feature Categorizes issue or PR as related to a new feature labels May 19, 2026
@Arsolitt
Arsolitt (Arsolitt) marked this pull request as ready for review May 19, 2026 13:03
@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 introduces robust TLS support for the MariaDB managed service, leveraging cert-manager to handle certificate lifecycle management. By providing a configurable tri-state TLS enablement policy and a dedicated requirement toggle, the changes allow for secure-by-default deployments while maintaining backward compatibility for existing services and migration paths. The implementation includes automated certificate chain generation, SAN policy management, and full integration with the MariaDB operator, all validated through a new suite of unit tests.

Highlights

  • TLS Support: Added TLS support for MariaDB managed services using cert-manager, including automated certificate chain generation and wiring into the MariaDB CR.
  • Tri-state Configuration: Introduced a tri-state tls.enabled field that defaults to the external access setting, providing flexible control over TLS enablement.
  • Security Enforcement: Added a tls.required toggle to control MariaDB's require_secure_transport setting, allowing for secure-by-default configurations with migration flexibility.
  • Testing: Implemented comprehensive helm unittests to verify TLS resource rendering and tri-state logic across various configuration scenarios.
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: **/zz_generated.*.go (1)
    • api/apps/v1alpha1/mariadb/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 implements TLS support for MariaDB with tri-state logic, automatically enabling TLS for external access unless configured otherwise. It adds cert-manager templates, updates RBAC for TLS secrets, and includes unit tests. Feedback highlights a high-severity issue where incorrect SANs in the certificate template would cause internal TLS verification failures. Other suggestions include adding standard labels to cert-manager resources and explicitly defining default values in values.yaml to improve schema visibility.

Comment thread packages/apps/mariadb/templates/certmanager.yaml
Comment thread packages/apps/mariadb/templates/certmanager.yaml
Comment thread packages/apps/mariadb/values.yaml 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: 2

🤖 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/mariadb/README.md`:
- Line 105: The README shows `version` under TLS because the `values.yaml` lacks
a section separator before the Version enum; open the MariaDB chart's
values.yaml and insert a new section marker (e.g., `@section` or the chart's
existing section delimiter style) immediately before the `version` parameter so
that `version` is moved out of the TLS section into its own section (or place it
into the "Application-specific parameters" section); this change to values.yaml
will let cozyvalues-gen regenerate the README with `version` shown in the
correct section.

In `@packages/system/mariadb-rd/cozyrds/mariadb.yaml`:
- Line 28: The keysOrder entry currently includes ["spec","tls"] but omits the
TLS child keys, causing unstable field ordering; update the keysOrder array (the
keysOrder symbol) to include ["spec","tls","enabled"] and
["spec","tls","required"] immediately after ["spec","tls"] so TLS child fields
render in the intended, stable order in the dashboard.
🪄 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: 7bca795b-ff4c-4a15-b0e7-4f66d5ad1e9a

📥 Commits

Reviewing files that changed from the base of the PR and between c5e9287 and 5c6adbf.

📒 Files selected for processing (12)
  • api/apps/v1alpha1/mariadb/types.go
  • api/apps/v1alpha1/mariadb/zz_generated.deepcopy.go
  • packages/apps/mariadb/Makefile
  • packages/apps/mariadb/README.md
  • packages/apps/mariadb/templates/_tls.tpl
  • packages/apps/mariadb/templates/certmanager.yaml
  • packages/apps/mariadb/templates/dashboard-resourcemap.yaml
  • packages/apps/mariadb/templates/mariadb.yaml
  • packages/apps/mariadb/tests/tls_test.yaml
  • packages/apps/mariadb/values.schema.json
  • packages/apps/mariadb/values.yaml
  • packages/system/mariadb-rd/cozyrds/mariadb.yaml

Comment thread packages/apps/mariadb/README.md Outdated
Comment thread packages/system/mariadb-rd/cozyrds/mariadb.yaml 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.

♻️ Duplicate comments (1)
packages/system/mariadb-rd/cozyrds/mariadb.yaml (1)

28-28: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add nested TLS entries to keysOrder for stable dashboard field ordering.

The keysOrder array includes ["spec","tls"] but omits ["spec","tls","enabled"] and ["spec","tls","required"]. This is inconsistent with the existing pattern used for backup, which includes both the parent key and all nested keys (e.g., ["spec","backup"], ["spec","backup","enabled"], etc.). Without the nested entries, TLS child fields may not render in a stable, predictable order in the dashboard.

Suggested fix
-    keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "replicas"], ["spec", "resources"], ["spec", "resourcesPreset"], ["spec", "size"], ["spec", "storageClass"], ["spec", "external"], ["spec", "tls"], ["spec", "version"], ["spec", "users"], ["spec", "databases"], ["spec", "backup"], ["spec", "backup", "enabled"], ["spec", "backup", "s3Region"], ["spec", "backup", "s3Bucket"], ["spec", "backup", "schedule"], ["spec", "backup", "cleanupStrategy"], ["spec", "backup", "s3AccessKey"], ["spec", "backup", "s3SecretKey"], ["spec", "backup", "resticPassword"]]
+    keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "replicas"], ["spec", "resources"], ["spec", "resourcesPreset"], ["spec", "size"], ["spec", "storageClass"], ["spec", "external"], ["spec", "tls"], ["spec", "tls", "enabled"], ["spec", "tls", "required"], ["spec", "version"], ["spec", "users"], ["spec", "databases"], ["spec", "backup"], ["spec", "backup", "enabled"], ["spec", "backup", "s3Region"], ["spec", "backup", "s3Bucket"], ["spec", "backup", "schedule"], ["spec", "backup", "cleanupStrategy"], ["spec", "backup", "s3AccessKey"], ["spec", "backup", "s3SecretKey"], ["spec", "backup", "resticPassword"]]
🤖 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/system/mariadb-rd/cozyrds/mariadb.yaml` at line 28, The keysOrder
entry lists ["spec","tls"] but is missing explicit nested entries for TLS;
update the keysOrder array to include ["spec","tls","enabled"] and
["spec","tls","required"] (mirroring the existing pattern used for
["spec","backup"] and its nested keys) so TLS child fields render in a stable
order in the dashboard; modify the keysOrder definition in mariadb.yaml (the
keysOrder array) to add those two nested entries adjacent to ["spec","tls"] to
preserve predictable field ordering.
🤖 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.

Duplicate comments:
In `@packages/system/mariadb-rd/cozyrds/mariadb.yaml`:
- Line 28: The keysOrder entry lists ["spec","tls"] but is missing explicit
nested entries for TLS; update the keysOrder array to include
["spec","tls","enabled"] and ["spec","tls","required"] (mirroring the existing
pattern used for ["spec","backup"] and its nested keys) so TLS child fields
render in a stable order in the dashboard; modify the keysOrder definition in
mariadb.yaml (the keysOrder array) to add those two nested entries adjacent to
["spec","tls"] to preserve predictable field ordering.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7b675810-7dfc-4f1d-9229-461aa5392df4

📥 Commits

Reviewing files that changed from the base of the PR and between 5c6adbf and b23ead9.

📒 Files selected for processing (12)
  • api/apps/v1alpha1/mariadb/types.go
  • api/apps/v1alpha1/mariadb/zz_generated.deepcopy.go
  • packages/apps/mariadb/Makefile
  • packages/apps/mariadb/README.md
  • packages/apps/mariadb/templates/_tls.tpl
  • packages/apps/mariadb/templates/certmanager.yaml
  • packages/apps/mariadb/templates/dashboard-resourcemap.yaml
  • packages/apps/mariadb/templates/mariadb.yaml
  • packages/apps/mariadb/tests/tls_test.yaml
  • packages/apps/mariadb/values.schema.json
  • packages/apps/mariadb/values.yaml
  • packages/system/mariadb-rd/cozyrds/mariadb.yaml
✅ Files skipped from review due to trivial changes (2)
  • packages/apps/mariadb/README.md
  • api/apps/v1alpha1/mariadb/zz_generated.deepcopy.go

@Arsolitt Arsolitt (Arsolitt) self-assigned this May 21, 2026

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 — three security/correctness blockers: the chart's own legacy backup CronJob will start failing the moment TLS becomes default-on, the server certificate is missing digital signature (modern TLS clients reject it), and the dashboard use role is granted get/list/watch on the CA private key. The cert-manager chain itself is well-shaped (RSA-4096, sane lifetimes, rotation policy) and the SAN list matches the upstream operator's InternalServiceName (<name>-internal); below are the three items that need to land before merge.

Business context: First-class TLS for the managed MariaDB service via a self-contained cert-manager chain in the tenant namespace, with a tri-state tls.enabled that auto-on for externally-published instances.

Blockers

B1: tls.required: true default breaks chart's own backup CronJob

Files: packages/apps/mariadb/templates/mariadb.yaml:88-92 + packages/apps/mariadb/templates/backup-script.yaml:14,29

Issue: After this PR ships, any tenant with both external: true (or any explicit tls.enabled: true) and backup.enabled: true will lose backups silently on upgrade. The MariaDB CR's tls.required: true renders require_secure_transport=ON on mysqld, which rejects plaintext TCP connections. The chart-internal backup CronJob still runs mysql -u "$MYSQL_USER" -h "$MYSQL_HOST" and mysqldump ... -h "$MYSQL_HOST" with no --ssl-* flags or --ssl-mode=REQUIRED.

Evidence: backup-script.yaml:14,29 are unchanged plain-mysql clients; MYSQL_HOST resolves to <name>-primary or <name>-secondary (line 44 of backup-cronjob.yaml), which are TCP services. require_secure_transport rejects plain TCP regardless of credentials. Manifests helm template-rendered against external: true, backup.enabled: true produce a CronJob that will exit non-zero on every fire.

Impact: Silent backup regression on the subset (external-published + backup-enabled tenants) that is precisely the riskiest cohort. The PR description notes backup is deprecated but the chart still ships it; the new default cannot break it without an explicit upgrade note plus a fallback path.

Fix — pick one of:

  • Mount <name>-ca-tls into the CronJob and pass --ssl-ca=/path/ca.crt --ssl-mode=VERIFY_CA (or VERIFY_IDENTITY) to both mysql and mysqldump.
  • Gate the tls.required: true default on backup.enabled being false, with an explicit warning in the README that operators using the legacy backup must move to BackupClass before enabling TLS.
  • Treat the legacy backup as fully retired and remove it in the same PR rather than half-supporting it.

B2: Server certificate missing digital signature key usage

File: packages/apps/mariadb/templates/certmanager.yaml:62-65

Issue: The leaf Certificate explicitly declares usages: [server auth, client auth, key encipherment]. Because the field is set explicitly, cert-manager will NOT apply its default key usages on top. digital signature is missing.

Evidence: For TLS 1.3 (and TLS 1.2 with any ECDHE/DHE cipher suite — which is the default since OpenSSL 1.1.0 and Go's crypto/tls), the server signs the ephemeral key-exchange parameters with the cert private key. RFC 5246 §7.4.1.4.1 and RFC 8446 §4.4 require the cert to assert digitalSignature for that operation when keyUsage is present and critical: true. cert-manager's standard server profile is digital signature, key encipherment, server auth; the omission here will be rejected by strict clients (Go's tls.Config{InsecureSkipVerify:false}, recent OpenJDK with strict KU checks, BoringSSL). key encipherment only covers the legacy RSA key-transport suites, which TLS 1.3 doesn't have at all.

Impact: TLS handshake failures from modern, RFC-conformant clients once they connect to a TLS-enabled MariaDB. The bug is silent on the chart side — cert-manager happily issues a cert with the wrong usages and mysqld serves it.

Fix: Add digital signature to the usages list in certmanager.yaml:62-65.

B3: Dashboard RBAC leaks the CA private key and the server private key to every tenant user

File: packages/apps/mariadb/templates/dashboard-resourcemap.yaml:21-23

Issue: When TLS is enabled, the {{ .Release.Name }}-dashboard-resources Role grants get/list/watch on Secrets <name>-ca-tls and <name>-tls. The RoleBinding subjects (line 39, via cozy-lib.rbac.subjectsForTenantAndAccessLevel "use") cover every group with use access or higher in the tenant — i.e. every tenant member. cert-manager Secrets of kubernetes.io/tls type carry tls.key alongside tls.crt and ca.crt. The Role lets any tenant member run kubectl -n <ns> get secret <name>-ca-tls -o yaml and recover both the CA private key and the server private key.

Evidence: cert-manager produces standard tls-type Secrets containing both tls.key and tls.crt; for the CA Certificate (certmanager.yaml:16-37), tls.key is the CA's private key. Kubernetes RBAC at the Secret level cannot scope to individual keys — any verb on the secret exposes every field. The cozy-lib.rbac.accessLevelMap defines use as level 1 (just above view), so the binding extends to tenant-<X>-use, tenant-<X>-admin, tenant-<X>-super-admin groups.

Impact: A leaked CA private key allows minting an arbitrarily-named server cert that any client trusting this CA (notably MariaDB clients pinned via --ssl-ca) will accept — instant MITM of every MariaDB connection in the tenant. A leaked server private key allows passive decryption of TLS sessions captured anywhere on the network path (for non-PFS suites) and direct impersonation of the MariaDB server.

Fix: Drop both Secret names from the dashboard Role. If the dashboard genuinely needs to display the trust bundle (ca.crt), publish that single field to a separate ConfigMap (e.g. via a small helper template that reads the Secret in a post-install Job and writes the CA cert into a ConfigMap, or via additionalOutputFormats on the Certificate object) and grant get on that ConfigMap instead. Under no circumstance should tls.key reach a use-level subject.

Non-blocking follow-ups

  1. dashboard-resourcemap.yaml exposes the TLS secrets in RBAC, but packages/system/mariadb-rd/cozyrds/mariadb.yaml doesn't list them under secrets.include — the two are out of sync. Once B3 is resolved by replacing the secret with a ConfigMap, cozyrds/mariadb.yaml is the place to add it under configMaps.include (or remove the dashboard reference entirely).

  2. certmanager.yaml:35-37: CA usages [signing, cert sign] cover the issuance path. Adding crl sign is conventional (even if no CRL is published today) and costs nothing.

  3. The SAN list (certmanager.yaml:69-78) covers the three FQDN forms per service plus the wildcard pod-FQDN for -internal. Upstream operator docs list a few extra short forms (<name>.<ns> without .svc, *.<name>-internal without <ns>.svc). Not needed in practice unless clients resolve via ndots-shortened forms, but worth adding for full alignment with upstream's documented set.

If the same certmanager.yaml pattern (B2) and dashboard RBAC pattern (B3) repeat across the sister TLS PRs (#2681 Kafka, #2682 OpenSearch, #2683 RabbitMQ, #2684 NATS, #2685 Qdrant), the fixes should land in lockstep — those PRs will likely surface the same two blockers once reviewed.

@Arsolitt

Copy link
Copy Markdown
Contributor Author

Addressed your review. Summary:

Blockers:

  • B1 (backup CronJob breaks under tls.required: true) — fixed in 88be3fc1e. The CronJob now projects <release>-tls's ca.crt into /etc/mysql-ssl/, and mysql / mysqldump conditionally append --ssl-ca=/etc/mysql-ssl/ca.crt --ssl-mode=VERIFY_IDENTITY when TLS is enabled. The cert SAN list already covers <release>-primary and <release>-secondary (the Service names backup uses via MYSQL_HOST), so VERIFY_IDENTITY succeeds. Backup remains plaintext when TLS is off — no regression for existing tenants.
  • B2 (leaf cert missing digital signature) — fixed in a7417e81b.
  • B3 (dashboard RBAC exposes both <release>-tls and <release>-ca-tls) — accepted as a series-wide tradeoff for this iteration. Tenants retrieve ca.crt via the same RBAC; the leaf-key exposure is the cost of avoiding a separate CA-only ConfigMap distribution mechanism. To revisit when a cluster-wide ca-bundle pattern lands.

Non-blocking follow-ups:

  • FU1 (cozyrds out of sync with dashboard RBAC) — tied to B3 decision, no change in this PR.
  • FU2 (crl sign on CA usages) — added in 4c1792e2d.
  • FU3 (short-form SAN variants <name>.<ns> and *.<name>-internal) — added in 87f3e0877.

Series-wide cleanups applied here too:

  • Schema cleanup (8798c26d0): dropped ["boolean","null"] from tls.enabled — that array form silently broke unmarshal into apiextv1.JSONSchemaProps (single-string Type) and disabled server-side defaulting. Template now uses kindIs "invalid" for tri-state null-detection.
  • Private keys unified to ECDSA P-256 for both CA and leaf in a8d14125e. Note for upgrade path: cert-manager will reissue the chain on first reconcile, so existing tenant trust anchors must be refreshed.

Ready for re-review.

@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 7c51f41a. Two blockers remain. One of them reopens a point I previously marked resolved: the TLS backup fix does not work against the client the backup image actually ships.

Business context: first-class TLS for the managed MariaDB service via a self-contained cert-manager chain (SelfSigned → CA → leaf) in the tenant namespace, with a tri-state tls.enabled that auto-enables for externally-published instances.

Blockers

B1 (reopened): the TLS backup path uses a client flag the backup image does not support
File: packages/apps/mariadb/templates/backup-script.yaml:15
Issue: with TLS on, the script sets SSL_OPTS="--ssl-ca=/etc/mysql-ssl/ca.crt --ssl-mode=VERIFY_IDENTITY" and passes it to mysql/mysqldump. --ssl-mode is an Oracle MySQL-8 client flag; the backup image (images/mariadb-backup/Dockerfile: alpine:3.20 + apk add mariadb-client) ships MariaDB's own client, which has no such option.
Evidence: running the image base — mysqldump --ssl-mode=VERIFY_IDENTITY on mariadb-client 10.11 → mysqldump: unknown variable 'ssl-mode=VERIFY_IDENTITY'. The client fatal-errors on the unknown variable before connecting, so the DB_LIST=$(mysql ... $SSL_OPTS ...) discovery and every subsequent mysqldump fail. The helm-unittest suite (tests/tls_test.yaml:859-872) only asserts the rendered script contains the string --ssl-mode=VERIFY_IDENTITY, so it is green while the runtime is broken. My earlier acceptance of this fix was wrong — the flag as written cannot work against this client.
Impact: every TLS-enabled backup job fails before dumping a single database — silent backup loss for exactly the external-published + backup-enabled cohort (where tls.required: true auto-applies).
Fix: use the MariaDB-client equivalent: SSL_OPTS="--ssl-ca=/etc/mysql-ssl/ca.crt --ssl-verify-server-cert" (--ssl-ca implies --ssl; --ssl-verify-server-cert adds the hostname check that VERIFY_IDENTITY provides). Update the test to assert the corrected flag.

B2: dashboard RBAC grants the CA and server private keys to every tenant use member
File: packages/apps/mariadb/templates/dashboard-resourcemap.yaml:22-23
Issue: with TLS on, the -dashboard-resources Role grants get/list/watch on Secrets <release>-ca-tls and <release>-tls, bound (line 39) to cozy-lib.rbac.subjectsForTenantAndAccessLevel "use" — every tenant member at use or above. Both are cert-manager kubernetes.io/tls Secrets carrying tls.key: -ca-tls is the CA secret (isCA: true) so its tls.key is the CA private key; -tls is the leaf with the server private key.
Evidence: certmanager.yaml CA secret (isCA: true) and leaf secret; Role rule at dashboard-resourcemap.yaml:18-25; RoleBinding at :39. RBAC is per-Secret and cannot scope to individual keys, so get exposes tls.key.
Impact: any tenant member can read the CA private key and mint certs the whole --ssl-ca trust chain accepts (MITM every MariaDB client), or read the server key to impersonate the server / decrypt non-PFS sessions.
Fix: remove both -ca-tls and -tls from the Role. If tenants need the trust bundle, publish only ca.crt to a separate ConfigMap (e.g. via trust-manager) or a ca.crt-only Secret, and grant read on that. tls.key must never reach a use-level subject. This is the same defect as the sibling rabbitmq/mongodb TLS PRs — a single shared fix (ca.crt-only trust anchor) would close it across the series.

Non-blocking follow-ups

  1. cozyrds/mariadb.yaml keysOrder lists ["spec","tls","required"] but not ["spec","tls","enabled"], so the dashboard can't toggle tls.enabled. This is a faithful regeneration (the field is a nullable *bool with no default) and matches the rest of the TLS series — a cross-PR follow-up if dashboard toggling is intended, not a mariadb-specific blocker.
  2. Inter-node and SAN coverage is thorough and matches the operator's -internal headless service; no SAN gap once B1's flag is corrected.

@github-actions github-actions Bot removed the size/XL This PR changes 500-999 lines, ignoring generated files label May 28, 2026
The dashboard Role already let the tenant read <release>-ca-bundle, but
the ApplicationDefinition did not list it, so it was never marked as a
tenant resource and never appeared through the API. The documentation
tells clients to fetch exactly that Secret, which worked with kubectl and
not in the interface the platform actually gives tenants.

Name it in secrets.include, the same way kafka names its CA certificates.
This works against the current API, unlike the label selector beside it,
which stays for the projection an unmerged controller will publish — so
the trust anchor is reachable now rather than only after that lands.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
Uninstall removes the cert-manager Secrets now, but turning managed TLS
off while the instance keeps running does not: the post-delete hook does
not run on an upgrade, and cert-manager does not own what it issued. The
note said the Secrets stay behind without saying what to do about it.

Not automating this on upgrade is deliberate. A hook that deletes Secrets
on every reconcile, including for instances that never had TLS, is a
worse thing to get wrong than an inert Secret that no tenant grant
exposes and nothing reads.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
The guard tested only readability, so a present-but-empty ca.crt passed
it and the run continued into exactly the unnamed client error the guard
was added to replace. Empty is reachable rather than theoretical: the
operator omits expired certificates when it assembles the bundle, so a CA
set that has fully expired leaves the file in place with nothing in it.

Check size as well, and say "missing or empty" so the message matches
both cases. Verified by running the rendered script against an absent and
an empty file.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
The comments described the CA-extraction controller in the present tense,
as something that copies the certificate today. It is not in this repo:
the controller, the spec.caCert field it reads, and the tenant-ca label
all arrive with a separate change, and against the current API the block
is pruned on apply. A reader would have gone looking for a controller
that is not there.

State that it is inert until that lands, why it is declared now anyway,
and that the tenant reaches the anchor meanwhile through the plain name
grant beside it, which needs nothing new.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
…rsion

The text said --ssl-ca alone leaves the hostname unchecked, without
saying for which client. Measured both: the 10.11 client the backup image
ships defaults --ssl-verify-server-cert to off, the 11.x client defaults
it on. The sentence was therefore right for one and wrong for the other,
in the paragraph a reader uses to decide whether their connection is
verified.

Also record why the tenant reaches the anchor through a Role here rather
than through the shared trust-anchor helper: that helper renders the
Secret from a CA supplied as a template value, and this CA only exists at
runtime, so the chart has no PEM to give it.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
The kindIs "invalid" guard on required had no coverage, and writing one
showed why: the schema types the field as a boolean and rejects null
before the template runs, so the guard is defensive rather than a live
path. Assert the rejection instead of inventing a rendering that cannot
happen, which also catches the schema being relaxed later.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
… operator

The comment said the operator offers no way to extend its SAN list, as a
standing fact. It is only true of the version pinned here: upstream has
since added serverCertAdditionalNames, which its SAN builder appends. Left
as an absolute, the next operator bump would inherit the workaround
without anyone reconsidering it.

Say which version the limitation belongs to and what to check at the next
bump, since that is the point where handing the leaf back to the operator
becomes possible and most of this template can go.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
The note said the hostname check is off on 10.11 and on across 11.x. The
default actually changed in 11.4. Measured all four versions this chart
offers: 10.6 and 10.11 leave it off, 11.4 and 11.8 enable it. Name the
versions rather than the series, since the reader is choosing between
exactly those.

Scope the key-bearing-Secret guard to the secrets block as well. It
scanned the whole file, so a Service whose name ends in -tls would have
tripped it while a Secret entry with a trailing comment slipped past —
the wrong answer in both directions for the check that decides whether a
private key can reach a tenant.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
…teardown

Two claims of mine were wrong about mechanism.

The empty-selector warning said such a selector would project every Secret
in the namespace. The lineage webhook computes those labels only for
objects whose ownership resolves to the application, so the reach is the
instance's own Secrets. Still the thing to prevent — that set contains
-ca-tls and -tls — but the stated reason should match the code, since it
is what a reader checks the guard against.

The teardown step told the user to delete both Secrets with no ordering.
The server keypair stays mounted in the running pods until the operator
reissues from its own CA and rolls the StatefulSet, so following that
literally leaves the next pod unable to start. Wait for the rollout
first.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
…ender

These strings flow into the ApplicationDefinition schema and become the
help text beside the fields in the console. They had grown into
paragraphs of rationale — 704 and 613 characters against 48 for the
neighbouring external — which is the wrong length for a form label and
duplicated what the README already explains at the right length.

Say what the field does and that it is opt-in; the reasoning stays in the
README, which is where a reader goes for it.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
Four claims of mine were broader than the thing they described.

The bats header said it pins the invariants deciding whether a private
key can reach a tenant. Three of its tests touch the declaration that the
API does not accept yet, so they pin an intended spelling and nothing
more; if the field lands under another name they stay green while the
feature does nothing. Say which tests are live and which are placeholders.

The certificate comment said the operator skips reconciliation entirely.
It skips issuance, which is why SANs go unvalidated, but it still reads
the Secret for the pod annotation hash and status.

The README mapped the client's verification default onto the version
enum, which selects the server image. The default belongs to the client
library, so an 11.4 client verifies against a 10.6 server.

The backup guard is described as catching a missing or unusable bundle.
On a fresh managed-TLS install the bundle can be present and non-empty
while holding only the operator's client CA, and the failure lands at
verification instead. Note the window, and note in the README that a new
instance stays down until cert-manager issues.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
tls.enabled read as a switch and was not one — TLS is served under every
value, and what the field actually chose was which authority issues the
server certificate. The same key means on/off in other charts here, so a
tenant carrying that habit across would have read this exactly backwards.

Replace it with tls.issuer: operator | cert-manager, defaulting to
operator. The value now states the behaviour instead of needing prose to
undo the name: operator is what runs today, so the fact that nothing
changes on upgrade is visible in the default rather than argued in a
comment. Being an enum, a typo is rejected by the schema, which a bool
could not do.

The explanation the old name required is gone with it — the helper
docblock and the CR comment lose the paragraphs that existed only to say
this was not a switch, and the README states the split in a table.

Not marked breaking: the field is introduced by this series and does not
exist on the target branch, so there is no released spelling to migrate
from. Breaking is measured against what is merged, not against the
intermediate history of the branch.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
…n exclude

The include selector matches by label and carries no name constraint, so
any Secret of this instance that acquires the tenant-ca label is promoted
to the tenant with its data copied verbatim. The label on the source is
policy-guarded; the one on the destination is not. Exclude is evaluated
first and short-circuits, so naming the dangerous Secrets closes the part
of that gap we can enumerate.

Listed by group with the reason each is dangerous: the private keys from
both issuers, the internal database credentials, the backup S3 keys and
restic password, and the registry pull secret.

The guarantee is only as wide as the list. Matching is by exact name with
no globs, so a Secret added by a future operator version, or one created
transiently during a rotation, is not covered — the protection that holds
in general is still that only the extraction controller writes the label.
mariadb-operator has no rotation-snapshot Secret of the kind that makes
this worse elsewhere; its Secret names are fixed.

The existing guard against a key-bearing name is rescoped to
secrets.include while here, since such a name under exclude is the
backstop working rather than a leak.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
The note explaining why this chart spells the field `tls.issuer` called
the other charts' switch `tls.issuer` as well, which inverted it: it read
as though those charts use this same key to mean on/off. They spell it
`tls.enabled`.

Restore that name and hedge the claim to "some other charts". The key is
not uniformly a switch elsewhere either — one chart uses it to select
whether a hostname is added to a certificate its engine serves regardless
— so an unqualified "other charts" overstates how settled the meaning is.

Link the epic tracking the convergence of the TLS surface, so the
divergence the note describes points somewhere instead of only being
asserted.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
… claim

Renaming the field left the suite describing the design it replaced:
titles about keeping TLS off, comments about a tri-state falling through
to external inheritance, one case named for a boolean value the enum no
longer has. The suite is where the contract is read, so it was still
teaching the old one.

The migration note also claimed the backup client's new verification is a
live change to running jobs. It is not: that CronJob reads the root
password from a Secret name the chart does not render, so it cannot start
in the first place. Say that, and point at the supported backup flow
instead of implying the deprecated one works.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
The awk that was supposed to read secrets.include reset on any two-space
key and then re-entered on the next four-space include:, so it swept
services.include as well. It passes today only because no Service happens
to end in -tls — the exact case the comment promised it handled would
have misfired on a Service name. The exclude reader had the same shape.

Gate both on the secrets: section header instead of on indentation alone.
Verified in both directions: a Service named -tls no longer trips the
guard, and a Secret named -tls under secrets.include still does.

Also correct which CA the backup client verifies against: the bundle
carries the chart's CA too under the cert-manager issuer, which is what
makes the unconditional --ssl-ca right in both configurations.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
The eight cases covering the backup client's TLS flags read as coverage
of a live path. They are not: that CronJob cannot start, because it reads
the root password from a Secret name and key the chart does not render,
and the mismatch predates this branch.

Keep the cases — the flag defect was real, and they are what will stop
--ssl-mode returning whenever the job is repaired or dropped — but say in
the header what they actually pin, so the suite does not quietly claim a
path it never exercises.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
…ines

The CA source was spelled `{{ .release }}-ca-bundle`. The webhook builds
the only context these templates are rendered against, and it carries
kind, name and namespace — no release. The template resolves to nothing,
so the name would come out as `-ca-bundle` and match no Secret.

The exclude entry a few lines down already names the same Secret the way
that works, `mariadb-{{ .name }}-ca-bundle`: the release prefix is a
literal and the instance name is the templated part. Spell the source the
same. One Secret described two ways in one file meant one of them was
wrong, and it was this one.

The declaration is inert until the extraction controller lands, so this
changes nothing that runs today. That is also why it could sit here
unnoticed: the test pinned the broken spelling and passed on it, which
would have made the name wrong exactly when it first started being read.
Pin the working spelling instead, and add a guard that no undefined key
is templated into a Secret name.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
…tion

The exclude backstop enumerated the internal credential Secrets but
omitted `-metrics-config`. The operator writes it for every instance —
`MetricsConfigSecretKeyRef` at the pinned 25.10.2 fixes the name as
`<name>-metrics-config` — and its `exporter.cnf` carries

    [client]
    user=...
    password=...

in plaintext. The exporter user is granted SELECT, PROCESS, REPLICATION
CLIENT, REPLICA MONITOR and SLAVE MONITOR on `*.*`, so that password
reads every table in every database.

It is not a hypothetical Secret: the chart sets `metrics.enabled: true`
unconditionally, so it exists on every ready instance. The include side
selects by label with no name constraint, and a nil `resourceNames`
matches everything, so the exclude list is what keeps it away from the
tenant. Exclude beats include, so naming it there is sufficient.

This is inside the enumerable part of the gap, not the part the comment
already disclaims: that carve-out covers Secrets a future operator adds
or creates mid-rotation, and this one exists today under a stable name.

Also complete a sentence in the suite that lost the words naming which
issuer it describes.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Both comments justify a security decision, and both overstated it.

The exclude list described `-ca`, `-server-cert` and `-client-cert` as
what the operator creates "when it issues its own" certificates. Only
`-server-cert` is conditional. The chart never sets the client refs, so
at the pinned 25.10.2 `WithCA`/`WithCert` gate on those refs being nil
and the operator issues the client leg — and `-ca`, which carries the CA
private key — under both issuers. The list was already right; the
comment invited a future reader to prune two live private-key names as
inapplicable under cert-manager.

The watch label was described as the difference between picking up a
cert-manager renewal and waiting for the next full resync. The operator
requeues every five minutes whenever TLS is on, so the label buys
promptness, not correctness. Say that, and note the operator sets the
same label on Certificates it builds itself, which is why the chart
mirrors it here.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
The CA source name was rewritten to a literal release prefix on the
premise that no release variable exists in the template context. That
premise came from the lineage webhook, which renders a different field:
resourceNames in the secrets selectors, where the context really is only
kind, name and namespace.

caCert.sourceSecretName is rendered by the CA-extraction controller
instead, and that one supplies a release variable explicitly. It also
parses with missingkey=error, so an undefined key would fail loudly
rather than resolve to an empty string — the silent collapse the rewrite
was meant to prevent could not have happened.

Restore `{{ .release }}-ca-bundle`. It is also the better spelling on its
own: the literal hardcodes an assumption about the release prefix and
would point at a Secret that does not exist if that prefix ever changed,
while the variable is the release name by construction. The reference
implementation for this field spells it the same way.

Drop the guard that forbade the variable. The controller's own tests pin
that spelling as correct, so the guard would have started failing the
canonical form the moment the field became live.

Keep the test pinning the source name, and say in it which renderer this
field belongs to, so the two contexts are not conflated again.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
The guard asserting that the trust anchor is surfaced to the tenant
grepped the whole file. The bundle is also named in a comment and in the
caCert declaration, so either of those could satisfy it while the
secrets.include entry it exists to protect was gone. Scope it to the
include block, the way the sibling key-bearing-Secret guard already does.
Verified by deleting the include entry with a matching string left
elsewhere in the file: the guard now fails, where before it passed.

The suite header described a comment as stating that spec.tls.issuer is
true. There is no such field on the MariaDB CR — issuer is a chart value,
and what the chart renders is spec.tls.enabled, which is what the rest of
that sentence already describes. Name the field the assertions actually
check.

Also drop the claim that an 11.4 client verifies by default. The default
that changed in 11.4 is the --ssl-verify-server-cert flag, not whether
verification happens: without --ssl-ca that client has no trust anchor,
and falls back to an anti-tampering check derived from the password hash
which establishes neither the chain nor the hostname, and which cannot be
derived at all for a passwordless user. Passing both flags is what gives
chain and hostname verification, on either client version. The documented
command was already correct; the sentence around it invited dropping the
flag that carries it.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
The backstop guards grepped the whole file for each name. Both lists sit
at the same indentation, so an entry in `include` satisfied a guard that
exists to prove the name is in `exclude` — and moving a name between them
is the single most dangerous edit this file allows, since exclude is what
keeps a Secret away from the tenant.

Moving `-ca`, the operator's CA private key, from exclude to include left
all ten guards green. So did moving `-root`. Extract the exclude block and
match inside it; both mutations now fail on the name that moved, and the
suite is unchanged otherwise.

Also correct two descriptions the TLS work outdated. The cleanup hook's
Role comment still said the rule covered two operator-generated Secrets;
it now covers four, and two of them are issued by cert-manager, which the
block above already explains. The runtime message called them all
operator-generated for the same reason.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Three assertions matched text that survived the mutation they exist to
catch.

The backup CA guard was pinned by three separate substrings: the two test
expressions and the message. Flipping its exit status to 0, or short
circuiting the condition with `false &&`, leaves all three intact and the
suite green — while the backup runs on with --ssl-ca pointing at a file
that is missing or empty, and in the exit-0 case reports success having
dumped nothing. Match the whole construct through its exit instead.

The exclude entries were checked by name without checking they still sit
under resourceNames. A nil resourceNames matches every name, so dropping
that key turns the backstop into a match-everything selector while each
name is still present in the block. Assert the key.

The leaf certificate pinned renewBefore but not duration. cert-manager
rejects a duration that does not exceed renewBefore, so pinning one of
the pair lets the other drift into a certificate that renders but cannot
be applied.

Each is verified by the mutation it was blind to: exit 1 to exit 0, a
short-circuited condition, a renamed resourceNames key, and an 8760h
duration shrunk to 1h all now fail the suite.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
…ledger

The backup CA case is called "checks the CA file exists and names it" but
only guarded the first half. Deleting the Secret name from the error left
the suite green, so the reason MYSQL_CA_SECRET is passed to the CronJob at
all — telling the operator which Secret to go look at, since the client
reports a missing CA without naming a file or its source — was unpinned.

The cozyrds suite header also counted its own coverage wrong. It claimed
two live tests and three placeholders; there are seven and four. The four
exclude guards are live and went unmentioned, and the case reading the
source key is inert like its neighbours but carried no placeholder marker.
That header is the file's honest-coverage statement, so an understated
ledger is the one error it cannot afford.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
@lexfrei Aleksei Sviridkin (lexfrei) added the do-not-merge/hold Indicates that a PR should not merge because someone has issued /hold label Jul 19, 2026
@lexfrei

Copy link
Copy Markdown
Contributor

Both blocking items are closed on f389ca3c, along with the follow-ups. Where each one stands:

Flagged Now
Backup client passes --ssl-mode=VERIFY_IDENTITY, which the MariaDB client rejects at argument parsing --ssl-ca + --ssl-verify-server-cert, the MariaDB spelling of the same intent (9bce599b)
Backup CronJob carries no TLS options at all, so it fails once the server enforces TLS The operator's -ca-bundle is projected into the job and the options apply unconditionally (5f615a2e, f384108f)
Dashboard Role hands use-level tenants the CA and server private keys The Role names only -credentials and the key-free -ca-bundle (2d26f7b2)
Leaf certificate missing digital signature usages is now server auth, digital signature (827ff10a, narrowed in b408dc88)
Dashboard RBAC and the ApplicationDefinition disagree on which TLS Secrets exist Both name -ca-bundle; every key-bearing name sits in secrets.exclude
crl sign missing from the CA usages Added (cafca53d)
Short-form SAN variants for ndots-shortened clients Added (bdf28bac), covering all four resolution depths

The client-flag fix was checked by running the pinned image base rather than by reading documentation: mysqldump --ssl-mode=VERIFY_IDENTITY returns unknown variable 'ssl-mode=VERIFY_IDENTITY', and --ssl-verify-server-cert is accepted. The test asserts the MariaDB dialect instead of the string it asserted before, which was green while the runtime was broken.

The certificate narrowing is worth calling out because it removes usages rather than adding them. client auth and key encipherment are both gone: the operator issues its own client certificates and never delegates that leg, so a server key with client auth could authenticate as a user declared REQUIRE X509, and key encipherment covers RSA key transport, which means nothing for a P-256 key.

One item is obsolete rather than fixed — the missing keysOrder entry for the TLS child field. keysOrder is generated from the key paths present in values.yaml (hack/update-crd.sh:101), not from the schema, so a field documented with @field but carrying no value never appears there. make generate reproduces the current file with no drift, so hand-adding the entry would be reverted on the next run. Giving tls.required a value would fix it mechanically, but the schema default would then be stamped into every existing instance by server-side defaulting, and those instances would no longer follow a later change of the chart default — so it stays absent deliberately.

The surface also changed since those reviews, which retires the premise behind several of the original points: tls.enabled no longer exists. TLS is always served, tls.issuer selects who issues the CA (operator or cert-manager), and nothing is derived from external any more — switching issuer re-issues under a new authority and breaks clients that pinned the previous ca.crt, so it is chosen rather than applied on upgrade. tls.required stays opt-in.

The comment claimed the block is pruned on apply. It is not: helm-controller
builds its server-side-apply patch against the schema the API server
publishes, so an undeclared field fails that patch with "field not declared
in schema" before any request reaches the server, and the HelmRelease fails
as a whole.

The distinction is load-bearing. It is no install rather than a degraded
one, which makes landing the CRD field a mechanically forced prerequisite
rather than a sequencing preference.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
…pply

The rejection happens at the API server's field manager under Flux v2.8's
Server-Side Apply, not client-side in helm-controller before the request is
sent — the run's own error carries an ErrStatus with code 500, so the request
did arrive. Under v2.7's client-side patch the undeclared field was dropped
silently and the block was genuinely inert, which is where the earlier wording
came from; v2.8 turned the same shape into a failed apply.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
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.

2 participants