Files
agent-skills/skills/wp-s3-media-offload/SKILL.md
T
MalinandClaude Sonnet 5 0b5e0598c4 skill: add wp-s3-media-offload
Documents the fleet's WP Offload Media pattern (warrior/stray MinIO,
dedicated bucket per site, WPS3Media plugin) and an open production bug
found while applying it to sateulera-dev: the public S3 API endpoint
(Caddy -> warrior) currently rejects all signed writes, unrelated to
credentials or plugin config.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 08:32:35 +02:00

9.3 KiB

name, description
name description
wp-s3-media-offload Use when a WordPress site's local wp-content/uploads is growing large or slowing down its host, and needs offloading to the fleet's warrior/stray MinIO pair via WP Offload Media. Covers the plugin (WPS3Media/"WP Offload Media" 4.0.0-cloudhost), the AS3CF_SETTINGS wp-config constant, per-site bucket creation, and the warrior→stray mirror. Also documents an OPEN, unresolved production bug (see bottom) that currently blocks actual uploads through the public endpoint.

WP S3 Media Offload (warrior/stray MinIO)

Pattern used to move a WordPress site's media library off local disk and onto the fleet's MinIO pair, serving images directly from there instead of the site's own host. First built for easycut.es (cabrera), attempted for sateulera-dev (funky/sateuleraes jail) on 2026-08-22 — plugin, bucket, and mirror are all correctly wired, but actual uploads are currently blocked fleet-wide by an open bug, see the bottom section before repeating this elsewhere.

Architecture

  • warrior (100.78.33.122, MinIO primary, self-signed cert → mc needs --insecure) holds one dedicated bucket per site — never reuse another site's bucket. Bucket policy download (anonymous public-read) so serve-from-s3 can point directly at objects with no CDN/signed-URL layer.
  • stray (100.118.140.10, disaster-recovery replica) mirrors that bucket continuously via the existing systemd template unit minio-mirror@<bucket>.service (/etc/systemd/system/minio-mirror@.service, already installed on stray — just mc --insecure mb stray/<bucket> then systemctl enable --now minio-mirror@<bucket>.service). Pull-based (mc mirror --watch --insecure warrior/%i stray/%i), no manual triggering needed once enabled.
  • Both buckets share the same MinIO access key fleet-wide (storgy — effectively unrestricted, not bucket-scoped IAM). No new credentials needed per site.

The plugin

Not the free "as3cf" plugin from wordpress.org — this fleet runs a licensed Pro build under the folder name WPS3Media (plugin header still says "WP Offload Media", version suffixed -cloudhost), ~34MB. There's no cached copy in any git repo — the source of truth is whichever site already has it installed (currently easycut.es on cabrera, /www/wwwroot/easycut.es/wp-content/plugins/WPS3Media). To add a new site, scp -r that directory straight into the new site's wp-content/plugins/, chown -R www:www it, then wp plugin activate WPS3Media (the folder name IS the plugin slug wp-cli needs here, despite the plugin header reading "WP Offload Media").

wp-config.php wiring

Add (adjust site name and bucket only — access/secret key are fleet-wide):

define( 'AS3CF_SETTINGS', serialize( array(
	'provider' => 's3compatible',
	'access-key-id' => '<fleet MinIO access key>',
	'secret-access-key' => '<fleet MinIO secret key>',
) ) );

The rest of the config (bucket name, endpoint, region, prefix) lives in wp_options under the key tantan_wordpress_s3 (no table-prefix literal in the option_name itself — wp option commands add/strip the site's prefix automatically), not wp-config. Set it via the plugin's own Settings UI after activation, or headlessly via wp-cli's --format=json (NOT --format=php — wp-cli only supports plaintext/json for option update, passing a hand-serialized PHP string directly gets silently truncated by shell quoting across an sshjexecwp hop): write a plain JSON file, scp it onto the jail's own filesystem path (not the host's /tmp — a redirect like < /tmp/x.json against a jexec command resolves on the HOST before entering the jail), then wp option update tantan_wordpress_s3 --format=json < /path/inside/jail/x.json. Known-good values (endpoint/region are shared across all sites on this MinIO instance, only bucket changes per site):

Key Value
bucket <per-site bucket name>
endpoint https://s3.palmasolutions.net
region eu-south-1 (dummy — required by the plugin's AWS-SDK compat layer, MinIO ignores it)
object-prefix wp-content/uploads/
serve-from-s3 true
copy-to-s3 true
remove-local-file true (only once you've confirmed images serve correctly from the bucket)
use-bucket-acls true
use-yearmonth-folders true
enable-object-prefix true

Rollout steps (repeatable)

  1. mc --insecure mb warrior/<bucket> + mc --insecure anonymous set download warrior/<bucket>
  2. On stray: mc --insecure mb stray/<bucket> + systemctl enable --now minio-mirror@<bucket>.service
  3. Before wiring the plugin, if the site is WordPress and has a wp-content/uploads/ folder worth shrinking first, run the fleet's existing webp-sibling pass (scripts/webp-convert.sh + deploy wp-plugins/granja-webp-siblings.php as an mu-plugin) — this is already standard per docs/new-site-checklist.md and typically cuts ~40-54% off what actually needs uploading/storing.
  4. Copy WPS3Media plugin folder to the target site's wp-content/plugins/
  5. wp plugin activate WPS3Media
  6. Set AS3CF_SETTINGS in wp-config.php (access/secret key only)
  7. Configure bucket/endpoint/region via plugin UI or wp option update tantan_wordpress_s3 --format=json < file.json (see wiring section above)
  8. Try a real signed write BEFORE running the bulk offload against the whole librarymc --insecure cp somefile.txt <alias>/<bucket>/test.txt using the exact endpoint/credentials the plugin will use. As of 2026-08-22 this fails with SignatureDoesNotMatch for ANY bucket/site through the public https://s3.palmasolutions.net endpoint — see the "OPEN BUG" section below before assuming a new site's setup is broken; it almost certainly isn't, the shared endpoint is.
  9. Once the endpoint bug above is actually fixed, trigger the offload: there's no reliable Pro-tools/Uploader-tool wp-cli command in this "-cloudhost" build (the Tools/license layer returns empty), so drive it directly instead — loop get_posts(post_type=attachment) and call wp_update_attachment_metadata($id, wp_get_attachment_metadata($id)) per ID via wp eval-file (this re-fires the exact same hook a real upload triggers, safely, for existing media).
  10. Verify: mc --insecure ls warrior/<bucket>/wp-content/uploads/ shows growing object count; front-end pages load images from https://s3.palmasolutions.net/<bucket>/.... Don't flip remove-local-file to true until spot-checked.

OPEN BUG (found 2026-08-22, NOT fixed): public endpoint breaks SigV4

Every authenticated S3 API request (PUT, even a bodyless LIST) through https://s3.palmasolutions.net (Caddy on madri → warrior) fails with SignatureDoesNotMatch, for ANY valid MinIO credential pair — confirmed this is not a plugin bug, not a credentials bug, and not a jail-network bug:

  • Same exact credentials succeed instantly hitting warrior directly (https://100.78.33.122:9000 or https://62.210.214.171:9000, bypassing Caddy entirely).
  • storage.palmasolutions.net (Caddy on the same madri host → stray's MinIO, over plain HTTP to the backend) signs correctly — so this isn't a generic "Caddy can't front MinIO" problem.
  • Two plausible fixes were tried on infra/madri/Caddyfile's s3.palmasolutions.net block and both disproven by direct retest: removing encode gzip (no effect — kept removed anyway, no downside) and forcing transport http { versions 1.1 } to rule out Caddy negotiating HTTP/2 to the HTTPS backend via ALPN (no effect either — kept anyway as reasonable hardening, but it is NOT the fix).
  • mc admin admin trace -v warrior failed to connect during this investigation ("unexpected end of JSON input") — packet-level capture (tcpdump/mitmproxy comparing the raw bytes MinIO receives via each path) is the logical next diagnostic step, not more guessing at Caddy directives.

Practical impact: no WordPress/AS3CF-style plugin (or any other SigV4 client) can currently write to ANY bucket through s3.palmasolutions.net — only direct-to-warrior access works. This almost certainly means easycut.es's own live AS3CF uploads are currently failing the same way (its wp-config point at this exact endpoint) — worth a dedicated check, since that site has a prior security incident history and any silent failure there deserves attention, not just this new site's setup.

Until this is root-caused, treat this skill's "Rollout steps" as correct-but-blocked at step 8 for any NEW site — the config is right, the endpoint isn't currently usable.

Gotchas

  • Finding the DB socket on panel-managed Linux hosts (cabrera-style BTPanel/aaPanel installs) is not obvious — wp-cli may report "missing MySQL extension" and the default /run/mysqld/mysqld.sock path is usually wrong. Check /tmp/mysql.sock first before assuming MySQL is down.
  • Bastille jails (funky, gringo, phreak) don't have this problem — wp-cli works normally inside jexec <jail> wp --allow-root --path=<docroot> ....
  • Confirm the site's own images aren't already coming from an unrelated external CDN before assuming local uploads are the bottleneck (sateulera-dev's product images are CloudFront-hosted already — this offload only applies to the ~950 theme/gallery/local files actually on-disk).