Golden AMIs: How We Provision CIS-Hardened WordPress in 30 Seconds
If you’re provisioning WordPress instances from bare EC2s, you’re burning 10+ minutes per tenant and hoping nothing drifts. We bake the entire stack into an immutable AMI. Here’s the architecture.
CIS Level 1
AWS Graviton
Ubuntu 24.04
Multi-Tenant
The Problem: Provisioning From Scratch Doesn’t Scale
If you’re running a multi-tenant WordPress platform on AWS, you’ve probably built something like this: Terraform spins up an EC2 instance, a user-data script or Ansible playbook installs packages, downloads WordPress, hardens the OS, configures Nginx, starts services, and eventually — 10 to 15 minutes later — you have a working site.
That works for 5 tenants. It falls apart at 50.
The failure modes are predictable:
apt-get install hits the network. WordPress downloads from wordpress.org. Plugins download from the plugin repo. A customer is staring at a loading screen while your provisioner fights a CDN.
The golden AMI pattern eliminates all of this. Build once. Validate once. Deploy the same proven image every time.
Architecture: The Packer Build Pipeline
We use HashiCorp Packer to build an Ubuntu 24.04 LTS AMI on ARM64 Graviton. Seven provisioner scripts run in sequence, each handling one concern. The output is an encrypted EBS-backed AMI with the full WordPress stack on disk.
## Packer Build Pipeline
source “amazon-ebs” “ubuntu” {
ami_name = “45sq-golden-ami-${var.version}”
instance_type = “t4g.small” # Graviton ARM64
encrypt_boot = true
source_ami = “ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-arm64-server-*”
}
build {
provisioner “shell” { scripts = [
“scripts/system-packages.sh”, # Phase 1
“scripts/mariadb-setup.sh”, # Phase 2
“scripts/nginx-base.sh”, # Phase 3
“scripts/wordpress-cache.sh”, # Phase 4
“scripts/cis-harden.sh”, # Phase 5
“scripts/aide-init.sh”, # Phase 6
“scripts/cleanup.sh”, # Phase 7
]}
}
Here’s what each phase does:
| Phase | Script | What It Does |
|---|---|---|
| 1 | system-packages | Installs Nginx, PHP 8.3-FPM, MariaDB 11.4 LTS, AWS CLI, CloudWatch Agent, SSM Agent, WP-CLI |
| 2 | mariadb-setup | Secures MariaDB — removes anonymous users, tunes InnoDB buffer pool for 2 GB RAM |
| 3 | nginx-base | Security headers, FastCGI page caching, PHP-FPM socket config, XML-RPC disabled |
| 4 | wordpress-cache | Pre-downloads WordPress core, Ollie FSE theme, plugins (CF7, Yoast, S3 Offload) to /opt/45sq/wordpress-cache/ |
| 5 | cis-harden | CIS Ubuntu 24.04 Level 1 — kernel hardening, SSH lockdown, UFW firewall, auditd, Fail2ban |
| 6 | aide-init | Initializes AIDE file integrity database, schedules daily integrity checks |
| 7 | cleanup | Strips build artifacts, SSH keys, logs, shell history — minimizes snapshot size |
Output: a 20 GB encrypted AMI with everything on disk. No network calls at deploy time. No apt-get. No curl. Everything is already there.
CIS Level 1 Hardening: Baked In, Not Bolted On
Most teams treat hardening as a post-deploy step. Run a script after launch, hope it completes, move on. We run the CIS Ubuntu 24.04 Level 1 benchmark as part of the Packer build. Every instance is compliant from the moment it boots.
The cis-harden.sh script covers:
Kernel & Network
- IP forwarding disabled
- ICMP redirects blocked
- Source routing rejected
- Unused filesystems masked (USB, HFS, UDF)
SSH & Access
- Root login disabled
- Key-only authentication
- Session timeouts enforced
- SSM Session Manager for shell access
Firewall & Monitoring
- UFW default-deny (allow 80, 443, 22)
- Fail2ban on SSH + wp-login.php
- Auditd for privilege escalation
- AIDE file integrity monitoring
On top of this: PHP has dangerous functions disabled and session cookies secured. Nginx enforces X-Frame-Options and X-Content-Type-Options headers, blocks access to wp-config.php and .env at the web server level. Services with no business on a WordPress server — snapd, CUPS, Avahi, Bluetooth — are masked.
This isn’t a checklist we run after launch. It’s infrastructure that ships compliant by default.
The Bootstrap: AMI to Live Site in 30 Seconds
The AMI handles the what — a hardened, pre-loaded image. The bootstrap handles the who — configuring that image for a specific tenant. The flow:
## Bootstrap Flow (tenant-config.sh)
Phase 1: Fetch tenant config JSON from S3
→ domain, DB creds, admin users, AI payload
Phase 2: Create MariaDB database + user
→ isolated DB per tenant, utf8mb4
Phase 3: Configure Nginx vhost
→ rate limiting on wp-login.php, tenant-specific logs
Phase 4: Copy WordPress from cache → wp core install
→ ~10 seconds, no network download
Phase 5: Inject AI-generated content + child theme
→ Home, Services, Contact pages with Gutenberg blocks
→ Contact Form 7, brand colors, template overrides
Phase 6: Activate monitoring + backups
→ CloudWatch Agent, daily S3 backup cron, AIDE
Phase 7: Write completion JSON, clean up temp files
→ Instance is live.
The entire bootstrap is a single shell script triggered via SSM Run Command. No Ansible. No Chef. No orchestration layer. The provisioner sends a tenant ID, the script reads a JSON config from S3, and applies it.
Total wall clock time: 30–45 seconds from EC2 launch to a customer-facing WordPress site with AI-generated content, a custom child theme, and full monitoring.
Why Not Containers?
Fair question. Containers are the default for multi-tenant SaaS. We chose AMIs for WordPress specifically because of what WordPress is.
WordPress expects cron jobs, file permissions, mail utilities, and a database on localhost. Containerizing that means either running multiple processes in a single container (an anti-pattern) or orchestrating 3–4 containers per tenant (expensive and complex at scale).
The tradeoffs stack up quickly:
| Concern | Containers (ECS/EKS) | Golden AMI (EC2) |
|---|---|---|
| Tenant isolation | Shared kernel, shared runtime | Own kernel, own firewall, own filesystem |
| CIS compliance | Container CIS + host CIS + orchestrator CIS | VM-level CIS only — well-documented, maps to audits |
| WordPress compat | Multi-process anti-pattern or 3–4 containers/tenant | Full Linux environment — cron, mail, DB on localhost |
| Cost per tenant | ECS/EKS overhead + NAT Gateway + ALB rules | t4g.small Graviton — ~$12/mo |
| Noisy neighbor | Shared compute, resource contention | Dedicated instance — no contention |
Golden AMIs give you container-like reproducibility with VM-level isolation. For WordPress multi-tenancy, that’s the right tradeoff.
CI/CD: The AMI Stays Fresh
A stale golden AMI is a liability. Unpatched kernel, outdated PHP, old WordPress — you’ve just traded deploy-time drift for image-level drift. We automate the entire lifecycle:
packer/** or bootstrap/** triggers a build. ShellCheck lints every script, PHP syntax is verified, Nginx configs are tested before the AMI is registered.
Old AMIs are cleaned up automatically. We keep the last two for rollback.
Multi-Tenant Operations at Scale
Launching a site is half the problem. Running hundreds of them without a dedicated ops team requires operational tooling baked into the image from day one.
Per-Tenant Monitoring
CloudWatch Agent ships Nginx access/error logs, MariaDB slow query logs, and custom metrics to tenant-specific log groups. Each tenant is independently observable.
Automated Backups
Daily cron at 02:00 UTC dumps the database and syncs the WordPress directory to a tenant-specific S3 bucket. The bootstrap configures this — no manual setup per tenant.
File Integrity Detection
AIDE runs daily checks against the WordPress directory baseline. Modified files outside of a deployment trigger CloudWatch alerts. This catches compromised plugins, injected backdoors, and unauthorized edits.
Dunning Lockout
A must-use plugin handles delinquent accounts. Day 3: admin notice with billing link. Day 7: wp-login.php returns 402 Payment Required. Password reset links pass through for account recovery.
The Numbers
| Metric | Before | After |
|---|---|---|
| Provisioning time | 10–15 minutes | 30–45 seconds |
| Configuration drift | Varies per deploy | Zero — immutable image |
| CIS compliance | Post-deploy hardening (if remembered) | Baked into AMI — compliant at boot |
| Network dependencies at deploy | apt repos, wordpress.org, GitHub, plugin CDNs | S3 only (tenant config JSON) |
| Failure recovery | Debug half-configured instance | Terminate and relaunch — 45 seconds |
Every instance is identical. Every instance is compliant. Every instance is monitored from boot. When you need to update the base — new PHP, new security patch, new WordPress — rebuild the AMI and let the next launch pick it up. You stop fixing servers and start replacing them.
We Write About This Stuff Every Week
AWS architecture, AI automation, infrastructure-as-code patterns, and operational deep-dives. No fluff. Subscribe and get it in your inbox.
Or explore the AI Site Launcher — the product this infrastructure powers.
