📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What is the difference between a forward proxy and a reverse proxy? Your ASP.NET Core app behind a reverse proxy logs the proxy's IP for every user and gets into an HTTPS redirect loop. What is wrong and how do you fix it safely?

Forward vs reverse proxies, what they change (TLS, headers, timeouts, buffering), X-Forwarded-* headers and the ForwardedHeaders middleware, outbound proxy settings, and spoofing risks.

Senior Debugging Intermediate aspnet-core forwarded-headers networking proxy reverse-proxy yarp

🎯 Key points

  • Forward proxy acts for clients (egress); reverse proxy acts for servers (TLS, routing, LB)
  • Behind a reverse proxy the app sees the proxy IP and often http
  • X-Forwarded-For/Proto carry the originals; ForwardedHeaders middleware applies them
  • Only trust known proxies/networks; default is loopback only; trusting all allows spoofing
  • UseForwardedHeaders must run first in the pipeline
  • Proxies impose timeouts, body limits, buffering and HTTP version limits
  • HttpClient honours HTTP_PROXY/HTTPS_PROXY/NO_PROXY for outbound calls

⏱ 30-second answer

  • A forward proxy sits in front of clients (corporate egress proxy): clients send requests through it to the internet. For HTTPS it tunnels with CONNECT. HttpClient picks it up from system settings or HTTP_PROXY/HTTPS_PROXY/NO_PROXY.
  • A reverse proxy sits in front of servers (nginx, Envoy, YARP, App Gateway, ingress): it terminates TLS, routes, load balances, caches and adds headers.

Behind a reverse proxy, the app sees the proxy's IP and often plain http. The proxy passes the originals in X-Forwarded-For and X-Forwarded-Proto, but ASP.NET Core only applies them via the ForwardedHeaders middleware, and by default only trusts proxies on loopback. So UseHttpsRedirection sees http, redirects to https, the proxy forwards as http again: a loop.

Fix: UseForwardedHeaders first, configured with the proxy's IPs or network in KnownProxies/known networks. Do not trust all sources, or clients can spoof their IP and scheme.

🗣 Full answer (senior)

Forward vs reverse

Forward proxy (client side)
  app ──► corp proxy ──► internet APIs
          (egress control, logging, maybe TLS inspection)

Reverse proxy (server side)
  users ──► [CDN/WAF] ──► reverse proxy / ingress ──► app instances
                          (TLS termination, routing, LB, headers)
Forward proxy Reverse proxy
Acts for Clients Servers
Who configures it Client / OS / environment variables Server operators
Typical products Squid, Zscaler, corporate proxies nginx, Envoy, HAProxy, YARP, Azure App Gateway, AWS ALB, ingress controllers
Purpose Egress control, caching, anonymity, inspection TLS offload, routing, load balancing, caching, WAF, compression

What a reverse proxy changes for your app

Aspect Without ForwardedHeaders With it configured
Connection.RemoteIpAddress Proxy IP (e.g. 10.240.0.4) Client IP from X-Forwarded-For
Request.Scheme http https from X-Forwarded-Proto
Request.Host Internal host Original host (if X-Forwarded-Host enabled and allowed)

Wrong values break HTTPS redirection, absolute URLs and OAuth/OIDC redirect_uri (identity provider rejects http://), rate limiting by IP, audit logs and geo rules.

Production example

After moving an app from IIS to AKS behind ingress-nginx, logins failed with "redirect_uri mismatch" (it was sending http://...), and some pages looped on HTTPS redirect. Logs showed every request from 10.244.1.12, the ingress pod. We added UseForwardedHeaders as the first middleware with ForwardedHeaders.XForwardedFor | XForwardedProto and added the ingress pod CIDR to the known networks. Logins worked, logs showed real client IPs, and the rate limiter started limiting real clients instead of the ingress.

🔍 What the interviewer is checking

Checks understanding of proxies in real deployments and a common class of bugs after moving behind a load balancer or ingress.

Green flags

  • Clear forward vs reverse distinction.
  • Knows X-Forwarded-For/Proto and the ForwardedHeaders middleware, including order and known proxies.
  • Mentions spoofing risk.
  • Aware of proxy timeouts, body limits and buffering.

Red flags

  • Disables HTTPS redirection to stop the loop.
  • Trusts all forwarded headers by default.

📚 Learn in depth

1. How X-Forwarded-For grows

client 203.0.113.9 ─► CDN 198.51.100.7 ─► ingress 10.244.1.12 ─► app

X-Forwarded-For: 203.0.113.9, 198.51.100.7
Remote address seen by app: 10.244.1.12

The middleware walks the list from the right, accepting each hop only if the previous one is a known proxy, up to ForwardLimit (default 1). With a CDN plus ingress you need ForwardLimit = 2 and both trusted.

2. Spoofing

Any client can send X-Forwarded-For: 1.2.3.4. If you trust all sources, an attacker can bypass IP allow-lists and rate limits, and fake HTTPS. That is why KnownProxies/known networks default to loopback only.

ASPNETCORE_FORWARDEDHEADERS_ENABLED=true turns the middleware on for XForwardedFor and XForwardedProto and clears the known proxies and networks so it trusts any source. It is a convenience for container platforms where only the proxy can reach the app; use it only when network rules guarantee that.

3. Order matters

UseForwardedHeaders() must run before UseHttpsRedirection, UseHsts, authentication, rate limiting, logging enrichment and anything that reads scheme, host or IP.

4. Other reverse-proxy effects

Effect Symptom Fix
Proxy timeout (e.g. nginx proxy_read_timeout 60 s) 504 on long requests Raise for specific routes or make the work async
Request body limit (nginx client_max_body_size 1 MB default) 413 on uploads Raise on the proxy and in Kestrel
Response buffering Server-sent events or streaming arrive all at once Disable buffering for those routes
Header size limits 400/502 with big cookies or tokens Raise limits or shrink headers
HTTP version gRPC fails through an HTTP/1.1-only hop Enable HTTP/2 end to end

5. Forward proxies for outbound calls

  • HttpClient.DefaultProxy reads system settings (Windows) and HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, NO_PROXY environment variables.
  • Forgetting NO_PROXY for internal hosts sends internal traffic through the corporate proxy (slow, or blocked).
  • TLS-inspecting proxies re-sign certificates with a corporate root, which must be trusted (see TLS question).

6. YARP

YARP (Yet Another Reverse Proxy) is a .NET library for building reverse proxies in ASP.NET Core: routes and clusters from configuration, load balancing, health checks, header transforms (it adds X-Forwarded-* by default), and session affinity. Useful for BFFs, gateways and gradual migrations (strangler fig).

7. Try it yourself

  1. Run your app behind nginx in Docker Compose with TLS on nginx. Log RemoteIpAddress and Scheme before and after enabling forwarded headers.
  2. Send curl -H "X-Forwarded-For: 1.2.3.4" directly to the app with "trust all" vs known proxies only.
  3. Build a 20-line YARP gateway in front of two instances and watch the headers it adds.

💻 Code

using Microsoft.AspNetCore.HttpOverrides;
using System.Net;

var builder = WebApplication.CreateBuilder(args);

builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
    options.ForwardLimit = 2;                                   // CDN + ingress
    options.KnownProxies.Clear();
    options.KnownProxies.Add(IPAddress.Parse("10.240.0.4"));    // ingress / app gateway private IP
    // or trust a whole subnet the proxies live in, via the options' known networks list
});

var app = builder.Build();

app.UseForwardedHeaders();      // FIRST: before HTTPS redirection, auth, rate limiting, logging
app.UseHsts();
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();

app.MapGet("/whoami", (HttpContext ctx) => new
{
    ip = ctx.Connection.RemoteIpAddress?.ToString(),
    scheme = ctx.Request.Scheme,
    host = ctx.Request.Host.Value
});

app.Run();
// Minimal YARP reverse proxy (package Yarp.ReverseProxy)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddReverseProxy().LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
var app = builder.Build();
app.MapReverseProxy();
app.Run();
# Outbound via a corporate forward proxy, but not for internal hosts
export HTTPS_PROXY=http://proxy.corp:3128
export NO_PROXY=localhost,127.0.0.1,.svc.cluster.local,.corp.internal

⚠ Common mistakes

  • Mistake: Not enabling forwarded headers behind a proxy. The app sees the proxy IP and http, which breaks HTTPS redirection, OAuth redirect URIs, rate limiting and audit logs. Instead: call UseForwardedHeaders() first, configured for your proxies.
  • Mistake: Trusting forwarded headers from anyone. Clients can then spoof their IP and scheme to bypass allow-lists and rate limits. Instead: list known proxies or networks, or only trust all when network rules guarantee only the proxy can reach the app.
  • Mistake: Registering UseForwardedHeaders after other middleware. Earlier middleware has already used the wrong scheme or IP. Instead: put it at the top of the pipeline.
  • Mistake: Forgetting the proxy's own limits. Timeouts, body size limits and buffering cause 504s, 413s and broken streaming that look like app bugs. Instead: align proxy settings with the app's needs per route.
  • Mistake: Setting HTTPS_PROXY without NO_PROXY. Internal calls go through the corporate proxy and fail or slow down. Instead: list internal domains and cluster suffixes in NO_PROXY.

🏛 Senior insight: trade-offs

A reverse proxy changes the request the app sees, so the app must be told which hops to trust. I think of X-Forwarded-* as claims that are only valid when they come from a known proxy; getting that trust boundary right is a security decision, not just a configuration fix.

Operationally, every proxy hop adds its own timeouts, size limits, buffering and HTTP version, and many "app" incidents are really proxy defaults. I keep proxy configuration in the same repository as the app, document each hop (CDN, WAF, gateway, ingress), and test through the real chain in staging. For .NET-centric gateways and BFFs, YARP gives a programmable reverse proxy without leaving the stack.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate says the reverse proxy also load balances Why: Probes load balancing in depth
    How to answer

    A load balancer spreads traffic across healthy instances.

    • Layer 4 (TCP/UDP: Azure Load Balancer, AWS NLB, Kubernetes Services via kube-proxy) picks a backend per connection using IPs and ports. Fast, protocol-agnostic, no HTTP awareness.
    • Layer 7 (HTTP: App Gateway, ALB, Front Door, ingress controllers, Envoy, YARP) picks per request, can route by path/host/header, terminate TLS, retry and add headers.

    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.

  2. When: Candidate mentions TLS-inspecting corporate proxies Why: Checks certificate trust handling
    How to answer

    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.

  3. When: Candidate is unsure how to express trusted proxy networks Why: Checks CIDR and IP basics
    How to answer

    An IP address identifies a network interface so packets can be routed to it.

    • IPv4: 32 bits, written 192.168.1.10. About 4.3 billion addresses, which ran out, so most networks use private ranges behind NAT.
    • IPv6: 128 bits, written 2001:db8::1. Enough addresses for everything to be globally routable.
    • CIDR 10.0.1.0/24 means the first 24 bits are the network, leaving 8 bits (256 addresses) for hosts.
    • Private ranges: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16. Loopback: 127.0.0.1, ::1.
    • NAT rewrites private source addresses to a public one on the way out, so many machines share one public IP.

🛠 Real-world problems: how you handle them

Login failures and redirect loops after moving behind ingress-nginx
Situation
[Customer portal] on ASP.NET Core [10] moved from [IIS] to [AKS] behind ingress-nginx with TLS terminated at the ingress.
Signals
OIDC errors "redirect_uri mismatch" (http instead of https), redirect loops on some pages, all logs showing client IP [10.244.1.12], rate limiter blocking everyone at once.
Investigation
Logged `Request.Scheme` and `RemoteIpAddress`: `http` and the ingress pod IP. Headers showed `X-Forwarded-For` and `X-Forwarded-Proto` were present but not applied.
Resolution
Added `UseForwardedHeaders` as the first middleware with XForwardedFor and XForwardedProto, trusting only the ingress subnet. Verified spoofed headers from inside the cluster are ignored.
Prevention
Service template includes forwarded headers configuration; smoke test checks `/whoami` returns https and a non-cluster IP through the ingress.

How to tell it (STAR)

**S:** After moving our [customer portal] to Kubernetes, logins failed and the rate limiter blocked everyone together. **T:** I had to fix it without weakening security. **A:** The app saw every request as `http` from the ingress IP because forwarded headers were not being applied. I added the ForwardedHeaders middleware first in the pipeline and trusted only the ingress subnet, so spoofed headers from elsewhere are ignored. **R:** Logins worked again, logs showed real client IPs, and rate limiting applied per user as intended.

↩ Usually asked after

More in Networking Basics

Last updated 23 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me