<?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>DEV Community: Javier Seng</title>
    <description>The latest articles on DEV Community by Javier Seng (@javierseng55).</description>
    <link>https://dev.to/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>DEV Community: Javier Seng</title>
      <link>https://dev.to/javierseng55</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/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://dev.to/javierseng55/building-a-cloud-native-s3-honeypot-detection-pipeline-on-aws-17o8</link>
      <guid>https://dev.to/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="396"&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://dev.to/javierseng55/building-a-secure-vpc-on-aws-public-private-subnets-with-bastion-host-and-nat-gateway-5lj</link>
      <guid>https://dev.to/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="379"&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://dev.to/javierseng55/deploying-a-static-website-on-a-custom-domain-with-https-using-aws-134</link>
      <guid>https://dev.to/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="385"&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="385"&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="543"&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="363"&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="436"&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="437"&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://dev.to/javierseng55/deploying-a-static-website-with-aws-s3-cloudfront-and-waf-web-acl-hd8</link>
      <guid>https://dev.to/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="436"&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="357"&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="373"&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="363"&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="277"&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="500"&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="501"&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="501"&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>
    <item>
      <title>From Zero to Cloud Hero: My First AWS EC2 &amp; S3 Web Project</title>
      <dc:creator>Javier Seng</dc:creator>
      <pubDate>Wed, 30 Apr 2025 02:49:53 +0000</pubDate>
      <link>https://dev.to/javierseng55/from-zero-to-cloud-hero-my-first-aws-ec2-s3-web-project-26jh</link>
      <guid>https://dev.to/javierseng55/from-zero-to-cloud-hero-my-first-aws-ec2-s3-web-project-26jh</guid>
      <description>&lt;p&gt;As someone pivoting into cloud security, I wanted to get hands-on with AWS. This blog documents how I built a basic web server on EC2, hosted an image on S3, and embedded that image in a live Apache-hosted web page — all secured and scalable. If you're starting your cloud journey, this is the post I wish I had.&lt;/p&gt;

&lt;p&gt;The goal:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Launch an EC2 instance (Ubuntu)&lt;/li&gt;
&lt;li&gt;Host a simple webpage using Apache&lt;/li&gt;
&lt;li&gt;Embed an image stored in an S3 bucket&lt;/li&gt;
&lt;li&gt;Apply basic security best practices&lt;/li&gt;
&lt;li&gt;Power it all from scratch with my own hands&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This post is a walkthrough — not just of what I did, but what I learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Spinning Up an EC2 Instance
&lt;/h2&gt;

&lt;p&gt;The first step was to launch a virtual server (EC2 instance) on AWS. Here's how I did it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Choose the AMI (Amazon Machine Image)&lt;/strong&gt;&lt;br&gt;
I picked the Ubuntu Server 24.04 LTS (HVM) image — a lightweight, stable OS that's commonly used in web hosting setups.&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%2F3zb56w0kaz6g5me2fo1h.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%2F3zb56w0kaz6g5me2fo1h.png" alt=" " width="800" height="528"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Select Instance Type&lt;/strong&gt;&lt;br&gt;
For this simple project, I went with a t2.micro instance — free tier eligible and perfectly sufficient for lightweight 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%2Fj803e3bu8cpo8o8h5d5m.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%2Fj803e3bu8cpo8o8h5d5m.png" alt=" " width="800" height="190"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Create a Key Pair&lt;/strong&gt;&lt;br&gt;
To securely SSH into the instance, I generated a key pair named aws-webserver-key and downloaded the .pem file.&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%2F9o3xcvx7ymy5i1q346vu.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%2F9o3xcvx7ymy5i1q346vu.png" alt=" " width="800" height="773"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Make sure to store the .pem file securely and restrict permissions with:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;chmod 400 aws-webserver-key.pem&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%2Fh547jaoel3ur23jytk8h.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%2Fh547jaoel3ur23jytk8h.png" alt=" " width="800" height="503"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Configure Network Settings&lt;/strong&gt;&lt;br&gt;
I created a new security group with the following inbound rules:&lt;/p&gt;

&lt;p&gt;SSH access from my IP only&lt;/p&gt;

&lt;p&gt;HTTP access from anywhere (to serve the webpage)&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%2Fiw59vkger89c6iwfofk1.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%2Fiw59vkger89c6iwfofk1.png" alt=" " width="800" height="441"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With the instance up and running, I connected via SSH:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ssh -i aws-webserver-key.pem ubuntu@&amp;lt;EC2-PUBLIC-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%2F01z67465l4lx1qhf55rb.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%2F01z67465l4lx1qhf55rb.png" alt=" " width="800" height="548"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Installing Apache and Hosting the Webpage
&lt;/h2&gt;

&lt;p&gt;Once connected to the EC2 instance via SSH, I set up a simple web server using Apache.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Update the System&lt;/strong&gt;&lt;br&gt;
After logging in, I made sure the system packages were up to date:&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%2Fe55sxj386sadx4ysrtif.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%2Fe55sxj386sadx4ysrtif.png" alt=" " width="800" height="548"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sudo apt update&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Install Apache&lt;/strong&gt;&lt;br&gt;
Next, I installed the Apache2 package:&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%2Fyncb9765th0zwmfs280g.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%2Fyncb9765th0zwmfs280g.png" alt=" " width="800" height="546"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sudo apt install apache2 -y&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Enable the Apache Firewall Rule&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Although not strictly necessary for this setup, I ran the following to allow Apache through the firewall:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sudo ufw allow 'Apache'&lt;/code&gt;&lt;br&gt;
&lt;code&gt;sudo ufw reload&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Then I checked the service status to confirm that Apache was running:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sudo systemctl status apache2&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%2Ffgh1dvf4yijz25peyuvu.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%2Ffgh1dvf4yijz25peyuvu.png" alt=" " width="800" height="549"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Test the Web Server&lt;/strong&gt;&lt;br&gt;
Opening the EC2 instance’s public IP address in a browser confirmed Apache was serving the default web page.&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%2Fibt5cm04f489zngq35m9.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%2Fibt5cm04f489zngq35m9.png" alt=" " width="800" height="398"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Customize the Web Page&lt;/strong&gt;&lt;br&gt;
I replaced the default /var/www/html/index.html with a simple HTML file using nano:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cd /var/www/html&lt;/code&gt;&lt;br&gt;
&lt;code&gt;sudo rm index.html&lt;/code&gt;&lt;br&gt;
&lt;code&gt;sudo nano index.html&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;I inserted a basic HTML page:&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&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;Hello Cloud Security&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;h1&amp;gt;Hello, Cloud Security!&amp;lt;/h1&amp;gt;
    &amp;lt;p&amp;gt;Powered by AWS EC2 and Apache&amp;lt;/p&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This immediately reflected in the browser when visiting the instance’s IP:&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%2Flnbasl2h1hb3zvldh3he.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%2Flnbasl2h1hb3zvldh3he.png" alt=" " width="800" height="398"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Hosting an Image in S3 and Embedding It in the Web Page
&lt;/h2&gt;

&lt;p&gt;To take the project a step further, I created an Amazon S3 bucket to host a public image and then embedded that image into my EC2-hosted web page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Create the S3 Bucket&lt;/strong&gt;&lt;br&gt;
In the S3 dashboard, I created a new bucket with a globally unique name.&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%2Fhac907ut6psmzbg2f9rj.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%2Fhac907ut6psmzbg2f9rj.png" alt=" " width="800" height="313"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I kept the default settings, including blocking public access, which is AWS’s secure-by-default posture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Adjust Bucket Permissions&lt;/strong&gt;&lt;br&gt;
To allow public read access to the image, I edited the bucket's permissions and added a Bucket Policy manually.&lt;/p&gt;

&lt;p&gt;Here's the policy I applied:&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": "PublicReadForObjects",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::javier-cloud-project-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%2Fiyak5v2sb0nkgqann9ns.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%2Fiyak5v2sb0nkgqann9ns.png" alt=" " width="800" height="376"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This allows only read access to objects inside the bucket — not the bucket itself, and not any write access.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Upload the Image&lt;/strong&gt;&lt;br&gt;
I uploaded a simple image file named green_apples.jpg to the bucket.&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%2Fauna2jjwd2kxmoy1qexx.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%2Fauna2jjwd2kxmoy1qexx.png" alt=" " width="800" height="329"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Confirm Public Access&lt;/strong&gt;&lt;br&gt;
After uploading, I verified the public URL for the image:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://javier-cloud-project-2025.s3.amazonaws.com/green_apples.jpg" rel="noopener noreferrer"&gt;https://javier-cloud-project-2025.s3.amazonaws.com/green_apples.jpg&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%2Fgilzeobfxmx9lquom7m9.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%2Fgilzeobfxmx9lquom7m9.png" alt=" " width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Embed Image in the Web Page&lt;/strong&gt;&lt;br&gt;
To finish the integration, I edited index.html on the EC2 server and added an img tag to load the image from S3.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;img src="https://javier-cloud-project-2025.s3.amazonaws.com/green_apples.jpg" alt="S3 Image" width="400"&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%2F0myvys5xt503dh4pd4bd.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%2F0myvys5xt503dh4pd4bd.png" alt=" " width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After refreshing the page in the browser, the image appeared successfully, loaded directly from the S3 bucket.&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%2F19ik263vhg3aginwd7dn.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%2F19ik263vhg3aginwd7dn.png" alt=" " width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Reflections and Takeaways
&lt;/h2&gt;

&lt;p&gt;This was my first real AWS project. It may look simple, but it taught me a lot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How to launch and configure EC2 instances from scratch&lt;/li&gt;
&lt;li&gt;SSH access, key pair security, and managing Linux servers&lt;/li&gt;
&lt;li&gt;Installing and configuring Apache to host a live web page&lt;/li&gt;
&lt;li&gt;The principles of least privilege and public access control with S3&lt;/li&gt;
&lt;li&gt;Writing and applying secure S3 bucket policies manually&lt;/li&gt;
&lt;li&gt;Connecting cloud services (EC2 and S3) in a practical way&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One of the most valuable takeaways was understanding AWS’s security-first design. From blocked public access by default, to strict key permissions, to required manual policy writing — this project forced me to think about security as part of every step.&lt;/p&gt;

&lt;p&gt;It also reminded me that doing is better than reading. A basic EC2 + S3 integration might seem trivial on paper, but doing it from scratch — and hitting permission issues along the way — made the knowledge stick.&lt;/p&gt;

&lt;p&gt;What’s Next?&lt;br&gt;
Now that I’ve completed this foundational project, I plan to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set up HTTPS using Let’s Encrypt and custom domains&lt;/li&gt;
&lt;li&gt;Explore infrastructure-as-code using Terraform or CloudFormation&lt;/li&gt;
&lt;li&gt;Dive deeper into IAM roles and access control models&lt;/li&gt;
&lt;li&gt;Build more offensive-security inspired labs on AWS as part of my cloud security journey&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Thanks for reading. If you're also learning AWS, feel free to connect with me — happy to share resources or ideas.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>career</category>
      <category>cloudpractitioner</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
