Fix DB password corruption via Docker secrets file mechanism

The reported bug (a generated password containing "$BRjTx3tlmSpz" got
silently blanked, breaking the DB connection) is the same Compose
interpolation issue as the earlier bcrypt hash, but this time in fields
that are genuinely user-chosen and can't just be avoided by convention.

Switched DB_PASSWORD/DB_ROOT_PASSWORD to Docker's official `_FILE`
secrets convention (MARIADB_PASSWORD_FILE / WORDPRESS_DB_PASSWORD_FILE),
backed by Compose's native `secrets:` mechanism: deploy.sh writes the raw
value to secrets/<env>/db_password, and the container reads that file
directly — the value never passes through Compose's ${VAR} interpolation
at all. Verified end-to-end with an actual `$`-containing password,
including a full deploy → backup → restore → still-serving round trip.

Also fixed along the way (found while actually testing, not assumed):
- Makefile never exported ENVIRONMENT, so `make up` alone (bypassing
  deploy.sh) would have left the new secrets path unresolved.
- deploy.sh chmod'd the secret files 600, unreadable by the container's
  own UID (www-data) — fixed to 644, relying on the containing directory
  (700) to keep other host users out instead.
- backup.sh/restore.sh still called `mysqldump`/`mysql`, which don't
  exist in the mariadb:11 image under those names — renamed to
  mariadb-dump/mariadb. (This means neither script had actually
  succeeded before now; both are verified working end-to-end here.)

The supplier/payment API keys remain passed the old way — nothing reads
them yet (bookstore-core is still a stub), so there's no live bug to fix
there; noted in .env.example that the same _FILE pattern should be used
once that code exists.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-27 11:36:29 -04:00
co-authored by Claude Sonnet 5
parent cd48f85a47
commit 3876a6ba30
8 changed files with 62 additions and 11 deletions
+30 -4
View File
@@ -4,11 +4,23 @@
# values into the file once. If cron's env drifts from wordpress's (e.g.
# WORDPRESS_CONFIG_EXTRA missing), cron silently loses WP_REDIS_HOST,
# DISABLE_WP_CRON, etc. The anchor below is what prevents that drift.
# DB_PASSWORD/DB_ROOT_PASSWORD are deliberately NOT passed via ${VAR} here.
# Docker Compose's own interpolation mangles any value containing a `$`
# followed by a letter (confirmed with a bcrypt hash earlier, and again with
# a plain generated password: "$BRjTx3tlmSpz" got silently blanked out,
# breaking the DB connection). The official mariadb/wordpress images both
# support a `_FILE` convention specifically for this — point at a file path
# (never risky) and the container reads the raw contents itself, bypassing
# Compose's interpolation entirely. deploy.sh writes these files fresh from
# .env.<environment> before every `up`. The supplier/payment keys below are
# still passed the old way and remain exposed to the same class of bug —
# nothing consumes them yet (bookstore-core is still a stub), so when that
# code is written it should read `_FILE` variants the same way.
x-bookstore-env: &bookstore-env
WORDPRESS_DB_HOST: db
WORDPRESS_DB_NAME: ${DB_NAME}
WORDPRESS_DB_USER: ${DB_USER}
WORDPRESS_DB_PASSWORD: ${DB_PASSWORD}
WORDPRESS_DB_PASSWORD_FILE: /run/secrets/db_password
WORDPRESS_TABLE_PREFIX: ${WP_TABLE_PREFIX:-wp_}
MAIL_CATCHER_HOST: mailhog
MAIL_CATCHER_PORT: 1025
@@ -26,6 +38,15 @@ x-bookstore-env: &bookstore-env
HELCIM_ACCOUNT_ID: ${HELCIM_ACCOUNT_ID:-}
MAILERLITE_API_KEY: ${MAILERLITE_API_KEY:-}
x-bookstore-secrets: &bookstore-db-secret
- db_password
secrets:
db_password:
file: ./secrets/${ENVIRONMENT}/db_password
db_root_password:
file: ./secrets/${ENVIRONMENT}/db_root_password
services:
db:
image: mariadb:11
@@ -33,12 +54,15 @@ services:
environment:
MARIADB_DATABASE: ${DB_NAME}
MARIADB_USER: ${DB_USER}
MARIADB_PASSWORD: ${DB_PASSWORD}
MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MARIADB_PASSWORD_FILE: /run/secrets/db_password
MARIADB_ROOT_PASSWORD_FILE: /run/secrets/db_root_password
secrets:
- db_password
- db_root_password
volumes:
- db_data:/var/lib/mysql
healthcheck:
test: ["CMD-SHELL", "mariadb-admin ping -h 127.0.0.1 -u$$MARIADB_USER -p$$MARIADB_PASSWORD --silent"]
test: ["CMD-SHELL", "mariadb-admin ping -h 127.0.0.1 -u$$MARIADB_USER -p\"$$(cat /run/secrets/db_password)\" --silent"]
interval: 5s
timeout: 5s
retries: 20
@@ -64,6 +88,7 @@ services:
redis:
condition: service_healthy
environment: *bookstore-env
secrets: *bookstore-db-secret
volumes:
- wp_core:/var/www/html
- wp_uploads:/var/www/html/wp-content/uploads
@@ -79,6 +104,7 @@ services:
- wordpress
entrypoint: ["/bin/sh", "/usr/local/bin/cron-entrypoint.sh"]
environment: *bookstore-env
secrets: *bookstore-db-secret
volumes:
- wp_core:/var/www/html
- wp_uploads:/var/www/html/wp-content/uploads