My upload reaches 100%, but the video still says “Processing.” Its bytes have arrived; something behind the website must turn them into a playable video. Until I publish it, those bytes should stay private—even though anyone can open example.com.
I’m building a small YouTube-like video product on AWS. This is my example architecture, not a description of YouTube’s infrastructure. The familiar actions give every infrastructure decision a job.
BROWSE
Browser -- HTTPS --> public web entry --> private video app
|
+-- SQL --> metadata DB
UPLOAD
Browser -- ask permission --> video app
Browser -- authorized upload -----------------> raw video storage
WATCH
Player -- HTTPS --> CDN -- cache miss --> processed video storage
MAINTAIN
Developer -- authenticated private access --> development tools
CI pipeline -- scoped cloud API calls -------> deploy the app
That is where the design will end up. I start with one server and add the other parts when a real action needs them.
Open the homepage on one server#
I start with one EC2 instance (a virtual server) running the video-sharing app. It serves the homepage at /, the watch page at /watch, and an API (HTTP endpoints called by the frontend) under /api. These can all belong to one application; a separate microservice is not required for each page. Even this first instance needs a VPC and subnet; I define those below as I separate the database.
When I open https://example.com, the browser needs an IP address (a network destination). DNS (the system that looks up names) helps resolve example.com to one. The browser then connects to the destination and sends an HTTP request.
At this first stage, the browser connects directly to the public server on TCP 443. The server returns HTML and browser code; that code requests published video metadata from /api/videos. The important boundary is between calling the application and accessing its storage directly. I keep the packet-level explanation in what happens when I open a website. The expandable exchange follows the initial homepage request.
Expand the DNS lookup, TCP setup, and TLS handshake
I’ll follow a fresh HTTPS connection using TCP (reliable, ordered delivery) and TLS 1.3 (encryption and server authentication). The three-way handshake belongs to TCP: SYN, SYN-ACK, ACK. These messages agree on starting sequence numbers; they do not encrypt anything. TLS then makes its own exchange over that TCP connection. TCP connection establishment
Here I assume no cached DNS answer, no existing connection, and a full TLS 1.3 handshake with a server certificate. Resumption, early data, a retry, and client certificates would change the exchange. TCP acknowledgments during TLS, compatibility messages, and connection closing are omitted. HTTP/3 uses QUIC over UDP instead of this TCP setup.
The hello messages exchange public key material; the private keys are not sent. Messages after ServerHello are encrypted. The certificate identifies the server, CertificateVerify proves control of its private key, and Finished checks the handshake. HTTP then travels with application traffic keys. This diagram shows the ordinary server-authenticated flow, not a TLS guarantee that every handshake has exactly these messages. TLS 1.3 message flow
DNS helps find the destination. It does not carry the page request or grant permission to enter.
An IP address gets us to a host; a port (a numbered destination for a service) helps its operating system deliver traffic to the right program. Think of a building address and a particular office inside it.
| Destination port | What might be listening there | Who needs it in this example? |
|---|---|---|
| TCP 443 | HTTPS website | Anyone visiting example.com |
| TCP 22 | SSH, an encrypted remote shell | Authorized operators, if enabled |
| TCP 5432 | PostgreSQL database | Video app and approved developers |
| TCP 8443 | Our internal HTTPS service | The public entry point |
These are conventions and choices, not guarantees. A program can listen on a different port. Moving Postgres to an unusual number doesn’t make it private.
A video page needs metadata, not database access#
I choose EC2 here so the process, network interface, and listening port stay visible. If I already package the backend as a container, ECS on Fargate (managed container execution) can run it without my managing the host operating system. I still choose task subnets, security groups, roles, and load-balancer integration. “Managed” changes my operational work; it does not erase the network decisions. Fargate networking
The app stores channels, video titles, ownership, and publication status in PostgreSQL, a relational database. GET /api/videos/42 returns the information needed to draw a watch page. Updating the title requires the uploader’s permission.
These are small structured records, separate from the video bytes. The backend queries the database and returns HTTP data; the browser never needs the database password.
I could initially run both programs on one machine, with Postgres listening only locally. One server, a public IP, HTTPS on 443, and a firewall rejecting other inbound connections can be a valid beginning.
Publicly reachable does not mean every port is open. And accepting an HTTPS connection does not mean accepting every request inside it.
The problem grows when that same machine serves watch pages, stores video metadata, holds database credentials, and accepts remote administration. One successful break-in can reach too much. Updating the machine can also take everything offline.
I move the database to its own managed resource, then put a public entry in front of the application. These are different callers and different interfaces:
flowchart TB
U[Anyone on the internet]
subgraph public[Public entry]
E[HTTPS entry point]
end
subgraph private[Private work]
S[Video app]
D[(Metadata database)]
end
U -->|HTTPS 443| E
E -->|HTTPS 8443| S
S -->|Postgres 5432| D
U -.->|5432 blocked| D
linkStyle 0,1,2 stroke:#2f9e68,stroke-width:2px
linkStyle 3 stroke:#d64545,stroke-width:2px
classDef entry fill:#3178c618,stroke:#3178c6,stroke-width:2px
classDef work fill:#2f9e6818,stroke:#2f9e68,stroke-width:2px
classDef data fill:#9263c618,stroke:#9263c6,stroke-width:2px
classDef rejected fill:#d6454518,stroke:#d64545,stroke-width:2px
class U,E entry
class S work
class D data
style public fill:transparent,stroke:#777e88,stroke-width:1.5px
style private fill:transparent,stroke:#777e88,stroke-width:1.5px Blue boxes mark the public side, green the application, and purple the data. Green arrows are allowed paths; red dashed arrows are blocked attempts. The browser gets video metadata without a database connection.
This is least privilege (giving each caller only the access its job needs). The separation reduces the damage one compromised component can cause. It doesn’t make that component impossible to compromise.
Give the private machines their own address space#
The app must reach Postgres, but neither needs a directly reachable internet address once the public entry forwards requests. I need an address space for that private communication.
A VPC, or virtual private cloud (an isolated virtual network) gives me a network whose addresses and routes I control. Azure calls its corresponding network a VNet. This is software-defined networking on the provider’s hardware, not a private rack of machines I own. An AWS VPC belongs to a Region, a geographical area where I deploy resources. AWS’s VPC model
I choose 10.20.0.0/16 for this example. CIDR (a compact way to write an address range) uses the number after the slash to say how many leading bits are fixed.
IPv4 address: 32 bits
10.20.0.0/16
└─ 16 fixed bits ─┘ 16 bits available to vary
Range: 10.20.0.0 through 10.20.255.255
Size: 2^16 = 65,536 addresses
A smaller slice:
10.20.21.0/24 → 10.20.21.0 through 10.20.21.255
2^8 = 256 addresses
These are address counts, not usable server counts; AWS reserves some addresses in each subnet. 10.0.0.0/8 is private address space: the public internet doesn’t route it to my VPC. Another company can use the same addresses inside its own network. If we later connect those networks, overlapping ranges become a problem. AWS subnet addressing
A private address isn’t a password. A machine that gains a permitted network path can attempt a connection. I still need rules about who may connect and what they may do.
A subnet is a slice, not a locked room#
A subnet (a smaller address range inside the network) lets me apply routing and subnet-level policy to a group of resources. In AWS, each subnet belongs to one Availability Zone (a separate failure location within a Region). A VPC can span several zones. AWS subnet boundaries
An instance’s network interface (its virtual network attachment) receives an address from its subnet. The subnet is an address-and-policy boundary, not a physical server, application, or permission to use the data.
I divide the addresses by job, and repeat the layout in two zones:
| Job | Zone A | Zone B | What lives here? |
|---|---|---|---|
| Public entry | 10.20.0.0/24 | 10.20.1.0/24 | Load balancer connections |
| Private application | 10.20.10.0/24 | 10.20.11.0/24 | Video app instances |
| Private data | 10.20.20.0/24 | 10.20.21.0/24 | Database deployment |
Why both job and zone? Job separation controls exposure. Zone separation helps survive a failure. Two subnets in the same zone solve only the first problem.
These /24s are an illustrative small application, not a sizing rule. Each has 251 assignable IPv4 addresses after AWS’s five reservations. I budget for peak instances, overlapping old and new deployments, and service network interfaces. Containers need space too: ECS tasks using awsvpc networking each get an interface; EKS (managed Kubernetes) commonly assigns VPC addresses to its pods (running container groups). A busy container tier may need a much larger range. I choose it before deployment: an existing subnet’s CIDR cannot be resized. Subnet sizing, EKS address planning
The load balancer also needs room to grow. AWS requires its ordinary Availability Zone subnets to be at least /27, with at least eight free addresses in each. A small public tier still needs spare capacity. ALB subnet requirements
A subnet named private-database has no special powers. Within a VPC, local routes normally provide paths between its subnets. Putting two machines in different subnets does not by itself stop them talking.
A public subnet needs a route, not a public-sounding name#
A route table (destination ranges and where to send their packets) answers: “For this address, which way?”
An internet gateway (the VPC’s connection to the internet) provides an internet path. A subnet with a direct route to it is called public. For direct IPv4 internet communication, a resource also needs a public IPv4 address or mapping and permissive firewall rules. A route alone does not expose every machine. AWS route tables
Here is my example’s routing plan:
| Subnet’s table | Destination | Send to |
|---|---|---|
| All three tiers | 10.20.0.0/16 | Local VPC routing |
| Public entry | 0.0.0.0/0 | Internet gateway |
| Private application | 0.0.0.0/0 | Outbound gateway, added when needed |
| Private data | Other destinations | No default internet route |
0.0.0.0/0 matches every IPv4 address. A more specific matching route wins: a request to 10.20.21.15 uses the /16 local route, not the /0 internet route.
Let the front door forward the work#
One application instance can restart or fail. I run copies in two zones and place the public entry in front of them. That entry is a load balancer (an entry point that distributes requests across healthy servers), which keeps the public address separate from the machines doing the work. An AWS Application Load Balancer, or ALB, can route HTTP requests and check whether targets are healthy. ALB behavior
I create a Route 53 alias record (a DNS record that follows an AWS resource) for example.com pointing at the ALB, rather than copying its current IP addresses. I obtain a certificate for example.com through ACM (AWS Certificate Manager), validate domain control, attach it to the ALB, and configure its listener, the rule for handling connections on port 443. Its target group (the set of backends eligible to receive requests) contains our private app instances on 8443. Route 53 routing to a load balancer, HTTPS listener certificates
flowchart TB
B[Browser]
subgraph V[Production VPC · 10.20.0.0/16]
subgraph P[Public subnets · Zones A and B]
L[One logical ALB across both zones]
end
subgraph A[Private application subnets]
SA[App · Zone A]
SB[App · Zone B]
end
subgraph D[Private data subnets]
DB[(Postgres primary)]
ST[(Standby in other zone)]
end
L -->|HTTPS 8443| SA
L -->|HTTPS 8443| SB
SA -->|Postgres 5432| DB
SB -->|Postgres 5432| DB
DB -->|Replication| ST
end
B -->|HTTPS 443| L
linkStyle 0,1,2,3,4,5 stroke:#2f9e68,stroke-width:2px
classDef entry fill:#3178c618,stroke:#3178c6,stroke-width:2px
classDef work fill:#2f9e6818,stroke:#2f9e68,stroke-width:2px
classDef data fill:#9263c618,stroke:#9263c6,stroke-width:2px
classDef rejected fill:#d6454518,stroke:#d64545,stroke-width:2px
class B,L entry
class SA,SB work
class DB,ST data
style V fill:transparent,stroke:#777e88,stroke-width:1.5px
style P fill:transparent,stroke:#777e88,stroke-width:1.5px
style A fill:transparent,stroke:#777e88,stroke-width:1.5px
style D fill:transparent,stroke:#777e88,stroke-width:1.5px The same colors mark public entry, application work, and data. This is a logical request map; the internet gateway and route tables provide its paths. The standby is for database failover, not a second writable database.
The ALB is not the webpage service: the video app still serves /, /watch, and /api. The ALB forwards requests to a healthy copy. The browser-to-ALB connection ends at the ALB. The ALB makes a separate connection to a backend using its private address. The video app makes another to Postgres. One browser action can produce several distinct network connections.
HTTPS on the backend leg encrypts it, but ALB does not validate the target’s certificate. This is not the browser’s hostname-and-certificate check repeated at the next hop. ALB target TLS
That proxy hop also changes the apparent caller: the backend sees the ALB’s connection. For the original client address, I configure the app to interpret X-Forwarded-For only through trusted proxies, not trust arbitrary values supplied by callers. Separate connections also have separate idle clocks: ALB defaults to 60 seconds; a backend closing earlier can cause a 502 race. I keep the backend’s idle timeout longer than the ALB’s. Forwarded headers, ALB idle timeout
For the database I choose Amazon RDS, managed relational database hosting, with public accessibility disabled. The database subnet group covers both zones; a Multi-AZ deployment supplies failover. Merely listing two subnets doesn’t enable database replication. Apps use the database’s DNS endpoint because its address can change during failover. RDS in a VPC
Failover still needs the client to cooperate. A connection pool keeps database connections open for reuse. I discard broken pooled connections and let new connections resolve the endpoint again. For Java, AWS recommends a DNS cache lifetime of no more than 60 seconds; caching forever can leave the app trying the old address. RDS failover and DNS
Two zones are my choice for this web tier and primary/standby database, not a rule for every distributed service. In a three-voter system split two-and-one, losing the zone with two voters removes the majority. Its failure placement needs a different design. Two zones also aren’t regional disaster recovery: I still need backups, tested restores, and enough surviving capacity.
A route says where. A firewall says whether.#
Now I can reach the right machines internally. I don’t want every internal machine to reach every service.
A security group (a stateful network firewall attached to supported resources’ network interfaces) filters connections by source or destination, protocol, and port. Stateful means it remembers allowed connections and permits their response traffic. It doesn’t understand whether an HTTP request is reading video metadata or deleting it. AWS security groups
I attach one group to the ALB, one to the app instances, and one to the RDS database. These are desired inbound rules for the production path; each row assumes no other attached group grants broader access:
| Group | Allow inbound from | Destination port | Meaning |
|---|---|---|---|
entry-sg | 0.0.0.0/0 | TCP 443 | Anyone can reach the web entry |
app-sg | entry-sg | TCP 8443 | Only the entry’s interfaces can reach the app |
database-sg | app-sg | TCP 5432 | Only the app can start database connections |
The corresponding outbound policy allows entry to app on 8443 and app to database on 5432. Health checks use the permitted backend port here. Other dependencies need deliberate rules of their own.
The process must actually listen on the interface the ALB reaches. Binding the backend only to 127.0.0.1 makes it local to that machine. Listening on 0.0.0.0:8443 with TLS accepts connections on its IPv4 interfaces; it does not assign a public IP or bypass the security group. The route, firewall, and running program all have to agree.
ALB -- HTTPS 8443 --> app interface -- accepted socket --> backend
| |
app-sg allows? listening here?
A running instance is not necessarily a ready application. I configure an ALB health check such as /health/ready on the backend port, allow its traffic, and monitor target health. Health checks guide routing; they are not an access-control boundary. If all registered targets are unhealthy, ALB can route to all of them rather than block all requests. ALB health checks
Referencing app-sg avoids tracking every changing application IP. It matches the relevant network interfaces associated with that group; it doesn’t authenticate a Java process or inherit the group’s rules. An attacker controlling an allowed application server has that network access too. Security group references
Security groups have allow rules, not explicit deny rules. Multiple attached groups contribute their allowed traffic together: a broad rule in one can undo the narrow intent of another. “Deny unless needed” requires checking the whole set.
Why an ACL needs a rule for the reply#
A network ACL (access control list) is another packet filter, applied at a subnet boundary. Unlike security groups, AWS network ACLs are stateless (they evaluate packets without remembering the connection). Rules have numbers; the first matching rule wins. They can explicitly allow or deny. They don’t filter communication between machines within the same subnet. AWS network ACLs
Suppose the app opens a database connection from temporary port 51000:
Port 5432 belongs to the database listener. Port 51000 belongs to this client’s connection. Replies reverse the source and destination.
A security group recognizes this as response traffic. A restrictive ACL needs matching return-path rules; an outbound rule allowing only destination 5432 would not allow this reply. Real rules cover the appropriate ephemeral port range (temporary ports chosen by clients), not just 51000. Both subnets’ ACLs matter. AWS ACL examples
I use security groups for precise service relationships. An ACL can add a subnet-wide guardrail, but duplicating every rule there adds another place to break the return path.
| Connection question | Security group | Network ACL |
|---|---|---|
| Where does it apply? | Supported resource interfaces | Traffic entering or leaving a subnet |
| Can it deny explicitly? | No; allow rules combine | Yes; lowest-numbered match wins |
| Does a permitted request permit replies? | Yes, through connection tracking | No; check the reverse direction separately |
| Can it separate two instances in one subnet? | Yes, with suitable group rules | No |
| What do I use it for here? | App-to-database permission | Additional subnet-wide restrictions |
Every subnet has an associated NACL. I can leave the default allow-all NACL while enforcing workload access with security groups; a new custom NACL starts by denying traffic. Tightening it is an extra responsibility, not a replacement for the resource rules. NACL defaults and evaluation
A working connection does not grant access to an unpublished video#
Alice owns an unpublished video. Bob changing the URL to /api/videos/42 must not reveal its private metadata or issue him a playback grant.
A packet filter sees addresses, protocols, and ports. It cannot decide whether the caller may view an unpublished video.
| Check | Question it answers | What it cannot establish |
|---|---|---|
| IP routing, network layer | Is there a path to this destination? | Whether I should use it |
| SG / ACL, network and transport fields | May this connection pass? | Whether this video is public |
| TLS | Is this connection encrypted to the checked server? | Permission to view a private video |
| Application authentication | Which user is calling? | Permission to every user’s data |
| Application authorization | May this user view this video? | Whether another service is safely configured |
| Database grants | May this DB role run this SQL operation? | The end user’s identity unless the app carries it through |
| Cloud IAM | May this principal perform this cloud API action? | An automatic route or ordinary database login |
IAM (identity and access management) controls actions such as changing infrastructure or reading cloud storage. Permission to modify an RDS instance isn’t automatically permission to query its tables. IAM database authentication is a separate, explicitly configured feature, and database privileges still matter. AWS IAM
A reachable service is not permission to use every operation it exposes. The network is doing its job even when the application refuses this request.
The app needs its own identity too#
Alice’s session identifies a person to our app. An IAM role (permissions a workload can use with temporary credentials) identifies the app to AWS. These are different identities: the cloud role does not tell Postgres which videos Alice owns.
I store the metadata database password in Secrets Manager. The app instance or task gets an IAM role allowed to read that specific secret; unrelated secrets are outside its grant. Retrieving the password still needs a network path to Secrets Manager, through an appropriate private endpoint or outbound internet path. The database then applies its own login and SQL permissions. Secret access policies
Uploading a video should not occupy the web server#
A video might be gigabytes; its title is a few words. Passing every upload through the app ties up its connections and bandwidth. I use S3 object storage (files stored under keys in buckets) for the bytes, with separate raw-upload and processed-video buckets. The database holds each video’s owner, status, and object references.
The browser first calls POST /api/uploads. The app authenticates the creator, checks the upload allowance, allocates a new video ID and object key, and returns a presigned URL (a temporary grant for a specific S3 operation). The browser uploads directly to S3 using that URL.
The URL limits the operation and object key; it does not give the browser the app’s secret credentials or permission to list the bucket. It is still a bearer credential: anyone holding it can use the grant while it is valid, potentially more than once. A URL signed with an instance or task role’s temporary credentials expires when those credentials expire, even if I requested a longer lifetime. I use short expirations and unique keys, and verify the completed object before processing it. S3 presigned uploads
For large files I use multipart upload (separately uploading pieces of one object) so interrupted pieces can be retried. The backend authorizes the upload session and appropriate part requests; abandoned uploads need cleanup. Browser uploads also need S3 CORS rules (which browser origins may make cross-origin requests). CORS is not authentication and does not stop a non-browser client. Multipart uploads, S3 CORS
I keep Block Public Access enabled. A public S3 API endpoint can accept our authorized upload while refusing anonymous reads. Publicly reachable is different from publicly readable. IAM and bucket policies control object actions; our application decides which creator may request a grant. S3 access controls
S3 is an AWS service; my bucket is a resource created in it. The bucket uses IAM and storage policies, not a security group. I keep object ACLs disabled with Bucket owner enforced ownership; an object ACL is a storage grant, not a network ACL. S3 Object Ownership
An upload is not yet a playable video#
Different viewers need different formats and bitrates. Transcoding (making new encoded versions of a video) is CPU-heavy work that should not run inside the upload request. I add a background worker (a program processing jobs outside the browser request) and SQS (a managed message queue) to hold work until a worker takes it.
After verifying upload completion, the app submits the video ID and immutable input version. The worker reads the raw video, writes playable versions and thumbnails, then reports completion through an authenticated internal app operation. The app validates the job ID and input version before accepting completion; a worker identity alone is not permission to mark any video complete. Only the app updates metadata. The video remains “processing” until the required outputs exist.
Worker -- GetObject --> raw bucket
Worker -- encode --> output files
Worker -- PutObject --> processed bucket
Worker -- report completion --> internal video API -- SQL --> metadata
Worker -- DeleteMessage --> queue
Standard SQS can deliver a job more than once. Processing the same input version must be idempotent (safe to repeat): use versioned output keys and prevent duplicate completion from corrupting metadata. The database update and queue submission also need recovery; I record pending work durably and retry submission if interrupted. A queue does not make two separate writes atomic. SQS delivery behavior
Long encodes need suitable visibility timeouts (how long a received job is hidden from other workers), extended while work continues. Failed jobs get bounded retries and a dead-letter queue (jobs set aside for investigation). I monitor the oldest waiting job, not merely whether a worker is running. SQS visibility
I could operate these workers on EC2 or containers. MediaConvert is the managed encoding alternative: submit jobs and grant its service role access to the selected input and output locations. Its processing fleet is AWS-managed; I would not draw it as my own EC2 workers in my subnet. MediaConvert, AWS video-on-demand design
The encoder should not inherit the web app’s access#
Segmentation (separating parts and controlling their communication) gives the public entry, application, and database different rules. Microsegmentation makes finer distinctions between workloads, even in the same private tier. These are architectural patterns, not AWS product names: subnets and NACLs can separate broad groups; security groups or workload policies can restrict individual callers. Neither pattern belongs exclusively to one control.
Video app [app-sg] -- SQL 5432 ------------------> metadata DB
Video worker [worker-sg] -- SQL 5432 ------------> BLOCKED
Video worker -- authenticated completion API ---> video app
Video worker -- scoped object operations -------> video buckets
| Workload | Permitted path | Separate permission required |
|---|---|---|
| Web app | Metadata DB on 5432 | App SQL role; no schema-administration grant |
| Video worker | S3 and SQS over HTTPS | Read raw inputs, write restricted outputs, consume processing jobs |
| Worker → internal app | HTTPS 8443 from worker-sg | Worker identity may report completion only |
| Worker → metadata DB | No DB SG allow rule | No database credential issued |
I add an inbound app-sg rule for worker-sg on 8443, and matching worker egress. The database still accepts only app-sg. The public ALB listener rejects /internal/* before its ordinary forwarding rule; the app also authenticates and authorizes internal callers. An internal-looking URL is not protection.
The worker needs no inbound rule for its queue polling; stateful rules permit replies to allowed outbound connections. Workloads sharing one interface and SG are not distinguished by process name. I need separate workload interfaces or additional enforcement to isolate them. A compromised encoder still has its granted file access, so blanket bucket permissions would defeat the intended separation.
A private endpoint changes the path to S3#
The bucket does not sit inside our database subnet. An S3 gateway endpoint (a route-table target for reaching S3 privately) gives the app a path to the regional S3 service without an internet or NAT gateway. Its policy restricts that path; it does not grant access missing from the relevant identity and resource policies.
Worker -- signed S3 request --> subnet route table
|
S3 prefix-list route
|
gateway endpoint -- GetObject --> S3
|
permission checks
A prefix list (a named set of IP ranges) lets the route identify S3 destinations. The endpoint does not need a security group. By contrast, an interface endpoint puts private network interfaces in selected subnets; its security group must allow the caller, usually on TCP 443. I use that model for private access to Secrets Manager or the processing queue later. S3 gateway endpoints, Interface endpoints
Worker access can use this private path. Browser uploads cannot: a bucket-wide rule requiring our VPC endpoint would reject uploads from home, including presigned requests. I scope policies for the intended callers instead of assuming every request must originate inside the VPC.
Pressing Play takes a different path from opening the page#
The watch page needs video metadata, but the player needs a stream of bytes. I keep those paths separate. The app returns the title, publication status, and a playback location; a CDN (content delivery network, which caches content near viewers) serves the video.
I use CloudFront at media.example.com, with the processed S3 bucket as its origin (the source it fetches on a cache miss). The homepage and API remain at example.com behind the ALB. CloudFront is another public entry with a different job, not a component inside our application subnet.
An adaptive player reads a manifest (a list of available versions and media segments) and requests short pieces at a suitable bitrate. The worker prepares those versions; the CDN delivers them. A cache miss still costs origin work, and popular content can create a burst before caches are warm. At an illustrative 4 Mb/s each, 1,000 simultaneous viewers need about 4 Gb/s of delivery; ten minutes is roughly 300 GB of payload. Caching reduces repeated origin reads, not the bytes delivered to viewers.
For a delivery-only estimate, suppose roughly 300 GB is billable in CloudFront’s US/Mexico/Canada pay-as-you-go tier at $0.085/GB: about $25.50. This assumes the monthly 1 TB free transfer allowance is already used and usage remains in the next 9 TB tier. Requests, storage, encoding, and other costs are extra; other locations and pricing plans differ. Rates checked September 2026. CloudFront pricing
I configure Origin Access Control, OAC (CloudFront authentication to the S3 origin) to always sign requests, and a bucket policy allowing reads from the intended distribution. Anonymous direct S3 reads remain blocked. The S3 bucket stays private even when a published video is watchable by everyone through the CDN. CloudFront origin access
| Decision | Public video | Unpublished or restricted video |
|---|---|---|
| App metadata API | Return published metadata | Check viewer entitlement |
| CDN viewer access | Public playback path may accept anyone | Require a signed URL or signed cookie on the restricted path |
| S3 origin access | Only intended delivery identities | Same origin restriction; OAC alone does not authorize viewers |
The app issues private playback grants only after checking permission. CloudFront verifies the grant before serving restricted content, including cached content. I keep separate public and restricted cache behaviors and protect both manifests and segments. Newly processed outputs stay on the restricted path; only an authorized publish operation makes a version available through the public path. An S3 upload URL and a CloudFront playback URL are different grants, signed and checked by different mechanisms. CloudFront private content, Signed cookies for multiple video files
Changing a public video to private also needs a delivery plan: previously public cached objects and already-issued grants do not disappear just because a database flag changed. I first close access through the old public path, then invalidate stale public copies and expire grants as appropriate. Versioned paths help avoid reusing old public URLs; downloaded copies cannot be recalled. Nor would I cache personalized API responses as public video files.
For media.example.com, I configure the CloudFront hostname and HTTPS certificate separately from the ALB. CloudFront’s ACM viewer certificate must be in us-east-1; the ALB certificate belongs in the ALB’s Region. CloudFront certificate requirements
The viewer-to-product request is north–south traffic (crossing the product’s external boundary). Calls between internal components are east–west traffic. An app-to-database request is outbound at the app and inbound at the database; “outbound” does not necessarily mean “to the internet.”
The app must sometimes call out#
File storage and job queues can use private endpoints. A third-party integration or package download may still need an internet destination. That outbound connection does not require opening an inbound administration port.
The application’s NAT gateway (a service that translates connection addresses) lets it initiate IPv4 connections outward, perhaps to fetch an update. The zonal public NAT gateway shown below translates the app’s source to its own private address; the internet gateway maps that to the NAT gateway’s public address. Replies follow the reverse mappings. It doesn’t accept unsolicited internet connections into the private server. AWS NAT behavior
The application starts this conversation. Public page requests take a different path through the load balancer; they do not come in through NAT.
One zonal NAT shared by both zones makes their outbound access depend on that zone. For independent egress, each private subnet can route through a NAT in its own zone. AWS also offers a regional NAT gateway: in automatic mode it expands with workloads across zones, without a public subnet to host it. Expansion can take up to 60 minutes, during which traffic may cross zones. The diagram above shows the zonal model. Regional NAT behavior
NAT also has a meter running when traffic is quiet. Using AWS’s published US East (Ohio) example rates, checked September 2026:
| Cost | Worked example |
|---|---|
| Two zonal gateways, 730 hours | 2 × 730 × $0.045 = $65.70 |
| 100 GB processed through them | 100 × $0.045 = $4.50 |
| Subtotal | $70.20, before public IPv4 and applicable data transfer charges |
Regional NAT is also billed per active zone-hour, not once for the entire Region. An S3 gateway endpoint can keep S3 traffic off this NAT path without endpoint hourly or processing fees. That saving does not apply to every kind of private endpoint. VPC pricing
An idle TCP mapping can disappear after 350 seconds; reuse then receives a reset. A pool of reusable connections therefore needs an idle policy too. NAT timeouts
NAT isn’t a web-content filter. An infected app with broad outbound access can also send stolen data outward. I would narrow outbound access and use private service endpoints where suitable. IPv6 needs its own routes and rules; an IPv4-only policy doesn’t cover it.
My laptop needs a different entrance#
The public watch page now works. But my database client on my laptop times out. The database has a private address, and my home network has no usable path into this VPC.
Connecting a VPN (an encrypted tunnel into another network) can add that path. It doesn’t turn the database public or change its password.
I use a separate development account and VPC, 10.30.0.0/16, with test data. Production remains 10.20.0.0/16. Developers build and test their changes in development; production access is a separate, narrower grant. The public product path does not pass through their VPN.
AWS Client VPN needs a target network association, routes, and authorization for the destination network. A connected tunnel alone isn’t permission to every address. With split tunneling (sending only selected destinations through the VPN), ordinary internet browsing can keep using the home connection. Client VPN networking, destination authorization
For the IPv4 VPC-subnet association model here, Client VPN translates client source addresses. I allow the VPN association’s security group into the development database on 5432, rather than assuming Postgres sees my home’s public IP or the original VPN client IP. Other attachment models differ. AWS’s explanation of Client VPN resource access
AWS Client VPN also charges while provisioned. Its published Ohio example is $0.10 for one endpoint associated with one subnet for an hour, plus $0.05 per active client-hour. Ten clients for that hour cost $0.60 before applicable IPv4 and transfer charges; this is not the cost of an always-on month. Client VPN pricing
Private DNS must work too: the laptop needs a resolver path that can resolve the database name. Sometimes the name already resolves without the VPN, but its private address is unreachable. Knowing an address and having a route to it are different things.
The model below uses the development database at 10.30.21.15 and a developer with read-only SQL access. It is a separate copy of the product’s data, not the production database. Start by connecting the VPN, then compare SELECT (read rows) with DELETE (remove rows). The same connection and login can allow one operation and refuse the other.
Connect the VPN first. Then try reading and deleting with the same read-only database user.
SELECT was not sent: the VPN is off. Database permissions have not been tested yet.
10.30.21.15 : 5432
No VPN route to 10.30.21.15 from this laptop.
An earlier check stopped this attempt.
An earlier check stopped this attempt.
An earlier check stopped this attempt.
A teaching model, not a live connection. DNS, the return route, and subnet ACLs are assumed correct. VPN connected includes successful VPN authentication and authorization.
If a database works only after I connect to my work VPN, this is a plausible explanation—not proof of the exact configuration. I would inspect the resolved address, laptop routes, VPN authorization, and firewall rules to find which boundary changed.
One company does not mean one shared network#
Two products can share a parent company without sharing a network or the same operating team.
For my hypothetical video company, I choose network boundaries by ownership and risk:
| Situation | Design I would consider | Reason |
|---|---|---|
| App and encoder for this video product | One VPC, distinct SGs and roles | Separate access without a network per feature |
| Development versus production | Separate accounts and VPCs | Development credentials should not control production |
| Independently owned second product | Separate workload accounts and VPCs | Independent policies, changes, and resource limits |
| Shared service | Explicit authenticated API and approved connectivity | Share the capability without granting blanket network access |
An AWS account is a boundary for resource ownership and cloud permissions. A VPC is a regional network boundary. They are not interchangeable; one account can contain multiple VPCs, and shared-VPC designs also exist. AWS recommends separating workloads and production/non-production accounts according to security and operational needs. Separation adds governance and connectivity work. AWS account design
A reporting team needs one private connection#
Later, a reporting job runs in a separate account and VPC, 10.40.0.0/16. It needs an approved viewing summary, not database administration. I expose an authenticated internal report operation on the video app. On the public ALB listener, a higher-priority rule returns a fixed 403 for /internal/* before the ordinary forwarding rule. The app separately checks the reporting identity, and the reporting client verifies the app’s TLS certificate. A path name alone is not protection. ALB fixed responses
VPC peering (a private network connection between two VPCs) can provide this path. It does not merge accounts, automatically install routes, or grant SQL permissions.
flowchart TB
subgraph r[Reporting VPC · 10.40.0.0/16]
R[Report job · 10.40.10.9]
end
P[Peering connection]
subgraph v[Product VPC · 10.20.0.0/16]
A[Private app · 10.20.10.8]
D[(Metadata database)]
end
R -->|HTTPS 8443| P
P -->|Forward packet| A
A -->|Scoped SQL| D
R -.->|SQL not allowed| D
linkStyle 0,1,2 stroke:#2f9e68,stroke-width:2px
linkStyle 3 stroke:#d64545,stroke-width:2px
classDef entry fill:#3178c618,stroke:#3178c6,stroke-width:2px
classDef work fill:#2f9e6818,stroke:#2f9e68,stroke-width:2px
classDef data fill:#9263c618,stroke:#9263c6,stroke-width:2px
classDef rejected fill:#d6454518,stroke:#d64545,stroke-width:2px
class R,A work
class P entry
class D data
style r fill:transparent,stroke:#777e88,stroke-width:1.5px
style v fill:transparent,stroke:#777e88,stroke-width:1.5px For that one connection trace, I need both directions:
| Where I configure it | Destination / permission | Why |
|---|---|---|
| Reporting subnet route table | 10.20.10.0/24 → peering | Reach the app subnet |
| App subnet route table | 10.40.10.0/24 → peering | Return traffic to the reporting subnet |
| Reporting SG outbound | App destination, TCP 8443 | Permit the connection to start |
| App SG inbound | Reporting caller, TCP 8443 | Admit that caller |
| Custom NACLs, if restrictive | Request and ephemeral return ports | Let both packet directions cross subnet boundaries |
| Internal API authorization | Reporting identity may fetch summaries | Prevent arbitrary app operations |
Peering routes, Peer security groups
The two VPCs must have non-overlapping ranges. This example is in one Region, where peer security-group references are supported. Additional app subnets need corresponding routes; replacing an app instance also requires a stable private name and working service discovery. Private DNS needs its own configuration—for example, associating an internal Route 53 private hosted zone with both VPCs. Peering alone does not share every private name. Peering setup, Private hosted zones
Peering is not transitive (connections do not automatically chain):
Development -- peer link --> Product -- peer link --> Reporting
Development -- no transit through Product ---------> Reporting
Those two links do not create a development-to-reporting route through the product VPC. At a larger network count, a Transit Gateway (a routing hub for connected networks) can reduce the web of pairwise connections. Its routing still needs deliberate isolation; the hub is not permission for everyone to reach everything. Peering limitations, Transit Gateway
Who notices when a database rule changes?#
The video platform now spans several accounts. I want the same rule everywhere: production databases accept connections only from approved application and maintenance callers. Someone opens a database security group to a wider range during debugging and forgets to undo it. AWS enforces the new rule faithfully—even though it violates our intended design.
The control plane (the systems that configure infrastructure) needs to detect that difference. The data plane (the systems handling live traffic) continues applying the installed rules. A successful policy edit is not proof that every network already enforces it.
I would first consider AWS Firewall Manager (central management of supported security policies). With AWS Organizations, it can apply policies to selected accounts and resources, including new ones entering scope. Remediation settings determine whether supported violations are automatically corrected. Terraform can define infrastructure; ongoing detection and correction still need an owner and a running process. Firewall Manager policies, Policy scope
The underlying idea is reconciliation (repeatedly comparing what should exist with what actually exists). If I need custom policy automation, its cycle looks like this; the version numbers are my example, not an AWS API contract.
Desired policy v42: approved callers only on database port 5432
|
read latest intent
v
Policy controller
| ^
apply differences | inspect installed rules
v |
Account / Region APIs
|
configure filters
v
Installed SG rules
|
allow or reject TCP
v
Database interface
The controller changes rules through management APIs. Database connections pass through the installed filters; they do not ask the controller for permission on every packet.
Suppose ten accounts need v42. Five are verified, two requests time out, one is throttled, one target was deleted, and one account is still pending. An eleventh account joins during the rollout.
| What happened | What the controller must do |
|---|---|
| Five verified | Record the observed configuration and version; keep checking for later changes |
| Two timeouts | Read back before retrying: the write may have succeeded despite the missing reply |
| One throttled | Retry with backoff and jitter, with an alert if it remains unresolved |
| One target deleted | Refresh inventory; distinguish intentional removal from missing required infrastructure |
| One pending, one new account | Discover both and apply the policy if they are in scope |
| An old v41 job wakes up | Discard stale work; serialize writes per target and verify the latest intent afterward |
Retries must be safe to repeat. A version field alone cannot stop an old request already in flight from reaching AWS; the writer must control ordering and recheck actual state. The dashboard should say “5 verified, others unresolved,” not “rollout complete.”
Convergence takes time and depends on permissions, reachable APIs, and retryable failures being resolved. An overly broad rule can remain active during that gap. I would test a policy on a small scope before expanding it, restrict who can change production rules, and alert on drift (configuration that no longer matches the intended rules). Automation can also spread a mistaken policy faster.
Shipping code doesn’t require a public admin port#
Developers need to change the application, but public users don’t need its deployment interface.
I prefer a deployment pipeline that builds a versioned artifact and replaces running instances. CI/CD (automated build, test, and deployment) uses a narrowly scoped cloud role, with short-lived credentials, to request the change.
With OIDC federation (using the CI provider’s signed identity to obtain cloud credentials), the job exchanges a token with AWS STS (the service that issues temporary credentials). The role’s trust policy restricts the provider, intended audience, and repository plus approved branch or deployment environment. Its permission policy separately limits what the job may deploy. A token from just any repository must not qualify. AWS’s GitHub OIDC trust guidance
This shows control flow; artifact downloads need their own permitted network path. The developer doesn’t need an inbound shell connection to every application server.
For exceptional shell access to EC2, Systems Manager Session Manager can use an agent’s outbound service connection, with IAM authorization, instead of public inbound SSH. The agent still needs its role and network access through NAT or suitable VPC endpoints. The VPN remains useful for approved private database tools and internal applications. Session Manager connectivity
Recommendations fail; the video should still play#
I add search over published titles and descriptions when the catalog grows. A private search service maintains an index, updated asynchronously from publication changes. Recommendations are another optional backend dependency. Neither gets a public database port or unrestricted access to account records.
These calls introduce a different boundary: failure isolation. A security group can reject an unauthorized connection. It cannot make an allowed service respond promptly or stop its callers from exhausting their own threads.
UNBOUNDED DEPENDENCY
Watch-page request --> recommendations stalls
--> request slots fill
--> more retries --> more load
--> watch pages fail too
ISOLATED DEPENDENCY
Watch-page request --> recommendations deadline expires
--> omit recommendations
--> return the playable page
| Failure | Design response |
|---|---|
| Recommendations is slow | Give it a short deadline within the page budget; omit that optional section |
| Repeated dependency failures | Circuit breaker: temporarily stop calls, then probe recovery |
| Retried requests amplify load | Retry only suitable operations, with a limit, backoff, and jitter (randomized delay) |
| Encoders consume all capacity | Separate worker capacity and bounded concurrency from the web tier |
| Uploads outpace processing | Monitor queue age, limit admissions, and scale within downstream capacity |
| Search index lags or fails | Keep direct video playback independent; show unavailable search rather than invented results |
Separate connection/thread pools—often called bulkheads (resource partitions that limit spillover)—keep one dependency from using every request slot. Queues absorb temporary bursts, not unlimited work. For example, if arrivals stay at 20 videos/minute and workers finish 12, backlog grows by 8 every minute until something changes. AWS resilience strategies, Circuit breakers
A fallback is safe only for optional work. If authorization for a private video fails, I refuse playback; I do not treat the outage as permission. Network isolation, authorization, and failure containment solve different problems.
More app servers can overwhelm one database#
A connection pool (open database connections kept for reuse) saves repeated setup. But each app instance owns its own pool. Adding instances multiplies their possible database connections.
Suppose I budget 80 connections for the application, after reserving room for operators and other clients. This is an example budget, not an RDS default. Six instances with a pool cap of 10 can use 60 connections. Twelve can demand 120—even if twelve exist only while old and new versions overlap during deployment.
I size pool caps against peak instance count and the database’s actual connection limit, then put a time limit on waiting for a free connection. RDS Proxy can share database connections across application clients and queue or reject excess demand. It adds cost and does not give the database unlimited query capacity. RDS Proxy connection pooling
Names I keep separate: services, resources, ACLs, traffic, and gateways
Service versus resource
My video app is a program I build. AWS uses “service” for a capability it operates, such as EC2 or S3. A resource is a particular instance of something I create or manage using that capability.
| AWS service | Resource in this product | What controls its access? |
|---|---|---|
| EC2, virtual servers | An app instance and its network interface | Security groups for traffic; IAM for cloud actions |
| Elastic Load Balancing | Our Application Load Balancer | ALB security group, listeners, and routing rules |
| RDS, managed databases | The metadata PostgreSQL instance | Security groups plus database authentication and grants |
| S3, object storage | Raw and processed video buckets | IAM and bucket policies, Block Public Access; no bucket security group |
| Secrets Manager | The app’s database secret | IAM and applicable resource policy; no security group on the secret |
“I use AWS” does not tell me which boundary protects a resource. Security groups attach where the service supports them; they are not a universal wrapper around AWS resources.
Network ACL versus object ACL
ACL means access control list. The rest of the name tells me what is being controlled.
| Control | Example decision | Looks at |
|---|---|---|
| Network ACL | Let a packet enter this subnet | Address, protocol, port, direction |
| S3 bucket/object ACL | Grant an AWS account access to this bucket/object | Storage grants, not network ports |
| S3 bucket policy | Allow or deny selected S3 actions under conditions | Identities, actions, resources, conditions |
For this bucket I use Bucket owner enforced ownership, the default for new buckets, which disables ACLs. I use policies instead. There is no object ACL to configure as a “second NACL.” S3 Object Ownership
Traffic direction depends on the boundary
Direction depends on the boundary I name. It is not an intrinsic label attached to an IP address.
| Term | Boundary being described | Example here |
|---|---|---|
| North–south | Outside ↔ inside the product system | Browser requests the web API; player requests a CDN segment |
| East–west | Communication among internal components | Worker reports completion to the app; app queries metadata |
| Inbound / ingress | Entering the resource under discussion | A SQL request entering the DB interface |
| Outbound / egress | Leaving that resource | The same SQL request leaving the app interface |
| Internet egress | An internal workload calls an internet destination | Backend calls an external email provider |
App interface Database interface
outbound -- TCP 5432 request --> inbound
inbound <-- response ---------- outboundThat SQL call is east–west overall, outbound at the app, and inbound at the database. Outbound does not necessarily mean internet traffic. Response packets reverse direction; a stateful SG recognizes them, while a NACL checks them separately.
The browser-to-S3 upload crosses the product’s external boundary even though it bypasses our VPC. Product boundaries and VPC boundaries are not identical.
Gateway names describe different jobs
| Component | Job here | Attach a security group to it? |
|---|---|---|
| Internet gateway | Internet routing for the public tier | No |
| NAT gateway | Translate outbound connections from private workloads | No; filter at the workloads and relevant subnet boundaries |
| S3 gateway endpoint | Route S3 traffic without NAT | No; endpoint and S3 permissions still apply |
| Interface endpoint | Private interface for a supported service API | Yes, on its endpoint interfaces |
| ALB | Accept HTTP requests and make backend connections | Yes |
A gateway is not one universal firewall. Its type tells me which path it provides.
How the network roles map to Azure
The concepts survive changing providers. The implementations aren’t exact synonyms.
| Networking job | AWS example used here | Azure counterpart |
|---|---|---|
| Isolated address space | VPC | Virtual Network, VNet |
| Address subdivision | Subnet, tied to one zone | Subnet, not tied to one zone |
| Stateful connection filtering | Security group | Network Security Group, NSG |
| Regional HTTP entry | Application Load Balancer | Application Gateway |
| Developer tunnel | Client VPN | VPN Gateway point-to-site |
| Explicit outbound translation | NAT Gateway | NAT Gateway |
| Private access to a managed service | VPC endpoint / PrivateLink, depending on service | Private Endpoint / Private Link |
| Permissions on cloud actions | IAM roles and policies | Azure RBAC; managed identities for workloads |
Azure NSGs can attach to subnets or network interfaces, have ordered allow and deny rules, and remain stateful. An NSG on a subnet is therefore not the same behavior as an AWS stateless subnet ACL. Azure NSGs, Azure network concepts
Azure’s managed web platforms also distinguish private inbound access from outbound VNet integration. I would choose those connections by direction, not assume “connected to the VNet” solves both. Azure’s private web application architecture
Turning this drawing into the first deployment#
The infrastructure is another versioned part of the product. I describe resources and their references with infrastructure as code (reviewable files that create and update cloud resources). CloudFormation or Terraform can express this dependency order; they do not choose safe permissions for me.
| Build in this order | Concrete output for the first release |
|---|---|
| Network | VPC, six subnets, route tables, internet gateway, and the required outbound paths |
| Boundaries and identities | Entry/app/database SGs, app role, and separate deployment role |
| Persistent state | RDS subnet group, private database, app database user, and secret |
| Application | Versioned build, private instances, DB hostname, secret reference, and TLS listener on 8443 |
| Public entry | ALB, target group, readiness check, ACM certificate, and HTTPS listener |
| Name | Route 53 alias for example.com pointing at the ALB |
This table builds the web/API foundation. Upload and playback add raw/processed buckets, scoped upload grants, processing jobs and workers, then a CloudFront distribution with origin and viewer policies. Search, recommendations, and a reporting network are later additions, not prerequisites for the first playable video.
For each application copy, I keep durable files out of its local disk and avoid login sessions that only that copy understands. A request can land on another healthy instance. The release runs database migrations with a separate, limited migration identity; ordinary app credentials do not need permission to change the schema. Old and new app versions must both tolerate the schema during a rolling replacement.
Build artifact -- deploy --> new app instances
|
readiness checks
|
admit into target group
|
drain and retire old instances
Before relying on the architecture, I exercise both its allowed and refused paths:
| Attempt | Expected result | If it fails unexpectedly, inspect |
|---|---|---|
Home browser opens / | Homepage loads | DNS, certificate, ALB listener and target health |
| Viewer requests another creator’s unpublished video | Application refuses | Login and video visibility checks |
| Home laptop connects to production Postgres | No direct connection | Public accessibility, routes, and SG exposure |
| Developer VPN connects to the development DB | Approved SQL works | Private DNS, VPN authorization, routes, SGs, NACLs, DB login |
| Same developer sends DELETE with read-only role | SQL permission denied | Database grants, not the VPN |
| Encoder opens a metadata DB connection | Blocked | Combined SG rules and unintended credentials |
| Browser uploads with an app-issued grant | Object stored | Grant, CORS, object key, signer and bucket policies |
| Anonymous request to a raw S3 object | Refused | Bucket and identity policies; Block Public Access |
| Restricted CDN segment without a valid grant | Refused, even if cached | Matching cache behavior and viewer access settings |
| Recommendations becomes unavailable | Watch page still works without recommendations | Deadline, fallback, and separate resource limits |
| One app copy is replaced | Other copy serves requests | Capacity, health checks, shared state, connection draining |
A timeout is not proof of one specific bad rule. I trace the destination and route, then packet filters, listener, TLS, and application permission. A clear authorization error means I reached a service capable of refusing the operation; opening more network access is usually the wrong response.
The request we let in can still be the dangerous one#
A database with no public route is harder to attack directly. But the public application is deliberately allowed to reach it. A SQL injection flaw can abuse that permitted path.
flowchart TB
A[Attacker]
W[Public web entry]
B[Vulnerable application]
D[(Private database)]
A -.->|Direct SQL blocked| D
A -->|Malicious HTTPS| W
W -->|Forward request| B
B -->|Unsafe SQL| D
linkStyle 0,1,2,3 stroke:#d64545,stroke-width:2px
classDef entry fill:#3178c618,stroke:#3178c6,stroke-width:2px
classDef work fill:#2f9e6818,stroke:#2f9e68,stroke-width:2px
classDef data fill:#9263c618,stroke:#9263c6,stroke-width:2px
classDef rejected fill:#d6454518,stroke:#d64545,stroke-width:2px
class A rejected
class W entry
class B work
class D data Here red traces the attack: the dashed shortcut is blocked, but the solid path reaches the database through the vulnerable app. Safe SQL construction and narrow database grants must protect that allowed network path too.
A DDoS attack (many sources overwhelming a service) can exhaust bandwidth or connection capacity before application code can help. Edge capacity and AWS Shield Standard address common network and transport attacks. HTTP floods can also exhaust expensive application work, so request filtering and bounded work still matter. AWS Shield protections
A WAF (web application firewall) inspects HTTP fields such as paths, query strings, headers, and supported body content at a protected entry such as CloudFront or ALB. Configured rules can block matching attack patterns; rate-based rules can curb abusive request rates. WAF inspection has limits and cannot establish that Bob owns Alice’s video. AWS WAF request inspection
Parameterized SQL (passing values separately from SQL instructions) keeps supplied values from becoming executable SQL syntax. It belongs in the application’s database access; WAF is an additional defense. Ownership checks and narrow database grants remain necessary even with safe queries.
| Attempt against the video platform | The boundary that addresses it | What that boundary cannot promise |
|---|---|---|
| Flood the public endpoint | Edge/DDoS protection; WAF for HTTP patterns | Unlimited capacity or zero cost |
| Connect directly to database port 5432 | Private routing and SG/NACL filtering | Safety of SQL sent by an allowed app |
| Make the encoder reach unrelated services | Workload isolation and scoped IAM | Safety of everything the encoder is allowed to read |
| Read someone else’s unpublished video | Application ownership checks and restricted playback grants | Protection if a valid bearer grant is stolen |
| Use a stolen deployment credential | Narrow role permissions and short-lived credentials | No damage while those credentials remain usable |
| Open databases across accounts by mistake | Reviewed policies, staged rollout, and reconciliation | Instant correction or a correct policy by itself |
When an attempt fails, I need evidence from the layer that made the decision:
| My question | Evidence to collect |
|---|---|
| Who changed the database security group? | CloudTrail management events: cloud caller, API action, time, and result |
| Was TCP traffic accepted or rejected at the interface? | VPC Flow Logs: addresses, ports, and ACCEPT/REJECT records |
| Which HTTP request reached the entry, and what happened? | ALB access logs for request/target results; WAF logs for rule actions |
| Did Bob read or publish Alice’s video? | Application audit logs: authenticated user, video, action, and authorization decision |
These logs need deliberate collection, retention, and access controls. Flow Logs do not contain SQL or HTTP bodies, and ACCEPT does not mean the application succeeded. CloudTrail records cloud API activity; it cannot supply the video ownership decision our application made. I would correlate timestamps and request IDs where available, without recording credentials or signed URLs. CloudTrail management events, Flow Logs, ALB access logs, WAF log fields
Backups need restore tests; secrets need rotation; dependencies need updates.
The price of separation is more routing, policies, monitoring, and failure cases. Load balancers, VPNs, NAT, private endpoints, and redundant databases can carry costs even at low traffic. A small product may sensibly start with fewer moving parts. The rule I keep is to expose only the interface that has a reason to be public.
If an attacker takes over an encoder, which videos can it read—and what else can it reach?
Comments
Signed in with GitHub. Be kind.