WordPress 7.1
Overview
This guide covers launching and using the WordPress 7.1 with Redis Cache on Ubuntu 24.04 AMI from SecureAnyCloud on Amazon EC2. It is one version of the SecureAnyCloud BookStack, CMS, Blogs and Wikis listing on AWS Marketplace. You pick it as the software version when you subscribe and launch.
WordPress is the free and open-source publishing platform and content management system written in PHP. It gives you posts and pages written in the block editor, block themes that you design in the Site Editor, media, comments, users with roles, and thousands of plugins and themes.
This image runs WordPress 7.1.4 (the current 7.1 release) with the Twenty Twenty-Five theme, served by nginx and PHP-FPM 8.3 from a local MariaDB 10.11 database. It adds a Redis 7.2 object cache: the Redis Object Cache plugin keeps WordPress's query and option results in Redis, so pages need fewer database queries.
What you get:
- A Redis object cache that is connected on first boot. Redis 7.2.17 is built from the pinned upstream source (Ubuntu 24.04's own Redis package, 7.0.15, lacks current Redis security fixes). The Redis Object Cache plugin 3.0.0 is active with its drop-in in place, so the cache works from the first page you load (Step 7).
- Redis with no network listener and its own password. Redis listens only on a local socket that only Redis and the web server user can open. It has no TCP port. Your instance's first boot creates a Redis account and password of its own. Redis's default account is off, and the WordPress account cannot run administrative, scripting, replication or flush-everything commands. Redis has no network access at all and keeps nothing on disk.
- No reachable installer, no shared database. WordPress's browser installer (
wp-admin/install.php) creates the site and the first administrator for whoever opens it first. This image ships no database (the MariaDB data directory is empty), nowp-config.phpand no reachable installer. Your instance's first boot creates the database and runs WordPress's own installer through WP-CLI, so the site is yours before the web server starts. - Secrets unique to your instance, never on a command line. The database password, the administrator password, the Redis password, WordPress's eight authentication keys and salts, and the TLS key are generated on your instance. Nothing from the image build is reused.
- HTTPS from the first boot. nginx serves WordPress on port 443 with a certificate generated on your instance's first boot. Port 80 redirects to HTTPS.
- Fail closed. nginx and Redis do not start until first boot has finished. If first boot cannot complete, the site stays offline instead of open.
- The Classic editor's TinyMCE is off by default. WordPress 7.1 still bundles TinyMCE 4.9.11 for the Classic editor, and that version has published stored cross-site scripting advisories with no 4.x fix. The image keeps it unreachable. A must-use plugin turns off the Classic (visual) editor and the Classic block, and nginx refuses every TinyMCE file. The block editor is unaffected (Step 6).
- Automatic minor security updates on. WordPress applies 7.1.x security releases by itself, and its scheduled jobs run from a system timer every 5 minutes instead of on visitors' page loads (Step 11).
- Closed-by-default settings. XML-RPC, pingbacks and trackbacks, and application passwords are off. Anonymous visitors cannot list users over the REST API, with
?author=, or from the sitemap. Sign-up is off. Avatars (Gravatar) and the emoji script are off, so pages load nothing from third-party servers. - Uploads never run. Files in
wp-content/uploadsare served as static files only, each withnosniffand a sandboxing content security policy, and anything but images, PDF, fonts, audio and video is sent as a download. PHP runs only from WordPress itself and fromwp-content/pluginsandwp-content/themes. - Requests from other sites cannot act for you. Every request that changes something (every form submission and REST write, on any address of the site) and the admin-ajax, admin-post and oEmbed proxy endpoints only work when the request comes from your site itself (Step 10).
- A rate-limited sign-in. nginx allows 10 sign-in attempts a minute from one address. It uses the address of the connection, never a header that a client sends.
- A PHP that cannot reach the shell. PHP cannot run shell commands, fork, change its environment, load extensions or use FFI, in the web server and in WP-CLI alike. Nothing written by the web server under
/var/www,/tmpor the session folder can be executed. The web server's PHP is confined to the site withopen_basedir. - WP-CLI as the web server user.
sudo wp ...runs WP-CLI aswww-data(the owner of the site files) in its own sandbox, never as root, so a plugin's code never runs with root's rights. - Hardened defaults. MariaDB has no network listener (WordPress talks to it over a local socket), and its
rootaccount signs in only through the socket as the operating system's root. nginx, PHP, MariaDB, Redis, first boot, the scheduled jobs and WP-CLI are blocked from the EC2 instance metadata service. - Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.
Prerequisites
- An AWS account that is subscribed to the listing, an EC2 key pair, and a VPC subnet with a route to the internet (WordPress checks
api.wordpress.orgfor updates). - A security group that allows TCP 22 (SSH), TCP 443 (WordPress) and TCP 80 (redirects to HTTPS) from the networks that should reach them. Keep port 22 open to your own networks only. Open port 443 to your visitors for a public site, or only to your team while you build it. Redis needs no port because it has no network listener.
Launch the instance
In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick WordPress 7.1 with Redis Cache on Ubuntu 24.04 as the software version and your Region, then choose Launch through EC2. Choose m5.large, your key pair, and a security group as described in Prerequisites.
To launch with the AWS CLI instead, replace the values in angle brackets:
aws ec2 run-instances --region <region> --image-id <ami-id-from-the-listing> \
--instance-type m5.large --key-name <your-key-pair> \
--security-group-ids <your-security-group> --subnet-id <your-subnet> \
--metadata-options HttpTokens=required,HttpEndpoint=enabled \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=wordpress}]'Connect to your instance
Connect over SSH with your key pair. For WordPress 7.1 with Redis Cache on Ubuntu 24.04, the SSH user is ubuntu.
ssh -i <your-key.pem> ubuntu@<public-ip>Check the services
First boot takes under a minute. It creates the database and the Redis account, and it runs WordPress's installer. Afterwards, nginx, PHP, MariaDB, Redis, the first-boot unit and the timer for WordPress's scheduled jobs are active:
systemctl is-active nginx php8.3-fpm mariadb redis-server wordpress-firstboot wordpress-cron.timeractive active active active active activenginx serves ports 80 and 443. PHP, MariaDB and Redis listen on local sockets only, so the only open ports are 22, 80 and 443:
sudo ss -tlnp | awk 'NR==1 || /nginx|sshd|php|maria|redis/' | awk '{print $1, $4, $6}' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g'
sudo ss -xlp | awk '/redis|mysqld|php/ {print $5}' | sort -uState Local Peer LISTEN 0.0.0.0:443 users:(("nginx"),("nginx"),("nginx")) LISTEN 0.0.0.0:80 users:(("nginx"),("nginx"),("nginx")) LISTEN 0.0.0.0:22 users:(("sshd"),("systemd")) LISTEN [::]:443 users:(("nginx"),("nginx"),("nginx")) LISTEN [::]:80 users:(("nginx"),("nginx"),("nginx")) LISTEN [::]:22 users:(("sshd"),("systemd")) /run/mysqld/mysqld.sock /run/php/php8.3-fpm.sock /run/redis/redis.sockCheck the WordPress release, the PHP, MariaDB and Redis versions, and how the downloads were verified:
sudo wp core version --extra php -v | head -1 mariadb --version redis-server --version cat /usr/share/doc/sac-wordpress/RELEASEWordPress version: 7.1.4 Database revision: 61833 TinyMCE version: 4.9110 (49110-20250317) Package language: en_US PHP 8.3.6 (cli) (built: Oct 19 2026 15:16:58) (NTS) mariadb Ver 15.1 Distrib 10.11.14-MariaDB, for debian-linux-gnu (x86_64) using EditLine wrapper Redis server v=7.2.17 sha=00000000:0 malloc=jemalloc-5.3.0 bits=64 build=47b9f0adc7c76f00 WordPress 7.1.4 source https://wordpress.org/wordpress-7.1.4.tar.gz sha256 881e53a4681ff2cd911651b11cccdede5ae477ddf0765af5d6a1a8b219a1d835 sha1 b175879c4fb2d35300f9abe1153da9b543a35a2b WP-CLI 2.12.0 (https://github.com/wp-cli/wp-cli/releases/download/v2.12.0/wp-cli-2.12.0.phar, sha256 ce34ddd838f7351d6759068d09793f26755463b4a4610a5a5c0a97b68220d85c) theme: Twenty Twenty-Five 1.5 (bundled) plugin: Redis Object Cache 3.0.0 (https://downloads.wordpress.org/plugin/redis-cache.3.0.0.zip, sha256 7a03342d3defbe94cac8d0470b42dc1a1c34331b19c47205df20ceccd3c8f54a; GPL-3.0; vendored Predis 2.4.1 MIT) object cache: Redis 7.2.17 (see /usr/share/doc/sac-redis) not shipped: Akismet, Hello Dolly, Twenty Twenty-Three, Twenty Twenty-Four option B (TinyMCE 4.9.11 off by default): /usr/local/lib/sac/wordpress/mu-plugins/sac-wp-no-tinymce.php + /etc/nginx/snippets/sac-wp-no-tinymce.confGet your sign-in details
The credentials file holds the address that WordPress is served as (your instance's public DNS name by default), the administration address, the administrator's user name, e-mail and password, and the database credentials. All of them are generated on this instance. Show the file with:
sudo cat /root/wordpress-credentials.txtWORDPRESS_URL=https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/ WORDPRESS_ADMIN_URL=https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/wp-admin/ WORDPRESS_ADMIN_USER=sac WORDPRESS_ADMIN_PASSWORD=<24 letters and digits> WORDPRESS_ADMIN_EMAIL=admin@example.com WORDPRESS_DB_NAME=wordpress WORDPRESS_DB_USER=wordpress WORDPRESS_DB_PASSWORD=<48 letters and digits>The Redis password is not in this file. WordPress reads it from WP_REDIS_PASSWORD in /var/www/wp-config.php, and you never need to type it (Step 7).
The administrator starts with the e-mail admin@example.com and the display name Site Administrator. Change both to your own under Users > Profile before you rely on WordPress's password e-mails. The administrator's public author name (site-admin) is not the sign-in name.
Visit the site and sign in
Open WORDPRESS_URL in your browser. The certificate is self-signed (generated on your instance), so your browser warns once. Compare the certificate's SHA-256 fingerprint with the one on the instance (sudo openssl x509 -in /etc/nginx/ssl/wordpress.crt -noout -fingerprint -sha256) and continue, or install a certificate from your own certificate authority (Step 13). A fresh site shows the Twenty Twenty-Five theme with WordPress's Hello world! post:
Open WORDPRESS_ADMIN_URL and sign in with WORDPRESS_ADMIN_USER and WORDPRESS_ADMIN_PASSWORD:
The dashboard opens with WordPress's welcome panel, the Redis Object Cache widget and the admin menu on the left:
The dashboard may say that a newer major WordPress release (such as 7.2) is available. A major upgrade is your decision and is never installed automatically (Step 11).
Write with the block editor (the Classic editor is off)
Posts lists your posts (a fresh site has WordPress's Hello world!). Add Post opens a new one:
Posts and pages open in the block editor: add paragraphs, headings, images, galleries, columns, buttons and patterns from the + inserter:
Appearance > Editor opens the Site Editor, where you design the theme's templates, header, footer, colours and fonts with the same blocks:
Why there is no Classic editor. WordPress 7.1 still ships TinyMCE 4.9.11 (TinyMCE version: 4.9110 in Step 3), the editor behind the Classic (visual) editor and the Classic block. TinyMCE 4.x has published high-severity stored cross-site scripting advisories (CVE-2026-47759, CVE-2026-47761 and CVE-2026-47762), and no 4.x release fixes them. A user who can write posts could store content that runs script in an editor's browser when the Classic editor opens it. This image therefore keeps TinyMCE unreachable by default, in two independent layers:
- The must-use plugin
sac-wp-no-tinymce(in/usr/local/lib/sac/wordpress/mu-plugins, owned by root, outside the web root, with no deactivate switch in the admin area) turns the visual editor off for every user, removes the Classic block and the old editor scripts, and keeps every editing screen on the block editor. - nginx refuses every TinyMCE file, in every spelling, and the script loader that could bundle it.
The block editor, the Site Editor, comments and the text-mode (HTML) editors keep working. Older posts written in the Classic editor open in the block editor, where their content is kept as HTML that you can convert to blocks.
sudo wp plugin list --status=must-use --fields=name,status sudo wp theme list --fields=name,status,version
H=$(sed -n 's/^set \$wp_host "\(.*\)";$/\1/p' /etc/nginx/wordpress-host.conf) for p in /wp-includes/js/tinymce/tinymce.min.js /wp-includes/js/tinymce/wp-tinymce.php /WP-INCLUDES/JS/TINYMCE/tinymce.min.js "/wp-admin/load-scripts.php?load%5Bchunk_0%5D=wp-tinymce" /wp-includes/js/jquery/jquery.min.js; do curl -sk -o /dev/null -w "$p: %{http_code}\n" --resolve "$H:443:127.0.0.1" "https://$H$p" donename status sac-wp-no-tinymce must-use sac-wordpress-hardening must-use name status version twentytwentyfive active 1.5 /wp-includes/js/tinymce/tinymce.min.js: 403 /wp-includes/js/tinymce/wp-tinymce.php: 403 /WP-INCLUDES/JS/TINYMCE/tinymce.min.js: 403 /wp-admin/load-scripts.php?load%5Bchunk_0%5D=wp-tinymce: 403 /wp-includes/js/jquery/jquery.min.js: 200(403 is nginx refusing the file; jQuery, which the admin area needs, still loads.)
What re-opens the advisories. The protection covers WordPress as shipped. Re-enabling the Classic editor brings back TinyMCE 4.9.11 and its unfixed stored cross-site scripting advisories. The Classic Editor plugin uses core's TinyMCE (off while user_can_richedit is false), so re-enabling it together with removing the mu-plugin brings TinyMCE back. So does removing sac-wp-no-tinymce.php or the nginx rule /etc/nginx/snippets/sac-wp-no-tinymce.conf. So does adding a plugin or theme that brings its own copy of TinyMCE, for example a page builder or form plugin with a visual editor.
SecureAnyCloud recommends keeping the Classic editor off until WordPress ships a fixed editor. If you need it in the meantime, give only trusted users the right to write posts, and check the release notes of each WordPress upgrade (Step 11).
The Redis object cache
The Redis Object Cache plugin is active and its drop-in (wp-content/object-cache.php) is in place, connected to Redis over the local socket /run/redis/redis.sock as the Redis account wordpress. Settings > Redis shows the connection:
To check from the command line, run:
sudo wp redis status | grep -E '^(Status|Client|Drop-in|Ping|Redis Version|WP_REDIS_PATH):'Status: Connected Client: Predis (v2.4.1) Drop-in: Valid Ping: PONG Redis Version: 7.2.17 WP_REDIS_PATH: "/run/redis/redis.sock"How Redis is locked down. Redis has no TCP port (port 0). It listens only on its socket, which only the redis user and the members of the redis-wp group (the web server user www-data) can open. Protected mode is on and the debug command is off. Redis keeps nothing on disk (save "", appendonly no), so the cache refills from the database after a restart. It uses at most 256 MB and drops the least recently used entries.
Its accounts are in /etc/redis/users.acl, which your instance's first boot writes with only the SHA-256 of the password:
sudo grep -E '^(port|unixsocket|unixsocketperm|protected-mode|aclfile|enable-debug-command|save|appendonly|maxmemory) ' /etc/redis/redis.conf
sudo stat -c '%a %U:%G %n' /etc/redis/users.acl /run/redis /run/redis/redis.sock id www-dataport 0 unixsocket /run/redis/redis.sock unixsocketperm 770 protected-mode yes save "" appendonly no aclfile /etc/redis/users.acl maxmemory 256mb enable-debug-command no 640 root:redis /etc/redis/users.acl 750 redis:redis-wp /run/redis 770 redis:redis-wp /run/redis/redis.sock uid=33(www-data) gid=33(www-data) groups=33(www-data),986(redis-wp)A connection without the password is refused (the default account is off). The commands below read WordPress's password from wp-config.php into the environment, so it never appears on a command line. The wordpress account can use the cache, but it cannot flush everything, read or change the configuration, replicate, run scripts, manage accounts or load modules:
sudo -u redis redis-cli -s /run/redis/redis.sock PING export REDISCLI_AUTH="$(sudo awk -F"'" '/WP_REDIS_PASSWORD/{print $6}' /var/www/wp-config.php)" for c in PING DBSIZE FLUSHALL "CONFIG GET dir" "REPLICAOF NO ONE" "SCRIPT FLUSH" "ACL LIST" "MODULE LIST"; do printf '%s: ' "$c"; sudo --preserve-env=REDISCLI_AUTH -u redis redis-cli -s /run/redis/redis.sock --user wordpress $c | head -1; done unset REDISCLI_AUTHNOAUTH Authentication required. PING: PONG DBSIZE: 166 FLUSHALL: NOPERM User wordpress has no permissions to run the 'flushall' command CONFIG GET dir: NOPERM User wordpress has no permissions to run the 'config|get' command REPLICAOF NO ONE: NOPERM User wordpress has no permissions to run the 'replicaof' command SCRIPT FLUSH: NOPERM User wordpress has no permissions to run the 'script|flush' command ACL LIST: NOPERM User wordpress has no permissions to run the 'acl|list' command MODULE LIST: NOPERM User wordpress has no permissions to run the 'module|list' commandDBSIZE is the number of cached entries, and it grows as pages are loaded. To empty the cache, select Flush Cache on Settings > Redis, or run:
sudo wp cache flushSuccess: The cache was flushed.Disable Object Cache on Settings > Redis (or sudo wp redis disable) removes the drop-in, and WordPress goes back to its built-in, per-request cache. sudo wp redis enable turns the object cache back on.
The closed-by-default settings
The image ships with these settings. Each item names the image default and where to change it:
- Permalinks: /%postname%/. Change under Settings > Permalinks.
- Anyone can register: off. Change under Settings > General.
- Pingbacks and trackbacks: off. Change under Settings > Discussion.
- Avatars (Gravatar): off. Change under Settings > Discussion.
- Time zone: UTC. Change under Settings > General.
- XML-RPC and application passwords: off. Controlled by the must-use plugin sac-wordpress-hardening.php.
- Theme and plugin file editor: off (DISALLOW_FILE_EDIT). Controlled in wp-config.php.
- Automatic core updates: minor (7.1.x) releases. Controlled in wp-config.php (WP_AUTO_UPDATE_CORE).
- Object cache: Redis over /run/redis/redis.sock. Controlled in wp-config.php (WP_REDIS_*) and under Settings > Redis.
sudo wp option get permalink_structure for o in users_can_register default_ping_status default_pingback_flag show_avatars timezone_string blog_public; do printf '%s=%s\n' "$o" "$(sudo wp option get $o)"; done
sudo grep -E "^define\( '(WP_AUTO_UPDATE_CORE|DISALLOW_FILE_EDIT|DISABLE_WP_CRON|FS_METHOD|FORCE_SSL_ADMIN|WPMU_PLUGIN_DIR|WP_REDIS_CLIENT|WP_REDIS_SCHEME|WP_REDIS_PATH)'" /var/www/wp-config.php/%postname%/ users_can_register=0 default_ping_status=closed default_pingback_flag=0 show_avatars=0 timezone_string=UTC blog_public=1 define( 'WP_REDIS_CLIENT', 'predis' ); define( 'WP_REDIS_SCHEME', 'unix' ); define( 'WP_REDIS_PATH', '/run/redis/redis.sock' ); define( 'FORCE_SSL_ADMIN', true ); define( 'DISALLOW_FILE_EDIT', true ); define( 'DISABLE_WP_CRON', true ); define( 'WP_AUTO_UPDATE_CORE', 'minor' ); define( 'FS_METHOD', 'direct' ); define( 'WPMU_PLUGIN_DIR', '/usr/local/lib/sac/wordpress/mu-plugins' );blog_public=1 means search engines may index the site. Set Settings > Reading > Search engine visibility while you build it.
Your data
Everything that makes your site lives in the wordpress database in MariaDB and in the uploads under /var/www/wordpress/wp-content/uploads. Plugins and themes that you add are under wp-content/plugins and wp-content/themes. Keep /var/www/wp-config.php with your backup, because it holds this site's database and Redis passwords, keys and salts. Redis holds only a cache, so it needs no backup.
Back up the database to a file that only root can read. MariaDB's root signs in over the local socket without a password, so no password appears on the command line:
sudo sh -c 'umask 077; mysqldump --single-transaction wordpress > /root/wordpress-$(date +%F).sql'
sudo tar -C /var/www -czf /root/wordpress-files-$(date +%F).tgz wp-config.php wordpress/wp-contentFor a whole-instance copy, snapshot the EBS volume. After you restore a database into this site, run sudo wp cache flush so that Redis does not serve entries from before the restore.
Security checks
Port 80 sends browsers to HTTPS, and the health endpoint answers for load balancer checks. The following are not reachable: XML-RPC, the web cron trigger, the configuration, the installer, the readme, WordPress's non-public PHP files, a path after index.php, PHP in uploads, the object cache drop-in and dotfiles.
H=$(sed -n 's/^set \$wp_host "\(.*\)";$/\1/p' /etc/nginx/wordpress-host.conf)
curl -s -o /dev/null -D - --resolve "$H:80:127.0.0.1" "http://$H/wp-admin/" | tr -d '\r' | awk 'NR==1 {print "port 80:", $2} tolower($1)=="location:" {print "redirects to:", $2}' | sed -E 's#https://[^/]+#https://<your host>#'
curl -sk --resolve "$H:443:127.0.0.1" "https://$H/healthz"; echo for p in /xmlrpc.php /wp-cron.php /wp-config.php /wp-admin/install.php /wp-admin/setup-config.php /readme.html /wp-includes/version.php /index.php/wp-json/wp/v2/users /wp-content/uploads/x.php /wp-content/object-cache.php /.git/config; do
curl -sk -o /dev/null -w "$p: %{http_code}\n" -H 'Sec-Fetch-Site: same-origin' --resolve "$H:443:127.0.0.1" "https://$H$p" done
curl -s -o /dev/null -w 'unknown host name: %{http_code}\n' -H 'Host: example.com' http://127.0.0.1/ || trueport 80: 301 redirects to: https://<your host>/wp-admin/ ok /xmlrpc.php: 403 /wp-cron.php: 403 /wp-config.php: 403 /wp-admin/install.php: 403 /wp-admin/setup-config.php: 403 /readme.html: 403 /wp-includes/version.php: 403 /index.php/wp-json/wp/v2/users: 404 /wp-content/uploads/x.php: 404 /wp-content/object-cache.php: 404 /.git/config: 404 unknown host name: 000A result of 000 means that nginx closed the connection without an answer. Anonymous visitors cannot list your users, and a form posted from another site is refused:
H=$(sed -n 's/^set \$wp_host "\(.*\)";$/\1/p' /etc/nginx/wordpress-host.conf) for p in /wp-json/wp/v2/users "/?rest_route=/wp/v2/users" "/?author=1" /wp-sitemap-users-1.xml; do curl -sk -o /dev/null -w "$p: %{http_code}\n" --resolve "$H:443:127.0.0.1" "https://$H$p" done curl -sk -o /dev/null -w 'POST /wp-login.php from another site: %{http_code}\n' -H 'Sec-Fetch-Site: cross-site' -d 'log=x&pwd=y' --resolve "$H:443:127.0.0.1" "https://$H/wp-login.php"/wp-json/wp/v2/users: 401 /?rest_route=/wp/v2/users: 403 /?author=1: 404 /wp-sitemap-users-1.xml: 404 POST /wp-login.php from another site: 403Only root can read the credentials file, and only root and the web server can read wp-config.php. The site files belong to the web server user so that WordPress can update itself, while the must-use plugins belong to root:
sudo stat -c '%a %U:%G %n' /root/wordpress-credentials.txt /var/www/wp-config.php /var/www /var/www/wordpress /var/www/wordpress/wp-content/uploads /var/www/wordpress/index.php /usr/local/lib/sac/wordpress/mu-plugins /usr/local/lib/sac/wordpress/mu-plugins/sac-wp-no-tinymce.php600 root:root /root/wordpress-credentials.txt 640 root:www-data /var/www/wp-config.php 755 root:root /var/www 750 www-data:www-data /var/www/wordpress 750 www-data:www-data /var/www/wordpress/wp-content/uploads 640 www-data:www-data /var/www/wordpress/index.php 755 root:root /usr/local/lib/sac/wordpress/mu-plugins 644 root:root /usr/local/lib/sac/wordpress/mu-plugins/sac-wp-no-tinymce.phpAn uploaded HTML file is sent as a download with nosniff and the sandboxing policy, never rendered as a page of your site:
H=$(sed -n 's/^set \$wp_host "\(.*\)";$/\1/p' /etc/nginx/wordpress-host.conf)
sudo -u www-data install -d -m 0750 /var/www/wordpress/wp-content/uploads/guide-demo echo '<b>demo</b>' | sudo -u www-data tee /var/www/wordpress/wp-content/uploads/guide-demo/demo.html >/dev/null curl -sk -D - -o /dev/null --resolve "$H:443:127.0.0.1" "https://$H/wp-content/uploads/guide-demo/demo.html" | tr -d '\r' | grep -iE '^(HTTP|content-disposition|content-security-policy|x-content-type)'
sudo -u www-data rm -rf /var/www/wordpress/wp-content/uploads/guide-demoHTTP/2 200 x-content-type-options: nosniff content-security-policy: default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'; sandbox content-disposition: attachmentWordPress and PHP are blocked from the EC2 instance metadata service, in the web stack and in WP-CLI, so a vulnerable plugin cannot fetch the instance's credentials. Redis needs no network at all, so it is denied every address (0.0.0.0/0 and ::/0, listed in either order):
for s in nginx php8.3-fpm mariadb redis-server wordpress-cron; do printf '%s: %s\n' "$s" "$(systemctl show -p IPAddressDeny --value $s.service)"; done
sudo wp eval '$r = wp_remote_get("http://169.254.169.254/latest/meta-data/", array("timeout" => 4)); echo is_wp_error($r) ? "metadata service: unreachable\n" : "metadata service: REACHABLE\n";'nginx: 168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32 php8.3-fpm: 168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32 mariadb: 168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32 redis-server: 0.0.0.0/0 ::/0 wordpress-cron: 168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32 metadata service: unreachableUpdates and scheduled jobs
Automatic updates. WordPress installs minor (7.1.x) releases by itself, normally within 12 hours of their release. This works because the site files belong to the web server user and WP_AUTO_UPDATE_CORE is minor. To do this, WordPress contacts api.wordpress.org (and downloads.wordpress.org) twice a day.
WordPress also checks there for plugin (including Redis Object Cache), theme and translation updates. It shows them in Dashboard > Updates and installs them only if you turn on their automatic updates. If your instance has no route to the internet, apply updates yourself.
Scheduled jobs. WordPress's scheduled jobs (update checks, publishing scheduled posts, cleanups) run every 5 minutes from wordpress-cron.timer as the web server user, instead of from visitors' page loads. wp-cron.php is not reachable:
systemctl is-enabled wordpress-cron.timer
sudo wp core check-update --minor sudo wp cron event list --fields=hook,recurrence --format=csv | tail -n +2 | sortenabled Success: WordPress is at the latest version. delete_expired_transients,"1 day" recovery_mode_clean_expired_keys,"1 day" rediscache_discard_metrics,"1 hour" wp_delete_temp_updater_backups,"1 week" wp_privacy_delete_old_export_files,"1 hour" wp_privacy_personal_data_cleanup_requests,"1 day" wp_scheduled_auto_draft_delete,"1 day" wp_scheduled_delete,"1 day" wp_site_health_scheduled_check,"1 week" wp_update_plugins,"12 hours" wp_update_themes,"12 hours" wp_update_user_counts,"12 hours" wp_version_check,"12 hours"Major upgrades. A new major release (such as 7.2) is not installed automatically. Take a snapshot and check that your plugins and theme support the release. Then use Dashboard > Updates or sudo wp core update. After any upgrade, check that the Classic editor is still off (Step 6) and that the object cache is still connected (Step 7).
Redis. Redis is built from the upstream source into /usr/local/bin, so apt does not update it. Redis security releases are listed at https://github.com/redis/redis/releases (7.2 branch). To move to a newer Redis, launch a newer version of this image and restore your site into it (Step 9).
Plugins and themes. Installing a plugin or theme from the admin area installs code that runs with your site's rights. Install only what you trust and keep it updated. The admin area cannot edit plugin or theme files (DISALLOW_FILE_EDIT).
Behind a proxy or load balancer
nginx removes X-Forwarded-For, X-Real-IP, Client-IP, X-Forwarded, Forwarded-For and Forwarded from every request and hands WordPress your configured host name. WordPress therefore sees the address of the connection and never builds links from a host that a request claims.
Behind an AWS load balancer, that address is the load balancer's, so the sign-in limit applies to all visitors together. To record visitors' addresses, use nginx's real_ip module with set_real_ip_from set to your load balancer's subnet only. Terminate HTTPS on port 443 of the instance, because WordPress forces HTTPS for the admin area.
Your own DNS name and certificate
Point a DNS record at the instance, then set the name that WordPress is served as. This command updates WP_HOME and WP_SITEURL in wp-config.php and rewrites the site address stored in the database. It mints a new self-signed certificate for the name, updates nginx and the cross-site rules, and updates the addresses in /root/wordpress-credentials.txt. The name persists across reboots:
sudo wordpress-set-hostname <your-domain>To use a certificate from your own certificate authority instead, copy it over the generated pair and reload nginx. Running wordpress-set-hostname again replaces it with a self-signed one:
sudo install -m 0644 <your-domain>.crt /etc/nginx/ssl/wordpress.crt
sudo install -m 0600 <your-domain>.key /etc/nginx/ssl/wordpress.key
sudo nginx -t && sudo systemctl reload nginxOnce browsers trust the certificate you can also add an HSTS header (add_header Strict-Transport-Security "max-age=31536000" always;) to /etc/nginx/snippets/sac-wordpress-headers.conf. The image does not send one, because browsers ignore HSTS from a site whose certificate they do not trust.
Passwords, e-mail and WP-CLI
wordpress-set-password gives the administrator a new random password, signs out every session and records the password in /root/wordpress-credentials.txt. Users change their own passwords under Users > Profile.
sudo wordpress-set-passwordE-mail. The image has no mail server, so WordPress cannot send password reset, comment or update e-mails until you add an SMTP plugin that you trust (configured with your provider, such as Amazon SES) or a local mail relay.
WP-CLI. Run it as sudo wp <command>. It runs as the web server user www-data in its own sandbox (no metadata service, no shell commands), with the same rights as the site. For example, run sudo wp plugin list, sudo wp user list, sudo wp core verify-checksums or sudo wp redis status.
Server components
- WordPress (owned by www-data, updated by WordPress): /var/www/wordpress
- Configuration (this instance's database and Redis passwords, keys and salts): /var/www/wp-config.php
- Uploads: /var/www/wordpress/wp-content/uploads
- Redis Object Cache plugin and its drop-in: /var/www/wordpress/wp-content/plugins/redis-cache, /var/www/wordpress/wp-content/object-cache.php
- SecureAnyCloud must-use plugins (owned by root): /usr/local/lib/sac/wordpress/mu-plugins/
- WP-CLI and its sandboxed wrapper: /usr/local/lib/sac/wordpress/wp-cli.phar, /usr/local/bin/wp
- Database (MariaDB datadir): /var/lib/mysql
- Redis server and client (built from upstream source): /usr/local/bin/redis-server, /usr/local/bin/redis-cli, redis-server.service
- Redis settings and accounts: /etc/redis/redis.conf, /etc/redis/users.acl
- Redis socket: /run/redis/redis.sock
- nginx site, request limits, cross-site and TinyMCE rules: /etc/nginx/sites-available/sac-wordpress, /etc/nginx/conf.d/sac-wordpress-guard.conf, /etc/nginx/conf.d/sac-xsite-guard.conf, /etc/nginx/snippets/sac-wordpress-*.conf, /etc/nginx/snippets/sac-wp-no-tinymce.conf
- PHP settings: /etc/php/8.3/fpm/conf.d/99-sac-wordpress.ini, /etc/php/8.3/cli/conf.d/99-sac-wordpress.ini
- MariaDB settings: /etc/mysql/mariadb.conf.d/99-sac-wordpress.cnf
- Scheduled jobs: wordpress-cron.timer, wordpress-cron.service
- Certificate and key: /etc/nginx/ssl/
- First boot and helpers: /usr/local/sbin/wordpress-firstboot.sh, wordpress-set-hostname, wordpress-set-password, wordpress-cred-roundtrip
- Licence, release and image notes: /usr/share/doc/sac-wordpress/, /usr/share/doc/sac-redis/
- Credentials: /root/wordpress-credentials.txt (mode 0600)
Security recommendations
- Allow port 22 in the security group from your own networks only. Do not open a port for Redis. It has none, and it should stay reachable only over its local socket.
- Change the administrator e-mail to your own (Step 4), keep its password secret, and rotate it if it may have been shared (Step 14). Give editors and authors their own accounts with the narrowest role that works.
- Keep
wp-config.phpand your database backups secret (Step 9). - Use your own DNS name and a certificate that your browsers trust (Step 13).
- Leave automatic minor updates on, keep plugins (including Redis Object Cache) and themes updated, and install only plugins and themes that you trust. Keep the Classic editor off, because re-enabling it brings back TinyMCE 4.9.11 and its unfixed advisories (Step 6).
- Keep the operating system patched. Unattended security upgrades are on, and
sudo apt-get update && sudo apt-get upgradeapplies everything else. MariaDB, PHP-FPM and PHP's zip extension come from Ubuntu's universe repository.sudo pro security-status --esm-appsshows whether Ubuntu Pro (ESM Apps) has security updates for them (none were pending when the image was built). Redis is not an Ubuntu package, so follow its releases as described in Step 11.
Support and licensing
WordPress is free software under the GNU General Public License, version 2 or later (a copy is at /usr/share/doc/sac-wordpress/LICENSE). The Twenty Twenty-Five theme is GPL-2.0-or-later, WP-CLI is MIT licensed, and WordPress's bundled libraries carry their own open-source licences.
Redis 7.2.17 is licensed under the three-clause BSD licence (a copy is at /usr/share/doc/sac-redis/COPYING). The Redis Object Cache plugin is GPL-3.0, and the Predis and Credis client libraries that it bundles are MIT licensed. There is no per-user or per-CPU licence fee and no licence key.
This image installs the upstream WordPress release without the Akismet and Hello Dolly plugins and the two older default themes. It also installs the upstream Redis 7.2.17 release and the Redis Object Cache plugin, plus the SecureAnyCloud first-boot setup, settings, must-use plugins and helpers described above. The AMI price covers SecureAnyCloud's hardening, first-boot setup and support.
WordPress is a trademark of the WordPress Foundation and Redis is a trademark of Redis Ltd. SecureAnyCloud is not affiliated with or endorsed by either.
- Email: sales@secureanycloud.com
- Website: https://secureanycloud.com
- Support: 24/7, with a 24 hour response SLA
Deploy on AWS
Launch WordPress 7.1 with Redis Cache on Ubuntu 24.04 on AWS with 24/7 support from SecureAnyCloud.
View on Marketplace
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com