From bafaca0991f024ee7ee87d869dbf798566355ecd Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Sun, 19 Jul 2026 02:25:24 +0300 Subject: [PATCH] fix(mariadb): use MariaDB TLS verification flags in backup script The backup image is alpine plus mariadb-client, so the backup script is parsed by the MariaDB client. That client implements no ssl-mode option: it is MySQL 8 syntax, and the MariaDB client rejects it while parsing arguments ("unknown variable", exit 7) before it ever opens a connection. Every TLS-enabled backup run therefore failed at the first client invocation. Replace it with the MariaDB equivalent, --ssl-ca combined with --ssl-verify-server-cert, which validates the chain against the CA and checks the server hostname against the certificate SANs. Also align the cluster-domain fallback in the certificate template with the rest of the catalog, which defaults to cozy.local rather than the upstream Kubernetes cluster.local. These names go into the leaf certificate SAN list that the backup client now verifies against, so a mismatched fallback would produce certificates that fail hostname verification. The previous assertions only checked that an SSL string appeared in the rendered script, so they stayed green against a script the client refuses to parse. They now pin the client dialect instead: the MariaDB verification flags must be present, the MySQL-only option must never be emitted in any TLS state, and both client invocations must carry the options. Signed-off-by: Aleksei Sviridkin --- .../apps/mariadb/templates/backup-script.yaml | 12 ++- .../apps/mariadb/templates/certmanager.yaml | 2 +- packages/apps/mariadb/tests/tls_test.yaml | 99 ++++++++++++++++--- 3 files changed, 97 insertions(+), 16 deletions(-) diff --git a/packages/apps/mariadb/templates/backup-script.yaml b/packages/apps/mariadb/templates/backup-script.yaml index 5b0715b7f..207bcca52 100644 --- a/packages/apps/mariadb/templates/backup-script.yaml +++ b/packages/apps/mariadb/templates/backup-script.yaml @@ -11,8 +11,18 @@ stringData: set -e set -o pipefail + {{- /* + The backup image is alpine + mariadb-client, so these options are parsed + by the MariaDB client, which implements no ssl-mode option and rejects it + at argument parsing time before connecting. --ssl-ca combined with + --ssl-verify-server-cert is the MariaDB equivalent of MySQL's strictest + mode: it validates the chain against the CA and checks the server + hostname against the certificate SANs. Keep this rationale in a template + comment rather than a shell comment so the MySQL-only option name never + appears in the rendered script. + */}} {{- if $tlsEnabled }} - SSL_OPTS="--ssl-ca=/etc/mysql-ssl/ca.crt --ssl-mode=VERIFY_IDENTITY" + SSL_OPTS="--ssl-ca=/etc/mysql-ssl/ca.crt --ssl-verify-server-cert" {{- else }} SSL_OPTS="" {{- end }} diff --git a/packages/apps/mariadb/templates/certmanager.yaml b/packages/apps/mariadb/templates/certmanager.yaml index 9547283c0..5c8c5ad04 100644 --- a/packages/apps/mariadb/templates/certmanager.yaml +++ b/packages/apps/mariadb/templates/certmanager.yaml @@ -1,6 +1,6 @@ {{- $tlsEnabled := (include "mariadb.tls.enabled" .) | eq "true" -}} {{- if $tlsEnabled }} -{{- $clusterDomain := (index .Values._cluster "cluster-domain") | default "cluster.local" }} +{{- $clusterDomain := (index .Values._cluster "cluster-domain") | default "cozy.local" }} --- # Self-signed bootstrap Issuer — used only to sign the CA Certificate below. apiVersion: cert-manager.io/v1 diff --git a/packages/apps/mariadb/tests/tls_test.yaml b/packages/apps/mariadb/tests/tls_test.yaml index 1d4f47b79..d9ed59bc2 100644 --- a/packages/apps/mariadb/tests/tls_test.yaml +++ b/packages/apps/mariadb/tests/tls_test.yaml @@ -851,27 +851,48 @@ tests: name: tls-ca # ------------------------------------------------------------------- - # E: backup script contains --ssl-mode=VERIFY_IDENTITY when TLS enabled - # VERIFY_IDENTITY is the strictest SSL mode — validates CA and hostname, - # matching the cert SAN list that already covers the primary/secondary - # service names used by MYSQL_HOST. + # Cluster-domain fallback. The platform injects _cluster.cluster-domain + # (networking.clusterDomain, "cozy.local"), but the chart-level fallback + # must match it: these FQDNs land in the leaf certificate SAN list, and the + # backup client verifies the server hostname against exactly that list. A + # fallback pointing at the upstream Kubernetes default instead of the + # Cozystack one would silently produce certificates that fail verification. # ------------------------------------------------------------------- - - it: "backup-script Secret contains --ssl-mode=VERIFY_IDENTITY when tls.enabled=true" + - it: "leaf certificate SANs fall back to cozy.local when cluster-domain is unset" templates: - - templates/backup-script.yaml + - templates/certmanager.yaml + documentIndex: 3 set: tls: enabled: true - backup: - enabled: true - s3Bucket: s3.example.org/test - cleanupStrategy: "--keep-last=3" + # helm-unittest deep-merges `set` over the suite defaults, so the key is + # blanked rather than removed to exercise the fallback branch. + _cluster: + cluster-domain: "" asserts: - - matchRegex: - path: stringData["backup.sh"] - pattern: "--ssl-mode=VERIFY_IDENTITY" + - contains: + path: spec.dnsNames + content: "my-mariadb.tenant-test.svc.cozy.local" + - notContains: + path: spec.dnsNames + content: "my-mariadb.tenant-test.svc.cluster.local" - - it: "backup-script Secret contains --ssl-ca=/etc/mysql-ssl/ca.crt when tls.enabled=true" + # ------------------------------------------------------------------- + # E: backup script uses MariaDB-native TLS verification flags. + # + # The backup image is alpine + mariadb-client, so the script is parsed by + # the MariaDB client, not the MySQL one. The two dialects differ: + # + # MySQL 8 : --ssl-mode=VERIFY_IDENTITY + # MariaDB : --ssl-ca= --ssl-verify-server-cert + # + # The MariaDB client has no --ssl-mode option at all and rejects it during + # argument parsing ("unknown variable 'ssl-mode=...'", exit 7) before any + # connection is attempted, so a single --ssl-mode occurrence fails every + # TLS-enabled backup run. These tests therefore assert the dialect, not the + # mere presence of some SSL string. + # ------------------------------------------------------------------- + - it: "backup-script Secret uses MariaDB TLS verification flags when tls.enabled=true" templates: - templates/backup-script.yaml set: @@ -882,9 +903,53 @@ tests: s3Bucket: s3.example.org/test cleanupStrategy: "--keep-last=3" asserts: + # --ssl-ca alone only supplies the trust anchor; without + # --ssl-verify-server-cert the MariaDB client skips hostname + # verification, which would silently downgrade the connection. - matchRegex: path: stringData["backup.sh"] pattern: "--ssl-ca=/etc/mysql-ssl/ca\\.crt" + - matchRegex: + path: stringData["backup.sh"] + pattern: "--ssl-verify-server-cert" + + # Regression guard: no MySQL-8-only option may reach the MariaDB client, in + # any TLS state. Anchored on the option name so both --ssl-mode=VALUE and a + # bare --ssl-mode are caught. + - it: "backup-script Secret never emits the MySQL-only --ssl-mode option (tls.enabled=true)" + templates: + - templates/backup-script.yaml + set: + tls: + enabled: true + backup: + enabled: true + s3Bucket: s3.example.org/test + cleanupStrategy: "--keep-last=3" + asserts: + - notMatchRegex: + path: stringData["backup.sh"] + pattern: "--ssl-mode" + + # Both client invocations must carry $SSL_OPTS. If only one does, that leg + # connects without TLS verification while the suite still looks green. + - it: "backup-script Secret passes SSL_OPTS to both mysql and mysqldump" + templates: + - templates/backup-script.yaml + set: + tls: + enabled: true + backup: + enabled: true + s3Bucket: s3.example.org/test + cleanupStrategy: "--keep-last=3" + asserts: + - matchRegex: + path: stringData["backup.sh"] + pattern: "mysql -u \"\\$MYSQL_USER\" -h \"\\$MYSQL_HOST\" \\$SSL_OPTS" + - matchRegex: + path: stringData["backup.sh"] + pattern: "mysqldump -u \"\\$MYSQL_USER\" -h \"\\$MYSQL_HOST\" \\$SSL_OPTS" - it: "backup-script Secret does not contain ssl flags when tls.enabled=false" templates: @@ -900,6 +965,12 @@ tests: - notMatchRegex: path: stringData["backup.sh"] pattern: "--ssl-mode" + - notMatchRegex: + path: stringData["backup.sh"] + pattern: "--ssl-ca" + - notMatchRegex: + path: stringData["backup.sh"] + pattern: "--ssl-verify-server-cert" - it: "backup CronJob has tls-ca volumeMount when external=true and tls.enabled unset" templates: