DEV Community

Cover image for Deploying Secure WordPress on AWS: A Multi-Tier Architecture Approach
Victor Ojeje
Victor Ojeje

Posted on

Deploying Secure WordPress on AWS: A Multi-Tier Architecture Approach

This project started as a learning exercise and ended up looking very close to how small production workloads are actually deployed. The goal was simple: run WordPress on AWS with proper network isolation, least-privilege access, and basic monitoring, without spending money or cutting corners.

Everything here was built inside Free Tier limits, but the design decisions follow the same patterns used in real environments.

Architecture Overview

I deployed WordPress using a classic two-tier setup inside a custom VPC.

Core components:

  • Custom VPC: 10.0.0.0/16
  • Public subnet: 10.0.1.0/24 for the web server
  • Private subnets: 10.0.10.0/24 and 10.0.11.0/24 for RDS
  • EC2 t2.micro running Ubuntu 24.04
  • RDS MySQL db.t4g.micro
  • Elastic IP for stable external access
  • Apache 2.4, PHP 8.1, WordPress

Traffic flow:

Internet → Internet Gateway → EC2 Compute (public subnet) → RDS (private subnets)
Enter fullscreen mode Exit fullscreen mode

Architecture and traffic flow

[Architecture and traffic flow]

The database has no public IP and no route to the internet. All access is controlled with security groups. I intentionally did not rely on NACLs or host firewalls to keep the design aligned with how AWS environments are usually managed.

Security Implementation

Security Groups

The security group setup is minimal and intentional.

Web tier security group:

  • SSH (22): allowed only from my admin IP
  • HTTP (80) and HTTPS (443): open to the internet

Database tier security group:

  • MySQL (3306): allowed only from the web tier security group

No IP-based rules between tiers. Only security group references.

EC2 Security Group Details

[EC2 security group details]

RDS security group details

[RDS security group details]

This matters. The first version used the EC2 private IP as the database allow list. That failed immediately after a reboot when the IP changed. Switching to security group IDs fixed the issue permanently and reflects how AWS expects environments to scale and change.

WordPress Hardening

I applied basic but meaningful WordPress security controls:

  • Disabled file editing in the admin dashboard
  • Limited login attempts to reduce brute-force risk
  • Disabled XML-RPC
  • Enforced sane file permissions (755 for directories, 644 for files)
  • Enabled HTTPS using a self-signed certificate

Let's Encrypt does not issue certificates for AWS public DNS names. Using a self-signed cert keeps traffic encrypted while avoiding extra costs. Which is fine in this case, as its a demo, not production.

Monitoring and Alerting

Both EC2 and RDS are monitored with CloudWatch.

Alarm configuration:

  • CPU utilization threshold: 70 percent
  • Action: SNS email notification

I chose 70 percent instead of waiting for 80 to 90 percent. On small instance types, sustained CPU spikes quickly translate into performance problems.

CloudWatch dashboard

[CloudWatch dashboard]

CloudWatch alarms

[CloudWatch alarms]

During normal usage, EC2 CPU sat around 12 to 18 percent. During load testing, alarms triggered correctly and SNS emails arrived within about 2 to 3 minutes. That confirms the alerting path actually works, not just that it exists.

Real Problems Encountered

Security Group Misconfiguration

Using IP addresses for internal trust broke the moment the instance restarted. This is a common mistake in AWS environments and a good lesson. In AWS, security groups are the trust boundary, not IPs.

WordPress URL Handling

WordPress stores absolute URLs inside the database. Changing the Elastic IP meant internal links broke even after updating wp-config.php.

The fix required a full database search and replace using WP-CLI. This isn't documented in most WordPress migration guides but breaks production migrations regularly.

Free Tier Cost Awareness

AWS does not stop you from spending money. An unattached Elastic IP started generating charges within hours.

Billing alarms should be set up immediately, even for learning projects. Free Tier does not mean free by default.

Design Trade-offs

Single-AZ RDS

Multi-AZ was skipped to stay within Free Tier. This means backups exist, but there is no automatic failover. Recovery requires restoring from a snapshot and takes several minutes. That is acceptable here, not for systems with tight recovery objectives.

Apache instead of Nginx

Apache was chosen for simplicity and WordPress compatibility. Nginx would scale better under high concurrency, but that advantage does not matter at this size.

Self-signed SSL

Encrypted traffic without a domain cost. Browser warnings are expected. This was a deliberate trade-off, not an oversight.

Results

WordPress live test

[WordPress live test]

  • WordPress reachable over HTTPS via Elastic IP
  • RDS fully isolated in private subnets
  • Database access restricted to the web tier only
  • CloudWatch alarms and SNS notifications verified
  • Automated RDS backups tested via snapshot restore
  • Total cost stayed at $0 within Free Tier limits

Skills Demonstrated

Cloud and Infrastructure:

  • VPC and subnet design
  • Security group based access control
  • EC2 and RDS provisioning
  • Elastic IP management

Linux and Systems:

  • Ubuntu server setup
  • Apache and PHP configuration
  • MySQL client usage
  • Service and permission management

Security:

  • Network isolation
  • Least-privilege enforcement
  • TLS configuration
  • Application-level hardening

Operations:

  • CloudWatch monitoring
  • SNS alerting
  • Backup validation
  • Cost awareness

Final Notes

This project reflects how small but real AWS environments are built. The focus was fully on correct fundamentals including isolation, access control, monitoring, and understanding trade-offs.


Victor Ogechukwu Ojeje

Email: ojejevictor@gmail.com

LinkedIn: linkedin.com/in/victorojeje

GitHub: github.com/escanut

Deployment screenshots and configuration details available in the project repository.

Top comments (0)