<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Javier Seng</title>
    <description>The latest articles on Forem by Javier Seng (@javierseng55).</description>
    <link>https://forem.com/javierseng55</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3107476%2F5c8cbae9-6210-4f67-8c07-6a1b728020b1.jpg</url>
      <title>Forem: Javier Seng</title>
      <link>https://forem.com/javierseng55</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/javierseng55"/>
    <language>en</language>
    <item>
      <title>Building a Cloud-Native S3 Honeypot Detection Pipeline on AWS</title>
      <dc:creator>Javier Seng</dc:creator>
      <pubDate>Sat, 17 May 2025 12:59:57 +0000</pubDate>
      <link>https://forem.com/javierseng55/building-a-cloud-native-s3-honeypot-detection-pipeline-on-aws-17o8</link>
      <guid>https://forem.com/javierseng55/building-a-cloud-native-s3-honeypot-detection-pipeline-on-aws-17o8</guid>
      <description>&lt;h2&gt;
  
  
  &lt;strong&gt;Building a Cloud-Native S3 Honeypot Detection Pipeline on AWS&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;u&gt;Table of Contents&lt;/u&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Introduction&lt;/li&gt;
&lt;li&gt;Step 1: Deploy a Private Honeypot Bucket&lt;/li&gt;
&lt;li&gt;Step 2: Log S3 Data Events with CloudTrail → CloudWatch&lt;/li&gt;
&lt;li&gt;Step 3: Create a CloudWatch Metric Filter&lt;/li&gt;
&lt;li&gt;Step 4: Alarm &amp;amp; SNS Notification&lt;/li&gt;
&lt;li&gt;Step 5: Lambda Automation to Tag VPC&lt;/li&gt;
&lt;li&gt;Testing the Pipeline&lt;/li&gt;
&lt;li&gt;Next-Level Enhancements&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Introduction&lt;/strong&gt;&lt;br&gt;
In this blog post, I’ll demonstrate how to build an end-to-end honeypot detection pipeline on AWS—catching unauthorized access to a decoy S3 file, alerting the team, and automatically tagging the attacker’s VPC. We’ll leverage AWS-native services like S3, CloudTrail, CloudWatch, Lambda, and SNS to create a turnkey security solution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Deploy a Private Honeypot Bucket&lt;br&gt;
Create your S3 bucket&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Name: javierlabs-sensitive-docs&lt;/li&gt;
&lt;li&gt;Region: ap-southeast-2&lt;/li&gt;
&lt;li&gt;Block all public access&lt;/li&gt;
&lt;li&gt;Upload decoy files:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;aws-keys.txt (fake credentials)&lt;br&gt;
passwords.csv&lt;br&gt;
internal-financials.xlsx&lt;br&gt;
README.txt (⚠️ Confidential banner)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq17mu3zba3s1pvavmmja.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq17mu3zba3s1pvavmmja.png" alt=" " width="800" height="397"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Lock it down &amp;amp; allow GuardDuty access:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "Version":"2012-10-17",
  "Statement":[
    {
      "Sid":"AllowGuardDutyAccess",
      "Effect":"Allow",
      "Principal":{"Service":"guardduty.amazonaws.com"},
      "Action":["s3:GetObject","s3:ListBucket"],
      "Resource":[
        "arn:aws:s3:::javierlabs-sensitive-docs",
        "arn:aws:s3:::javierlabs-sensitive-docs/*"
      ],
      "Condition":{"StringEquals":{"AWS:SourceAccount":"070978211986"}}
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: Log S3 Data Events with CloudTrail → CloudWatch&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enable CloudTrail S3 data events for read/write on your bucket.&lt;/li&gt;
&lt;li&gt;Send logs to CloudWatch Logs, using a log group like /aws/cloudtrail/honeypot-logs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Create a CloudWatch Metric Filter&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the /aws/cloudtrail/honeypot-logs log group:&lt;br&gt;
Pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{ ($.eventName = "GetObject" || $.eventName = "HeadObject")
  &amp;amp;&amp;amp; $.requestParameters.key = "aws-keys.txt" }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Metric:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Namespace: HoneypotDetection&lt;/li&gt;
&lt;li&gt;Name: AccessedAWSKeysFile&lt;/li&gt;
&lt;li&gt;Value: 1&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Alarm &amp;amp; SNS Notification&lt;/strong&gt;&lt;br&gt;
From the metric (HoneypotDetection/AccessedAWSKeysFile), create an alarm:&lt;/p&gt;

&lt;p&gt;Trigger when &amp;gt;= 1 in 1 datapoint.&lt;/p&gt;

&lt;p&gt;Attach SNS action to topic honeypot-alerts.&lt;/p&gt;

&lt;p&gt;Name: AlertOnAWSKeysAccess.&lt;/p&gt;

&lt;p&gt;Confirm your email subscription in SNS.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftp5d3roipiq0o8iwmmcq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftp5d3roipiq0o8iwmmcq.png" alt=" " width="800" height="174"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: Lambda Automation to Tag VPC&lt;/strong&gt;&lt;br&gt;
TagAttackerIP Function&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import json, boto3
from datetime import datetime, timedelta

LOOKBACK_MINUTES = 5
VPC_ID = "vpc-078816b7d00f13bd4"

def lambda_handler(event, context):
    print("Alarm Event:", json.dumps(event, indent=2))
    if event['detail'].get('alarmName') != 'AlertOnAWSKeysAccess':
        return {"status":"ignored"}

    region = event.get('region','ap-southeast-2')
    ct = boto3.client('cloudtrail', region_name=region)
    end = datetime.utcnow()
    start = end - timedelta(minutes=LOOKBACK_MINUTES)

    ip = None
    for action in ("GetObject","HeadObject"):
        resp = ct.lookup_events(
            LookupAttributes=[{"AttributeKey":"EventName","AttributeValue":action}],
            StartTime=start, EndTime=end, MaxResults=10
        )
        for evt in resp.get("Events",[]):
            p = json.loads(evt["CloudTrailEvent"])
            if (p.get("eventSource")=="s3.amazonaws.com"
                and p.get("requestParameters",{}).get("key")=="aws-keys.txt"):
                ip = p.get("sourceIPAddress"); break
        if ip: break

    if not ip: return {"status":"no_ip_found"}
    print("Found attacker IP:", ip)

    ec2 = boto3.client('ec2', region_name=region)
    ec2.create_tags(
        Resources=[VPC_ID],
        Tags=[{"Key":"SuspectedAttackerIP","Value":ip}]
    )
    return {"status":"success","ip":ip}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;IAM Policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "Statement":[
    {"Action":"cloudtrail:LookupEvents","Effect":"Allow","Resource":"*"},
    {"Action":"ec2:CreateTags","Effect":"Allow",
     "Resource":"arn:aws:ec2:ap-southeast-2:070978211986:vpc/vpc-078816b7d00f13bd4"}
  ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Testing the Pipeline&lt;/strong&gt;&lt;br&gt;
Trigger a HEAD/GET:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;curl -I &amp;lt;honeypot url&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;CloudWatch Alarm goes ALARM, email arrives.&lt;/p&gt;

&lt;p&gt;Lambda logs show “Found attacker IP: …”.&lt;/p&gt;

&lt;p&gt;VPC tags now include the attacker's IP address&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpz1t9clwrs60wkbo2a8h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpz1t9clwrs60wkbo2a8h.png" alt=" " width="800" height="154"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6cjtthy90cg1ls75vz3a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6cjtthy90cg1ls75vz3a.png" alt=" " width="800" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next-Level Enhancements&lt;br&gt;
Persist hits to DynamoDB with TTL.&lt;/p&gt;

&lt;p&gt;Auto-block via WAF or Security Groups.&lt;/p&gt;

&lt;p&gt;Enrich with Threat Intel feeds.&lt;/p&gt;

&lt;p&gt;Deploy cross-region honeypots.&lt;/p&gt;

&lt;p&gt;Use CloudFront signed URLs for advanced deception.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;By combining AWS-native logging, monitoring, and serverless automation, you can build a robust real-time honeypot detection and response platform with minimal overhead—ideal for any cloud security engineer’s portfolio.&lt;/p&gt;

&lt;p&gt;Happy hunting!&lt;br&gt;
Feel free to leave feedback or ask questions in the comments.&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>beginners</category>
      <category>javascript</category>
      <category>programming</category>
    </item>
    <item>
      <title>Building a Secure VPC on AWS: Public &amp; Private Subnets with Bastion Host and NAT Gateway</title>
      <dc:creator>Javier Seng</dc:creator>
      <pubDate>Fri, 09 May 2025 05:20:50 +0000</pubDate>
      <link>https://forem.com/javierseng55/building-a-secure-vpc-on-aws-public-private-subnets-with-bastion-host-and-nat-gateway-5lj</link>
      <guid>https://forem.com/javierseng55/building-a-secure-vpc-on-aws-public-private-subnets-with-bastion-host-and-nat-gateway-5lj</guid>
      <description>&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;This project simulates a production-ready AWS environment with strong security boundaries, using a custom Virtual Private Cloud (VPC), public/private subnet separation, a bastion host for controlled admin access, and a NAT gateway for outbound-only internet access from private instances.&lt;/p&gt;

&lt;p&gt;In real-world infrastructure, it’s common to isolate frontend and backend components at the networking layer. Direct access to critical systems (like databases or internal APIs) is tightly restricted, and secure channels (like bastion hosts or session managers) are used for administrative access. This architecture helps organizations enforce least privilege, reduce their attack surface, and follow zero-trust design principles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Business Value
&lt;/h2&gt;

&lt;p&gt;This architecture lays the foundation for secure, enterprise-grade infrastructure in the cloud. By enforcing strict network boundaries between public and private resources, leveraging a bastion host for administrative access, and using a NAT Gateway to support outbound communication without exposing backend systems, organizations can meet both operational and compliance requirements. It minimizes exposure to the internet, isolates workloads by function, and aligns with cloud security best practices — making it suitable for any workload involving sensitive data, regulated environments, or production backend systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Creating a custom VPC (10.0.0.0/16)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Segmenting it into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1. 2 Public Subnets (for public-facing resources like bastion hosts)&lt;/li&gt;
&lt;li&gt;&lt;ol&gt;
&lt;li&gt;2 Private Subnets (for sensitive backend servers)&lt;/li&gt;
&lt;/ol&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Launching a bastion host in a public subnet for secure administrative SSH access&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Deploying a private EC2 instance that has no direct internet exposure&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Setting up a NAT Gateway so the private EC2 can still reach the internet for package updates&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Configuring route tables to enforce proper traffic flow and isolation&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Part 1: Creating the VPC and Subnet Architecture&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To begin, I created a custom VPC called SecureVPC with a CIDR block of 10.0.0.0/16, which gives us 65,536 IP addresses — more than enough for segregated subnets across multiple availability zones.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0ym0e4lhn60vyts95q0d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0ym0e4lhn60vyts95q0d.png" alt=" " width="800" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I then divided the network into four /24 subnets (256 IPs each):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two public subnets for internet-facing resources (e.g., the bastion host)&lt;/li&gt;
&lt;li&gt;Two private subnets for sensitive resources (e.g., backend EC2s, databases)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Subnetting this way helps segment environments by function and prepares us for things like high availability, fault tolerance, and security zoning.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnv2dzyqs2wp4ag0ok8jw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnv2dzyqs2wp4ag0ok8jw.png" alt=" " width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Creating and Attaching the Internet Gateway&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To allow outbound traffic from the public subnets, I created an Internet Gateway and attached it to the VPC. This is required for any resource (like a bastion host) to have direct internet access.&lt;/p&gt;

&lt;p&gt;This gateway will only be referenced in the route table for public subnets — not private ones.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fat010o1lr8mpp1jt45sp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fat010o1lr8mpp1jt45sp.png" alt=" " width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Making Subnets Truly Public (Auto-Assign IP)&lt;/strong&gt;&lt;br&gt;
Even if a subnet routes to an Internet Gateway, instances launched inside it won’t be reachable unless they have public IP addresses. So I enabled auto-assign public IPv4 for the public subnets. This ensures EC2s launched there (e.g., bastion host) get a public IP automatically.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwpmekx2or89b775t75bq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwpmekx2or89b775t75bq.png" alt=" " width="800" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Deploying a Bastion Host for Secure Admin Access&lt;/strong&gt;&lt;br&gt;
Next, I launched an EC2 instance (Amazon Linux 2023) in PublicSubnet1. This bastion host acts as a jump server, allowing me to SSH into private EC2 instances securely.&lt;/p&gt;

&lt;p&gt;Security group settings were crucial here:&lt;/p&gt;

&lt;p&gt;Inbound Rule: SSH (port 22) allowed only from my public IP&lt;/p&gt;

&lt;p&gt;Outbound Rule: Open to all (default)&lt;/p&gt;

&lt;p&gt;This setup follows the principle of least privilege and ensures that only authorized admins can reach the private backend network.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmugf6ybyfz86vpbo70ze.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmugf6ybyfz86vpbo70ze.png" alt=" " width="800" height="395"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: SSH Access Test to Bastion&lt;/strong&gt;&lt;br&gt;
I tested SSH access from my terminal using the PEM key and confirmed that access was restricted correctly.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ssh -i ~/Desktop/AWS_projects/aws-webserver-key.pem ec2-user@&amp;lt;bastion-ip&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flo0w089slutgejr9poj5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flo0w089slutgejr9poj5.png" alt=" " width="800" height="476"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 6: Launching a Private EC2 Instance&lt;/strong&gt;&lt;br&gt;
I then launched a private EC2 instance in PrivateSubnet1. It had no public IP, which means it is not accessible from the internet under any circumstances.&lt;/p&gt;

&lt;p&gt;Security group:&lt;/p&gt;

&lt;p&gt;Inbound Rule: SSH (port 22) allowed only from the bastion subnet CIDR&lt;/p&gt;

&lt;p&gt;Outbound Rule: Open (to allow NAT access)&lt;/p&gt;

&lt;p&gt;This is a textbook example of private-by-default security, and it can be applied to database servers, internal APIs, and sensitive workloads.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg6sx03ewyh59q9rc0a8d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg6sx03ewyh59q9rc0a8d.png" alt=" " width="800" height="370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 7: Configuring the NAT Gateway&lt;/strong&gt;&lt;br&gt;
To let the private EC2 instance reach the internet (for package updates, outbound requests), I set up a NAT Gateway in PublicSubnet1.&lt;/p&gt;

&lt;p&gt;The NAT Gateway uses an Elastic IP and allows outbound internet traffic only — meaning private instances can access the internet, but no one from outside can initiate connections back in.&lt;/p&gt;

&lt;p&gt;This is the key to controlled outbound access in private zones.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8vj2rb49ai5258dd31z4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8vj2rb49ai5258dd31z4.png" alt=" " width="800" height="335"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 8: Updating Private Route Table&lt;/strong&gt;&lt;br&gt;
I created a new PrivateRouteTable and associated it with PrivateSubnet1. I added the following routes:&lt;/p&gt;

&lt;p&gt;10.0.0.0/16 → local (VPC internal communication)&lt;/p&gt;

&lt;p&gt;0.0.0.0/0 → NAT Gateway&lt;/p&gt;

&lt;p&gt;This ensures private instances route external requests through NAT, and internal traffic stays within the VPC.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4lw56ojpoi4pmkjhc9ci.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4lw56ojpoi4pmkjhc9ci.png" alt=" " width="800" height="395"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 9: Final Internet Access Test&lt;/strong&gt;&lt;br&gt;
From my Mac:&lt;/p&gt;

&lt;p&gt;SSH’d into the bastion ✅&lt;br&gt;
From the bastion:&lt;/p&gt;

&lt;p&gt;SSH’d into the private EC2 ✅&lt;br&gt;
From private EC2:&lt;/p&gt;

&lt;p&gt;Ran curl ifconfig.me and sudo yum update -y ✅&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F36lpxn14plunoy8vzjdz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F36lpxn14plunoy8vzjdz.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This proved that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The EC2 in the private subnet had outbound internet&lt;/li&gt;
&lt;li&gt;It remained unreachable from the outside world&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Public subnets use an Internet Gateway; private subnets use a NAT Gateway&lt;/li&gt;
&lt;li&gt;Bastion hosts should always restrict SSH to a trusted admin IP&lt;/li&gt;
&lt;li&gt;Route tables are the glue that define how traffic flows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This architecture prevents direct public exposure of sensitive resources while preserving needed functionality.&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>beginners</category>
      <category>basic</category>
      <category>aws</category>
    </item>
    <item>
      <title>Deploying a Static Website on a Custom Domain with HTTPS Using AWS</title>
      <dc:creator>Javier Seng</dc:creator>
      <pubDate>Thu, 08 May 2025 07:37:17 +0000</pubDate>
      <link>https://forem.com/javierseng55/deploying-a-static-website-on-a-custom-domain-with-https-using-aws-134</link>
      <guid>https://forem.com/javierseng55/deploying-a-static-website-on-a-custom-domain-with-https-using-aws-134</guid>
      <description>&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;This project builds on my previous work where I deployed a static website using Amazon S3 and CloudFront. In this phase, I extended the setup to serve the site on a custom domain (javierlab.com) with full HTTPS support, DNS configuration, and a custom 404 error page. The goal was to transform the existing setup into a polished, production-grade deployment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why This Project Matters (Business Value)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A custom domain with HTTPS isn't just a nice-to-have—it's a standard for any serious business or professional online presence. This project demonstrates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Secure delivery with AWS Certificate Manager (ACM)&lt;/li&gt;
&lt;li&gt;Professional branding with Route 53 and domain configuration&lt;/li&gt;
&lt;li&gt;Improved UX with a custom error page&lt;/li&gt;
&lt;li&gt;Scalability and performance using AWS's global edge network via CloudFront&lt;/li&gt;
&lt;li&gt;Hands-on knowledge of DNS, SSL/TLS, and error routing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All these are critical for roles in cloud security, DevOps, and infrastructure automation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Was Already Set Up
&lt;/h2&gt;

&lt;p&gt;From the previous project:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A static website was hosted in an S3 bucket: javier-static-site-2025&lt;/li&gt;
&lt;li&gt;A CloudFront distribution was set up to serve the S3 content globally&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This project extends that setup without duplicating it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step-by-Step Breakdown
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Purchase and Register the Domain&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Domain javierlab.com was purchased via AWS Route 53&lt;/p&gt;

&lt;p&gt;A public hosted zone was automatically created&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Request SSL Certificate in ACM&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Region used: us-east-1 (N. Virginia) (required for CloudFront SSL)&lt;/p&gt;

&lt;p&gt;Added both javierlab.com and &lt;a href="http://www.javierlab.com" rel="noopener noreferrer"&gt;www.javierlab.com&lt;/a&gt; as domain names&lt;/p&gt;

&lt;p&gt;Selected DNS validation (recommended for automation)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft2u0fbvfntu8wk4bfywp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft2u0fbvfntu8wk4bfywp.png" alt=" " width="800" height="385"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft9pzlr7iks7loqn5sasm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft9pzlr7iks7loqn5sasm.png" alt=" " width="800" height="386"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Validate Domain Ownership&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Used "Create records in Route 53" to automatically add the necessary CNAME records&lt;/p&gt;

&lt;p&gt;Waited for AWS to validate the domain (takes a few minutes)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv0plh0nx08o1xuc3mo92.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv0plh0nx08o1xuc3mo92.png" alt=" " width="800" height="386"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Upload Custom 404 Error Page&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Reused the existing S3 bucket (javier-static-site-2025)&lt;/p&gt;

&lt;p&gt;Uploaded error.html with styling and a link back to the homepage&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzoed6vlaozjjgnbo1i01.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzoed6vlaozjjgnbo1i01.png" alt=" " width="800" height="544"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: Configure CloudFront to Use SSL and Serve Custom Errors&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Attached the validated ACM certificate to the CloudFront distribution&lt;/p&gt;

&lt;p&gt;Added alternate domain names:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;javierlab.com&lt;/li&gt;
&lt;li&gt;&lt;a href="http://www.javierlab.com" rel="noopener noreferrer"&gt;www.javierlab.com&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Configured a custom error response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP error code: 404
Custom error page path: /error.html
Response code: 404
TTL: 10 seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqucbwslse25j76bsgm2p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqucbwslse25j76bsgm2p.png" alt=" " width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 6: Set Up Route 53 Alias Records&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Created A records to route traffic to CloudFront:&lt;/p&gt;

&lt;p&gt;javierlab.com → CloudFront&lt;br&gt;
&lt;a href="http://www.javierlab.com" rel="noopener noreferrer"&gt;www.javierlab.com&lt;/a&gt; → CloudFront&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmsv06tgfh70bxqedeeic.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmsv06tgfh70bxqedeeic.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5nt2pylpmb9n71bqgxbk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5nt2pylpmb9n71bqgxbk.png" alt=" " width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 7: Test the Setup&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Opened &lt;a href="https://javierlab.com" rel="noopener noreferrer"&gt;https://javierlab.com&lt;/a&gt; and confirmed successful HTTPS delivery&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fduna62b1v9fkody41tvb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fduna62b1v9fkody41tvb.png" alt=" " width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tested a non-existent page (/thispagedoesnotexist) and confirmed custom 404 handling&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsv6c8uag0e3ri4r18z2l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsv6c8uag0e3ri4r18z2l.png" alt=" " width="800" height="438"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;(Optional) Step: Redirect &lt;a href="http://www.javierlab.com" rel="noopener noreferrer"&gt;www.javierlab.com&lt;/a&gt; to javierlab.com using S3&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To ensure consistent traffic routing and avoid duplicate content across subdomains, I configured a redirection bucket:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Created an S3 bucket named &lt;a href="http://www.javierlab.com" rel="noopener noreferrer"&gt;www.javierlab.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Enabled static website hosting, but selected “Redirect requests”&lt;/li&gt;
&lt;li&gt;Target: javierlab.com&lt;/li&gt;
&lt;li&gt;Protocol: https&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhjaj3lia9z3s2xd123ik.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhjaj3lia9z3s2xd123ik.png" alt=" " width="800" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Created an A record in Route 53 for &lt;a href="http://www.javierlab.com" rel="noopener noreferrer"&gt;www.javierlab.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Type: A – IPv4 address&lt;/li&gt;
&lt;li&gt;Alias: Yes → Targeted the S3 static website endpoint&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fub3cmmrnk0ovqq7sprwb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fub3cmmrnk0ovqq7sprwb.png" alt=" " width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This ensures all traffic going to &lt;a href="http://www.javierlab.com" rel="noopener noreferrer"&gt;www.javierlab.com&lt;/a&gt; is redirected cleanly to the root domain javierlab.com.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Final Thoughts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This project refined an existing S3+CloudFront deployment by layering on key production features: HTTPS, domain management, and UX-focused error handling. It’s a clear, job-ready demonstration of practical AWS architecture and hands-on cloud implementation. Perfect foundation for my next move: infrastructure-as-code with Terraform and CI/CD automation.&lt;/p&gt;

&lt;p&gt;Stay tuned.&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>tutorial</category>
      <category>aws</category>
      <category>cybersecurity</category>
    </item>
    <item>
      <title>Deploying a Static Website with AWS S3, CloudFront, and WAF Web ACL</title>
      <dc:creator>Javier Seng</dc:creator>
      <pubDate>Sat, 03 May 2025 11:16:36 +0000</pubDate>
      <link>https://forem.com/javierseng55/deploying-a-static-website-with-aws-s3-cloudfront-and-waf-web-acl-hd8</link>
      <guid>https://forem.com/javierseng55/deploying-a-static-website-with-aws-s3-cloudfront-and-waf-web-acl-hd8</guid>
      <description>&lt;p&gt;In this post, I’ll walk through how I deployed a static website using Amazon S3 and distributed it globally with CloudFront. Then, I applied security hardening using AWS Web ACL to defend against common threats.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Build the Static Site&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I created a simple HTML and CSS layout:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;index.html&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcpg1oubuc28tv58ikqy8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcpg1oubuc28tv58ikqy8.png" alt=" " width="800" height="604"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang="en"&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset="UTF-8" /&amp;gt;
    &amp;lt;meta name="viewport" content="width=device-width, initial-scale=1.0" /&amp;gt;
    &amp;lt;title&amp;gt;CloudFront Static Site&amp;lt;/title&amp;gt;
    &amp;lt;style&amp;gt;
      body {
        background-color: #0e0e0e;
        color: #f4f4f4;
        font-family: sans-serif;
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100vh;
        margin: 0;
      }
      .container {
        text-align: center;
      }
    &amp;lt;/style&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;div class="container"&amp;gt;
      &amp;lt;h1&amp;gt;Hello, CloudFront!&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;This site is served from an S3 bucket and distributed globally with AWS CloudFront.&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;style.css&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F295p2stm98okxj37r30z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F295p2stm98okxj37r30z.png" alt=" " width="800" height="437"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;body {
  background-color: #0e0e0e;
  color: #f4f4f4;
  font-family: sans-serif;
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
  margin: 0;
}

.container {
  text-align: center;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I then uploaded both files to a new S3 bucket named:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;javier-static-site-2025&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5sagta630wmsojoo3uj1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5sagta630wmsojoo3uj1.png" alt=" " width="800" height="370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Enable Static Website Hosting on S3&lt;/strong&gt;&lt;br&gt;
In the S3 bucket settings, I enabled static website hosting:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp9jomr2d5yx1gpt96p0h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp9jomr2d5yx1gpt96p0h.png" alt=" " width="800" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Selected “Host a static website”&lt;/p&gt;

&lt;p&gt;Entered index.html as the index document&lt;/p&gt;

&lt;p&gt;Left the error document blank&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Configure Bucket Policy for Public Read Access&lt;/strong&gt;&lt;br&gt;
To allow CloudFront (and users) to access the files, I added a bucket policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PublicRead",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::javier-static-site-2025/*"
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4hlspgdvtt8bwlo7yhis.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4hlspgdvtt8bwlo7yhis.png" alt=" " width="800" height="374"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Preview the Website on S3&lt;/strong&gt;&lt;br&gt;
After applying the policy, I accessed the site directly via the S3 static hosting endpoint:&lt;/p&gt;

&lt;p&gt;&lt;a href="http://javier-static-site-2025.s3-website-ap-southeast-2.amazonaws.com" rel="noopener noreferrer"&gt;http://javier-static-site-2025.s3-website-ap-southeast-2.amazonaws.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2eeivjvw5v47r0fay4f1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2eeivjvw5v47r0fay4f1.png" alt=" " width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The site loaded correctly, showing the custom “Hello, CloudFront!” message.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: Set Up a CloudFront Distribution&lt;/strong&gt;&lt;br&gt;
I created a CloudFront distribution to globally accelerate and cache content:&lt;/p&gt;

&lt;p&gt;Origin Domain: S3 website endpoint (not the default bucket domain)&lt;/p&gt;

&lt;p&gt;Origin Access: Public (since I allowed public access in the S3 bucket policy)&lt;/p&gt;

&lt;p&gt;Viewer Protocol Policy: Redirect HTTP to HTTPS&lt;/p&gt;

&lt;p&gt;Allowed Methods: GET, HEAD (default)&lt;/p&gt;

&lt;p&gt;Compression: Enabled&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F861b994jjaszil82j3xn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F861b994jjaszil82j3xn.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqn6ivzt0o2gvhmy7r5sz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqn6ivzt0o2gvhmy7r5sz.png" alt=" " width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 6: Link CloudFront to Web ACL (WAF)&lt;/strong&gt;&lt;br&gt;
I created a Web ACL named CloudFrontWebACL and associated it with the CloudFront distribution. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0dce7p0tm8fmryu8xs6y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0dce7p0tm8fmryu8xs6y.png" alt=" " width="800" height="707"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Adding AWS WAF Rules
&lt;/h2&gt;

&lt;p&gt;With the Web ACL CloudFrontWebACL created and associated with my CloudFront distribution, I added several rules to strengthen the site’s security posture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Managed Rule Sets Added&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;AWSManagedRulesCommonRuleSet&lt;br&gt;
Covers a wide range of common threats like LFI, bad bots, size restrictions, and more.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;AWSManagedRulesAmazonIpReputationList&lt;br&gt;
Blocks traffic from known malicious IPs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;AWSManagedRulesSQLiRuleSet&lt;br&gt;
Specifically targets SQL injection attempts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Custom Rate Limiting Rule – LimitRequestsByIP&lt;br&gt;
Blocks any IP that makes more than 10 requests in a 5-minute window.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For all managed rules, I set the action override to “Block” to ensure they actively drop malicious traffic rather than just counting matches.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffwlysnextlj8iqx39lo4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffwlysnextlj8iqx39lo4.png" alt=" " width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testing the Web ACL&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the part where studying for the eJPT certification came in handy. To validate the effectiveness of each rule, I ran several tests using curl.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Rate Limiting&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;for i in {1..25}; do
  curl -s -o /dev/null "https://&amp;lt;CloudFront URL&amp;gt;" &amp;amp;
done
wait
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1ry63n2awhta6y77u6b5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1ry63n2awhta6y77u6b5.png" alt=" " width="800" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Result:&lt;br&gt;
Requests beyond the 10-request threshold were blocked as expected. I confirmed this via the WAF metrics panel in CloudWatch, which showed exactly 25 blocked requests.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwgf8j917w62jtc3agu9z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwgf8j917w62jtc3agu9z.png" alt=" " width="800" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi9doc32sl1zv5vk7gbcm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi9doc32sl1zv5vk7gbcm.png" alt=" " width="800" height="278"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. SQL Injection Attempt&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;curl "https://&amp;lt;CloudFront URL&amp;gt;/?id=1%27%3B%20DROP%20TABLE%20users--"&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Result:&lt;br&gt;
The request was blocked with a 403 error, showing that the SQLiRuleSet triggered correctly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmqm6on7ine03a2o2h7d3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmqm6on7ine03a2o2h7d3.png" alt=" " width="800" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. SQLMap User-Agent Fingerprint&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl -A "sqlmap/1.0" https://&amp;lt;CloudFront URL&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result:&lt;br&gt;
Blocked with a 403 error. This confirmed that malicious User-Agent headers were being filtered properly by the common rule set.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5kzcww5r1wmbajvpvheu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5kzcww5r1wmbajvpvheu.png" alt=" " width="800" height="502"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Path-Based Recon&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;curl https://&amp;lt;CloudFront URL&amp;gt;/admin&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6srdezlkhsbq70ei5wtd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6srdezlkhsbq70ei5wtd.png" alt=" " width="800" height="502"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Result:&lt;br&gt;
Returned a 404 error from S3 (object not found), but was not blocked by WAF. This is expected — WAF only triggers on known threat patterns, not missing pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CloudWatch Logs Verification&lt;/strong&gt;&lt;br&gt;
To confirm that the WAF rules were actively blocking traffic, I reviewed the metrics and charts under the “WAF &amp;gt; Web ACLs &amp;gt; CloudFrontWebACL” section.&lt;/p&gt;

&lt;p&gt;The rate limiting rule showed 25 blocked requests at the exact timestamp of my curl loop test.&lt;/p&gt;

&lt;p&gt;SQLi and User-Agent tests were also reflected in blocked request counts under the relevant rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;This project helped me understand how to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Deploy a static site via S3 and accelerate it globally with CloudFront&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Configure bucket permissions and static hosting&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Set up and customize AWS WAF rules&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Simulate attacks and observe real-time block metrics in CloudWatch&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The final result is a minimal, fast, and well-protected static site — an ideal foundation for future personal projects.&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>aws</category>
      <category>tutorial</category>
      <category>discuss</category>
    </item>
  </channel>
</rss>
