<?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: Bilal Bukhari</title>
    <description>The latest articles on DEV Community by Bilal Bukhari (@bilal_bukhari_75aeb34a969).</description>
    <link>https://dev.to/bilal_bukhari_75aeb34a969</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4122348%2Fb013453b-52bd-461f-8846-9e7e301491ec.png</url>
      <title>DEV Community: Bilal Bukhari</title>
      <link>https://dev.to/bilal_bukhari_75aeb34a969</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bilal_bukhari_75aeb34a969"/>
    <language>en</language>
    <item>
      <title>Building Secure AWS Cloud Networking: VPC Peering, Private EC2, NAT Gateway, and S3 Gateway Endpoint</title>
      <dc:creator>Bilal Bukhari</dc:creator>
      <pubDate>Sat, 17 Oct 2026 12:17:44 +0000</pubDate>
      <link>https://dev.to/bilal_bukhari_75aeb34a969/building-secure-aws-cloud-networking-vpc-peering-private-ec2-nat-gateway-and-s3-gateway-endpoint-4n45</link>
      <guid>https://dev.to/bilal_bukhari_75aeb34a969/building-secure-aws-cloud-networking-vpc-peering-private-ec2-nat-gateway-and-s3-gateway-endpoint-4n45</guid>
      <description>&lt;p&gt;&lt;em&gt;A Hands-On Implementation of Multi-VPC Connectivity, Controlled Internet Access, IAM-Based Authentication, and Private AWS Service Routing&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;By Syed Bilal Bukhari&lt;/strong&gt;&lt;br&gt;
Cloud Computing / AWS / Cloud Networking · Foundational to Intermediate · Region: US East (N. Virginia), &lt;code&gt;us-east-1&lt;/code&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In cloud networking, "it connects" and "it connects the way I intended" are different statements. A packet leaving an EC2 instance is steered by route tables, filtered by security groups, translated by gateways, and authorized (for AWS APIs) by IAM. When something fails, the cause sits in one of those layers, and you can only find it if you know which path the traffic is supposed to take.&lt;/p&gt;

&lt;p&gt;I built this lab to make those paths explicit. It has two VPCs in &lt;code&gt;us-east-1&lt;/code&gt;, one public EC2 instance, and one private EC2 instance with no public IPv4 address. The private instance has three jobs: talk to the public VPC over VPC Peering, reach the internet outbound-only through a NAT Gateway, and reach Amazon S3 using an IAM role instead of stored access keys.&lt;/p&gt;

&lt;p&gt;This is a foundational-to-intermediate lab, not a production design. I will point out where it falls short as I go. I will also separate what the AWS Console configuration shows from what the terminal tests prove, because those are two different kinds of evidence.&lt;/p&gt;
&lt;h2&gt;
  
  
  Project Objectives
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Give the public EC2 instance internet access through an Internet Gateway.&lt;/li&gt;
&lt;li&gt;Connect two VPCs privately using VPC Peering, with non-overlapping CIDR blocks.&lt;/li&gt;
&lt;li&gt;Reach the private EC2 instance over SSH through the public instance as a jump host.&lt;/li&gt;
&lt;li&gt;Let the private instance start outbound internet connections through a NAT Gateway, without a public IP.&lt;/li&gt;
&lt;li&gt;Add an S3 Gateway Endpoint so S3-bound traffic from the private route table has a dedicated route.&lt;/li&gt;
&lt;li&gt;Authenticate the private instance to AWS with an IAM role rather than permanent access keys.&lt;/li&gt;
&lt;li&gt;Validate each step with real terminal tests and Console configuration.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  AWS Services and Tools
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;What it did in this lab&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Amazon VPC&lt;/strong&gt; (two VPCs)&lt;/td&gt;
&lt;td&gt;Isolated networks: &lt;code&gt;mys-vpc-001&lt;/code&gt; and &lt;code&gt;myp-vpc-002&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Subnets and route tables&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Decide what is "public" or "private" through their routes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Internet Gateways&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One per VPC; the target for internet-routable traffic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;VPC Peering&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Private routing between the two VPCs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;NAT Gateway + Elastic IP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Outbound-only IPv4 internet access for the private subnet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;S3 Gateway VPC Endpoint&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Route-table target for regional S3 traffic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IAM role&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Temporary credentials for the private instance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Security Groups&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Instance-level traffic filtering (ICMP, SSH)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;EC2 (t3.micro, Amazon Linux 2023)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One instance in each VPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MobaXterm, ping, curl, AWS CLI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SSH access and connectivity verification&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F22xmfkqfo8zxg8xthbhk.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F22xmfkqfo8zxg8xthbhk.png" alt="AWS cloud networking architecture showing two VPCs connected by VPC Peering, a public EC2 instance, a private EC2 instance, a NAT Gateway, and an S3 Gateway Endpoint." width="800" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 1. Architecture of the lab. Orange arrows are internet-bound traffic, the dashed blue line is inter-VPC traffic over peering, and green arrows are S3 traffic through the Gateway Endpoint. The private EC2 instance has no direct path to an Internet Gateway; its internet traffic goes through the NAT Gateway first.&lt;/em&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Public VPC&lt;/th&gt;
&lt;th&gt;Private VPC&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Name / CIDR&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;mys-vpc-001&lt;/code&gt; / 10.0.0.0/24&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;myp-vpc-002&lt;/code&gt; / 10.0.1.0/24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subnets&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;mys-subnet-01&lt;/code&gt; 10.0.0.0/28 (us-east-1a)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;myp-private-subnet-002&lt;/code&gt; 10.0.1.0/28 and &lt;code&gt;myp-nat-public-subnet&lt;/code&gt; 10.0.1.16/28 (both us-east-1b)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EC2&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;my-public-ec2&lt;/code&gt;, 10.0.0.6, public IPv4&lt;/td&gt;
&lt;td&gt;Private EC2, 10.0.1.6, no public IPv4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gateways&lt;/td&gt;
&lt;td&gt;Internet Gateway&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;myp-igw-002&lt;/code&gt;, NAT Gateway &lt;code&gt;myp-nat-gateway-002&lt;/code&gt; (10.0.1.22 + Elastic IP)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  Public VPC Configuration
&lt;/h2&gt;

&lt;p&gt;The Public VPC has one public subnet. The instance &lt;code&gt;my-public-ec2&lt;/code&gt; is a t3.micro running Amazon Linux 2023, with private IP 10.0.0.6 and a public IPv4 address. I reached it over SSH with key authentication from MobaXterm. It plays two roles: the internet-facing test host and the jump host for the private instance.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkssnbiqjxcnmh2urs21x.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkssnbiqjxcnmh2urs21x.png" alt="AWS EC2 console showing the running public and private EC2 instances and their status checks." width="800" height="200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 2. The EC2 console showing running t3.micro instances that passed 3/3 status checks. &lt;code&gt;my-public-ec2&lt;/code&gt; is in us-east-1a; the instance used for private-side testing is in us-east-1b.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A subnet is public because of its route table, not its name. &lt;code&gt;mys-public-rt&lt;/code&gt; sends VPC-local traffic locally, sends &lt;code&gt;0.0.0.0/0&lt;/code&gt; to the Internet Gateway, and sends &lt;code&gt;10.0.1.0/24&lt;/code&gt; to the peering connection.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0dvucyfks6mecs8alhjw.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0dvucyfks6mecs8alhjw.png" alt="AWS public VPC route table showing the local route, the VPC Peering route to 10.0.1.0/24, and the default route to the Internet Gateway." width="800" height="302"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 3. &lt;code&gt;mys-public-rt&lt;/code&gt; (captured from the Edit routes view). All three routes are Active: local, &lt;code&gt;10.0.1.0/24&lt;/code&gt; via the peering connection, and &lt;code&gt;0.0.0.0/0&lt;/code&gt; via the Internet Gateway.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Private VPC Configuration
&lt;/h2&gt;

&lt;p&gt;The VPC is called "Private VPC", but that name is only a label. Whether a subnet is public or private depends on its route table. This VPC deliberately contains one of each:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;myp-private-subnet-002&lt;/code&gt;&lt;/strong&gt; (10.0.1.0/28) holds the private EC2 instance at 10.0.1.6. Its route table has no route to an Internet Gateway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;myp-nat-public-subnet&lt;/code&gt;&lt;/strong&gt; (10.0.1.16/28) holds only the NAT Gateway. Its route table, &lt;code&gt;myp-nat-public-rt&lt;/code&gt;, has a default route to the Internet Gateway &lt;code&gt;myp-igw-002&lt;/code&gt;, which makes it a public subnet.&lt;/li&gt;
&lt;/ul&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjufxfzzclwsxbt54w3um.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjufxfzzclwsxbt54w3um.png" alt="AWS VPC resource map showing the private subnet, NAT public subnet, NAT Gateway, Internet Gateway, and S3 Gateway Endpoint." width="800" height="345"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 4. The Private VPC resource map: two subnets in us-east-1b, three route tables, and the connections &lt;code&gt;myp-igw-002&lt;/code&gt;, &lt;code&gt;myp-nat-gateway-002&lt;/code&gt; (public NAT, one ENI and one EIP) and &lt;code&gt;myp-s3-endpoint&lt;/code&gt; (gateway endpoint to S3). The right edge of the original capture is cut off.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  VPC Peering
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Concepts
&lt;/h3&gt;

&lt;p&gt;VPCs are isolated by default. A VPC Peering connection creates a private routing path between two VPCs using private IP addresses. It is not a gateway and not a VPN, and it does not provide internet access.&lt;/p&gt;

&lt;p&gt;Four properties shaped the design:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Non-overlapping CIDRs are mandatory.&lt;/strong&gt; If both VPCs used the same range, a route for that range could not say which VPC a destination belongs to. Here the VPCs use 10.0.0.0/24 and 10.0.1.0/24.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Request and accept.&lt;/strong&gt; One VPC (the requester, &lt;code&gt;mys-vpc-001&lt;/code&gt;) asks and the other (the accepter, &lt;code&gt;myp-vpc-002&lt;/code&gt;) accepts. Until it is accepted, the connection is not usable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Both sides need routes.&lt;/strong&gt; An Active peering connection only creates the possibility of traffic. Each VPC's route table needs a route to the other VPC's CIDR with the peering connection as the target.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Peering is not transitive.&lt;/strong&gt; AWS documents that two VPCs cannot use a third, peered VPC as a transit path. Peering also does not bypass Security Groups or Network ACLs, which still filter the traffic.&lt;/li&gt;
&lt;/ul&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwp9lnn9fr85k8ggc0gjb.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwp9lnn9fr85k8ggc0gjb.png" alt="AWS VPC Peering console showing the public-private-peering connection in Active status." width="800" height="139"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 5. &lt;code&gt;public-private-peering&lt;/code&gt; (&lt;code&gt;pcx-0763ca5cff1e5a68e&lt;/code&gt;) in Active status, with requester VPC &lt;code&gt;vpc-044079de0026185ef&lt;/code&gt;. The accepter column is off-screen in this capture, so the accepter details come from my project notes.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Routes on both sides
&lt;/h3&gt;

&lt;p&gt;The Public VPC's route (Figure 3) sends 10.0.1.0/24 to the peering connection. The Private VPC's route table, &lt;code&gt;myp-private-rt-002&lt;/code&gt;, has the mirror route plus two more:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Destination&lt;/th&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10.0.1.0/24&lt;/td&gt;
&lt;td&gt;local&lt;/td&gt;
&lt;td&gt;Traffic inside the Private VPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10.0.0.0/24&lt;/td&gt;
&lt;td&gt;Peering connection&lt;/td&gt;
&lt;td&gt;Traffic to the Public VPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.0.0.0/0&lt;/td&gt;
&lt;td&gt;NAT Gateway &lt;code&gt;nat-04d646b2df4be7880&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Everything else, IPv4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pl-63a5400a&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Gateway endpoint &lt;code&gt;vpce-0f6dfcad71b59e9d9&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;S3 (regional prefix list)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyr7x59fmfu17fo7kvwbx.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyr7x59fmfu17fo7kvwbx.png" alt="AWS private route table showing routes to the local VPC, the peered VPC, the NAT Gateway, and the S3 Gateway Endpoint." width="800" height="280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 6. &lt;code&gt;myp-private-rt-002&lt;/code&gt; with all four routes Active. This is also the evidence that the S3 prefix-list route exists (see the S3 section).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;AWS picks the most specific matching route (longest prefix match). A packet to 10.0.0.6 matches 10.0.0.0/24 and goes over peering, not to the NAT Gateway, even though 0.0.0.0/0 also matches. A packet to an S3 address falls in a range from the S3 prefix list, which is more specific than 0.0.0.0/0, so it takes the endpoint route. Anything else falls through to the NAT Gateway.&lt;/p&gt;
&lt;h3&gt;
  
  
  Reachability test
&lt;/h3&gt;

&lt;p&gt;From the public instance (10.0.0.6) I pinged the private instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ping &lt;span class="nt"&gt;-c&lt;/span&gt; 4 10.0.1.6
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgtbwq3g2c86nfzpdh65r.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgtbwq3g2c86nfzpdh65r.png" alt="Terminal output showing four successful ping replies from the public EC2 instance at 10.0.0.6 to the private EC2 instance at 10.0.1.6." width="800" height="124"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 7. Four packets transmitted, four received, 0% loss. Replies took between 0.452 and 0.468 ms. The final min/avg/max line is cut off in my capture; the mean of the four replies is about 0.457 ms.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This proves ICMP reachability between instances in separate VPCs. It does not prove that every application port works, because ping only exercises ICMP. The first attempts were blocked until I added the right Security Group rule (see Challenges).&lt;/p&gt;

&lt;h2&gt;
  
  
  SSH Access Between VPCs
&lt;/h2&gt;

&lt;p&gt;I confirmed that the public instance could reach port 22 on the private instance over the peering link. For access from my own machine I used MobaXterm's SSH gateway (jump host) feature, which opens the session to the private address through the public instance, so the private machine never needs a public IPv4 address. I did not copy the private key onto the jump host.&lt;/p&gt;

&lt;p&gt;Port 22 on the private instance should accept connections only from the jump host's side, not from &lt;code&gt;0.0.0.0/0&lt;/code&gt;. I did not capture screenshots of the Security Group rules, so I am describing that intent rather than showing the rule set.&lt;/p&gt;

&lt;h2&gt;
  
  
  NAT Gateway for Outbound Internet Access
&lt;/h2&gt;

&lt;p&gt;The private instance has no public IP, so by itself it cannot reach the internet. A public NAT Gateway in a public subnet fixes that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Private EC2 → private route table → NAT Gateway → Internet Gateway → Internet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why each piece is there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The NAT Gateway is in a public subnet&lt;/strong&gt; (&lt;code&gt;myp-nat-public-subnet&lt;/code&gt;) because it needs a route to an Internet Gateway, and it has an Elastic IP that serves as its public address.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The NAT subnet's route table points to &lt;code&gt;myp-igw-002&lt;/code&gt;.&lt;/strong&gt; Without that, the NAT Gateway has no way out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The private instance keeps only its private IP.&lt;/strong&gt; The NAT Gateway translates its source address on the way out and maps replies back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inbound is limited by design.&lt;/strong&gt; AWS documents that connections through a NAT Gateway must start from inside the VPC, so unsolicited inbound connections from the internet cannot reach the private instance this way.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A NAT Gateway does not replace Security Groups, and it is not application-level security. It simply provides address translation for outbound IPv4.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo65qt611exbxv3cowjt4.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo65qt611exbxv3cowjt4.png" alt="AWS NAT Gateway details showing Available status, private IP address 10.0.1.22, and the public NAT Gateway configuration." width="800" height="282"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 8. &lt;code&gt;myp-nat-gateway-002&lt;/code&gt; in Available state: connectivity type Public, private IPv4 10.0.1.22, in &lt;code&gt;myp-nat-public-subnet&lt;/code&gt; of &lt;code&gt;myp-vpc-002&lt;/code&gt;. The account ID in the ARN and the Elastic IP are redacted.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;From the private instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="nt"&gt;--max-time&lt;/span&gt; 15 https://www.google.com
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzdsddhkwpxfirjjuc2yt.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzdsddhkwpxfirjjuc2yt.png" alt="Terminal output showing a successful HTTPS request from the private EC2 instance, returning HTTP/2 200 through the NAT Gateway." width="800" height="102"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 9. &lt;code&gt;HTTP/2 200&lt;/code&gt; received from the private EC2 prompt (&lt;code&gt;ip-10-0-1-6&lt;/code&gt;). I cropped the screenshot to remove the cookie headers that followed.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Before the routing was right, this same command timed out. That is covered in Challenges.&lt;/p&gt;

&lt;h2&gt;
  
  
  S3 Gateway Endpoint and IAM Role
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The endpoint
&lt;/h3&gt;

&lt;p&gt;A VPC endpoint lets resources in a VPC reach an AWS service without going over the public internet path. There are two types: &lt;strong&gt;Interface endpoints&lt;/strong&gt; (an ENI with private IPs in your subnet, powered by PrivateLink) and &lt;strong&gt;Gateway endpoints&lt;/strong&gt; (a route-table target, available for S3 and DynamoDB). I used a Gateway endpoint because it fits the lab: it needs no ENI in my subnet, and AWS states there is no additional charge for it.&lt;/p&gt;

&lt;p&gt;Because it works through routing, it is associated with route tables. When I associated &lt;code&gt;myp-private-rt-002&lt;/code&gt;, AWS added the route &lt;code&gt;pl-63a5400a → vpce-0f6dfcad71b59e9d9&lt;/code&gt;, where &lt;code&gt;pl-63a5400a&lt;/code&gt; is the managed prefix list for regional S3 addresses. That route is visible in Figure 6.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxkrjfo38bw64km212ctz.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxkrjfo38bw64km212ctz.png" alt="AWS VPC endpoint console showing the S3 Gateway Endpoint in Available status in us-east-1." width="799" height="279"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 10. &lt;code&gt;myp-s3-endpoint&lt;/code&gt; (&lt;code&gt;vpce-0f6dfcad71b59e9d9&lt;/code&gt;): type Gateway, service &lt;code&gt;com.amazonaws.us-east-1.s3&lt;/code&gt;, VPC &lt;code&gt;myp-vpc-002&lt;/code&gt;, status Available.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Three limits are worth stating. The endpoint covers S3 only, so other traffic still needs the NAT Gateway. It serves only the route tables it is associated with, and AWS documents that resources across a peering connection cannot use it. And an endpoint policy and an IAM policy do different jobs: one limits what can pass through the endpoint, the other limits what the caller may do. My screenshots show no custom endpoint policy, so I am not claiming one was applied.&lt;/p&gt;

&lt;h3&gt;
  
  
  The IAM role
&lt;/h3&gt;

&lt;p&gt;Initially, &lt;code&gt;aws configure list&lt;/code&gt; showed that credentials were not configured. After attaching an IAM role to the private instance, it reported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;access_key : ************ : iam-role
secret_key : ************ : iam-role
region     : us-east-1    : imds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The credentials come from the EC2 Instance Metadata Service as temporary credentials that rotate, so no access key is stored on disk. This is the approach AWS recommends over putting long-term keys on an instance. The network endpoint and IAM are separate: the endpoint provides a route; IAM decides whether the call is allowed.&lt;/p&gt;

&lt;p&gt;My screenshots do not show the role's name or policy, so I am not stating what permissions it had. For a production version I would scope the role to the specific bucket and actions, for example &lt;code&gt;s3:ListBucket&lt;/code&gt; on one bucket and &lt;code&gt;s3:GetObject&lt;/code&gt; on the prefixes actually needed, rather than broad S3 access. Note that &lt;code&gt;aws s3 ls&lt;/code&gt; with no arguments also needs &lt;code&gt;s3:ListAllMyBuckets&lt;/code&gt;, which a tightly scoped role might deliberately omit.&lt;/p&gt;

&lt;h3&gt;
  
  
  S3 listing
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3 &lt;span class="nb"&gt;ls &lt;/span&gt;s3://supplychain-de-2026-pk84726/
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh6ad1y1njk49s7h3u22o.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh6ad1y1njk49s7h3u22o.png" alt="AWS CLI output from the private EC2 instance listing the historical and raw prefixes of an S3 bucket." width="800" height="48"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 11. The private instance lists the &lt;code&gt;historical/&lt;/code&gt; and &lt;code&gt;raw/&lt;/code&gt; prefixes of an existing bucket.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I want to be precise about what this shows. The listing proves authenticated S3 API access from the private instance. The route table (Figure 6) shows that the intended network path for S3 addresses is the Gateway Endpoint. But the private subnet also has a NAT default route, so this command would likely have worked through NAT too. I did not run a packet trace or flow-log analysis, so I am not claiming that this specific request used the endpoint. To prove that, I would temporarily remove the NAT default route and confirm the S3 listing still works while &lt;code&gt;curl&lt;/code&gt; to Google fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validation and Testing
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Command (run from)&lt;/th&gt;
&lt;th&gt;Observed result&lt;/th&gt;
&lt;th&gt;Evidence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A. Public EC2 internet&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;curl -I --max-time 15 https://www.google.com&lt;/code&gt; (public EC2)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;HTTP/2 200&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Recorded in my notes; not pictured&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B. Peering reachability&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ping -c 4 10.0.1.6&lt;/code&gt; (10.0.0.6)&lt;/td&gt;
&lt;td&gt;4 sent, 4 received, 0% loss&lt;/td&gt;
&lt;td&gt;Figure 7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C. SSH reachability&lt;/td&gt;
&lt;td&gt;SSH to port 22 on 10.0.1.6; MobaXterm jump host&lt;/td&gt;
&lt;td&gt;Connected&lt;/td&gt;
&lt;td&gt;Recorded in my notes; not pictured&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D. Private EC2 internet via NAT&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;curl -I --max-time 15 https://www.google.com&lt;/code&gt; (10.0.1.6)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;HTTP/2 200&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Figure 9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E. AWS CLI present&lt;/td&gt;
&lt;td&gt;&lt;code&gt;aws --version&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;AWS CLI 2.36.47&lt;/td&gt;
&lt;td&gt;Recorded in my notes; not pictured&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;F. IAM role credentials&lt;/td&gt;
&lt;td&gt;&lt;code&gt;aws configure list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Credentials from &lt;code&gt;iam-role&lt;/code&gt;; region from &lt;code&gt;imds&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Recorded in my notes; not pictured&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;G. S3 service access&lt;/td&gt;
&lt;td&gt;&lt;code&gt;aws s3 ls&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Listed an accessible bucket&lt;/td&gt;
&lt;td&gt;Recorded in my notes; not pictured&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H. S3 prefix listing&lt;/td&gt;
&lt;td&gt;&lt;code&gt;aws s3 ls s3://supplychain-de-2026-pk84726/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;PRE historical/&lt;/code&gt;, &lt;code&gt;PRE raw/&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Figure 11&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Console figures (3 to 6, 8, 10) show how the network is configured; the terminal tests (B, D, H) show that traffic and API calls worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenges and Troubleshooting
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Overlapping VPC CIDR blocks.&lt;/strong&gt; My first pair of VPCs had overlapping ranges. VPC Peering requires non-overlapping CIDRs, so that pair could not be peered as designed. I created a replacement VPC, &lt;code&gt;myp-vpc-002&lt;/code&gt;, with 10.0.1.0/24, which is the one used throughout this article.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Peering "Pending Acceptance" and a blackhole route.&lt;/strong&gt; A route that points at a peering connection that has not been accepted shows a blackhole status. The cause was the pending request; accepting it moved the connection to Active, and the routes then showed Active (Figures 3 and 6).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ping blocked by Security Groups.&lt;/strong&gt; The route was in place, but ICMP did not get through until I added the appropriate Security Group rule. Routing and filtering are separate layers, and peering does not bypass the filter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NAT internet access timed out.&lt;/strong&gt; Before the route-table configuration was corrected, &lt;code&gt;curl&lt;/code&gt; from the private instance timed out. Two routes have to be right for this path: the private route table's default route to the NAT Gateway, and the NAT subnet's default route to the Internet Gateway. After correcting the routing, the same command returned &lt;code&gt;HTTP/2 200&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AWS CLI credentials missing.&lt;/strong&gt; The instance had no role, so the CLI had no credentials. Attaching an IAM role fixed it, and &lt;code&gt;aws configure list&lt;/code&gt; then showed &lt;code&gt;iam-role&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;S3 endpoint route verification.&lt;/strong&gt; After creating the endpoint, I checked the private route table and confirmed the &lt;code&gt;pl-63a5400a&lt;/code&gt; route was Active rather than assuming the association worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security Considerations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Subnet isolation.&lt;/strong&gt; The private instance has no public IPv4 address and no inbound route from the internet. Its only inbound path is over the peering link, including SSH forwarded through the jump host.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security Groups&lt;/strong&gt; remain the instance-level control. Port 22 should be limited to the jump host, and ICMP opened only as needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The jump host&lt;/strong&gt; concentrates access in one place, but it is itself an exposed system that needs patching and restriction. A managed option such as AWS Systems Manager Session Manager could avoid an open SSH port; I did not implement it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IAM roles&lt;/strong&gt; avoid stored keys but still need least-privilege policies, which I have described rather than demonstrated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NAT&lt;/strong&gt; blocks unsolicited inbound connections but does not filter outbound traffic by destination.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An S3 endpoint policy&lt;/strong&gt; could restrict which buckets are reachable; I have not shown one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tests show the network behaves as designed on the paths I tested. That does not make the design hardened.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost Considerations
&lt;/h2&gt;

&lt;p&gt;A lab like this is cheap only while you remember to remove it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NAT Gateway&lt;/strong&gt; is billed per hour while it exists, plus per GB processed. In a small lab it is usually the largest line item.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Public IPv4 addresses&lt;/strong&gt;, including the NAT Gateway's Elastic IP and the public instance's address, are billed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EC2&lt;/strong&gt; t3.micro instances accrue instance-hours while running.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data transfer.&lt;/strong&gt; AWS documents that peering traffic within one Availability Zone is free. My instances sit in different AZs (us-east-1a and 1b), so inter-AZ charges could apply to larger transfers; my ping and curl tests are trivial. The NAT Gateway is in the same AZ as the private instance, which avoids an extra cross-AZ hop on that path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The S3 Gateway Endpoint&lt;/strong&gt; has no additional hourly or processing charge.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Prices change, so check the &lt;a href="https://aws.amazon.com/vpc/pricing/" rel="noopener noreferrer"&gt;VPC pricing page&lt;/a&gt; rather than trusting figures in an article. When you finish, terminate the instances, delete the NAT Gateway, and release its Elastic IP, which keeps billing if left allocated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Lessons Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A route table defines "public" and "private".&lt;/strong&gt; The Private VPC contains a public subnet because the NAT Gateway needs one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An Active peering connection moves no packets by itself.&lt;/strong&gt; It needs routes on both sides and permissive Security Groups.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A timeout usually points at a missing route.&lt;/strong&gt; The NAT timeout was a routing problem, not a problem with the destination.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Longest-prefix matching lets the routes coexist.&lt;/strong&gt; The peer CIDR and the S3 prefix list win over the 0.0.0.0/0 default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configuration evidence and test evidence answer different questions.&lt;/strong&gt; A successful &lt;code&gt;aws s3 ls&lt;/code&gt; does not by itself show which path carried the request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connectivity and authorization fail separately.&lt;/strong&gt; The endpoint gave a route; the IAM role gave permission.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The lab verified these outcomes: the public instance reached the internet; two non-overlapping VPCs were joined by an Active peering connection with a 4/4 ping and 0% loss; the private instance, with no public IPv4 address, made outbound HTTPS requests through a NAT Gateway; and it listed an S3 bucket's prefixes using IAM role credentials, with an Active S3 Gateway Endpoint route in its route table.&lt;/p&gt;

&lt;p&gt;A production-oriented version would add NAT Gateways per Availability Zone (or the regional NAT option), least-privilege IAM and an S3 endpoint policy, documented Security Group rules, a managed access path instead of an SSH jump host, VPC Flow Logs, and infrastructure-as-code. I also have not shown that S3 requests used the endpoint, which a route-removal test or flow logs could settle.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Amazon VPC documentation: &lt;a href="https://docs.aws.amazon.com/vpc/" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/vpc/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;VPC peering connections: &lt;a href="https://docs.aws.amazon.com/vpc/latest/peering/working-with-vpc-peering.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/vpc/latest/peering/working-with-vpc-peering.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;What is VPC peering (Peering Guide): &lt;a href="https://docs.aws.amazon.com/AmazonVPC/latest/PeeringGuide/Welcome.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AmazonVPC/latest/PeeringGuide/Welcome.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;VPC peering, connectivity options whitepaper: &lt;a href="https://docs.aws.amazon.com/whitepapers/latest/aws-vpc-connectivity-options/vpc-peering.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/whitepapers/latest/aws-vpc-connectivity-options/vpc-peering.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Configure route tables: &lt;a href="https://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Route_Tables.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Route_Tables.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;How route priority works: &lt;a href="https://docs.aws.amazon.com/vpc/latest/userguide/route-tables-priority.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/vpc/latest/userguide/route-tables-priority.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Internet gateways: &lt;a href="https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Internet_Gateway.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Internet_Gateway.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;NAT gateways: &lt;a href="https://docs.aws.amazon.com/vpc/latest/userguide/vpc-nat-gateway.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/vpc/latest/userguide/vpc-nat-gateway.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Gateway endpoints: &lt;a href="https://docs.aws.amazon.com/vpc/latest/privatelink/vpce-gateway.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/vpc/latest/privatelink/vpce-gateway.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Gateway endpoints for Amazon S3: &lt;a href="https://docs.aws.amazon.com/vpc/latest/privatelink/vpc-endpoints-s3.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/vpc/latest/privatelink/vpc-endpoints-s3.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;IAM roles for Amazon EC2: &lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/roles-usingrole-ec2instance.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/IAM/latest/UserGuide/roles-usingrole-ec2instance.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Amazon VPC pricing: &lt;a href="https://aws.amazon.com/vpc/pricing/" rel="noopener noreferrer"&gt;https://aws.amazon.com/vpc/pricing/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>cloud</category>
      <category>networking</category>
      <category>devops</category>
    </item>
    <item>
      <title>Launching an EC2 Instance, Connecting via RDP</title>
      <dc:creator>Bilal Bukhari</dc:creator>
      <pubDate>Tue, 29 Sep 2026 16:11:41 +0000</pubDate>
      <link>https://dev.to/bilal_bukhari_75aeb34a969/launching-an-ec2-instance-connecting-via-rdp-4hkn</link>
      <guid>https://dev.to/bilal_bukhari_75aeb34a969/launching-an-ec2-instance-connecting-via-rdp-4hkn</guid>
      <description>&lt;p&gt;Everything I'd built with AWS up to this point was S3 — storage, permissions, static hosting. Today moved into actual compute: launching a virtual machine on EC2, connecting into it remotely over RDP, and learning how to back it up and move that backup around using snapshots.&lt;/p&gt;

&lt;h2&gt;
  
  
  Topics Covered
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Launching an EC2 instance&lt;/li&gt;
&lt;li&gt;Connecting to it via RDP&lt;/li&gt;
&lt;li&gt;Taking EBS snapshots&lt;/li&gt;
&lt;li&gt;Sending and receiving snapshots&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Launching an EC2 Instance
&lt;/h2&gt;

&lt;p&gt;EC2 (Elastic Compute Cloud) is AWS's virtual machine service — instead of storing objects like S3, it gives you an actual running server you can log into and use. Launching one means choosing an AMI (Amazon Machine Image, essentially the OS template), an instance type (which determines CPU/RAM), a key pair for access, and a security group controlling what traffic can reach it.&lt;/p&gt;

&lt;p&gt;For this lab, I launched a Windows instance on a &lt;code&gt;t3.micro&lt;/code&gt; — small, cheap, enough to test with. A few minutes after launch, it showed up in the console as &lt;strong&gt;Running&lt;/strong&gt;, with all status checks passed:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4876hituk2o2iusjd8p7.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4876hituk2o2iusjd8p7.png" alt=" " width="798" height="180"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The instance state and status checks matter more than they look like at first glance — "running" just means the VM booted, while the status checks confirm the underlying hardware and the OS itself are actually healthy. A running instance that fails a status check is not one you want to trust yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Connecting via RDP
&lt;/h2&gt;

&lt;p&gt;Since this was a Windows instance, the way in is RDP (Remote Desktop Protocol) rather than SSH. The security group has to explicitly allow inbound traffic on port 3389 for this to work at all — by default, nothing gets in.&lt;/p&gt;

&lt;p&gt;The process: retrieve the administrator password using the key pair created at launch, then connect using any RDP client (Windows' own Remote Desktop Connection app works fine) pointed at the instance's public IP. Once connected, it behaves exactly like a normal Windows desktop — except it's running on AWS hardware somewhere in a data center rather than the physical machine in front of me.&lt;/p&gt;

&lt;p&gt;This is the part that made cloud computing feel tangible in a way S3 hadn't. A bucket is abstract. A full remote desktop you're actively controlling is not.&lt;br&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4876hituk2o2iusjd8p7.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4876hituk2o2iusjd8p7.png" alt=" " width="798" height="180"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Taking Snapshots
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;snapshot&lt;/strong&gt; is a point-in-time backup of an EBS volume (the virtual hard disk attached to an EC2 instance). Rather than backing up files individually, a snapshot captures the entire volume's state, incrementally — after the first full snapshot, later ones only store the blocks that changed.&lt;/p&gt;

&lt;p&gt;I took three snapshots over the course of the lab, each showing as &lt;code&gt;Completed&lt;/code&gt; with the full volume captured:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvwk5v0f4sm9spt6kpxlp.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvwk5v0f4sm9spt6kpxlp.png" alt=" " width="800" height="174"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Snapshots are the foundation for a lot of what makes EC2 usable in production: recovering from a bad update, migrating a volume to a new instance, or scaling by launching new instances directly from a known-good image.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Sending and Receiving Snapshots
&lt;/h2&gt;

&lt;p&gt;Beyond just creating them locally, snapshots can be copied — including across regions and across AWS accounts. Sharing a snapshot means modifying its permissions to grant another account access, after which that account can copy it into their own environment and create a volume from it.&lt;/p&gt;

&lt;p&gt;This is the piece that turns a snapshot from "personal backup" into an actual transfer mechanism — moving a fully configured server environment from one place to another without manually reinstalling anything, just by handing over the snapshot and letting the other side spin up a volume from it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;S3 taught me about storage and access control in the abstract. EC2 and snapshots are the first time I was working with something that behaves like a real, persistent machine — one that can be backed up, broken, restored, and handed off. That distinction between object storage and block storage tied to a running instance is a core piece of the AWS mental model, and this was the lab where it actually clicked instead of just being a definition I'd read.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Now that I can launch, connect to, and snapshot an instance, the next logical step is automating this instead of doing it by hand through the console — looking at either the AWS CLI or Infrastructure as Code (Terraform or CloudFormation) to spin up the same setup repeatably.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>ec2</category>
      <category>cloud</category>
      <category>devops</category>
    </item>
    <item>
      <title>Deploying a React E-Commerce Frontend on AWS S3</title>
      <dc:creator>Bilal Bukhari</dc:creator>
      <pubDate>Fri, 25 Sep 2026 20:32:37 +0000</pubDate>
      <link>https://dev.to/bilal_bukhari_75aeb34a969/deploying-a-react-e-commerce-frontend-on-aws-s3-1iie</link>
      <guid>https://dev.to/bilal_bukhari_75aeb34a969/deploying-a-react-e-commerce-frontend-on-aws-s3-1iie</guid>
      <description>&lt;p&gt;Most of what I've built on S3 so far has been about infrastructure — buckets, permissions, presigned URLs, events. Today was different: I took an actual frontend, a tech/gaming products landing page, and got it live on the internet using the same S3 static hosting setup I'd worked with before, just applied to something real this time instead of a placeholder template.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Page Itself
&lt;/h2&gt;

&lt;p&gt;It's a storefront-style landing page — hero section, top-rated products, featured categories, a trending products carousel, brand logos, and a footer. Nothing backend-driven yet, just the frontend shell of an e-commerce site: gaming peripherals, RAM, SSDs, cooling gear, that kind of catalog.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting It Onto S3
&lt;/h2&gt;

&lt;p&gt;The deployment side was the same pattern I'd already built muscle memory for by this point: create the bucket, enable static website hosting, set &lt;code&gt;index.html&lt;/code&gt; as the index document, turn off Block Public Access, and attach a bucket policy granting public &lt;code&gt;s3:GetObject&lt;/code&gt; read access. What's different this time is that instead of one HTML file, this was a full asset folder — multiple image directories, nested paths, a real project structure instead of a single page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live here:&lt;/strong&gt; &lt;a href="http://laptop-605365943139-us-east-1-an.s3-website-us-east-1.amazonaws.com/index.html" rel="noopener noreferrer"&gt;http://laptop-605365943139-us-east-1-an.s3-website-us-east-1.amazonaws.com/index.html&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Actually Different About a Real Project vs a Test Upload
&lt;/h2&gt;

&lt;p&gt;Uploading one file to test hosting is a five-minute task. Uploading an entire project with a proper folder structure surfaces a different set of problems — relative paths that assumed a different root, asset folders that need to preserve their structure exactly, and making sure every referenced file actually made it into the bucket at the path the HTML expects it at. S3 doesn't care about folder structure conceptually (it's really just keys with slashes in them), but the moment an HTML file references &lt;code&gt;./icon/cart.png&lt;/code&gt; and that path doesn't resolve to an actual key in the bucket, you get a broken image instead of a build error — no failure message, just something quietly not showing up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This One Felt Different
&lt;/h2&gt;

&lt;p&gt;Every previous S3 lab was a controlled exercise — one bucket, one file, one concept being tested. This was the first time it was "take something that looks like a real product and put it in front of people," asset pipeline and all. It's a small step, but it's the difference between knowing how static hosting works and actually shipping something with it.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>s3</category>
      <category>cloud</category>
      <category>devops</category>
    </item>
    <item>
      <title>Building a Netflix-Inspired Frontend with React</title>
      <dc:creator>Bilal Bukhari</dc:creator>
      <pubDate>Thu, 24 Sep 2026 17:25:51 +0000</pubDate>
      <link>https://dev.to/bilal_bukhari_75aeb34a969/building-a-netflix-inspired-frontend-with-react-22p2</link>
      <guid>https://dev.to/bilal_bukhari_75aeb34a969/building-a-netflix-inspired-frontend-with-react-22p2</guid>
      <description>&lt;h1&gt;
  
  
  Building and Deploying a Netflix-Inspired React Landing Page on AWS S3 🎬☁️
&lt;/h1&gt;

&lt;p&gt;I recently finished building and deploying a &lt;strong&gt;Netflix-inspired frontend landing page using React&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The project started as a way to practice React and frontend development, but I also wanted to take it one step further and actually put the finished application online using &lt;strong&gt;Amazon S3&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So instead of keeping the project running only on &lt;code&gt;localhost&lt;/code&gt;, I built the production version and deployed it as a live website using AWS S3.&lt;/p&gt;

&lt;h2&gt;
  
  
  🌐 Live Demo
&lt;/h2&gt;

&lt;p&gt;🎬 &lt;strong&gt;Live Website:&lt;/strong&gt; &lt;a href="http://netflixlanding-605365943139-us-east-1-an.s3-website-us-east-1.amazonaws.com" rel="noopener noreferrer"&gt;http://netflixlanding-605365943139-us-east-1-an.s3-website-us-east-1.amazonaws.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;💻 &lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a href="https://github.com/himbukharidev" rel="noopener noreferrer"&gt;https://github.com/himbukharidev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can open the live website and interact with the frontend directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  🛠️ Technologies Used
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;React&lt;/strong&gt; — frontend development&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vite&lt;/strong&gt; — development/build tool&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JavaScript&lt;/strong&gt; — application logic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTML &amp;amp; CSS&lt;/strong&gt; — structure and styling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React Router&lt;/strong&gt; — navigation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Axios&lt;/strong&gt; — HTTP requests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lucide React&lt;/strong&gt; — icons&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS S3&lt;/strong&gt; — static website hosting&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  🎬 The Project
&lt;/h1&gt;

&lt;p&gt;The goal was to build a complete Netflix-inspired landing page rather than just recreating a single section.&lt;/p&gt;

&lt;p&gt;The page includes multiple sections and interactive elements.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hero Section
&lt;/h3&gt;

&lt;p&gt;The landing page starts with a large hero section designed around the visual style of a modern streaming platform.&lt;/p&gt;

&lt;p&gt;It contains the main promotional content and call-to-action elements.&lt;/p&gt;

&lt;h3&gt;
  
  
  🎞️ Movie &amp;amp; Show Posters
&lt;/h3&gt;

&lt;p&gt;The page contains different sections displaying movie and show posters.&lt;/p&gt;

&lt;p&gt;I used reusable React components to avoid creating completely separate markup for every poster and section.&lt;/p&gt;

&lt;h3&gt;
  
  
  🖱️ Interactive Movie Details
&lt;/h3&gt;

&lt;p&gt;The posters aren't just static images.&lt;/p&gt;

&lt;p&gt;When a user clicks on a poster, a &lt;strong&gt;movie details popup/modal appears&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The selected movie's information is displayed inside the popup along with its poster, giving the interface a more interactive experience.&lt;/p&gt;

&lt;h3&gt;
  
  
  🃏 Cards
&lt;/h3&gt;

&lt;p&gt;The landing page also contains additional card-based sections to make the page feel more complete rather than just being a collection of movie posters.&lt;/p&gt;

&lt;h3&gt;
  
  
  ❓ FAQ Section
&lt;/h3&gt;

&lt;p&gt;I added an FAQ section where users can interact with the questions and view their answers.&lt;/p&gt;

&lt;p&gt;This was another opportunity to practice interactive React components.&lt;/p&gt;

&lt;h3&gt;
  
  
  📌 Footer
&lt;/h3&gt;

&lt;p&gt;The page finishes with a complete footer containing additional navigation and information.&lt;/p&gt;




&lt;h1&gt;
  
  
  ⚛️ Building the UI with React
&lt;/h1&gt;

&lt;p&gt;One of the main things I wanted to practice was &lt;strong&gt;component-based development&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of putting everything into one large component, I separated the interface into reusable pieces.&lt;/p&gt;

&lt;p&gt;A simplified structure looks something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;App
├── Navbar
├── Hero
├── Movie Sections
│   └── Movie Cards
├── Movie Details Modal
├── Cards
├── FAQ
└── Footer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The movie details popup was particularly useful for practicing React state.&lt;/p&gt;

&lt;p&gt;When a user selects a poster, the application keeps track of the selected movie and displays the appropriate information inside the modal.&lt;/p&gt;

&lt;p&gt;This allowed me to practice the basic React flow of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User clicks poster
       ↓
Update state
       ↓
Selected movie changes
       ↓
Modal appears
       ↓
Movie information is displayed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  📱 Making the Landing Page Responsive
&lt;/h1&gt;

&lt;p&gt;I also worked on making the frontend responsive across different screen sizes.&lt;/p&gt;

&lt;p&gt;The layout changes depending on the available screen space, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Movie poster dimensions&lt;/li&gt;
&lt;li&gt;Card sizes&lt;/li&gt;
&lt;li&gt;Spacing&lt;/li&gt;
&lt;li&gt;Typography&lt;/li&gt;
&lt;li&gt;Hero section&lt;/li&gt;
&lt;li&gt;Navigation&lt;/li&gt;
&lt;li&gt;Content layout&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal was to keep the same overall design while making the page usable on smaller screens.&lt;/p&gt;




&lt;h1&gt;
  
  
  ☁️ Deploying the React Application to AWS S3
&lt;/h1&gt;

&lt;p&gt;This was one of the most useful parts of the project for me.&lt;/p&gt;

&lt;p&gt;Building the application locally is one thing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Taking that application and making it accessible through a public URL is another.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Since this project is a frontend/static application, &lt;strong&gt;Amazon S3&lt;/strong&gt; is suitable for hosting the production build.&lt;/p&gt;

&lt;p&gt;The deployment process was essentially:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;React Application
       ↓
npm run build
       ↓
Production Build
       ↓
Upload Build Files
       ↓
Amazon S3 Bucket
       ↓
Static Website Hosting
       ↓
Live Website
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  1. Creating the Production Build
&lt;/h2&gt;

&lt;p&gt;Once the React application was finished, I generated the production build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vite generated the production-ready files in the &lt;code&gt;dist&lt;/code&gt; directory.&lt;/p&gt;

&lt;p&gt;These are the files that need to be served to visitors instead of the development files used by &lt;code&gt;npm run dev&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Creating the S3 Bucket
&lt;/h2&gt;

&lt;p&gt;I created an S3 bucket specifically for the website.&lt;/p&gt;

&lt;p&gt;The bucket is used to store the static files generated by the React build.&lt;/p&gt;

&lt;p&gt;Instead of running a development server on my computer, the files are stored in AWS and served from there.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Uploading the Build
&lt;/h2&gt;

&lt;p&gt;I uploaded the contents of the production build to the S3 bucket.&lt;/p&gt;

&lt;p&gt;The important part here is uploading the &lt;strong&gt;contents of &lt;code&gt;dist&lt;/code&gt;&lt;/strong&gt;, rather than simply uploading the entire project folder.&lt;/p&gt;

&lt;p&gt;The bucket therefore contains the files needed by the browser to load the application.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Configuring Static Website Hosting
&lt;/h2&gt;

&lt;p&gt;I configured the S3 bucket for &lt;strong&gt;static website hosting&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This tells S3 that the bucket is being used to serve a website rather than simply storing files.&lt;/p&gt;

&lt;p&gt;The website's entry point is the React application's &lt;code&gt;index.html&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Making the Website Accessible
&lt;/h2&gt;

&lt;p&gt;After configuring the required S3 settings and permissions, the files could be accessed through the S3 website endpoint.&lt;/p&gt;

&lt;p&gt;This allowed the React application to be opened from a browser without running anything locally.&lt;/p&gt;

&lt;p&gt;And that's the part I liked most about the deployment:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The application went from a local React project to an actual live website hosted on AWS.&lt;/strong&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  🔄 Updating the Deployed Application
&lt;/h1&gt;

&lt;p&gt;Another useful thing I learned was that deployment doesn't mean the project is permanently finished.&lt;/p&gt;

&lt;p&gt;When I make changes to the React application, I can create a new production build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and upload the updated build files to S3.&lt;/p&gt;

&lt;p&gt;So the workflow becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Make changes
     ↓
Test locally
     ↓
npm run build
     ↓
Upload updated files
     ↓
Refresh live website
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gave me a simple introduction to the basic idea of a &lt;strong&gt;frontend deployment workflow&lt;/strong&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  🧠 What I Learned
&lt;/h1&gt;

&lt;p&gt;This project helped me practice both frontend development and basic cloud deployment.&lt;/p&gt;

&lt;h3&gt;
  
  
  React
&lt;/h3&gt;

&lt;p&gt;I became more comfortable with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reusable components&lt;/li&gt;
&lt;li&gt;Props and state&lt;/li&gt;
&lt;li&gt;Interactive UI&lt;/li&gt;
&lt;li&gt;Modals&lt;/li&gt;
&lt;li&gt;Rendering movie data&lt;/li&gt;
&lt;li&gt;Component organization&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Frontend Development
&lt;/h3&gt;

&lt;p&gt;I practiced:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Responsive layouts&lt;/li&gt;
&lt;li&gt;CSS&lt;/li&gt;
&lt;li&gt;UI structure&lt;/li&gt;
&lt;li&gt;Cards&lt;/li&gt;
&lt;li&gt;Navigation&lt;/li&gt;
&lt;li&gt;FAQ interactions&lt;/li&gt;
&lt;li&gt;Designing a complete landing page&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  AWS
&lt;/h3&gt;

&lt;p&gt;Most importantly, I got hands-on experience with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Amazon S3&lt;/li&gt;
&lt;li&gt;S3 buckets&lt;/li&gt;
&lt;li&gt;Static website hosting&lt;/li&gt;
&lt;li&gt;Uploading production files&lt;/li&gt;
&lt;li&gt;Hosting a frontend application on AWS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The AWS part was especially useful because it showed me that cloud services aren't only something to read about — I can actually use them to deploy something I've built.&lt;/p&gt;




&lt;h1&gt;
  
  
  🚧 What's Not Included
&lt;/h1&gt;

&lt;p&gt;This is a &lt;strong&gt;frontend project&lt;/strong&gt;, so it does not currently include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User authentication&lt;/li&gt;
&lt;li&gt;User accounts&lt;/li&gt;
&lt;li&gt;Watchlists&lt;/li&gt;
&lt;li&gt;Custom backend&lt;/li&gt;
&lt;li&gt;Database&lt;/li&gt;
&lt;li&gt;Personalized recommendations&lt;/li&gt;
&lt;li&gt;Actual video streaming&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those would require additional backend and infrastructure work.&lt;/p&gt;

&lt;p&gt;For this project, I intentionally focused on the &lt;strong&gt;frontend experience and deployment&lt;/strong&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  🎯 Final Thoughts
&lt;/h1&gt;

&lt;p&gt;This project was more than just recreating a familiar UI.&lt;/p&gt;

&lt;p&gt;I got to take the project through the complete basic workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Build
  ↓
Test
  ↓
Create production build
  ↓
Deploy
  ↓
Make it publicly accessible
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The combination of &lt;strong&gt;React + AWS S3&lt;/strong&gt; gave me practical experience with taking a frontend application from local development to a real deployed website.&lt;/p&gt;

&lt;p&gt;The project is now live, so feel free to check it out.&lt;/p&gt;

&lt;p&gt;🎬 &lt;strong&gt;Live Demo:&lt;/strong&gt; &lt;a href="http://netflixlanding-605365943139-us-east-1-an.s3-website-us-east-1.amazonaws.com" rel="noopener noreferrer"&gt;http://netflixlanding-605365943139-us-east-1-an.s3-website-us-east-1.amazonaws.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;💻 &lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/himbukharidev" rel="noopener noreferrer"&gt;https://github.com/himbukharidev&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🔗 Connect With Me
&lt;/h2&gt;

&lt;p&gt;If you're interested in following my journey as I continue learning &lt;strong&gt;Java, Spring Boot, React, and AWS&lt;/strong&gt;, you can connect with me on LinkedIn:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LinkedIn:&lt;/strong&gt; &lt;a href="http://www.linkedin.com/in/bilalbukhari-dev" rel="noopener noreferrer"&gt;www.linkedin.com/in/bilalbukhari-dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>aws</category>
    </item>
    <item>
      <title>Presigned URLs and Event Notifications with S3 and SNS</title>
      <dc:creator>Bilal Bukhari</dc:creator>
      <pubDate>Sat, 19 Sep 2026 20:34:05 +0000</pubDate>
      <link>https://dev.to/bilal_bukhari_75aeb34a969/presigned-urls-and-event-notifications-with-s3-and-sns-56m0</link>
      <guid>https://dev.to/bilal_bukhari_75aeb34a969/presigned-urls-and-event-notifications-with-s3-and-sns-56m0</guid>
      <description>&lt;p&gt;Up until now, every file I've touched in S3 has gone through the console, or through a bucket policy that made things public for everyone. Today's problem was different: how do you let one specific person upload or download one specific file, temporarily, without making anything public and without giving them AWS credentials at all? That's what presigned URLs solve. Alongside that, I set up SNS to get notified whenever something actually happens in a bucket, instead of having to go check manually.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem With "Just Make It Public"
&lt;/h2&gt;

&lt;p&gt;Public bucket policies work fine for something like a static website, where everyone should see the same content. They fall apart the moment access needs to be temporary or user-specific — a private document one customer should download, or an upload slot for one user that shouldn't stay open forever. Making the whole bucket public to solve that is the wrong tool for the job, and it's the kind of shortcut that turns into a real security problem later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing Presigned URLs
&lt;/h2&gt;

&lt;p&gt;A presigned URL is a normal S3 object URL with a signature and expiry baked into the query string, generated using AWS credentials that already have permission on that object. Anyone holding that URL can perform the specific action it was signed for — a GET to download, a PUT to upload — until it expires, without needing any AWS credentials of their own.&lt;/p&gt;

&lt;p&gt;I generated these using the AWS SDK rather than the console, since that's how this actually gets used in a real app: the backend decides someone is allowed to access a specific file, generates a short-lived signed URL for it, and hands that URL back to the client. The backend never has to stream the file itself, and the client never touches an AWS secret key.&lt;/p&gt;

&lt;p&gt;The part worth paying attention to is the expiry window. Set it too long and you've basically recreated a public link with extra steps. Set it too short and legitimate users start hitting expired links mid-download. I ended up treating this as something to tune per use case rather than a single fixed number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding SNS for Event Notifications
&lt;/h2&gt;

&lt;p&gt;The second half of today was hooking up &lt;strong&gt;SNS (Simple Notification Service)&lt;/strong&gt; to S3, so that instead of periodically checking a bucket to see if anything changed, the bucket tells me when something does. S3 can publish an event — object created, object removed, etc. — directly to an SNS topic, and anything subscribed to that topic (an email address, an endpoint, another service) gets notified.&lt;/p&gt;

&lt;p&gt;Wiring this up is less about writing code and more about configuration: create the SNS topic, give the S3 bucket permission to publish to it, then configure the bucket's event notifications to fire on the events that matter (I focused on object creation for now, since "someone just uploaded something" is the one I actually needed to react to). Once that was in place, uploading a test file into the bucket produced a notification within seconds, with no polling involved anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why These Two Belong Together
&lt;/h2&gt;

&lt;p&gt;On their own, presigned URLs and SNS solve different problems — one is about controlled access, the other is about knowing when something happened. Put together, they start to look like the shape of a real upload pipeline: a backend issues a presigned URL so a user can upload directly to S3, and the moment that upload lands, an SNS notification fires so the rest of the system can react — kick off processing, update a database record, whatever needs to happen next — without the backend ever having to sit there polling the bucket to find out.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Right now the SNS notification just proves the event fired — nothing is actually consuming it yet. The obvious next step is subscribing something more useful than my own inbox to that topic, likely an SQS queue or a Lambda function, so the notification actually triggers work instead of just being observed.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>s3</category>
      <category>sns</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Replacing Basic Auth with JWT and OAuth2 in Spring Security</title>
      <dc:creator>Bilal Bukhari</dc:creator>
      <pubDate>Fri, 18 Sep 2026 20:17:10 +0000</pubDate>
      <link>https://dev.to/bilal_bukhari_75aeb34a969/replacing-basic-auth-with-jwt-and-oauth2-in-spring-security-h9k</link>
      <guid>https://dev.to/bilal_bukhari_75aeb34a969/replacing-basic-auth-with-jwt-and-oauth2-in-spring-security-h9k</guid>
      <description>&lt;p&gt;Basic auth was always the placeholder. It worked, but sending credentials on every single request never sat right — no real session, no expiry, no way to log someone out without changing their password. This week I finally replaced it with JWT, and wired up OAuth2 alongside it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Move Off Basic Auth
&lt;/h2&gt;

&lt;p&gt;The app was already stateless, so on paper basic auth and JWT look similar — no server-side session either way. The difference is what's actually being sent. With basic auth, the raw username and password go out with every request, base64-encoded, which is not encryption, just encoding. With JWT, credentials are exchanged once at login, and after that a signed token carries the identity and role claims — the password never has to leave the client again after that first request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing JWT
&lt;/h2&gt;

&lt;p&gt;The flow ended up looking like this: a user logs in with username/password, the server verifies it, and issues a signed JWT containing the username and role as claims. Every request after that carries the token in the &lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt; header instead of credentials.&lt;/p&gt;

&lt;p&gt;On the server side, I added a filter that runs before Spring Security's normal authentication step — it pulls the token out of the header, validates the signature, checks it hasn't expired, and if everything checks out, builds an &lt;code&gt;Authentication&lt;/code&gt; object from the claims and sets it into the security context. From that point on, the rest of the role-based access control I'd already built (USER vs ADMIN) just works unchanged, because it was always checking the authority on the authenticated principal, not caring how that principal got authenticated in the first place.&lt;/p&gt;

&lt;p&gt;That was actually the most satisfying part — the RBAC layer didn't need to be touched at all. Swapping the authentication mechanism underneath it and having the authorization layer keep working exactly as before is a good sign the earlier design was reasonably sound.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding OAuth2
&lt;/h2&gt;

&lt;p&gt;Alongside JWT, I set up OAuth2 login so users can authenticate through an external provider instead of only the app's own username/password flow. Spring Security's OAuth2 client support handles most of the heavy lifting here — the redirect to the provider, the callback, and pulling back user info — but I still had to decide how an OAuth2-authenticated user maps onto the same role system as everyone else, since a first-time OAuth2 login doesn't come with a role attached the way a normal registration does.&lt;/p&gt;

&lt;p&gt;The approach I landed on: on first OAuth2 login, create a local user record with a default role, so from that point forward they're indistinguishable from a normal registered user as far as the rest of the app is concerned. Two different front doors, same house once you're inside.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I Got Stuck
&lt;/h2&gt;

&lt;p&gt;Token expiry handling was the annoying part. It's easy to issue a token that works — it's less obvious, at first, how to handle a client whose token has expired mid-session without just throwing a generic 401 and leaving them confused. I ended up making sure expired-token responses are distinguishable from invalid-token responses, so the client side can actually tell "log in again" apart from "something is wrong," instead of treating every failure the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Different Now
&lt;/h2&gt;

&lt;p&gt;Basic auth is gone. Every request now carries a token instead of raw credentials, that token has a real expiry, and there are two legitimate ways to authenticate — direct login or OAuth2 — both of which land in the same role-based system underneath. It's a lot more moving parts than the app had a week ago, but each part is doing something basic auth genuinely couldn't.&lt;/p&gt;

&lt;p&gt;Next up: refresh tokens, since right now an expired token just means logging in again from scratch, which isn't going to hold up once this is actually deployed somewhere.&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>security</category>
      <category>jwt</category>
    </item>
    <item>
      <title>Implementing Role-Based Access Control in Spring Boot — Users, Admins, and Where the Line Gets Drawn</title>
      <dc:creator>Bilal Bukhari</dc:creator>
      <pubDate>Tue, 15 Sep 2026 19:53:24 +0000</pubDate>
      <link>https://dev.to/bilal_bukhari_75aeb34a969/implementing-role-based-access-control-in-spring-boot-users-admins-and-where-the-line-gets-23bp</link>
      <guid>https://dev.to/bilal_bukhari_75aeb34a969/implementing-role-based-access-control-in-spring-boot-users-admins-and-where-the-line-gets-23bp</guid>
      <description>&lt;p&gt;There's a moment in every backend project where "anyone can call any endpoint" stops being acceptable, and you have to actually decide who gets to see what. That moment hit me this week, and the answer was Spring Security with role-based access control (RBAC).&lt;/p&gt;

&lt;p&gt;Here's what I built: a registration system where a user can sign up as either a regular &lt;strong&gt;USER&lt;/strong&gt; or an &lt;strong&gt;ADMIN&lt;/strong&gt;, and depending on which one they are, they see a completely different slice of the application. A regular user can only view their own basic info — username, email, that's it. An admin, on the other hand, can pull up the full list of every registered user in the system.&lt;/p&gt;

&lt;p&gt;Simple to describe. Not quite as simple to get right the first time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Letting Users Pick a Role at Registration
&lt;/h2&gt;

&lt;p&gt;The registration endpoint takes a role field alongside the usual username/password/email — but I didn't want to just trust whatever the client sends blindly forever, since that's an easy way to let anyone register as an admin. For now, at this stage of the project, the role is accepted at signup and stored against the user, tied to Spring Security's &lt;code&gt;GrantedAuthority&lt;/code&gt; model, so every user ends up carrying a &lt;code&gt;ROLE_USER&lt;/code&gt; or &lt;code&gt;ROLE_ADMIN&lt;/code&gt; authority from the moment their account exists.&lt;/p&gt;

&lt;p&gt;That authority is what everything downstream checks against — not a name, not a flag scattered through the code, just one consistent thing Spring Security already knows how to reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Locking Down Endpoints by Role
&lt;/h2&gt;

&lt;p&gt;This is where Spring Security actually earns its keep. Instead of writing &lt;code&gt;if (user.getRole().equals("ADMIN"))&lt;/code&gt; checks inside every controller method — which gets messy and easy to forget — the access rules live in the security configuration itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/api/users/me&lt;/code&gt; → accessible to any authenticated user, returns only their own data&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/api/admin/users&lt;/code&gt; → accessible only to &lt;code&gt;ROLE_ADMIN&lt;/code&gt;, returns the full user list&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a regular user tries hitting the admin endpoint directly, Spring Security shuts it down before the request even reaches the controller — a clean 403, no leaked data, no custom guard clause needed. That was honestly satisfying to see work correctly on the first real test.&lt;/p&gt;

&lt;p&gt;Worth noting: the app is already configured stateless (no server-side sessions), but JWT isn't in the picture yet — right now, authentication happens via HTTP Basic on each request. It works, but it's clearly a placeholder until token-based auth is in place.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Part That Actually Took Time
&lt;/h2&gt;

&lt;p&gt;The RBAC rules themselves weren't the hard part — wiring up the &lt;code&gt;UserDetailsService&lt;/code&gt; correctly so that authorities actually loaded and attached to the authenticated user on every request was where I spent most of my time. It's one of those things where the code looks fine, compiles fine, and then silently doesn't work because the authority string format doesn't match what the security config is checking for (&lt;code&gt;ADMIN&lt;/code&gt; vs &lt;code&gt;ROLE_ADMIN&lt;/code&gt; cost me a solid chunk of debugging time — Spring Security expects the &lt;code&gt;ROLE_&lt;/code&gt; prefix by convention, and it fails quietly rather than yelling at you about it).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters Beyond the Exercise
&lt;/h2&gt;

&lt;p&gt;This is the first time in this project that "the backend works" and "the backend is secure" stopped being the same statement. A working endpoint that returns data to anyone who asks isn't done — it's just functional. Adding the role layer is what actually makes it something you could put in front of real users without immediately regretting it.&lt;/p&gt;

&lt;p&gt;Next up: extending this with proper password hashing policies and actually implementing JWT for authentication — the app is already configured stateless, so this is the natural next piece rather than a rework.&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>backend</category>
      <category>security</category>
    </item>
    <item>
      <title>Static Website Hosting on Amazon S3: From Bucket to Live URL</title>
      <dc:creator>Bilal Bukhari</dc:creator>
      <pubDate>Mon, 14 Sep 2026 20:07:26 +0000</pubDate>
      <link>https://dev.to/bilal_bukhari_75aeb34a969/static-website-hosting-on-amazon-s3-from-bucket-to-live-url-2hhi</link>
      <guid>https://dev.to/bilal_bukhari_75aeb34a969/static-website-hosting-on-amazon-s3-from-bucket-to-live-url-2hhi</guid>
      <description>&lt;p&gt;Continuing from where I left off with S3 fundamentals, today's focus was turning a plain bucket into an actual live website using S3's static website hosting feature — no EC2 instance, no server management, just a bucket serving HTML directly over the web.&lt;/p&gt;

&lt;h2&gt;
  
  
  Topics Covered
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Static website hosting configuration&lt;/li&gt;
&lt;li&gt;Index and error documents&lt;/li&gt;
&lt;li&gt;Public access blocking&lt;/li&gt;
&lt;li&gt;Bucket policies for public reads&lt;/li&gt;
&lt;li&gt;Testing the live endpoint&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Enabling Static Website Hosting
&lt;/h2&gt;

&lt;p&gt;By default, an S3 bucket is just object storage — it doesn't know how to behave like a website. Enabling &lt;strong&gt;static website hosting&lt;/strong&gt; (found under the bucket's Properties tab) turns it into one by telling S3 two things: which file to serve when someone visits the root URL, and which file to show if a requested page doesn't exist.&lt;/p&gt;

&lt;p&gt;Once enabled, S3 generates a dedicated &lt;strong&gt;website endpoint&lt;/strong&gt; in the form:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;http://&amp;lt;bucket-name&amp;gt;.s3-website.&amp;lt;region&amp;gt;.amazonaws.com&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;For this lab, I set up the bucket &lt;code&gt;firststaticsite-605365943139-eu-north-1-an&lt;/code&gt; in &lt;code&gt;eu-north-1&lt;/code&gt;, uploaded a landing page template, and had it live at that endpoint within minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="http://firststaticsite-605365943139-eu-north-1-an.s3-website.eu-north-1.amazonaws.com/" rel="noopener noreferrer"&gt;http://firststaticsite-605365943139-eu-north-1-an.s3-website.eu-north-1.amazonaws.com/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Index and Error Documents
&lt;/h2&gt;

&lt;p&gt;Two settings drive how the site behaves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Index document&lt;/strong&gt; — the file served when someone visits the root of the site (almost always &lt;code&gt;index.html&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error document&lt;/strong&gt; — an optional custom page shown when a requested file doesn't exist, instead of S3's default XML error.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Getting these right matters more than it seems — a missing or misconfigured index document is one of the most common reasons a freshly enabled static site fails to load.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Public Access Block
&lt;/h2&gt;

&lt;p&gt;By default, AWS blocks public access to every new bucket as a safety measure — which makes sense for general storage, but breaks a static website, since visitors need to reach the files without any AWS credentials.&lt;/p&gt;

&lt;p&gt;To fix this, &lt;strong&gt;Block Public Access&lt;/strong&gt; has to be explicitly turned off at the bucket level (Permissions tab). This is a deliberate extra step AWS added specifically to prevent people from accidentally exposing private data — so it's a conscious trade-off, not a default you stumble into.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Bucket Policy for Public Reads
&lt;/h2&gt;

&lt;p&gt;Turning off Block Public Access alone isn't enough — the bucket also needs an explicit &lt;strong&gt;bucket policy&lt;/strong&gt; granting read access to everyone. Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PublicReadGetObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3:GetObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::firststaticsite-605365943139-eu-north-1-an/*"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This grants &lt;code&gt;s3:GetObject&lt;/code&gt; (read-only) to anyone, scoped strictly to objects inside this one bucket — it doesn't expose anything else in the account.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Testing the Live Endpoint
&lt;/h2&gt;

&lt;p&gt;Once hosting is enabled, public access is unblocked, and the policy is in place, the site becomes reachable by anyone with the URL — not just the account owner logged into the AWS console. That last part is an easy trap: a page can look "live" while you're logged in and still fail for an outside visitor if permissions aren't fully open. Testing in an incognito window (or asking someone else to open the link) is the real test of whether the site is actually public.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Static website hosting is a good next step after learning buckets and objects, because it forces you to actually deal with the permission model instead of just uploading files. The distinction between "the object exists in the bucket" and "the object is publicly reachable" is the core lesson here, and it's one that carries over directly into more advanced AWS work later on — IAM policies, CloudFront distributions, and beyond.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloud</category>
      <category>devops</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Amazon S3 Fundamentals: Buckets, Objects, Storage Classes, and Access Control</title>
      <dc:creator>Bilal Bukhari</dc:creator>
      <pubDate>Sun, 13 Sep 2026 18:57:32 +0000</pubDate>
      <link>https://dev.to/bilal_bukhari_75aeb34a969/amazon-s3-fundamentals-buckets-objects-storage-classes-and-access-control-published-false-5ll</link>
      <guid>https://dev.to/bilal_bukhari_75aeb34a969/amazon-s3-fundamentals-buckets-objects-storage-classes-and-access-control-published-false-5ll</guid>
      <description>&lt;p&gt;Amazon S3 (Simple Storage Service) is typically the first AWS service most engineers encounter when they begin working with cloud infrastructure. It is deceptively simple on the surface, but has considerable depth once access control, storage tiering, and cost optimization enter the picture. This post covers the core S3 concepts I worked through, supported by a short hands-on lab.&lt;/p&gt;

&lt;h2&gt;
  
  
  Topics Covered
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Buckets&lt;/li&gt;
&lt;li&gt;Objects&lt;/li&gt;
&lt;li&gt;Uploading files&lt;/li&gt;
&lt;li&gt;Storage classes&lt;/li&gt;
&lt;li&gt;Bucket policies&lt;/li&gt;
&lt;li&gt;ACLs and permissions&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Buckets
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;bucket&lt;/strong&gt; is the top-level container in S3 — the root under which every object is stored. Bucket names must be &lt;strong&gt;globally unique&lt;/strong&gt; across all of AWS, not just within a single account, which is why they often follow a pattern like &lt;code&gt;firstbucket-&amp;lt;account-id&amp;gt;-&amp;lt;region&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Each bucket is also tied to a specific &lt;strong&gt;region&lt;/strong&gt; at creation time (e.g., &lt;code&gt;eu-north-1&lt;/code&gt;, Stockholm). This choice affects latency, cost, and data residency, since the underlying data physically resides in that region's data centers.&lt;/p&gt;

&lt;p&gt;For this lab, I created a bucket named &lt;code&gt;firstbucket-605365943139-eu-north-1-an&lt;/code&gt;. Immediately after creation, it was empty — zero objects — and ready to receive uploads.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Objects
&lt;/h2&gt;

&lt;p&gt;Everything you store inside a bucket — images, videos, documents, backups, logs — is called an &lt;strong&gt;object&lt;/strong&gt;. Each object consists of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;data&lt;/strong&gt; itself (the file)&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;key&lt;/strong&gt; (essentially its full path/name inside the bucket)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metadata&lt;/strong&gt; (content type, size, last modified date, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's no real folder structure in S3 under the hood — folders are a UI convenience. What actually exists is a flat namespace of keys, and prefixes (like &lt;code&gt;images/photo.jpg&lt;/code&gt;) just simulate folder hierarchy.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Uploading
&lt;/h2&gt;

&lt;p&gt;Uploading is the process of adding an object to a bucket. In the AWS console, this is a straightforward &lt;strong&gt;Upload&lt;/strong&gt; action, though the same operation is typically automated through the CLI or an SDK in production systems.&lt;/p&gt;

&lt;p&gt;For this lab, I uploaded two JPEG images directly into the bucket. Once uploaded, S3 surfaces key metadata for each object — type, size, last modified timestamp, and storage class:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Storage Class&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WhatsApp Image ...11.01.41 PM.jpg&lt;/td&gt;
&lt;td&gt;jpeg&lt;/td&gt;
&lt;td&gt;201.5 KB&lt;/td&gt;
&lt;td&gt;Standard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WhatsApp Image ...11.17.02 PM.jpeg&lt;/td&gt;
&lt;td&gt;jpeg&lt;/td&gt;
&lt;td&gt;123.0 KB&lt;/td&gt;
&lt;td&gt;Standard&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  4. Storage Classes
&lt;/h2&gt;

&lt;p&gt;Not all data needs to be instantly available all the time, and S3 gives you multiple &lt;strong&gt;storage classes&lt;/strong&gt; to balance cost vs. retrieval speed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;S3 Standard&lt;/strong&gt; — frequently accessed data, highest availability, most expensive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 Intelligent-Tiering&lt;/strong&gt; — automatically moves objects between tiers based on access patterns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 Standard-IA / One Zone-IA&lt;/strong&gt; — infrequently accessed data, cheaper storage but a retrieval fee.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 Glacier / Glacier Deep Archive&lt;/strong&gt; — long-term archival, very cheap storage, but retrieval can take minutes to hours.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By default, newly uploaded objects land in &lt;strong&gt;Standard&lt;/strong&gt;, which is exactly what I saw in the lab. Picking the right class for the right data is one of the easiest ways to cut cloud storage costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Bucket Policies
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;bucket policy&lt;/strong&gt; is a JSON document attached to the bucket itself that defines who can do what — for example, allowing public read access to a bucket used for a static website, or restricting access to specific IAM roles or AWS accounts.&lt;/p&gt;

&lt;p&gt;Because a bucket policy applies at the bucket level, it's the go-to tool when you want a single, centralized rule for everything inside that bucket (e.g., "only this Lambda function's role can write objects here").&lt;/p&gt;

&lt;h2&gt;
  
  
  6. ACLs and Permissions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Access Control Lists (ACLs)&lt;/strong&gt; are an older, more granular way to control access — at the level of individual buckets &lt;em&gt;or&lt;/em&gt; individual objects. They predate bucket policies and IAM, and AWS now recommends disabling ACLs in favor of bucket policies and IAM policies for most use cases, since ACLs can get messy to manage at scale.&lt;/p&gt;

&lt;p&gt;Broadly, S3 access boils down to three tools working together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;IAM policies&lt;/strong&gt; — control what an AWS identity (user/role) can do across services.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bucket policies&lt;/strong&gt; — control access to a specific bucket.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ACLs&lt;/strong&gt; — legacy, fine-grained access on buckets/objects (mostly discouraged today).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Understanding how these layers interact — and that they're evaluated together, not in isolation — is one of the more important "aha" moments when learning S3.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;This lab was intentionally small in scope, but it establishes the core mental model required before working with S3 in more advanced contexts — static website hosting, data lake architectures, or backend integrations. The next step in this learning path is exploring IAM in more depth and integrating S3 into a Spring Boot application.&lt;/p&gt;

&lt;p&gt;If you're working through AWS fundamentals as well, I'd be interested to hear which concept took longest to click. For me, it was distinguishing bucket policies, ACLs, and IAM policies — three mechanisms that all govern access, but at different layers and with different intended use cases.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloud</category>
      <category>devops</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
