Repository navigation
feat(mariadb): add TLS support via cert-manager - #2680
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 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: LLM
Signed-off-by: Aleksei Sviridkin <[email protected]>
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: LLM Signed-off-by: Aleksei Sviridkin <[email protected]>
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: LLM Signed-off-by: Aleksei Sviridkin <[email protected]>
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: LLM Signed-off-by: Aleksei Sviridkin <[email protected]>
…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: LLM Signed-off-by: Aleksei Sviridkin <[email protected]>
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: LLM Signed-off-by: Aleksei Sviridkin <[email protected]>
…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: LLM Signed-off-by: Aleksei Sviridkin <[email protected]>
…by name The dashboard Role and the ApplicationDefinition granted the operator's key-free mariadb-<name>-ca-bundle Secret directly by name. Every other converged engine (mongodb, postgres, nats, qdrant) instead renders a TenantProjection that lifts ca.crt into the canonical <release>.tenant-ca object, reached through core.cozystack.io/tenantsecrets rather than a per-Secret grant. Add templates/tenant-projection.yaml sourcing from the operator's ca-bundle, drop the name grant from the dashboard Role and from the ApplicationDefinition's secrets.include (the existing tenant-ca matchLabels selector now has something to match), and remove the ApplicationDefinition's caCert field, which named a field the ApplicationDefinition CRD does not define and would have rejected the whole object under server-side apply. Update the README and the pinned RBAC tests to the new tenant-ca name. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <[email protected]>
The file asserted that the ApplicationDefinition declares the CA source itself -- sourceSecretName, sourceKey -- and that the operator's ca-bundle is granted by name in secrets.include. The chart renders none of that any more. The trust anchor now reaches the tenant as the key-free "<release>.tenant-ca" projection: a TenantProjection sentinel in the chart names the extraction source, and the ApplicationDefinition selects the result on internal.cozystack.io/tenant-ca. Three assertions therefore pinned fields the file no longer contains, and a fourth pinned a by-name grant this design deliberately withholds. Pin the grant surface instead, the way nats-rd and mongodb-rd do: compare the whole value of secrets.include against the credentials Secret plus the tenant-ca selector, so neither an added name nor an added label slips past a per-dimension check, and assert that neither the credentials Secret nor the projection is named in exclude, which is evaluated first and wins. The source declaration stays pinned where it is rendered, by packages/apps/mariadb/tests/tenant_projection_test.yaml, which compares sourceSecretName against an exact key-free name -- a stronger guard than a grep here could make, since this file never sees the chart. Every assertion guarding a key-bearing Secret is kept. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <[email protected]>
The publication guards asserted notExists on the labels internal.cozystack.io/publish-ca-cert and its -release variant. Neither name exists anywhere in the tree. The chart renders only k8s.mariadb.com/watch into secretTemplate.labels, so a notExists on a path no template can produce holds under every value, and the two cases titled for a CA publication source could not fail for what they named. The labels that decide tenant visibility are tenant-ca, which the ApplicationDefinition's secrets.include selects on, and tenantresource, which tenantsecrets serves on. Both are now asserted absent on each key-bearing Certificate. Exclude-by-name in the ApplicationDefinition stays the backstop behind them, not a substitute. The documents are addressed by the Secret each Certificate writes plus an explicit kind rather than by documentIndex, because an index follows the slot: reordering the template would slide a labelled key-bearing Certificate out from under the assertion and pass green. metadata.name cannot serve, as the CA Certificate and the CA Issuer share the name <release>-ca. The operator-issuer case promised a label check its body never made; it asserts only that the template renders nothing. Under that issuer no Certificate exists to label, so zero documents is the whole statement available, and the title now says that instead. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <[email protected]>
… certificates The comment sent the reader to certmanager_test.yaml, which this chart does not have. Its cert-manager coverage — the rendering of the CA and leaf Certificates under each tls.issuer value — lives in tls_test.yaml. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <[email protected]>
…nager Secrets The rule sold itself as least privilege while granting -ca-tls and -tls under every tls.issuer, including operator, where neither Secret is ever rendered. The comment gave no reason for that, which reads as an oversight in a rule whose whole point is scope. Unconditional is what the template's own logic supports, so the reason is now written rather than the grant narrowed. The delete above is unconditional too and leans on --ignore-not-found: a release issued by cert-manager and later switched to tls.issuer: operator still has both Secrets on the cluster, and uninstall renders the Role from the values current at that moment. Gating the names on the issuer would drop the grant for exactly that release, failing the delete on RBAC and leaving the CA and server private keys in a namespace the tenant shares with its other applications. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <[email protected]>
The unit suite pins what the chart renders — the sentinel, its source name, its key — from the same assumptions the chart itself encodes, so the two agree by construction and neither can catch the other being wrong. Only a live mariadb-operator says whether <release>-ca-bundle is the right name, and only a live apiserver says whether the lineage webhook promotes the projection to something a tenant can read. Both converged engines already carry this step; mariadb had none. The fixture sets no tls.issuer, which is the case worth covering rather than a gap: TLS is always served and the projection template is ungated, so the operator reconciles the source here too. A negative satisfied by an absent label is also satisfied by a webhook that stamped nothing all run, so the credentials Secret leads as a positive control. For the same reason the key-bearing names are checked for private key material before their absence from the registry is required, the count of names that reached that check is asserted non-zero, and the read must answer NotFound specifically: an RBAC denial or an unavailable aggregated apiserver also makes it fail, and reading that as "safely absent" is how the check would stop discriminating without ever going red. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <[email protected]>
The migration procedure was headed "the order that avoids downtime" and closed by claiming step 3 is the only step that can refuse a connection. Step 1 restarts the instance: under cert-manager the CR gains serverCertSecretRef and serverCASecretRef, so the name of the Secret projected into the pods changes, and a pod-template change rolls the StatefulSet. The chart supports single-replica instances, and for those a rolling restart is plain downtime. The document already knew this in the other direction — the going-back section warns that deleting the Secret before the operator has reissued and rolled the StatefulSet leaves the next pod unable to start, and prints the rollout-status command — so the two halves contradicted each other. Both claims are now scoped to what they are true of: the order still matters, because it keeps enforcement behind the client migration, and step 3 remains the only step that refuses a client by policy rather than by restarting the server under it. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <[email protected]>
Both suites matched only the Secret name list in the hook's script, so replacing kubectl delete with kubectl get left all 106 cases green while the CA private key and the server private key survived the release. The verb is the claim these tests exist to make; the names alone are just text that happens to appear in a shell script. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <[email protected]>
The notContains element omitted optional: true, and contains compares each list element whole rather than as a subset, so the element it described equalled nothing the template can render. Mounting the server key Secret as the backup CA volume — the exact substitution the assertion names — left it green, and only the neighbouring positive caught the mutation. The comment two cases below already described this trap as handled, which is how it survived being reintroduced. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <[email protected]>
The exclude checks read .spec.secrets.exclude[].resourceNames[]?, which
sees only name-shaped entries. An exclude entry selecting by label was
therefore invisible to every one of them, and adding one revokes the
tenant's connection string and its trust anchor at once: matchName
returns true for every name when resourceNames is nil, and exclude is
evaluated before include and wins. The test that existed to stop exactly
that edit let it through.
The empty-selector scan keyed on the literal token matchLabels, so an
empty matchExpressions and a bare {} passed it anywhere in the file,
including under services, which the comment claimed the same pass
covered. Both are legal: the selector type inlines metav1.LabelSelector.
One more comment stated the mechanism backwards — an exclude entry that
lost resourceNames makes the backstop total rather than voiding it — and
the check under it required only that some entry carry the key, so an
added entry without one went unnoticed.
Comparing spec.secrets and spec.services whole closes all of it. The
comparison is against the rendered chart rather than the source file,
because templates/cozyrd.yaml globs cozyrds/* and ships every match:
reading one file leaves a second one, dotfiles included, granting
whatever it likes. A document count fails closed on that.
The two per-name tests are kept ahead of the comparison, subsumed but
more legible, because the runner stops at the first failure. The
block-extraction helper is gone: the cozytest runner injects return 0
before a column-zero brace, so its status was discarded there.
Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <[email protected]>
The case titled "renders regardless of tls.issuer" set issuer: operator, which is the chart default, so both sides of its claim were the same side. Hiding the sentinel behind the cert-manager gate — the one edit the title rules out — left it green. Being ungated is the projection's entire contract, so each state is now its own case: the default, operator, cert-manager, and an explicit null, which the issuer helper treats as unset rather than as a spelling of the default. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <[email protected]>
1bbe3df to
9ece9dc
Compare
The chart asks cert-manager for two Secrets and names them after the release: "<release>-ca-tls" for the CA and "<release>-tls" for the leaf. The release prefix is "mariadb-", so an application named foo produces mariadb-foo-ca-tls, and an application named foo-ca produces mariadb-foo-ca-tls as well. One release's CA private key and another release's server private key claim the same object. Two sibling applications in one tenant namespace are enough to hit it: whichever cert-manager writes last owns the Secret, and each instance then serves or trusts material issued for the other. The suffix set is what makes it ambiguous. It holds both "-tls" and "-ca-tls", and one is a tail of the other, so a hyphen leaves nothing to say where the release name ends. A dot does: an application name is validated as a DNS-1035 label and the release prefix as ^[a-z0-9-]*$, so neither can contain one, and "<release>.ca-tls" and "<release>.tls" split uniquely. Secret names are DNS-1123 subdomains, which admit the dot. Only the two suffixes this chart chooses move. The Certificate and Issuer object names keep the hyphen, because their suffixes are not a pair where one is a tail of the other, and the operator-managed "<release>-ca-bundle" that the trust-anchor projection reads is not this chart's to rename. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <[email protected]>
…esolves Two claims the chart makes about itself stopped being true when the anchor moved to a projection. certmanager.yaml said the ApplicationDefinition names the operator's key-free "<release>-ca-bundle" as the publication source. It does not: the TenantProjection this chart renders names it, and the ApplicationDefinition carries no name for it at all — it selects the published anchor by label. The ApplicationDefinition's own include comment then listed the two cert-manager Secrets among those the lineage webhook resolves to this instance. The webhook resolves an object through ownerReferences, or failing that the Flux helm label, and returns nothing when it has neither. A cert-manager Secret has neither while enableCertificateOwnerRef stays false, which is the upstream default this platform does not override, and is the same fact the uninstall hook relies on when it deletes those Secrets itself. What does resolve is the operator's own "-ca", "-server-cert", "-client-cert" and "-ca-bundle". The two names stay in exclude, because one setting flip is all it takes to make the sentence true. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <[email protected]>
The issuer resolver maps anything that is not exactly "cert-manager" to operator issuance, so a typo fails open: the instance keeps the operator's CA and nothing at render time reports that the value was not understood. The enum is the only thing that refuses it, and one kubebuilder marker generates that enum into both the chart's values.schema.json and the ResourceDefinition's copy, so a single lost marker opens both guards at once. The suite already pins the rejection for the sibling field on this object and left this one, the direction that fails quietly, uncovered. Removing the enum from the schema fails this case and no other. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <[email protected]>
The clause claimed the ApplicationDefinition names neither Secret. It names this one: "mariadb-<name>.ca-tls" sits in its secrets.exclude list, and the bats guard pins it there. What is true is narrower — neither is named as the source the anchor is published from — so say that and say where the name does appear, or the sentence invites a reader to conclude the ApplicationDefinition has no reference to the Secret at all. Rewrap the SAN-list paragraph's one 124-column line back into the block it sits in, which is otherwise at 78. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <[email protected]>
…oves them Both cleanup suites asserted the delete as "(?s)kubectl delete secret -n \S+.*<name>". Under (?s) the dot matches a newline, so the gap runs past the end of the command: any earlier "kubectl delete secret" satisfies the opening and the names may then sit on any later command in the script. The pattern never established that they are arguments of the delete at all. Moving the two cert-manager Secrets off the delete onto a "kubectl get", leaving the password delete above them untouched, keeps both suites green while the CA private key and the server private key outlive the release in a namespace the tenant shares with its other applications — the one outcome this hook exists to prevent. The comments beside the assertions already claimed the verb was pinned, so a reader trusting them had no reason to re-check; they stand unchanged because the patterns now do what they say. Pin the whole invocation with its argument list instead, spelling out the shell's line continuations rather than stepping over them. Whitespace and backslashes are the only separators the pattern admits, and no command can be written in those, so a match cannot leave the command it began in. That mutation now reddens both suites, as does dropping one name from the list, as does swapping the verb for a read. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <[email protected]>
IvanHunters
left a comment
There was a problem hiding this comment.
Verdict
LGTM with non-blocking notes
Reviewed at b24ed36f0 against merge-base 3a70f9142. The base-to-head render diff is purely additive on every corner I rendered. The emitted CR shape is accepted by mariadb-operator 25.10.2's validating webhook, and the CA bundle the tenant anchor is lifted from does pick up the chart's CA. That leaves three MINOR craft items and one uncovered surface: no test starts an instance on the cert-manager path.
Findings
[MINOR] packages/apps/mariadb/templates/mariadb.yaml:125, multi-paragraph # prose ships into live objects
Six touched templates gain narrative # comments (mariadb.yaml 41 lines, certmanager.yaml 72, cleanup-pvc.yaml 29, dashboard-resourcemap.yaml 15, backup-cronjob.yaml 16, backup-script.yaml 16). Unlike {{/* */}}, these are copied verbatim into rendered manifests, so they land in the MariaDB CR, the dashboard Role and the cleanup Job in every customer cluster, and in the stored Helm release manifest. They also rot independently of the code beside them. Keep a one-line pointer and move the reasoning to the docs site.
$ grep -cE '^[[:space:]]*#' rendered-base/default.yaml rendered/head-default.yaml
rendered-base/default.yaml: 59
rendered/head-default.yaml: 99
$ grep -cE '^[[:space:]]*#' rendered/issuer-cm.yaml
175
[MINOR] packages/apps/mariadb/values.yaml:75, external's description does not mention its new certificate role
external now feeds the leaf certificate SAN list (certmanager.yaml:152-155) on top of switching the Service to LoadBalancer. Its @param text is unchanged, and cozyvalues-gen copies that text verbatim into values.schema.json, the README parameter table and the -rd openAPISchema the dashboard renders. An operator reading only the parameter table learns that external: true exposes the instance. The table does not tell them that under the default tls.issuer: operator the certificate cannot carry the external hostname, so --ssl-verify-server-cert from outside fails no matter which CA is trusted. The README says this; the field description, which is what the dashboard shows, does not.
[MINOR] packages/system/mariadb-rd/cozyrds/mariadb.yaml:75, the exclude backstop misses one enumerable operator credential
The comment above the list calls it the enumerable dangerous set. Against the operator's own key table at the pinned version, one credential Secret name is absent: mariadb-<name>-agent-auth, the generated basic-auth password for the Galera agent sidecar (api/v1alpha1/mariadb_keys.go:361, mounted at /var/run/secrets/mariadb-operator/agent). It is latent, not live. This chart never sets spec.galera (templates/mariadb.yaml emits replication only), so the Secret is never created today. <release>-backup-script is also outside the list; it holds the script and no credentials, so it is only a completeness point. Adding agent-auth now costs a line and removes the coupling between the backstop and a spec field someone may enable later.
$ yq eval-all 'select(.kind=="MariaDB") | .spec | keys' rendered/default.yaml | grep -c galera
0
Claim mismatches
[PARTIAL] "Merge is blocked on #3299, the CRD on main does not carry spec.caCert". Stale as of this revision. grep -rn caCert over the head tree returns only internal/controller/cacert internals; the branch emits a TenantProjection sentinel instead, and packages/system/cozystack-controller/definitions/internal.cozystack.io_tenantprojections.yaml already exists at the merge base (git cat-file -e succeeds there). Nothing in the diff needs a CRD field that main lacks.
[PARTIAL] "The tenant no longer receives <release>-ca-tls or <release>-tls". Against main those Secrets do not exist and were never granted: the base cozyrd is exclude: [] with include: [mariadb-{{ .name }}-credentials]. The sentence describes a delta against an earlier revision of this branch.
[PARTIAL] The "Fixes along the way" section (the --ssl-mode=VERIFY_IDENTITY flag, the leaf clientAuth usage) reads as fixes to shipped behaviour. Both are intra-branch: the base backup-script.yaml passes no SSL options at all, and no leaf certificate exists on main. Worth rewording so a reviewer diffing against main is not looking for code that was never there.
Caveats
- The
tls.issuer: cert-managerpath has no live coverage, which the PR body states. I closed as much of it statically as the source allows. The webhook accepts a CR carrying both refs (internal/webhook/v1alpha1/webhook.go:26-49, the reject fires only whencaSecretRef == nil && certSecretRef != nil). The operator skips issuance entirely when bothshouldIssueCAandshouldIssueCertare false andcertIssuerRefis nil (pkg/controller/certificate/controller.go:67-91), so it never contends with cert-manager for those Secrets. AndTLSServerCASecretKey()returnsServerCASecretRef.Namewhen set (api/v1alpha1/mariadb_keys.go:92-107), soreconcileTLSCABundlefolds the chart CA into<release>-ca-bundleand the tenant anchor does verify the served certificate. None of that proves an instance reaches Ready and completes a handshake. That is the check to run on a disposable cluster before the path is advertised. - The SAN list is hand-maintained and currently exact. I compared it against
TLSServerDNSNames()at 25.10.2:ServiceNameVariantsyields the same four depths for<name>,-primaryand-secondary,HeadlessServiceNameVariants(meta, "*", "<name>-internal")the same four for the wildcard, pluslocalhost. The chart'scozy.localfallback matches bothnetworking.clusterDomain(packages/core/platform/values.yaml:62) and the operator'sclusterName(packages/system/mariadb-operator/values.yaml). An operator bump can silently break this, and as the template says, the unit suite compares the list against a copy of itself. - Applying the new
spec.tlsblock onto MariaDB objects that already exist was reasoned, not executed (no live cluster). The values are identical to what the operator's mutating webhook already stores:SetDefaultswrites&TLS{Enabled: true}whenspec.tlsis nil (api/v1alpha1/mariadb_types.go:835-839), andIsTLSRequiredderefs a nilRequiredto false (:923-930), sorequired: falseis the value already in effect. No field changes value, but live apply was not replayed. chart_lintreports one render error forpackages/apps/mariadb. It is a bootstrap stub artifact: the harness passes_clusteras a sequence, socozy-lib.images-registryfails onindex. With_clusteras a map the chart renders clean on every corner below.review-helper upstream-srccached the pinned operator tree outside the review prefix, at/var/folders/.../T/cozy-upstream/mariadb-operator-mariadb-operator-25.10.2. Extend the cleanup command if you want it gone.
Checked and correct
- Upgrade from N-1, rendered rather than reasoned.
git worktreeat the merge base against head, four corners (default,external=true,replicas=1,backup.enabled=true): the only identity delta on each is+ TenantProjection/mariadb-test-ca. Nothing renamed, nothing removed, no immutable break. - Fresh install. cert-manager is a hard dependency of the operator's own PackageSource (
packages/core/platform/sources/mariadb-operator.yamllistscozystack.cert-manager) and the system bundle emits it, so the Issuer and Certificate CRDs are served before any instance renders. - Configuration matrix. Eleven corners rendered: issuer unset /
operator/cert-manager, crossed withexternal,required,replicas=1,backup.enabled=true, andtls: null. Every one renders exceptissuer=cert-manager, external=true, _namespace=null, which is the intendedrequiredguard, and_namespace.hostis always emitted (packages/apps/tenant/templates/namespace.yaml:125), so that guard is not operator-reachable. - Test non-vacuity. Thirteen mutations, each in its own checkout, all covered: dropping
serverCASecretRef, hardcodingrequired: false, removing the dot from the leafsecretName, re-addingclient auth, removing the cert-manager names from the cleanup delete, dropping--ssl-verify-server-cert, repointing the projection source at<release>-ca, inverting the helper default, dropping the external SAN, dropping the watch label, and three on the cozyrd (dropping.ca-tlsfrom exclude, adding an unrestricted include, adding a label-only exclude). No gaps. make generateis a no-op on this tree:git status --porcelainis empty after running it, sovalues.schema.json, the README table, the-rdopenAPISchemaandkeysOrderare all current.- No reconcile amplifier. No
checksum/*annotation is added, and the operator's bundle builder dedupes by certificate ID (pkg/pki/pem.go:87-102), so-ca-bundleis stable across reconciles even though it reads itself. - Certificate rotation gets less frequent, not more. The chart's leaf is
duration: 8760hwithrenewBefore: 720h; the operator's own default isDefaultCertLifetime = 3 monthsat 33% renew-before (pkg/pki/x509.go:15-16,pkg/pki/renewal.go:9). Since the pod annotation hashes the server certificate (internal/controller/mariadb_controller_tls.go:207-218), an operator-issued instance already rolls roughly every two months; moving to cert-manager rolls it less often. tls.required: truedoes not strand the exporter. The operator writestls=trueplus the client cert into the exporter my.cnf wheneverIsTLSEnabled()(internal/controller/mariadb_controller_metrics.go:125-150), which is already true today.- The backup CronJob really cannot start. Its
MYSQL_ROOT_PASSWORDreadssecretKeyRef.name: {{ .Release.Name }}, the operator's root Secret is<name>-root(api/v1alpha1/mariadb_keys.go:13-23), and the rendered chart emits only-backup-script,-backupand-credentials. The newexit 1on a missing CA therefore reaches no running path. - The mechanical anti-pattern sweep is clean on the diff: no
|| true/|| :/2>/dev/nullon a decision command in the new shell, no mutating verb granted cluster-wide (the cleanup Role stays namespaced and name-scoped), and the new bats fixtures round-trip through the realyq/jq. - E2E selection reaches this change:
hack/select-e2e.shon the changed-file list emits the full suite,mariadbincluded, so the new chainsaw block runs. - No mutable-identity hazard.
tls.issuergates whether the Certificate and Issuer objects render; it never feeds ametadata.name, a selector, or a content hash, so flipping it prunes only cert-manager objects and leaves no data-bearing resource to be re-identified.
Recommended follow-ups
- Run
cozystack-pr-teston a disposable cluster withtls.issuer: cert-manager, bothreplicas=1andreplicas=2, and confirm the pods reach Ready and a client with the published anchor completes a verified handshake. That is the one surface no static check reaches. - Add
mariadb-<name>-agent-authto the cozyrd exclude list so the backstop stays complete if Galera is ever enabled. - The comment at
templates/backup-script.yamlsays a fully expired CA set yields "a present but empty ca.crt".BundleCertificatePEMsreturnserrors.New("no certificate PEMs were found")when every certificate is skipped (pkg/pki/pem.go:65-67), so the reconcile fails and the Secret keeps its previous content rather than going empty. The-stest is still worth keeping; the stated reason is not what happens.
Andrei Kvapil (kvaps)
left a comment
There was a problem hiding this comment.
LGTM. This lands the CA-consume contract the way #2811 settled it, and the two things I went looking for as risks both turn out to be handled deliberately.
The trust anchor follows the agreed shape. A TenantProjection of type: CACert over a key-free source, and the tenant reads it through the label-gated core.cozystack.io/tenantsecrets path rather than by name. The choice of <release>-ca-bundle as the source is the part worth keeping: it is already key-free, it exists under both tls.issuer values where a cert-manager Secret does not, and it carries the previous CA alongside the new one across a rollover, so the tenant reads one stable object regardless of which issuer is active. That is the CA-agnostic consume contract rather than a homogenised CA.
No TLS Secret is granted in the resourcemap, key-bearing or not — including -ca-bundle, on the grounds that a name grant conveys whatever occupies the name while the label path conveys only what the platform vouched for. That reasoning matches the mongodb chart almost word for word, which is what I want: one argument, applied the same way in both places.
TLS being served under both issuers is not a behaviour change, and the comment explains why in the one place it matters. mariadb.yaml sends an explicit tls.enabled: true because the operator substitutes an enabled spec only when spec.tls is absent entirely — so sending the block without it would have turned TLS off. That inverts the obvious reading of the diff, and it is the kind of thing that gets "simplified" later by someone who assumes the explicit true is redundant.
required is opt-in and typed to stay that way. Required *bool with no default keeps "unset" distinguishable from an explicit false, and the template's kindIs "invalid" guard preserves that distinction rather than collapsing it. The comment states that flipping it disconnects every client not yet on TLS and is therefore a decision rather than something an upgrade does, with the flip named as a future announced change. That sequencing is correct: serve first, enforce later, and never enforce as a side effect of an upgrade.
api/ is additive throughout — a new optional struct defaulting to {}, an enum defaulting to operator, and the pointer above. Existing MariaDB objects render unchanged.
CI is green including E2E on b24ed36f0.
One cosmetic note, not a request: the Go field is Tls where the repo's other initialisms would suggest TLS. The JSON tag is tls either way and the linter evidently does not enforce it, so this only matters if someone later turns that rule on.
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.Downstream repositories
packages/apps/mariadb/values.schema.jsongains thetlsobject, socozystack/terraform-provider-cozystackneeds the field modelled forcozystack_mariadb; tracked in cozystack/terraform-provider-cozystack#40.Release note