An end-to-end cloud network design for a public API: global entry, TLS, WAF, L7 routing, private subnets, egress control, private endpoints, and how each choice affects security, latency and operations.
Layered from outside in:
Plus observability at every hop (flow logs, edge logs, traces with traceparent), and automated certificates and network rules as code.
User ββDNS (api.example.com, CNAME β edge)βββΊ
Global edge: CDN + WAF + DDoS protection, TLS 1.3 at the edge, cache static/GETs
β TLS (re-encrypted), origin locked to edge only (private link or header + IP allow-list)
βΌ
Region A Region B (active/active or warm standby)
ββββββββββββββββββββββ VNet / VPC ββββββββββββββββββββ
β public subnet: L7 gateway / ingress (only edge IPs allowed) β
β β β
β private subnet: AKS / ECS pods (no public IPs) β
β NetworkPolicy: ingress only from gateway; egress allow-listβ
β β β
β data subnet: private endpoints βββΊ SQL, Redis, Storage, β
β Key Vault (public access disabled) β
β β
β egress: NAT gateway or firewall (FQDN allow-list, logging) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
| Component | Security | Performance | Reliability |
|---|---|---|---|
| Edge/CDN + WAF | DDoS absorption, OWASP rules, bot and rate limits | TLS and caching near users, connection reuse to origin | Health-based failover between regions |
| L7 gateway / ingress | Only edge can reach it; mTLS optional | HTTP/2 to backends, compression | Health probes, draining |
| Private app subnets | No direct internet exposure; least-privilege rules | Low intra-region latency | Zone-redundant node pools |
| Private endpoints | Data never on public internet; public access off | Same-region placement | Zone-redundant data services |
| Egress firewall/NAT | Blocks exfiltration, audited allow-list | Watch SNAT port limits | Static outbound IPs for partner allow-lists |
X-Forwarded-For trust for edge and gateway hops.A retailer exposed an AKS ingress publicly with a WAF policy on a CDN in front. A penetration test found the ingress public IP directly and bypassed the WAF. We put the gateway behind private link from the edge (and as a fallback, an IP allow-list for the edge's service tag plus a secret header check), removed public IPs from nodes, moved SQL and storage to private endpoints, and routed egress through a firewall with an FQDN allow-list. A later credential-stuffing attack was absorbed by WAF rate limits without touching the app.
A staff-level design question that ties the networking topics together. The interviewer wants to see the candidate make and justify trade-offs between security, latency, cost and operability, not just list cloud products.
Green flags
Red flags
DNS: api.example.com CNAME to the edge endpoint; TTL 300 s; DNSSEC where supported; CAA records restricting which CAs can issue certificates.
Edge: anycast IPs terminate TLS close to users (saving round trips: Sydney users do not pay the handshake to a US origin). Cache what is cacheable (Cache-Control from the app). WAF managed rule sets plus custom rate limits by IP and by API key. Health probes per origin for failover.
Origin protection: private link from edge to regional gateway when available; otherwise restrict to the edge's published IP ranges or service tags and validate an edge-injected header or ID.
Regional gateway/ingress: path and host routing, request size limits, timeouts aligned with the app, HTTP/2 to backends, session affinity off by default.
App tier: private subnets, no public IPs, NSGs/security groups default-deny, Kubernetes NetworkPolicies (ingress from gateway namespace only; egress to DNS, data subnet and egress gateway), workload identity for credentials.
Data tier: private endpoints + private DNS zones linked to the VNet; public network access disabled; separate subnet with its own rules.
Egress: NAT gateway for predictable outbound IPs (partners can allow-list them) and enough SNAT ports; or a firewall with FQDN rules and logging for exfiltration control.
without edge: DNS 20 + TCP 50 + TLS 50 + request 50 + app 30 β 200 ms cold
with edge: DNS 20 + TCP 5 + TLS 5 + edgeβorigin (warm) 50 + app 30 β 110 ms cold
| Failure | Mitigation |
|---|---|
| Region outage | Edge health probes fail over to region B; data replication strategy decides RPO |
| WAF false positives | Detection mode first, per-rule exclusions, logs to SIEM |
| Certificate expiry at any hop | Automated renewal; expiry alerts per hop |
| SNAT exhaustion | NAT gateway with enough IPs; connection reuse |
| Private DNS misconfiguration | IaC for zones and links; startup checks resolve to private IPs |
| Bypass of the edge | Origin lockdown and regular external scans |
traceparent through all hops; correlate edge request IDs with app traces.Terraform/Bicep for VNets, rules, private endpoints and DNS; Helm/Kustomize for NetworkPolicies and ingress; policy-as-code (Azure Policy, OPA) to forbid public IPs on data services.
Sketch this design for one of your services. For each hop, write down: who terminates TLS, which IPs may connect, the timeout, the health check and where its logs go. Then try to reach the origin directly from the internet and confirm it is blocked.
// App-side pieces of the design
var builder = WebApplication.CreateBuilder(args);
// Trust edge + gateway hops for client IP and scheme
builder.Services.Configure<ForwardedHeadersOptions>(o =>
{
o.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
o.ForwardLimit = 2;
o.KnownProxies.Clear();
o.KnownProxies.Add(IPAddress.Parse("10.10.1.10")); // regional gateway private IP
});
// Reject requests that did not come through the edge (defence in depth behind IP restrictions)
var expectedEdgeId = builder.Configuration["Edge:Id"]!;
var app = builder.Build();
app.UseForwardedHeaders();
app.Use(async (ctx, next) =>
{
if (ctx.Request.Path.StartsWithSegments("/health")) { await next(); return; }
if (ctx.Request.Headers["X-Azure-FDID"] != expectedEdgeId) // Front Door ID header
{
ctx.Response.StatusCode = StatusCodes.Status403Forbidden;
return;
}
await next();
});
// Cache headers the edge can honour
app.MapGet("/catalogue/{id}", (int id) => Results.Ok(GetItem(id)))
.CacheOutput(p => p.Expire(TimeSpan.FromMinutes(5)));
app.Run();
# Terraform sketch: SQL reachable only through a private endpoint
resource "azurerm_mssql_server" "orders" {
name = "sql-orders-prod"
public_network_access_enabled = false
# ...
}
resource "azurerm_private_endpoint" "orders_sql" {
name = "pe-sql-orders"
subnet_id = azurerm_subnet.data.id
private_service_connection {
name = "sql"
private_connection_resource_id = azurerm_mssql_server.orders.id
subresource_names = ["sqlServer"]
is_manual_connection = false
}
private_dns_zone_group {
name = "sql"
private_dns_zone_ids = [azurerm_private_dns_zone.sql.id]
}
}
Mistake: Putting a WAF or CDN in front but leaving the origin publicly reachable. Why it is wrong: Attackers find the origin IP (certificate transparency logs, scans, old DNS records) and bypass every edge control, including rate limits. Instead: Lock the origin to the edge with private link, service-tag or IP restrictions and an edge ID header check, and scan regularly from outside to prove it.
Mistake: Giving app nodes or databases public IPs "for convenience". Why it is wrong: It widens the attack surface, and one wrong firewall rule exposes data directly to the internet. Instead: Use private subnets, private endpoints with public access disabled, and a bastion, VPN or just-in-time access for administration.
Mistake: Ignoring egress.
Why it is wrong: Unrestricted outbound access makes data exfiltration and supply-chain callbacks easy, and default outbound SNAT ports run out under load, causing intermittent connection failures.
Instead: Route egress through a NAT gateway or firewall with an allow-list, logging and enough outbound IPs, and reuse connections with a shared HttpClient from IHttpClientFactory.
Mistake: Designing the path without timeouts and limits per hop. Why it is wrong: If an inner hop waits longer than an outer one, the outer hop gives up first, returns 504 and the client retries while the original request still runs, which amplifies load. Instead: Make timeouts shrink from the edge inwards and align request body size limits at every hop.
Mistake: Forgetting private DNS when adding private endpoints. Why it is wrong: The app still resolves the public name to the public IP, so connections fail once public access is disabled, or quietly go over the internet while it is enabled. Instead: Link private DNS zones to the VNet in infrastructure as code and check at startup that data hosts resolve to private addresses.
Mistake: Building it by hand in the portal. Why it is wrong: Undocumented rules drift between environments and cause outages nobody can explain. Instead: Define networks, rules, DNS and certificates in Terraform or Bicep, deploy them through a pipeline with review, and enforce guardrails with Azure Policy or OPA.
At staff level the question is about defence in depth with clear trust boundaries and the operational cost of each layer. Every hop needs a reason: the edge for latency, DDoS protection and the WAF; the gateway for routing and origin protection; private networking so data is never exposed; egress control so a compromise is contained. Each hop also adds certificates, timeouts, logs and failure modes, so I keep the number of hops deliberate and documented.
The same design on each major cloud
| Layer | Azure | AWS | Google Cloud |
|---|---|---|---|
| Global edge + WAF | Front Door Premium with WAF | CloudFront + AWS WAF + Shield | Cloud CDN + global external Application Load Balancer + Cloud Armor |
| Origin lockdown | Private link from Front Door, X-Azure-FDID check |
CloudFront VPC origins or managed prefix list + custom header | Cloud Armor policy, serverless NEGs or internal origins |
| Regional L7 | Application Gateway or AKS ingress | Application Load Balancer | Regional Application Load Balancer or GKE Gateway |
| Private data access | Private Endpoint + Private DNS zone | PrivateLink interface endpoints + Route 53 private zones | Private Service Connect + Cloud DNS private zones |
| Egress control | NAT Gateway, Azure Firewall | NAT Gateway, Network Firewall | Cloud NAT, Cloud NGFW |
| Observability | Application Insights, NSG/VNet flow logs | CloudWatch, X-Ray, VPC Flow Logs | Cloud Trace, Cloud Logging, VPC Flow Logs |
The concepts carry across providers, but the details differ: which services support private origins, how service tags or prefix lists are published, how flow logs are priced and sampled, and how quickly global configuration changes propagate. Using OpenTelemetry in the .NET app keeps traces portable whichever backend is chosen.
Trade-offs I make explicit
| Decision | Option A | Option B |
|---|---|---|
| TLS to origin | Terminate at edge only (simpler, faster) | Re-encrypt every hop (zero trust, more certificates) |
| Regions | Active/active (lower RTO, harder data replication) | Active/passive (cheaper, slower failover) |
| Service-to-service | Service mesh (mTLS, L7 balancing) | NetworkPolicies + app-level TLS (less to operate) |
| Egress | Firewall with FQDN rules (control, audit) | NAT gateway only (cheaper, less control) |
I make the design verifiable: origin lockdown tested by external scans, synthetic probes through the full path from several regions, flow logs and edge logs correlated with traces, and policy-as-code that prevents public IPs on data services. The design is only as strong as its weakest bypass, and the most common bypass is a forgotten public endpoint.
A firewall allows or blocks traffic based on rules (source, destination, port, protocol, and for L7 firewalls/WAFs, HTTP content).
Troubleshoot by symptom: timeout usually means dropped by a firewall or routing, refused means reached the host but nothing listening, reset after idle often means a firewall or load balancer idle timeout removed the connection state. Test from the source (nc -vz, Test-NetConnection), check rules and flow logs in both directions, and set TCP keep-alives or shorter pool idle timeouts below the firewall's idle timeout.
A load balancer spreads traffic across healthy instances.
Health checks remove unhealthy instances (in Kubernetes: readiness for traffic, liveness for restarts). Algorithms: round robin, least connections/requests, hashing. Sticky sessions pin a user to one instance, which hurts scaling and failover. Connection draining stops new traffic to an instance before shutdown so in-flight requests finish.
gRPC uses long-lived HTTP/2 connections, so an L4 balancer balances connections, not calls: one client connection carries all its calls to one pod. Use L7 balancing (mesh/Envoy) or client-side load balancing.
HTTPS is HTTP inside TLS. TLS gives confidentiality (encryption), integrity (tamper detection) and server authentication (certificates).
TLS 1.3 handshake (one round trip): the client sends supported ciphers, a key share, SNI (hostname) and ALPN (h2); the server replies with its key share, certificate chain and a signature proving it owns the private key. Both derive session keys via ECDHE (forward secrecy).
The client validates: chain to a trusted root, not expired, hostname matches the SAN, and optionally revocation.
For "remote certificate is invalid": read the SslPolicyErrors / chain status, inspect with openssl s_client -showcerts, then fix the real cause (expired cert, missing intermediate, wrong hostname, or a private/corporate root CA not trusted in the container). Never return true from ServerCertificateCustomValidationCallback in production.
How to tell it (STAR)
**S:** A [penetration test] showed our [retail API] could be reached directly, skipping the WAF we relied on. **T:** As the lead for the platform, I had to close the gap and harden the rest of the path. **A:** I locked the origin to the edge with private link and header checks, removed public IPs, moved data services to private endpoints and put egress behind a firewall, all in Terraform with policies to prevent regressions. **R:** The retest found no direct path, and a later [credential-stuffing] attack was stopped by WAF rate limits without touching the app.
Last updated 23 Oct 2026.