WP KeyCDN Media Offload archived
WordPress plugin to integrate with KeyCDN push zone.
by Michael Miller · github.com/mjmiller41/wp-keycdn-plugin · website
Install
The author publishes release zips, so WP-CLI can install straight from GitHub:
wp plugin install https://github.com/mjmiller41/wp-keycdn-plugin/releases/download/v0.1.0/wp-keycdn-offload.zipReadme
WP KeyCDN Media Offload
A WordPress plugin that offloads media to a KeyCDN Push Zone via FTPS with async background processing, soft-delete quarantine, manifest-based integrity verification, and WooCommerce compatibility.
Table of Contents
- Prerequisites
- Install Action Scheduler
- Install the Plugin
- Configure Credentials
- Activate and Configure
- Test — FTP Connection
- Test — Single Upload
- Test — Deletion
- Test — Bulk Offload
- Enable Local File Removal
- Verify the Reconciliation Job
- WP-CLI Reference
- Common Problems & Fixes
1. Prerequisites
1.1 Verify PHP extensions
SSH into your web server and run:
php -m | grep -E "^ftp$|^openssl$|^intl$"
All three must appear:
ftp
intl
openssl
If any are missing, install them. On Ubuntu/Debian:
sudo apt install php8.x-ftp php8.x-intl php8.x-openssl
sudo systemctl restart php8.x-fpm # or apache2, depending on your stack
On a Docker-based local environment, rebuild the PHP image with:
RUN docker-php-ext-install ftp
RUN apt-get install -y libicu-dev && docker-php-ext-install intl
Confirm FTP has SSL support (required for FTPS):
php -r "var_dump(function_exists('ftp_ssl_connect'));"
# Must output: bool(true)
1.2 WordPress requirements
- WordPress 6.0 or higher
- PHP 7.4 or higher
- Action Scheduler must be active before this plugin runs (see Part 2)
1.3 KeyCDN account setup
A. Create a Push Zone:
- Log in to app.keycdn.com
- Go to Zones → Add Zone
- Set Zone Type to Push
- Give it a name (e.g.,
mysite-media) - Note the Zone URL shown after creation — it will look like
https://mysite-media-xyz.kxcdn.com
B. Create a subuser for FTP access:
- Go to Account → Subusers → Add Subuser
- Set a username (e.g.,
mysite-ftp) and a strong password - Assign it access to your Push Zone only
- Note the username and password — these become your FTP credentials
C. Verify you can connect manually:
ftp -p ftp.keycdn.com
# Enter the subuser name and password when prompted
# Type: ls
# You should see your zone name as a directory
2. Install Action Scheduler
The plugin depends on Action Scheduler for background processing. You have two options:
Option A — Install as a standalone plugin (recommended for non-WooCommerce sites):
- Download from:
https://wordpress.org/plugins/action-scheduler/ - Upload to
wp-content/plugins/action-scheduler/ - Activate it in Plugins → Installed Plugins
Option B — WooCommerce (already bundles Action Scheduler):
If WooCommerce is already active on your site, Action Scheduler is already present — skip this step.
Verify Action Scheduler is active:
wp eval "echo function_exists('as_enqueue_async_action') ? 'OK' : 'MISSING';"
# Must output: OK
3. Install the Plugin
Option A — From the cloned repo (production/staging):
cd /path/to/wordpress/wp-content/plugins/
git clone https://github.com/mjmiller41/wp-keycdn-plugin.git wp-keycdn-plugin
Option B — Zip and upload via admin UI:
cd /path/to/wp-keycdn-plugin
zip -r wp-keycdn-plugin.zip . -x "*.git*"
Then in WordPress: Plugins → Add New → Upload Plugin → choose the zip file.
4. Configure Credentials
Credentials are entered in the plugin settings screen after activation (see Part 5, Step 3). The plugin stores the FTP password encrypted in the database using AES-256-CTR.
Recommended: Define a stable encryption key in
wp-config.phpso the stored password survives WordPress salt rotations:// Must never change after first activation define( 'KEYCDN_ENCRYPTION_KEY', 'a-long-random-string-change-this' ); define( 'KEYCDN_ENCRYPTION_SALT', 'another-long-random-string-change-this' );Generate the random strings with:
php -r "echo bin2hex(random_bytes(32)) . PHP_EOL; echo bin2hex(random_bytes(32)) . PHP_EOL;"Without these constants the plugin falls back to WordPress's built-in salts — rotating those salts will permanently break decryption of your stored password.
Power users: If you prefer to keep credentials entirely out of the database, you can define them as constants in wp-config.php instead and they will take precedence over anything entered in the UI:
define( 'KEYCDN_ZONE_URL', 'https://mysite-media-xyz.kxcdn.com' );
define( 'KEYCDN_FTP_HOST', 'ftp.keycdn.com' );
define( 'KEYCDN_FTP_USER', 'mysite-ftp' );
define( 'KEYCDN_FTP_PASS', 'your-subuser-password' );
5. Activate and Configure
Step 1 — Activate the plugin
Via WP Admin:
Go to Plugins → Installed Plugins, find WP KeyCDN Media Offload, click Activate.
Via WP-CLI:
wp plugin activate wp-keycdn-plugin
Activation creates:
- The
{prefix}cdn_offload_logdatabase table - The
wp-content/uploads/_cdn_trash/quarantine directory with.htaccessblocking direct access - Default option values
- Two recurring Action Scheduler jobs (
keycdn_reconcile_manifestandkeycdn_purge_trash)
Verify the table was created:
wp db query "SHOW TABLES LIKE '%cdn_offload_log';"
# Must return one row
Verify the trash directory:
ls -la /path/to/wordpress/wp-content/uploads/_cdn_trash/
# Must exist and contain .htaccess
Step 2 — Verify Action Scheduler jobs were scheduled
wp eval "
echo as_next_scheduled_action('keycdn_reconcile_manifest') ? 'reconcile: OK' : 'reconcile: MISSING';
echo PHP_EOL;
echo as_next_scheduled_action('keycdn_purge_trash') ? 'purge_trash: OK' : 'purge_trash: MISSING';
"
Both lines must say OK. If either says MISSING, Action Scheduler was not active when the plugin was activated. Activate Action Scheduler first, then deactivate and reactivate this plugin.
Step 3 — Enter credentials and configure settings
Go to KeyCDN Offload → Settings:
| Field | Value |
|---|---|
| Zone URL | Your https://yourzone.kxcdn.com URL |
| FTP Host | ftp.keycdn.com |
| FTP Username | Your subuser name |
| FTP Password | Your subuser password |
| Auto-Offload on Upload | ✅ Checked |
| Remove Local Files | ☐ Unchecked (leave off until testing is complete) |
| Quarantine TTL | 30 days |
| WooCommerce Compatibility | ✅ Checked (if WooCommerce is active) |
Click Save Changes.
Step 4 — Verify configuration is readable
wp eval "
\$enc = new KeyCDN\Offload\Core\Encryption();
\$cred = new KeyCDN\Offload\Core\Credentials(\$enc);
echo 'Zone URL: ' . \$cred->get_zone_url() . PHP_EOL;
echo 'FTP User: ' . \$cred->get_ftp_user() . PHP_EOL;
echo 'FTP Pass: ' . ('' !== \$cred->get_ftp_pass() ? '[SET]' : '[EMPTY]') . PHP_EOL;
echo 'Configured: ' . (\$cred->is_configured() ? 'YES' : 'NO') . PHP_EOL;
"
The last line must say Configured: YES.
6. Test — FTP Connection
wp eval "
\$enc = new KeyCDN\Offload\Core\Encryption();
\$cred = new KeyCDN\Offload\Core\Credentials(\$enc);
\$ftp = new KeyCDN\Offload\Core\FtpClient(\$cred);
try {
\$ftp->connect();
echo 'FTP connection: SUCCESS' . PHP_EOL;
\$list = \$ftp->list_dir('/');
echo 'Zone root listing (' . count(\$list) . ' entries):' . PHP_EOL;
foreach (array_slice(\$list, 0, 5) as \$e) { echo ' ' . (\$e['name'] ?? '?') . PHP_EOL; }
\$ftp->disconnect();
} catch (Exception \$e) {
echo 'FAILED: ' . \$e->getMessage() . PHP_EOL;
}
"
Expected output:
FTP connection: SUCCESS
Zone root listing (N entries):
your-zone-name/
7. Test — Single Upload
7.1 Upload a test image
- Go to Media → Add New in WordPress admin
- Upload any JPG or PNG image
- Note the attachment ID from the URL bar (e.g.,
post=42)
7.2 Check the Action Scheduler queue
Go to Tools → Scheduled Actions (or WooCommerce → Status → Scheduled Actions if using WooCommerce). Filter by Group: keycdn-offload.
You should see a keycdn_upload_attachment job in Pending or Complete state.
Force it to run immediately:
wp action-scheduler run --group=keycdn-offload --limit=5
7.3 Verify the manifest
Replace 42 with your actual attachment ID:
wp db query "SELECT size_slug, state, remote_path, byte_size FROM $(wp db prefix)cdn_offload_log WHERE attachment_id = 42;"
Expected output — one row per image size, all in confirmed state:
+-----------+-----------+---------------------------+-----------+
| size_slug | state | remote_path | byte_size |
+-----------+-----------+---------------------------+-----------+
| full | confirmed | /2026/06/test-image.jpg | 204800 |
| thumbnail | confirmed | /2026/06/test-image-150.… | 12345 |
| medium | confirmed | /2026/06/test-image-300.… | 45678 |
+-----------+-----------+---------------------------+-----------+
If any row shows failed, check the Action Scheduler log:
wp db query "
SELECT l.log_date, l.message
FROM $(wp db prefix)actionscheduler_logs l
JOIN $(wp db prefix)actionscheduler_actions a ON a.action_id = l.action_id
WHERE a.hook = 'keycdn_upload_attachment'
ORDER BY l.log_date DESC
LIMIT 10;
"
7.4 Verify the file is on the CDN
# Replace with the remote_path value from the manifest query above
curl -I "https://yourzone.kxcdn.com/2026/06/test-image.jpg"
# HTTP/2 200 confirms the file is live on the CDN
7.5 Verify URL rewriting
# Replace 42 with your attachment ID
wp eval "echo wp_get_attachment_url(42);"
# Must output the CDN URL:
# https://yourzone.kxcdn.com/2026/06/test-image.jpg
# NOT the local URL:
# https://yoursite.com/wp-content/uploads/2026/06/test-image.jpg
8. Test — Deletion
- In WordPress admin go to Media Library, select the test image, click Delete Permanently
- Flush the Action Scheduler queue:
wp action-scheduler run --group=keycdn-offload --limit=5 - Verify the file is removed from the CDN (propagation can take up to 30 minutes):
curl -I "https://yourzone.kxcdn.com/2026/06/test-image.jpg" # Must return 404 - Verify the quarantine directory received the local copy:
ls -lR /path/to/wordpress/wp-content/uploads/_cdn_trash/
9. Test — Bulk Offload
Via Admin UI
- Go to KeyCDN Offload → Bulk Offload
- Click Start Bulk Offload
- The progress bar updates every 3 seconds
- When complete, review counts at KeyCDN Offload → Status Log
Via WP-CLI (recommended for large libraries)
wp keycdn-offload bulk-offload --batch-size=50
Monitor progress:
watch -n 10 'wp keycdn-offload status'
10. Enable Local File Removal (Optional)
Only enable this after confirming uploads and URL rewriting are working correctly in production.
Via admin UI: Go to KeyCDN Offload → Settings → check Remove Local Files → Save.
Via WP-CLI:
wp option update keycdn_offload_remove_local 1
After this is enabled, the plugin moves local files to _cdn_trash/ in the background after each confirmed upload. Files remain in quarantine for 30 days (configurable via Quarantine TTL) before permanent deletion, giving you a recovery window if the CDN copy is ever lost.
11. Verify the Reconciliation Job
The plugin checks CDN integrity every 24 hours automatically. Run it manually to confirm it works:
wp keycdn-offload reconcile
Expected output:
Running reconciliation...
Success: Reconciliation complete.
Confirm last_verified_at timestamps were updated:
wp db query "SELECT attachment_id, size_slug, state, last_verified_at FROM $(wp db prefix)cdn_offload_log LIMIT 10;"
12. WP-CLI Reference
# Queue all un-offloaded media for CDN upload
wp keycdn-offload bulk-offload
# Same with a larger batch size
wp keycdn-offload bulk-offload --batch-size=100
# Show per-state file counts from the manifest
wp keycdn-offload status
# Run the CDN integrity reconciliation check immediately
wp keycdn-offload reconcile
# Hard-delete quarantine files that have exceeded the TTL
wp keycdn-offload purge-trash
# Manually flush the Action Scheduler queue
wp action-scheduler run --group=keycdn-offload --limit=25
Read the full README on GitHub →
Releases
| Tag | Published | Asset | Downloads |
|---|---|---|---|
| v0.1.0 | Jun 14, 2026 | wp-keycdn-offload.zip | 0 |