Skip to content

[critical] Generate the MariaDB app-user password in the Helm chart instead of hardcoding "admin" - #453

Merged
ostefano merged 1 commit into
MISP:masterfrom
elhoim:fix-helm-mariadb-hardcoded-password
Sep 16, 2026
Merged

ostefano merged 1 commit into
MISP:masterfrom
elhoim:fix-helm-mariadb-hardcoded-password

Conversation

@elhoim

@elhoim elhoim commented Sep 16, 2026

Copy link
Copy Markdown
Member

BLUF

  • Priority: critical.
  • What: kubernetes/helm-chart/templates/secrets.yaml renders the MariaDB application user's password from the literal string "admin".
  • Impact: on a default install (vault disabled, networkPolicy disabled) any pod in the cluster can run mysql -h <release>-mariadb -u misp -padmin misp and read or write the whole threat-intel database, including users and auth_keys.
  • Fix: generate mariadb-password with common.secrets.passwords.manage, exactly like the root, replication and backup passwords in the same Secret.
  • Blast radius: two lines in one template. No values, no other manifest, no container image.
  • Caveat: existing installs keep their current password on helm upgrade (by design — see Upgrade behaviour); the hole is closed for new installs, and a manual rotation recipe is given below.

What was wrong

templates/secrets.yaml builds the <release>-mariadb Secret. Three of its four keys are generated:

mariadb-backup-password:      {{ $backupPassword | b64enc | quote }}
mariadb-replication-password: {{ $replicationPassword | b64enc | quote }}
mariadb-root-password:        {{ $rootPassword | b64enc | quote }}
mariadb-password:             {{ "admin" | b64enc }}     <-- literal

The fourth had no common.secrets.passwords.manage call and no values lookup — the literal was its only source, and it could not be overridden through values.

The value is live, not dead code:

  • values.yaml sets mariadb.auth.existingSecret: "misp-mariadb" with username: "misp" / database: "misp", so the bitnami mariadb subchart takes the application user's password from this very Secret's mariadb-password key.
  • templates/deployment.yaml sets MYSQL_PASSWORD_FILE: /vault/mariadb/mariadb-password and mounts the same Secret, so misp-core authenticates with the same literal.
  • The whole file is gated by {{- if not .Values.misp.vault.enabled }}, and vault defaults to disabled — this is the default-install path.

Concrete impact

With the shipped defaults, the MariaDB service is reachable cluster-wide and the credentials misp / admin are guessable without any access to the cluster's Secrets. A compromised sidecar, a tenant pod or a build runner in the same cluster gets full read/write on the MISP database, which includes user records and API auth keys.

What this changes

One new variable, declared next to its three siblings and using the identical call shape:

{{- $mariadbPassword := include "common.secrets.passwords.manage" (dict "secret" $mariadbSecretName "key" "mariadb-password" "length" 20 "providedValues" (list "mariadb.auth.password") "honorProvidedValues" true "context" $) | trimAll "\"" | b64dec }}

and the Secret key now renders it:

  mariadb-password: {{ $mariadbPassword | b64enc | quote }}

Resulting behaviour, in the helper's documented precedence order:

  1. mariadb.auth.password, when the user sets it (--set mariadb.auth.password=... now works, it was previously ignored);
  2. otherwise the value already present in the <release>-mariadb Secret, so repeated helm upgrade runs are stable;
  3. otherwise a fresh 20-character random password.

Nothing else is touched. values.yaml is deliberately not modified: mariadb.auth.rootPassword, replicationPassword and backupPassword are not declared there either — the path resolves from the bitnami mariadb subchart defaults (auth.password: "" in mariadb 20.5.3), which is what makes the override hook work. Chart.yaml version is also left alone; bump it if the chart's release process expects one.

Upgrade behaviour (please read)

common.secrets.passwords.manage consults the existing Secret before generating, so an installation that already has mariadb-password: YWRtaW4= keeps admin after helm upgrade. That is the right default — rewriting the Secret without a matching ALTER USER inside the running MariaDB would lock misp-core out of its own database — but it means existing deployments must rotate manually, e.g.:

NEW=$(openssl rand -base64 18 | tr -d '/+=' | cut -c1-20)
kubectl exec -it <release>-mariadb-0 -- mysql -uroot -p"$ROOT_PW" \
  -e "ALTER USER 'misp'@'%' IDENTIFIED BY '$NEW'; FLUSH PRIVILEGES;"
kubectl patch secret <release>-mariadb --type merge \
  -p "{\"data\":{\"mariadb-password\":\"$(printf %s "$NEW" | base64 -w0)\"}}"
kubectl rollout restart deployment/<release>-misp

New installs get a random password with no action required.

How it was verified

  • Re-read templates/secrets.yaml at HEAD; the new call is byte-identical to the three sibling calls apart from the key name and the values path.
  • Confirmed auth.password exists (as "") in the vendored charts/mariadb-20.5.3.tgz values, so common.utils.getValueFromKey resolves the full path and returns an empty value when unset — i.e. the generation branch is taken, and failOnNew semantics are unchanged because the key already exists in any pre-existing Secret.
  • Grepped the chart, kubernetes/ and the docs: the literal admin password appeared nowhere else, so no documentation needed a matching change.
  • The repository's syntax sweep is unaffected (its YAML check deliberately excludes kubernetes/helm-chart/templates/).
  • Helm was not available in the environment used to prepare this change, so the template was not rendered. Reviewer render check:
helm template misp kubernetes/helm-chart | grep -A6 'name: misp-mariadb'

The mariadb-password value should now differ between two runs in an empty namespace, and should equal the base64 of --set mariadb.auth.password=hunter2 when that flag is passed.

Text-level check that needs no tooling:

! grep -q '"admin" | b64enc' kubernetes/helm-chart/templates/secrets.yaml \
  && grep -q '"key" "mariadb-password"' kubernetes/helm-chart/templates/secrets.yaml \
  && echo OK

The mariadb Secret rendered mariadb-password from the literal string
"admin", while the root, replication and backup passwords in the very
same Secret are generated through common.secrets.passwords.manage. The
key is consumed by the bitnami mariadb subchart (mariadb.auth
.existingSecret) and by misp-core (MYSQL_PASSWORD_FILE), and the file
is on the default install path, so any workload in the cluster could
reach the MISP database with a known credential.

Generate mariadb-password the same way as its siblings: honour an
explicit mariadb.auth.password, otherwise reuse the value already
stored in the Secret, otherwise generate a 20-character random one.

Existing installations keep their current password on upgrade, since
the helper reads the existing Secret first; rotating them needs an
ALTER USER in MariaDB alongside the Secret update.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ostefano
ostefano merged commit 9789a82 into MISP:master Sep 16, 2026
6 checks passed
@elhoim
elhoim deleted the fix-helm-mariadb-hardcoded-password branch September 19, 2026 22:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants