Golden AMI for aiopscrew.com on AWS — 45Squared Blog
Cloud Infrastructure

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.

Packer
CIS Level 1
AWS Graviton
Ubuntu 24.04
Multi-Tenant
30–45s
Tenant Ready Time
CIS L1
Compliance at Boot
0 Drift
Immutable Infrastructure

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:

❌ Speed. Every 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.
❌ Drift. Instance #1 got PHP 8.3.12. Instance #47 got PHP 8.3.14 because Ubuntu pushed an update overnight. Now you’re debugging inconsistencies that only exist on some tenants.
❌ Security. CIS hardening scripts run post-deploy — if they run at all. One missed step and you’ve got an instance with password auth enabled and no firewall.
❌ Reliability. An apt mirror goes down. GitHub rate-limits you. A WordPress.org CDN node is slow. Your provisioner fails at step 37 of 52 and leaves a half-configured instance behind.

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:

📊 Weekly rebuilds. GitHub Actions triggers a Packer build every Sunday at 00:00 UTC. New AMI picks up the latest OS patches, PHP updates, and WordPress releases automatically.
📊 Change-triggered builds. Push to packer/** or bootstrap/** triggers a build. ShellCheck lints every script, PHP syntax is verified, Nginx configs are tested before the AMI is registered.
📊 Semantic versioning. Production gets major versions (X.0.0), staging gets minor (1.X.0). Current version lives in SSM Parameter Store — the provisioner always knows which AMI to launch.
📊 Zero long-lived credentials. CI/CD authenticates to AWS via OIDC federation. No access keys in GitHub Secrets. Build instances use IMDSv2 exclusively — SSRF-based credential theft is off the table.

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

Before (Terraform + Ansible)
10–15 min
Per tenant provisioning
After (Golden AMI + Bootstrap)
30–45 sec
Per tenant provisioning
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.

Exit mobile version