feat(mariadb): add TLS support via cert-manager - #2680
feat(mariadb): add TLS support via cert-manager#2680Arsolitt (Arsolitt) wants to merge 68 commits into
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds 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. ChangesMariaDB TLS Feature
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Summary of ChangesHello, 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
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
Using Gemini Code AssistThe 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
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 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
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
📒 Files selected for processing (12)
api/apps/v1alpha1/mariadb/types.goapi/apps/v1alpha1/mariadb/zz_generated.deepcopy.gopackages/apps/mariadb/Makefilepackages/apps/mariadb/README.mdpackages/apps/mariadb/templates/_tls.tplpackages/apps/mariadb/templates/certmanager.yamlpackages/apps/mariadb/templates/dashboard-resourcemap.yamlpackages/apps/mariadb/templates/mariadb.yamlpackages/apps/mariadb/tests/tls_test.yamlpackages/apps/mariadb/values.schema.jsonpackages/apps/mariadb/values.yamlpackages/system/mariadb-rd/cozyrds/mariadb.yaml
5c6adbf to
b23ead9
Compare
There was a problem hiding this comment.
♻️ Duplicate comments (1)
packages/system/mariadb-rd/cozyrds/mariadb.yaml (1)
28-28:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd nested TLS entries to
keysOrderfor stable dashboard field ordering.The
keysOrderarray includes["spec","tls"]but omits["spec","tls","enabled"]and["spec","tls","required"]. This is inconsistent with the existing pattern used forbackup, 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
📒 Files selected for processing (12)
api/apps/v1alpha1/mariadb/types.goapi/apps/v1alpha1/mariadb/zz_generated.deepcopy.gopackages/apps/mariadb/Makefilepackages/apps/mariadb/README.mdpackages/apps/mariadb/templates/_tls.tplpackages/apps/mariadb/templates/certmanager.yamlpackages/apps/mariadb/templates/dashboard-resourcemap.yamlpackages/apps/mariadb/templates/mariadb.yamlpackages/apps/mariadb/tests/tls_test.yamlpackages/apps/mariadb/values.schema.jsonpackages/apps/mariadb/values.yamlpackages/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
Aleksei Sviridkin (lexfrei)
left a comment
There was a problem hiding this comment.
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-tlsinto the CronJob and pass--ssl-ca=/path/ca.crt --ssl-mode=VERIFY_CA(orVERIFY_IDENTITY) to bothmysqlandmysqldump. - Gate the
tls.required: truedefault onbackup.enabledbeing 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
-
dashboard-resourcemap.yamlexposes the TLS secrets in RBAC, butpackages/system/mariadb-rd/cozyrds/mariadb.yamldoesn't list them undersecrets.include— the two are out of sync. Once B3 is resolved by replacing the secret with a ConfigMap,cozyrds/mariadb.yamlis the place to add it underconfigMaps.include(or remove the dashboard reference entirely). -
certmanager.yaml:35-37: CA usages[signing, cert sign]cover the issuance path. Addingcrl signis conventional (even if no CRL is published today) and costs nothing. -
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>-internalwithout<ns>.svc). Not needed in practice unless clients resolve viandots-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.
|
Addressed your review. Summary: Blockers:
Non-blocking follow-ups:
Series-wide cleanups applied here too:
Ready for re-review. |
There was a problem hiding this comment.
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
cozyrds/mariadb.yamlkeysOrderlists["spec","tls","required"]but not["spec","tls","enabled"], so the dashboard can't toggletls.enabled. This is a faithful regeneration (the field is a nullable*boolwith 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.- Inter-node and SAN coverage is thorough and matches the operator's
-internalheadless service; no SAN gap once B1's flag is corrected.
8798c26 to
7c51f41
Compare
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>
7c51f41 to
f389ca3
Compare
|
Both blocking items are closed on
The client-flag fix was checked by running the pinned image base rather than by reading documentation: The certificate narrowing is worth calling out because it removes usages rather than adding them. One item is obsolete rather than fixed — the missing The surface also changed since those reviews, which retires the premise behind several of the original points: |
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>
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.issuerselects who issues the server certificate:operator(the CA mariadb-operator manages itself, the default and today's behaviour) orcert-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.requiredrefuses 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.external. Existing instances keep the CA they have and change nothing on upgrade.<release>-ca-tlsor<release>-tls— the CA private key and the server private key. It receives the operator'sca.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
--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.clientAuth. BecauseREQUIRE X509accepts any certificate signed by a trusted CA, the server's own key was usable as a client credential. Removed.enableCertificateOwnerRef: false, so removing theCertificateleft the Secrets behind. Both are now cleaned up by the existing post-delete hook.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
SELECTon 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
maindoes not carryspec.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.secretKeyRefnames 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.clusterNameis hardcoded, so on a cluster with a non-default domain the SANs and the name the operator dials diverge. Pre-existing, raised separately.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
6cb1bd236and 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