From c4fb89469904e97707c9ec1f299ee5b8a20b6b44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Wagner=20=E2=9E=96=20a6a2f5842?= Date: Thu, 7 May 2026 10:22:07 +0200 Subject: [PATCH] fix(perms,mysql): auto-fix storage ownership + skip self-signed cert verify (#1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two recurring foot-guns in production stacks rolled into one image-level fix: 1. storage/ + bootstrap/cache/ ownership drift. ENABLE_LARAVEL_PERMS was opt-in (default 0) and only chmod'd the top-level dir — so any subdir created later by a different UID (root, nobody, …) stayed un-writable for www-data. Symptom: workkit:db:backup pumping a multi-GB mysqldump into a doomed bash redirect that fails with "Permission denied" only after the pipeline starts. Now default-on (=1), recursive chown + chmod ug+rwX, SGID on dirs so future files inherit the group, and we pre-create the subdirs that ship empty (incl. storage/backups/) so artisan never creates one as the wrong user. 2. mysql client TLS verification against self-signed in-cluster certs. `php artisan db` failed with "TLS/SSL error: self-signed certificate in certificate chain" because modern mysql/mariadb clients auto-enable ssl-verify-server-cert when a password is on argv. Drops a /etc/mysql/conf.d/00-laravel-client.cnf with ssl-verify-server-cert=OFF so the connection still negotiates TLS but skips the chain check — the right tradeoff for a private docker network. Override per-host with MYSQL_CLIENT_VERIFY=ON or by mounting a stricter .cnf. Co-authored-by: Claude Opus 4.7 (1M context) --- Dockerfile | 8 ++++- README.md | 13 +++---- scripts/start-container | 76 +++++++++++++++++++++++++++++++++++------ 3 files changed, 80 insertions(+), 17 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9e7b591..6ea3c1a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -169,7 +169,13 @@ RUN mkdir -p /.composer && chmod 0777 /.composer ENV ENABLE_QUEUE=false ENV ENABLE_SCHEDULER=false ENV ENABLE_HORIZON=false -ENV ENABLE_LARAVEL_PERMS=0 +# storage/ + bootstrap/cache/ ownership and writable-bit fixes — runs +# every container start, idempotent. Set to "0" to opt out. +ENV ENABLE_LARAVEL_PERMS=1 +# Self-signed certs are normal for in-cluster MySQL on a private docker +# network; the client still negotiates TLS but skips chain verification. +# Set to "ON" if you've mounted a real CA or run against an external host. +ENV MYSQL_CLIENT_VERIFY=OFF EXPOSE 80 diff --git a/README.md b/README.md index 689bcfc..f8a4207 100644 --- a/README.md +++ b/README.md @@ -117,12 +117,13 @@ services: ## Runtime Environment Variables -| Variable | Default | Description | -|------------------------|---------|-----------------------------------------------------------| -| `ENABLE_QUEUE` | `false` | Start `artisan queue:work` via supervisor | -| `ENABLE_SCHEDULER` | `false` | Start `artisan schedule:work` via supervisor | -| `ENABLE_HORIZON` | `false` | Start `artisan horizon` via supervisor | -| `ENABLE_LARAVEL_PERMS` | `0` | Fix `storage/` and `bootstrap/cache/` permissions on boot | +| Variable | Default | Description | +|------------------------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `ENABLE_QUEUE` | `false` | Start `artisan queue:work` via supervisor | +| `ENABLE_SCHEDULER` | `false` | Start `artisan schedule:work` via supervisor | +| `ENABLE_HORIZON` | `false` | Start `artisan horizon` via supervisor | +| `ENABLE_LARAVEL_PERMS` | `1` | Pre-create + chown -R www-data + chmod ug+rwX (with SGID) on `storage/` and `bootstrap/cache/` on every boot. Idempotent. Set to `0` to opt out. | +| `MYSQL_CLIENT_VERIFY` | `OFF` | Whether the bundled mysql/mariadb client verifies the server cert chain. Default `OFF` is right for in-cluster MySQL with a self-signed cert (the connection is still encrypted). Set to `ON` if you connect to a real public host. | ## What's Included diff --git a/scripts/start-container b/scripts/start-container index 9f311d4..39fa45d 100755 --- a/scripts/start-container +++ b/scripts/start-container @@ -29,21 +29,75 @@ if command -v git >/dev/null 2>&1; then fi fi -if [ "${ENABLE_LARAVEL_PERMS:-0}" = "1" ]; then - echo " ENABLE_LARAVEL_PERMS=1 — applying targeted writable-dir fixes" - for p in /var/www/html/storage /var/www/html/bootstrap/cache; do - if [ -d "$p" ]; then - chmod ug+rwX "$p" 2>/dev/null || true - else - echo " WARN: $p does not exist" - fi - done +# ENABLE_LARAVEL_PERMS defaults to 1 — set to 0 to opt out (e.g. when +# you manage perms yourself with a custom entrypoint or volume init). +# The fix is idempotent and cheap on already-correct trees, so leaving +# it on is the right default. +if [ "${ENABLE_LARAVEL_PERMS:-1}" != "0" ]; then + echo " Applying Laravel writable-dir fixes (ENABLE_LARAVEL_PERMS=${ENABLE_LARAVEL_PERMS:-1})" + + APP_ROOT="/var/www/html" + + if [ -d "$APP_ROOT/storage" ] || [ -d "$APP_ROOT/bootstrap/cache" ]; then + # Pre-create the subdirs that ship empty in fresh Laravel repos + # plus the ones common Blax packages need (workkit:db:backup writes + # here). Doing it once at boot — as root — means whichever artisan + # command runs first never creates them owned by the wrong user. + mkdir -p \ + "$APP_ROOT/storage/app/public" \ + "$APP_ROOT/storage/app/private" \ + "$APP_ROOT/storage/framework/cache/data" \ + "$APP_ROOT/storage/framework/sessions" \ + "$APP_ROOT/storage/framework/views" \ + "$APP_ROOT/storage/framework/testing" \ + "$APP_ROOT/storage/logs" \ + "$APP_ROOT/storage/backups" \ + "$APP_ROOT/bootstrap/cache" \ + 2>/dev/null || true + + # Recursive chown so any stray files written earlier as the wrong + # user (root, nobody, …) get repaired. chown is fast on already- + # correct trees — pure stat, no I/O writes. + chown -R www-data:www-data "$APP_ROOT/storage" "$APP_ROOT/bootstrap/cache" 2>/dev/null || true + + # ug+rwX gives both owner and group write; capital X only adds +x + # on directories. SGID on the dirs (g+s) makes new files inherit + # the www-data group — so a file written by a sidecar container + # running as a different UID still ends up group-writable. + chmod -R ug+rwX "$APP_ROOT/storage" "$APP_ROOT/bootstrap/cache" 2>/dev/null || true + find "$APP_ROOT/storage" "$APP_ROOT/bootstrap/cache" -type d -exec chmod g+s {} + 2>/dev/null || true + else + echo " WARN: $APP_ROOT/storage and $APP_ROOT/bootstrap/cache do not exist (volume not mounted yet?)" + fi else - echo " Skipping Laravel chmod (set ENABLE_LARAVEL_PERMS=1 to enable)" + echo " Skipping Laravel writable-dir fixes (ENABLE_LARAVEL_PERMS=0)" fi mkdir -p /var/log/supervisor /var/log/nginx /var/log/php +# --------------------------------------------------------------------------- +# 1b) MySQL/MariaDB client defaults +# --------------------------------------------------------------------------- +# Self-hosted Laravel stacks typically talk to a MySQL container on a +# private docker network. Modern mysql/mariadb server images ship a +# self-signed cert by default, and modern clients auto-enable +# ssl-verify-server-cert when a password is passed on argv (as Laravel's +# `php artisan db` does) — which then fails with +# "TLS/SSL error: self-signed certificate in certificate chain". +# +# Disabling verification here keeps the connection encrypted (the client +# still negotiates TLS) but skips the chain check — the right tradeoff +# for in-cluster traffic where the network is the trust boundary. +# Override per host: set MYSQL_CLIENT_VERIFY=ON, or mount your own +# /etc/mysql/conf.d/*.cnf with stricter settings (later files override +# earlier ones in lexical order). +mkdir -p /etc/mysql/conf.d +cat > /etc/mysql/conf.d/00-laravel-client.cnf <}" echo " ENABLE_QUEUE=${ENABLE_QUEUE:-false}" echo " ENABLE_SCHEDULER=${ENABLE_SCHEDULER:-false}" echo " ENABLE_HORIZON=${ENABLE_HORIZON:-false}" +echo " ENABLE_LARAVEL_PERMS=${ENABLE_LARAVEL_PERMS:-1}" +echo " MYSQL_CLIENT_VERIFY=${MYSQL_CLIENT_VERIFY:-OFF}" end_ts=$(date +%s) echo " Preflight took $((end_ts - start_ts))s"