Commit Graph

17 Commits

Author SHA1 Message Date
Fabian Wagner 3dca9978d9 fix(backup): widen mode + fail fast when backup dir isn't writable
mkdir was 0755 — owner-only write. The dir often gets created once at
deploy time as root or whoever ran the first artisan command, then
www-data tries to write to it at runtime and bash redirects fail with
"Permission denied" mid-pipeline (after mysqldump has already started
streaming, leaving a 0-byte .enc behind).

- mkdir(0775) so group writes too; ensure-group-write is the typical
  pattern for shared deploy/runtime users.
- Best-effort chmod 0775 on existing dirs to repair narrow modes when
  we own the path.
- is_writable() pre-flight before kicking off any pipeline. Throws a
  RuntimeException with the exact `chown` + `chmod` command to run,
  including the discovered owner and the current process user — so the
  fix is one paste away instead of grepping man pages.

This pairs with docker-laravel's start-container change that pre-creates
storage/backups/ owned by www-data on every boot. Either layer alone is
enough; both together means the failure mode disappears whether the
deployment uses our image or not.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 10:05:46 +02:00
Fabian Wagner 40901c0f5e fix(backup): stream through openssl pipe — kills 500MB+ memory exhaustion
The pre-1.1.1 pipeline ran each stage as a separate file step:
  mysqldump → file.sql
  xz → file.sql.xz  (read whole file into memory)
  Crypt::encryptString(file_get_contents(file.sql.xz))
The third step blew up with "Allowed memory size exhausted" on
mid-three-digit-MB compressed dumps because Laravel's Crypt envelope
reads the whole input, base64-encodes (+33%), and JSON-wraps it for
the MAC. Peak memory was ~3.5× the file size.

New pipeline is one shell pipe:
  bash -c 'set -o pipefail; mysqldump | xz -3 | openssl enc \
    -aes-256-cbc -pbkdf2 -iter 600000 -salt -pass env:WK_KEY > out'

Zero PHP-side allocation for the payload — the encryption runs in the
openssl process, not the PHP heap. The output file format is the
standard `openssl enc -salt` format (starts with "Salted__"), so it's
also restorable with vanilla openssl on any host:
  openssl enc -d -aes-256-cbc -pbkdf2 -iter 600000 -pass env:K \
    -in backup.sql.xz.enc | xz -d | mysql ...

Encryption is still APP_KEY-derived: the base64-stripped APP_KEY is
fed to openssl as the passphrase, PBKDF2 (600k iters) stretches it
into the AES key. A backup is still only restorable by a deployment
that knows the same APP_KEY.

Other changes:
- xz default level dropped from 9 to 3 (tunable via --xz-level or
  config('workkit.backup.xz_level')). For SQL dumps -9 buys a few %
  size at multiples of the time cost; -3 is the sweet spot.
- Restore detects and rejects pre-1.1.1 Crypt-format backups with a
  clear error (those need a one-off Crypt::decryptString, which would
  hit the same memory wall, so we don't auto-fall-back).
- "bad decrypt" / "bad magic" stderr now translates to "APP_KEY
  mismatch" so operators don't have to recognise openssl errors.

Verified end-to-end against the dev DB (981 blogs / 659 users / 209
roles): backup in 1.2s, openssl-format output, restore into throwaway
DB matches every spot-checked row count.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 14:26:17 +02:00
Fabian Wagner aacd486a40 feat: db backup/restore/prune commands with APP_KEY encryption
Three new artisan commands modelled on blax-intranet's database
backup/restore pattern, generalised so any project pulling in
laravel-workkit gets them for free:

- workkit:db:backup
  mysqldump → xz -9 → Crypt::encryptString → storage/backups/db_<conn>_<ts>.sql.xz.enc
  Password is passed via MYSQL_PWD so it doesn't appear in `ps`.
- workkit:db:restore
  Picks newest backup (or --file=…), inverts the pipeline, and pipes
  the .sql into the mysql CLI. Detects .enc / .xz suffixes so plain
  dumps and partially-encoded files work too. Confirms before
  overwriting unless --force is set; emits a clear "APP_KEY
  mismatch" message when Crypt::decryptString fails.
- workkit:db:prune-backups
  Drops backups older than --days (defaults to 30 / config).
  --dry-run shows what would go.

Encryption uses Laravel's Crypt facade, so the AES-256 key is
derived from APP_KEY — same key the rest of the app uses for cookies
and encrypted columns. A backup is restorable only by a deployment
that knows the host's APP_KEY.

Shipped config/workkit.php (mergeConfigFrom + publishes) for
overriding the backup directory and retention window without
hard-coding env-specific values in the package.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 12:01:32 +02:00
Fabian Wagner aa51e0aa27 I api responses misc methods 2026-04-28 08:06:04 +02:00
Fabian Wagner e2c663522c feat(misc): add reusable response and options helpers 2026-04-17 09:50:33 +02:00
Fabian Wagner 7bf997d798 chore: use 'websocket' as default PUSHER app key 2026-04-16 08:25:28 +02:00
Fabian Wagner 9b3ea29d9a fix: missing newlines in generated docker-compose websocket config 2026-04-15 10:46:57 +02:00
Fabian Wagner 012eec7ee9 A plug-n-pray command 2026-04-15 10:10:41 +02:00
Fabian Wagner b83eaefa2e A expiration scopes 2025-12-04 11:34:19 +01:00
Fabian Wagner 67a51602c4 A sanctum & AuthenticateByBearerTokenMiddleware 2025-12-01 16:59:45 +01:00
Fabian Wagner ab6d2f29d2 I expirations 2025-11-25 10:48:09 +01:00
Fabian Wagner 8ef0640737 BF app config 2025-11-21 16:19:24 +01:00
Fabian Wagner cc13986135 BF wrong namespace 2025-11-21 10:49:43 +01:00
Fabian Wagner c5ee4d3080 BF wrong naming 2025-11-20 16:10:01 +01:00
Fabian Wagner d04c7186be R service provider 2025-11-20 16:08:16 +01:00
Fabian Wagner 84cc1d15d4 AI extended workkit 2025-11-20 15:53:34 +01:00
a6a2f5842 0f260ab95f init 2025-11-20 15:04:18 +01:00