Project 1: Identity-Aware Reverse Proxy (Building a PEP)
Project 1: Identity-Aware Reverse Proxy (Building a PEP)
Core Zero Trust Principle: âNever trust, always verify.â This project teaches you to implement the Policy Enforcement Point (PEP) - the gatekeeper that stands between untrusted requests and protected resources.
Project Overview
| Attribute | Value |
|---|---|
| Difficulty | Level 2: Intermediate |
| Time Estimate | Weekend (8-16 hours) |
| Main Language | Go |
| Alternative Languages | Python (FastAPI), Node.js, Rust |
| Knowledge Area | Network Security / HTTP Proxies |
| Key Technologies | HTTP, JWT, OAuth2, RS256 Cryptography |
| Main Book | âZero Trust Networksâ by Gilman & Barth |
What Youâre Building: A transparent reverse proxy that sits in front of a vulnerable âbackendâ service. It intercepts every request, checks for a valid cryptographically signed identity token (JWT), and only forwards the request if the token is valid.
Why It Matters: In a Zero Trust Architecture, âthe network is hostile.â Even if a request successfully reaches your service through all network layers, you cannot trust it until you verify the identity attached to it. This proxy is the literal implementation of that principle.
Learning Objectives
By completing this project, you will be able to:
- Implement a Policy Enforcement Point (PEP) - The core gatekeeper component of Zero Trust Architecture
- Build a transparent HTTP reverse proxy - Forward traffic while preserving headers, cookies, and request context
- Parse and validate JWT tokens cryptographically - Verify RS256 signatures using public key cryptography
- Perform secure header injection - Pass verified identity information to backend services safely
- Implement header sanitization - Prevent header injection attacks from malicious clients
- Design fail-closed security systems - Ensure that failures result in denied access, not open access
- Apply the âtrust boundaryâ concept - Understand where trust begins and ends in a distributed system
Deep Theoretical Foundation
What is a Reverse Proxy?
A reverse proxy is a server that sits in front of one or more backend servers and intercepts requests from clients. Unlike a forward proxy (which protects clients), a reverse proxy protects servers.
+------------------------------------------------------------------+
| Forward Proxy vs Reverse Proxy |
+------------------------------------------------------------------+
FORWARD PROXY (Protects Clients)
================================
[ Client A ] ----+
|
[ Client B ] ----+---> [ Forward Proxy ] ---> [ Internet ]
|
[ Client C ] ----+
- Clients are protected behind the proxy
- Server doesn't know real client IP
- Common uses: Corporate firewalls, anonymizers
REVERSE PROXY (Protects Servers) - What We're Building
=======================================================
[ Internet ] ---> [ Reverse Proxy ] ---> [ Backend Server ]
[ Client ] ---> [ Identity-Aware Proxy ] ---> [ Vulnerable App ]
|
+---> [ Another Backend ]
- Servers are protected behind the proxy
- Client doesn't know real server
- Common uses: Load balancing, SSL termination, AUTHENTICATION
+------------------------------------------------------------------+
In Zero Trust, the reverse proxy becomes an Identity-Aware Proxy - it doesnât just forward traffic, it validates identity on every single request.
The Policy Enforcement Point (PEP) in Zero Trust
The PEP is one of three core components defined by NIST 800-207:
+------------------------------------------------------------------+
| NIST 800-207 Zero Trust Components |
+------------------------------------------------------------------+
CONTROL PLANE
+----------------------------------------+
| |
| +---------------+ +------------+ |
| | Policy Engine | | Policy | |
| | (PE) |<-->| Administrator|
| | "The Brain" | | (PA) | |
| +-------+-------+ +------+-----+ |
| | | |
+-----------+-------------------+--------+
| |
================|===================|=================
| |
DATA PLANE |
+-----------+-----------+ |
| | |
[ Client ] ---> [ PEP ] ------+-------+
"The Gate"
|
v
[ Resource ]
"Protected App"
+------------------------------------------------------------------+
| PEP Responsibilities |
+------------------------------------------------------------------+
1. INTERCEPT all traffic destined for protected resources
2. EXTRACT identity credentials (JWT, mTLS cert, API key)
3. VALIDATE credentials cryptographically (don't call IdP every time)
4. CONSULT the PDP for authorization (in complex setups)
5. INJECT verified identity into request (for backend consumption)
6. FORWARD or BLOCK the request based on validation
7. LOG all access attempts for audit
+------------------------------------------------------------------+
Key Insight: The PEP is âdumbâ by design. It enforces decisions, it doesnât make them. In simple cases (like this project), the decision is âIs the JWT valid?â In complex cases, it consults an external Policy Decision Point (PDP).
JWT Anatomy: Header, Payload, Signature
JSON Web Tokens (JWT) are the standard for passing identity in Zero Trust systems. Understanding their structure is essential.
+------------------------------------------------------------------+
| JWT Structure |
+------------------------------------------------------------------+
A JWT is three Base64-encoded strings separated by dots:
HEADER.PAYLOAD.SIGNATURE
Example (shortened for readability):
eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJkb3VnbGFzIn0.S1gn4tur3...
+------------------------------------------------------------------+
| HEADER |
+------------------------------------------------------------------+
{
"alg": "RS256", // Algorithm used for signing
"typ": "JWT", // Token type (always "JWT")
"kid": "key-2024" // Key ID (which public key to use for verification)
}
Purpose: Tells the verifier HOW to verify the signature
Security: This is NOT encrypted - anyone can read it (Base64 != encryption)
+------------------------------------------------------------------+
| PAYLOAD (Claims) |
+------------------------------------------------------------------+
{
"sub": "douglas@example.com", // Subject (who is this)
"iss": "https://idp.example.com",// Issuer (who created this token)
"aud": "api.example.com", // Audience (who should accept this)
"exp": 1703980800, // Expiration (Unix timestamp)
"iat": 1703977200, // Issued At
"nbf": 1703977200, // Not Before
// Custom claims (your application-specific data)
"roles": ["admin", "developer"],
"department": "engineering",
"device_id": "macbook-pro-42"
}
STANDARD CLAIMS:
sub - Subject: The user/entity identifier
iss - Issuer: The identity provider URL
aud - Audience: Which services should accept this token
exp - Expiration: When the token expires (UNIX timestamp)
iat - Issued At: When the token was created
nbf - Not Before: Token is invalid before this time
jti - JWT ID: Unique identifier for this specific token
Security: This is NOT encrypted - anyone can read it
Sensitive data should NOT be in the payload
+------------------------------------------------------------------+
| SIGNATURE |
+------------------------------------------------------------------+
SIGNATURE = Algorithm(
Base64UrlEncode(header) + "." + Base64UrlEncode(payload),
SECRET_OR_PRIVATE_KEY
)
For RS256:
SIGNATURE = RSA_SHA256(
"eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJkb3VnbGFzIn0",
PRIVATE_KEY
)
Purpose: Proves the token was created by someone with the private key
Proves the payload hasn't been modified since signing
Security: This is what makes JWTs secure - tamper with anything and
the signature becomes invalid
+------------------------------------------------------------------+
| VERIFICATION PROCESS |
+------------------------------------------------------------------+
[ Received JWT ]
|
v
1. Split into header.payload.signature
|
v
2. Base64-decode header -> Get algorithm (RS256)
|
v
3. Use PUBLIC KEY to verify:
SHA256(header.payload) == RSA_DECRYPT(signature, PUBLIC_KEY)
|
+---> If match: Token is VALID (not tampered, signed by IdP)
|
+---> If no match: Token is INVALID (reject immediately)
|
v
4. Check claims: exp > now? iss == expected? aud == us?
|
v
5. Extract identity from payload
|
v
6. ALLOW request (inject identity headers)
+------------------------------------------------------------------+
RS256 vs HS256: Why RS256 is Preferred in Zero Trust
+------------------------------------------------------------------+
| Symmetric (HS256) vs Asymmetric (RS256) |
+------------------------------------------------------------------+
HS256 (HMAC-SHA256) - SYMMETRIC
================================
[ Identity Provider ] [ Your Proxy ]
| |
| SHARED SECRET KEY | SAME SECRET KEY
| (knows it) | (must know it too)
| |
+--- Creates token with key ----+
| |
+--- Verifies token with key ---+
PROBLEM: The proxy MUST have the secret key.
If the proxy is compromised, attacker can FORGE tokens.
RS256 (RSA-SHA256) - ASYMMETRIC
================================
[ Identity Provider ] [ Your Proxy ]
| |
| PRIVATE KEY | PUBLIC KEY
| (signs tokens) | (verifies tokens)
| NEVER SHARED | CAN BE PUBLIC
| |
+--- Creates token with --------+
| PRIVATE key |
| |
+--- Verifies token with -------+
PUBLIC key only
ADVANTAGE: The proxy only has the public key.
If the proxy is compromised, attacker CANNOT forge tokens.
They can only verify existing tokens.
+------------------------------------------------------------------+
| Why RS256 for Zero Trust |
+------------------------------------------------------------------+
1. PRINCIPLE OF LEAST PRIVILEGE
The PEP only needs to VERIFY tokens, not create them.
RS256 enforces this - the PEP literally cannot forge tokens.
2. KEY DISTRIBUTION
You can publish the public key openly (even via HTTP).
The identity provider keeps the private key in a secure vault.
3. ROTATION
When rotating keys, you only need to update the IdP.
All PEPs just fetch the new public key.
4. FORENSICS
If a token is forged, you know the IdP itself was compromised
(not just any of the many PEPs in your system).
+------------------------------------------------------------------+
The HTTP Request/Response Lifecycle Through a Proxy
Understanding how HTTP flows through your proxy is essential for debugging.
+------------------------------------------------------------------+
| HTTP Request Lifecycle Through Identity-Aware Proxy |
+------------------------------------------------------------------+
TIMELINE:
Client Proxy (PEP) Backend
| | |
| 1. HTTP Request | |
| GET /api/data | |
| Authorization: | |
| Bearer eyJhbG... | |
|---------------------->| |
| | |
| | 2. Extract JWT |
| | from header |
| | |
| | 3. Verify signature |
| | using public key |
| | |
| | 4. Check expiration |
| | Check audience |
| | Check issuer |
| | |
| | 5. SANITIZE headers |
| | (remove any |
| | X-ZT-* headers |
| | from client) |
| | |
| | 6. INJECT identity |
| | X-ZT-Identity: |
| | douglas@example.com|
| | X-ZT-Roles: admin |
| | |
| | 7. Forward request |
| |---------------------->|
| | |
| | | 8. Process
| | | request
| | |
| | 9. Backend response |
| |<----------------------|
| | |
| 10. Forward response | |
|<----------------------| |
| | |
+------------------------------------------------------------------+
| FAILURE SCENARIOS |
+------------------------------------------------------------------+
No JWT Present:
- Step 2 fails: No Authorization header
- Response: 401 Unauthorized
- Header: WWW-Authenticate: Bearer
Invalid Signature:
- Step 3 fails: Signature mismatch
- Response: 403 Forbidden
- Log: "Token signature verification failed"
Expired Token:
- Step 4 fails: exp < current time
- Response: 401 Unauthorized
- Hint: Include "Token expired" in response
Wrong Audience:
- Step 4 fails: aud != this proxy's identifier
- Response: 403 Forbidden
- This token was meant for a different service
+------------------------------------------------------------------+
Header Sanitization and Injection Attacks
One of the most critical security concerns for an identity-aware proxy is header injection.
+------------------------------------------------------------------+
| Header Injection Attack |
+------------------------------------------------------------------+
THE ATTACK:
Attacker sends:
GET /admin/delete-all HTTP/1.1
Authorization: Bearer <valid-but-limited-user-token>
X-ZT-Identity: admin@corp.com <-- INJECTED BY ATTACKER
X-ZT-Roles: superadmin <-- INJECTED BY ATTACKER
If the proxy doesn't sanitize:
Backend receives:
GET /admin/delete-all HTTP/1.1
X-ZT-Identity: admin@corp.com <-- Backend trusts this!
X-ZT-Roles: superadmin <-- Privilege escalation!
THE DEFENSE (What Your Proxy Must Do):
1. STRIP all X-ZT-* headers from incoming requests
2. Validate the JWT
3. INJECT X-ZT-* headers based on the validated JWT payload
Result:
Backend receives:
GET /admin/delete-all HTTP/1.1
X-ZT-Identity: bob@corp.com <-- From validated JWT
X-ZT-Roles: user <-- From validated JWT
+------------------------------------------------------------------+
| HEADER SANITIZATION FLOW |
+------------------------------------------------------------------+
Incoming Request Headers:
+---------------------------+
| Authorization: Bearer ... |
| X-ZT-Identity: EVIL | <-- Must be removed
| X-ZT-Roles: admin | <-- Must be removed
| X-Forwarded-For: 1.2.3.4 |
| Cookie: session=abc |
+---------------------------+
|
v
[SANITIZATION: Remove all X-ZT-* headers]
|
v
+---------------------------+
| Authorization: Bearer ... |
| X-Forwarded-For: 1.2.3.4 |
| Cookie: session=abc |
+---------------------------+
|
v
[JWT VALIDATION: Extract real identity]
|
v
[INJECTION: Add trusted X-ZT-* headers]
|
v
+---------------------------+
| Authorization: Bearer ... |
| X-Forwarded-For: 1.2.3.4 |
| Cookie: session=abc |
| X-ZT-Identity: bob@corp | <-- From validated JWT
| X-ZT-Roles: user | <-- From validated JWT
| X-ZT-Verified: true | <-- Proxy attestation
+---------------------------+
+------------------------------------------------------------------+
Trust Boundary Concepts
The âtrust boundaryâ is an imaginary line that separates trusted from untrusted components.
+------------------------------------------------------------------+
| Trust Boundaries in ZTA |
+------------------------------------------------------------------+
TRADITIONAL MODEL (Network Perimeter):
+--[ UNTRUSTED ]--------------------+
| |
| [ Internet ] |
| [ Attackers ] |
| |
+-----------------------------------+
|
================ TRUST BOUNDARY (Firewall)
|
+--[ TRUSTED ]----------------------+
| |
| [ All Internal Services ] | <-- Everything trusts
| [ All Internal Users ] | everything here
| [ All Internal Traffic ] |
| |
+-----------------------------------+
ZERO TRUST MODEL (Identity Perimeter):
+--[ UNTRUSTED ]--------------------+
| |
| [ Internet ] |
| [ ALL Internal Traffic Too! ] | <-- Network location
| [ Even Requests from Localhost ] | means nothing
| |
+-----------------------------------+
|
================ TRUST BOUNDARY (PEP / Your Proxy)
|
+--[ TRUSTED ]----------------------+
| |
| [ Only this specific request ] |
| [ With this verified identity ] |
| [ For this specific resource ] |
| [ At this moment in time ] |
| |
+-----------------------------------+
KEY INSIGHT: Trust is established PER-REQUEST, not per-network-location.
+------------------------------------------------------------------+
| TRUST ZONES IN YOUR PROXY ARCHITECTURE |
+------------------------------------------------------------------+
[ UNTRUSTED ZONE ]
Everything outside the proxy:
- Client requests
- HTTP headers (can be spoofed)
- IP addresses (can be spoofed)
- Even "localhost" connections
|
v
+==========================================+
| YOUR PROXY (PEP) |
| TRUST BOUNDARY |
| |
| Cryptographic Verification: |
| - JWT signature validated |
| - Token not expired |
| - Audience matches |
| - Issuer is known |
| |
+==========================================+
|
v
[ CONDITIONALLY TRUSTED ZONE ]
Communication between proxy and backend:
- Identity headers can be trusted
- BUT: Backend should still validate roles
- BETTER: Use mTLS between proxy and backend (Project 4)
|
v
[ BACKEND ]
Trusts X-ZT-* headers from the proxy
(But should verify proxy identity via mTLS or network isolation)
+------------------------------------------------------------------+
Complete Project Specification
Functional Requirements
| ID | Requirement | Acceptance Criteria |
|---|---|---|
| FR-1 | Accept HTTP requests on configurable port | Proxy listens on specified port (default: 8080) |
| FR-2 | Forward valid requests to backend | Request reaches backend with original path, query params, body |
| FR-3 | Reject requests without JWT | Return 401 with WWW-Authenticate: Bearer header |
| FR-4 | Reject requests with invalid JWT signature | Return 403 with error message |
| FR-5 | Reject requests with expired JWT | Return 401 with âToken expiredâ message |
| FR-6 | Sanitize incoming headers | Remove all X-ZT-* headers from client requests |
| FR-7 | Inject identity headers | Add X-ZT-Identity, X-ZT-Roles from validated JWT |
| FR-8 | Log all access attempts | Log timestamp, source IP, path, decision (allow/deny), identity |
| FR-9 | Support configurable public key | Load public key from file path specified in config |
| FR-10 | Preserve request headers | Forward Cookie, Content-Type, Accept, etc. unchanged |
Non-Functional Requirements
| ID | Requirement | Target |
|---|---|---|
| NFR-1 | Latency overhead | < 5ms added latency per request |
| NFR-2 | Concurrent connections | Handle 1000+ concurrent connections |
| NFR-3 | Fail-closed design | Any error in verification results in DENY |
| NFR-4 | No external calls for verification | Verify JWT locally using public key |
| NFR-5 | Graceful shutdown | Complete in-flight requests before shutdown |
| NFR-6 | Structured logging | JSON log format for easy parsing |
API Contracts
Incoming Request (from Client):
GET /api/data HTTP/1.1
Host: proxy.example.com:8080
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
Cookie: session=abc123
Outgoing Request (to Backend):
GET /api/data HTTP/1.1
Host: backend.internal:8081
Content-Type: application/json
Cookie: session=abc123
X-ZT-Identity: douglas@example.com
X-ZT-Roles: admin,developer
X-ZT-Verified: true
X-ZT-Token-Exp: 1703980800
X-Forwarded-For: 192.168.1.100
X-Forwarded-Host: proxy.example.com
Error Response (No Token):
HTTP/1.1 401 Unauthorized
Content-Type: application/json
WWW-Authenticate: Bearer realm="Zero Trust Proxy"
{
"error": "authentication_required",
"message": "No JWT found in Authorization header",
"code": "ERR_NO_TOKEN"
}
Error Response (Invalid Token):
HTTP/1.1 403 Forbidden
Content-Type: application/json
{
"error": "invalid_token",
"message": "JWT signature verification failed",
"code": "ERR_INVALID_SIGNATURE"
}
Example JWT Payloads
Standard User Token:
{
"sub": "douglas@example.com",
"iss": "https://idp.example.com",
"aud": "zt-proxy.example.com",
"exp": 1703980800,
"iat": 1703977200,
"roles": ["user", "developer"],
"department": "engineering",
"device_id": "macbook-42"
}
Admin Token:
{
"sub": "admin@example.com",
"iss": "https://idp.example.com",
"aud": "zt-proxy.example.com",
"exp": 1703980800,
"iat": 1703977200,
"roles": ["admin", "superuser"],
"permissions": ["read", "write", "delete", "admin"],
"department": "security"
}
Service Account Token:
{
"sub": "service:backup-worker",
"iss": "https://idp.example.com",
"aud": "zt-proxy.example.com",
"exp": 1703980800,
"iat": 1703977200,
"type": "service",
"roles": ["service-account"],
"permissions": ["read"]
}
Real World Outcome
When you complete this project, you will have a fully functional identity-aware security gateway. Here is exactly what you will see:
Terminal Setup
# Terminal 1: Start your 'Vulnerable' Backend (simple Python server)
$ cd /tmp && echo "SECRET DATA - Should be protected" > secret.txt
$ python3 -m http.server 8081
Serving HTTP on 0.0.0.0 port 8081 (http://0.0.0.0:8081/) ...
# Terminal 2: Generate RSA key pair for signing
$ openssl genrsa -out idp_priv.pem 2048
$ openssl rsa -in idp_priv.pem -pubout -out idp_pub.pem
# Start your Identity-Aware Proxy
$ ./zta-proxy --backend http://localhost:8081 --public-key ./idp_pub.pem --port 8080
[INFO] 2024-12-27T10:00:00Z Starting Zero Trust Proxy
[INFO] 2024-12-27T10:00:00Z Loaded public key from ./idp_pub.pem
[INFO] 2024-12-27T10:00:00Z Backend configured: http://localhost:8081
[INFO] 2024-12-27T10:00:00Z Proxy listening on :8080
[INFO] 2024-12-27T10:00:00Z Ready to accept connections
# Terminal 3: Test requests
Test Case 1: Request WITHOUT a Token
$ curl -i http://localhost:8080/secret.txt
HTTP/1.1 401 Unauthorized
Content-Type: application/json
WWW-Authenticate: Bearer realm="Zero Trust Proxy"
Date: Fri, 27 Dec 2024 10:00:05 GMT
Content-Length: 89
{
"error": "authentication_required",
"message": "No JWT found in Authorization header",
"code": "ERR_NO_TOKEN"
}
Proxy Log:
[DENY] 2024-12-27T10:00:05Z | 127.0.0.1 | GET /secret.txt | NO_TOKEN | -
Test Case 2: Request with INVALID Signature
# Create a forged token (signed with wrong key)
$ FORGED_TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJoYWNrZXJAZXZpbC5jb20iLCJyb2xlcyI6WyJhZG1pbiJdfQ.FORGED_SIGNATURE"
$ curl -i -H "Authorization: Bearer $FORGED_TOKEN" http://localhost:8080/secret.txt
HTTP/1.1 403 Forbidden
Content-Type: application/json
Date: Fri, 27 Dec 2024 10:00:10 GMT
Content-Length: 91
{
"error": "invalid_token",
"message": "JWT signature verification failed",
"code": "ERR_INVALID_SIGNATURE"
}
Proxy Log:
[DENY] 2024-12-27T10:00:10Z | 127.0.0.1 | GET /secret.txt | INVALID_SIG | hacker@evil.com
Test Case 3: Request with EXPIRED Token
# Generate an expired token (exp in the past)
$ EXPIRED_TOKEN=$(./generate-test-token --user bob@example.com --exp "2024-01-01T00:00:00Z")
$ curl -i -H "Authorization: Bearer $EXPIRED_TOKEN" http://localhost:8080/secret.txt
HTTP/1.1 401 Unauthorized
Content-Type: application/json
Date: Fri, 27 Dec 2024 10:00:15 GMT
Content-Length: 85
{
"error": "token_expired",
"message": "JWT has expired",
"expired_at": "2024-01-01T00:00:00Z",
"code": "ERR_EXPIRED"
}
Proxy Log:
[DENY] 2024-12-27T10:00:15Z | 127.0.0.1 | GET /secret.txt | EXPIRED | bob@example.com
Test Case 4: Request with VALID Token
# Generate a valid token
$ VALID_TOKEN=$(./generate-test-token \
--user douglas@example.com \
--roles "admin,developer" \
--exp "2024-12-28T00:00:00Z" \
--private-key ./idp_priv.pem)
$ curl -i -H "Authorization: Bearer $VALID_TOKEN" http://localhost:8080/secret.txt
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Content-Length: 35
X-ZT-Verified: true
Date: Fri, 27 Dec 2024 10:00:20 GMT
SECRET DATA - Should be protected
Proxy Log:
[ALLOW] 2024-12-27T10:00:20Z | 127.0.0.1 | GET /secret.txt | VALID | douglas@example.com | roles=admin,developer
Backend Log (Python server):
127.0.0.1 - - [27/Dec/2024 10:00:20] "GET /secret.txt HTTP/1.1" 200 -
Test Case 5: Verify Header Injection to Backend
# Add a debug endpoint to see what headers the backend receives
$ curl -i -H "Authorization: Bearer $VALID_TOKEN" http://localhost:8080/headers-debug
# Backend sees these headers:
{
"X-ZT-Identity": "douglas@example.com",
"X-ZT-Roles": "admin,developer",
"X-ZT-Verified": "true",
"X-ZT-Token-Exp": "1703721600",
"X-Forwarded-For": "127.0.0.1",
"X-Forwarded-Host": "localhost:8080",
"Cookie": "...",
"User-Agent": "curl/8.1.2"
}
Test Case 6: Header Injection Attack (Must Be Blocked)
# Attacker tries to inject their own identity header
$ curl -i \
-H "Authorization: Bearer $VALID_TOKEN" \
-H "X-ZT-Identity: admin@hacked.com" \
-H "X-ZT-Roles: superadmin" \
http://localhost:8080/secret.txt
HTTP/1.1 200 OK
...
# Backend receives (attacker headers stripped and replaced):
{
"X-ZT-Identity": "douglas@example.com", <-- From validated JWT, not attacker
"X-ZT-Roles": "admin,developer", <-- From validated JWT, not attacker
...
}
Proxy Log:
[ALLOW] 2024-12-27T10:00:30Z | 127.0.0.1 | GET /secret.txt | VALID | douglas@example.com | SANITIZED: X-ZT-Identity,X-ZT-Roles
The Core Question Youâre Answering
âHow can I protect backend services from unauthorized access while making authorization decisions based on WHO the user is, not WHERE theyâre connecting from?â
Before you write any code, sit with this question. Traditional security assumes that anything inside the corporate network is trustworthy - a firewall protects the perimeter, and once youâre âinside,â you have access. But what happens when an attacker compromises a single machine inside that perimeter? They now have unrestricted access to everything. The identity-aware proxy flips this model: it doesnât care where a request originates. It only cares whether the request carries cryptographic proof of identity - a valid, unexpired, properly-signed token that proves WHO is making the request. This single architectural change eliminates entire categories of attacks, from lateral movement to credential theft.
Concepts You Must Understand First
Stop and research these before coding:
- HTTP Reverse Proxy Architecture
- What is the difference between a forward proxy and a reverse proxy?
- How does a reverse proxy preserve the original client request (headers, body, query parameters)?
- What happens to the
Hostheader when a request passes through a proxy? - How do
X-Forwarded-ForandX-Forwarded-Protoheaders work, and why are they important? - Book Reference: âComputer Networks, 5th Edâ by Tanenbaum - Ch. 7: Application Layer
- JWT Token Structure and Cryptographic Verification
- What are the three components of a JWT, and what purpose does each serve?
- Why is Base64URL encoding used instead of standard Base64?
- How does RS256 signature verification work mathematically?
- What is the difference between encryption and signing? (A JWT is signed, not encrypted)
- Book Reference: âSerious Cryptography, 2nd Edâ by Aumasson - Ch. 5: MACs, Ch. 8: RSA
- Asymmetric Cryptography (Public Key Infrastructure)
- Why can a public key verify a signature but not create one?
- What does it mean when we say âonly the identity provider has the private keyâ?
- How does key rotation work with JWKS (JSON Web Key Sets)?
- What is the âkidâ (Key ID) header in a JWT, and when do you need it?
- Book Reference: âZero Trust Networksâ by Gilman & Barth - Ch. 6: Trusting Identities
- OAuth2 and Token-Based Authentication
- What is the difference between an access token and a refresh token?
- What are the standard JWT claims (
sub,iss,aud,exp,iat)? - Why does the
aud(audience) claim matter for security? - What is token replay, and how do short-lived tokens mitigate it?
- Book Reference: âOAuth 2 in Actionâ by Richer & Sanso - Ch. 7-8
- HTTP Header Security
- What is header injection, and why is it dangerous?
- Why must a proxy strip certain headers before forwarding requests?
- What is the difference between
Set()andAdd()when modifying headers? - How can a malicious client exploit a proxy that doesnât sanitize headers?
- Book Reference: âThe Tangled Webâ by Michal Zalewski - Ch. 3: HTTP
- Trust Boundaries and Fail-Closed Design
- What is a trust boundary, and where does it exist in your architecture?
- What does âfail-closedâ mean, and why is it critical for security?
- If your JWT parsing library throws an exception, should the request be allowed or denied?
- How do you ensure that your proxy is the ONLY way to reach the backend?
- Book Reference: âZero Trust Networksâ by Gilman & Barth - Ch. 1-3
Questions to Guide Your Design
HTTP Proxy Implementation:
- How will you handle different HTTP methods (GET, POST, PUT, DELETE)?
- Will you copy the request body, or stream it to reduce memory usage?
- How will you handle chunked transfer encoding and keep-alive connections?
- What happens if the backend is slow or unresponsive - how will you timeout?
JWT Validation Logic:
- In what order should you validate claims (signature first? expiration first?)?
- What error message should you return for each failure case (missing token, invalid signature, expired)?
- Should you cache verified tokens to improve performance, and what are the security trade-offs?
- How will you handle tokens signed with an unknown key (key rotation scenario)?
Header Management:
- Which headers should you copy from the client to the backend?
- Which headers should you strip to prevent injection attacks?
- What headers should you inject after successful validation?
- How will you handle the
Authorizationheader - forward it or strip it?
Error Handling and Logging:
- What information should you log for security auditing without leaking sensitive data?
- How will you structure log entries for easy parsing by SIEM systems?
- Should error responses reveal why a token was rejected, or is that a security risk?
- How will you handle panics/exceptions to ensure fail-closed behavior?
Network Architecture:
- How will you ensure the backend ONLY accepts connections from your proxy?
- What happens if someone discovers the backendâs direct IP address?
- Should the proxy and backend communicate over TLS (consider mTLS for P04)?
Thinking Exercise
Before writing any code, trace through these scenarios on paper:
Scenario A: Legitimate Request Flow
1. Client has token: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbGljZUBjb3JwLmNvbSIsImV4cCI6MTcwNDAwMDAwMCwicm9sZXMiOlsidXNlciJdfQ.[VALID_SIGNATURE]
2. Client sends: GET /api/data HTTP/1.1
Host: proxy:8080
Authorization: Bearer <token above>
Trace through your proxy:
- What do you extract from the Authorization header?
- How do you split the token into its three parts?
- What cryptographic operation verifies the signature?
- What headers do you add before forwarding to the backend?
- What does the backend see in the request?
Scenario B: Header Injection Attack
1. Attacker has a valid token for "bob@corp.com" (a regular user)
2. Attacker sends: GET /admin/delete-all HTTP/1.1
Host: proxy:8080
Authorization: Bearer <bob's valid token>
X-ZT-Identity: admin@corp.com
X-ZT-Roles: superadmin
Trace through your proxy:
- The token is cryptographically valid (signature checks out)
- But the attacker injected fake identity headers
- If you forward the request as-is, what does the backend trust?
- What MUST your proxy do before injecting verified headers?
- Write the exact headers the backend should receive
Scenario C: Expired Token
Current time: 1704067200 (Jan 1, 2024 00:00:00 UTC)
Token exp claim: 1703980800 (Dec 31, 2023 00:00:00 UTC)
1. The signature is valid (cryptographically correct)
2. But the exp claim is in the past
Questions:
- At which step in your validation should you check expiration?
- What HTTP status code should you return?
- Should you reveal "token expired" in the response, or just say "unauthorized"?
- What if the server clock is slightly off - should you allow a "grace period"?
Work through each scenario with pseudocode before implementing. This exercise reveals edge cases you might otherwise miss.
Hints in Layers
Hint 1: Start with a âpass-throughâ proxy first
Before adding any JWT logic, build a simple reverse proxy that forwards ALL requests to the backend unchanged. Use Goâs httputil.ReverseProxy or Pythonâs httpx library. Verify that requests arrive at the backend with all headers intact. This establishes your baseline - every subsequent change should break something intentionally (rejecting unauthenticated requests) before you fix it (allowing authenticated ones).
Hint 2: Parse the JWT before verifying it
Donât try to do everything at once. First, extract the token from the Authorization: Bearer <token> header. Then split on . to get three parts. Then Base64URL-decode the header and payload (note: Base64URL, not Base64 - no padding, with - and _ instead of + and /). Print the decoded claims to your log. Only after you can reliably extract claims should you add signature verification.
Hint 3: The Director function is your modification point
In Goâs httputil.ReverseProxy, the Director function is called before each request is forwarded. This is where you modify headers. The pattern is:
proxy.Director = func(req *http.Request) {
// 1. Strip dangerous headers: req.Header.Del("X-ZT-Identity")
// 2. Add your verified headers: req.Header.Set("X-ZT-Identity", claims.Sub)
// 3. Update the target host: req.URL.Host = "backend:8081"
}
But remember: validation should happen BEFORE the Director is called. If the token is invalid, you should return an error response without ever calling the proxy.
Hint 4: Use a real JWT library, but understand what it does
Donât implement RS256 verification from scratch - cryptography is easy to get wrong. Use github.com/golang-jwt/jwt/v5 in Go, python-jose in Python, or jsonwebtoken in Node.js. But before you use it, read the source code for the verification function. Understand that itâs computing SHA256(header.payload) and then verifying that against the decrypted signature using the public key. This understanding will help you debug âinvalid signatureâ errors.
Hint 5: The order of operations for security
Your request handling should follow this exact order:
- Receive request (log: request received)
- Check for Authorization header (fail: 401)
- Extract Bearer token (fail: 401)
- Parse JWT structure (fail: 400)
- Verify signature with public key (fail: 403)
- Check expiration claim (fail: 401)
- Check audience claim (fail: 403)
- Check issuer claim (fail: 403)
- Strip all X-ZT-* headers from request
- Inject verified identity headers
- Forward to backend
- Return response (log: allow/deny + identity)
Any failure at steps 2-8 must result in a DENY. This is fail-closed design.
Solution Architecture
Component Diagram
+------------------------------------------------------------------+
| Identity-Aware Proxy Architecture |
+------------------------------------------------------------------+
+-----------------+
| Configuration |
| - Backend URL |
| - Public Key |
| - Port |
| - Log Level |
+--------+--------+
|
v
+------------------+ +------------------+ +------------------+
| | | | | |
| HTTP Listener |----->| Auth Middleware |----->| Reverse Proxy |
| | | | | |
| - Port binding | | - Extract JWT | | - Forward req |
| - TLS (optional)| | - Verify sig | | - Copy headers |
| - Conn mgmt | | - Sanitize hdrs | | - Inject hdrs |
| | | - Inject hdrs | | - Copy response |
+------------------+ +--------+---------+ +--------+---------+
| |
| |
+--------v---------+ +--------v---------+
| | | |
| JWT Validator | | Backend Client |
| | | |
| - Parse token | | - HTTP client |
| - Verify RS256 | | - Timeout mgmt |
| - Check claims | | - Error handling|
+------------------+ +------------------+
|
|
+--------v---------+
| |
| Access Logger |
| |
| - Structured |
| - JSON format |
| - Audit trail |
+------------------+
+------------------------------------------------------------------+
Data Flow Diagram
+------------------------------------------------------------------+
| Request Flow |
+------------------------------------------------------------------+
CLIENT REQUEST
|
v
+----+----+
| Receive |
| Request |
+---------+
|
v
+----+----+
| Is TLS |----> [If HTTPS: Terminate TLS]
| Enabled?|
+---------+
|
v
+----+----+ +-------------+
| Extract |-------->| 401 No Auth |
| JWT | no JWT +-------------+
+---------+
| has JWT
v
+----+----+ +-------------+
| Parse |-------->| 403 Invalid |
| & Verify| bad | Token |
| Signature| sig +-------------+
+---------+
| valid sig
v
+----+----+ +-------------+
| Check |-------->| 401 Expired |
| Claims | exp +-------------+
| (exp) |
+---------+
| valid
v
+----+----+
|Sanitize | Strip: X-ZT-*, X-Forwarded-* (optional)
| Headers |
+---------+
|
v
+----+----+
| Inject | Add: X-ZT-Identity, X-ZT-Roles, X-Forwarded-For
| Headers |
+---------+
|
v
+----+----+
| Forward |
| to |------> BACKEND SERVER
| Backend |
+---------+
|
v
+----+----+
| Return |
| Response|------> CLIENT
+---------+
+------------------------------------------------------------------+
Key Design Decisions to Make
Before implementing, consider these architectural choices:
| Decision | Option A | Option B | Recommendation |
|---|---|---|---|
| JWT Library | Standard library | Third-party (go-jwt) | Third-party for better claim validation |
| Proxy Library | httputil.ReverseProxy |
Manual implementation | Use standard library, customize Director |
| Key Storage | File path | Environment variable | File path for development, consider vault for production |
| Logging | Printf | Structured (zerolog/zap) | Structured JSON for production |
| Config Format | CLI flags | YAML/JSON file | CLI flags for simplicity, file for complex setups |
| Error Responses | Plain text | JSON | JSON for API compatibility |
| Header Prefix | X-ZT-* |
X-Identity-* |
X-ZT-* (Zero Trust naming convention) |
Technology Choices to Consider
Go (Recommended):
net/http- HTTP server and clienthttputil.ReverseProxy- Proxy implementationgithub.com/golang-jwt/jwt/v5- JWT parsing and validationgithub.com/rs/zerolog- Structured logging
Python (FastAPI Alternative):
fastapi- HTTP frameworkhttpx- Async HTTP clientpython-jose[cryptography]- JWT handlingstructlog- Structured logging
Node.js Alternative:
http-proxy-middleware- Proxy implementationjsonwebtoken- JWT handlingpino- Structured logging
Phased Implementation Guide
Phase 1: Basic HTTP Proxy (Forward Traffic)
Goal: Get a transparent reverse proxy working that forwards all traffic.
Deliverables:
- HTTP server listening on port 8080
- All requests forwarded to backend on port 8081
- Request headers preserved
- Response returned to client
Steps:
- Create HTTP server on configurable port
- For each request, create a client request to the backend
- Copy headers from original request
- Forward the response back to the client
- Handle connection errors gracefully
Verification:
# Start backend
$ python3 -m http.server 8081
# Start your proxy
$ go run main.go --backend http://localhost:8081
# Test (should return backend content)
$ curl http://localhost:8080/
Phase 2: JWT Extraction and Parsing
Goal: Extract JWT from Authorization header and decode it (without verification).
Deliverables:
- Extract
Authorization: Bearer <token>header - Parse JWT into header, payload, signature components
- Decode Base64URL-encoded payload
- Return 401 if no token present
Steps:
- Check for
Authorizationheader in request - Validate format is
Bearer <token> - Split token on
.into three parts - Base64URL decode header and payload
- Parse JSON payload into struct
- Log extracted claims for debugging
Verification:
# No token - should get 401
$ curl -i http://localhost:8080/
HTTP/1.1 401 Unauthorized
# With token - should forward (even if invalid, for now)
$ curl -i -H "Authorization: Bearer eyJ..." http://localhost:8080/
Phase 3: Cryptographic Signature Verification
Goal: Verify JWT signature using RS256 with the IdPâs public key.
Deliverables:
- Load RSA public key from PEM file
- Verify RS256 signature on JWT
- Return 403 for invalid signatures
- Check
expclaim for expiration
Steps:
- Load public key from file at startup
- Parse PEM format into RSA public key
- For each request, verify signature:
- Create signing input:
base64(header) + "." + base64(payload) - Verify SHA256 signature with public key
- Create signing input:
- Check
expclaim against current time - Return appropriate error codes
Verification:
# Generate test keys
$ openssl genrsa -out priv.pem 2048
$ openssl rsa -in priv.pem -pubout -out pub.pem
# Create a valid token (use jwt.io or a script)
$ VALID_TOKEN=$(./generate-token --key priv.pem --sub test@example.com)
# Test with valid token
$ curl -H "Authorization: Bearer $VALID_TOKEN" http://localhost:8080/
# Should forward
# Test with tampered token
$ curl -H "Authorization: Bearer eyJ...tampered" http://localhost:8080/
HTTP/1.1 403 Forbidden
Phase 4: Header Injection and Sanitization
Goal: Safely inject verified identity into request headers for the backend.
Deliverables:
- Remove all
X-ZT-*headers from incoming requests - Inject
X-ZT-Identityfromsubclaim - Inject
X-ZT-Rolesfromrolesclaim (if present) - Add
X-Forwarded-Forwith client IP
Steps:
- Before validation, strip all
X-ZT-*headers from request - After validation, extract identity claims from payload
- Create new headers:
X-ZT-Identity: Subject from JWTX-ZT-Roles: Comma-separated rolesX-ZT-Verified: âtrueâX-ZT-Token-Exp: Expiration timestamp
- Inject headers into forwarded request
Verification:
# Create a debug endpoint in backend that echoes headers
$ curl -H "Authorization: Bearer $VALID_TOKEN" \
-H "X-ZT-Identity: evil@hacker.com" \
http://localhost:8080/debug-headers
# Verify evil header was stripped and replaced with verified identity
Phase 5: Error Handling and Logging
Goal: Implement production-ready error handling and audit logging.
Deliverables:
- Structured JSON logging for all access
- Detailed error messages (without leaking sensitive info)
- Graceful handling of backend failures
- Timeout configuration
Steps:
- Implement structured logger with JSON output
- Log every request with: timestamp, source IP, path, decision, identity
- Handle backend connection errors (return 502 Bad Gateway)
- Add request timeout handling (return 504 Gateway Timeout)
- Ensure no stack traces or internal errors leak to client
Log Format:
{
"timestamp": "2024-12-27T10:00:00Z",
"level": "info",
"event": "access",
"client_ip": "192.168.1.100",
"method": "GET",
"path": "/api/data",
"decision": "allow",
"identity": "douglas@example.com",
"roles": ["admin", "developer"],
"latency_ms": 12,
"backend_status": 200
}
Testing Strategy
Unit Test Scenarios
| Component | Test Case | Expected Result |
|---|---|---|
| JWT Parser | Valid JWT format | Parse without error |
| JWT Parser | Missing header | Return error |
| JWT Parser | Invalid Base64 | Return error |
| Signature Verifier | Valid signature | Return true |
| Signature Verifier | Tampered payload | Return false |
| Signature Verifier | Wrong public key | Return false |
| Claim Validator | Future exp |
Return valid |
| Claim Validator | Past exp |
Return expired error |
| Claim Validator | Missing exp |
Return error (configurable) |
| Header Sanitizer | Request with X-ZT-* | Headers removed |
| Header Sanitizer | Request without X-ZT-* | No change |
| Header Injector | Valid identity | Correct headers added |
Integration Test Scenarios
| Scenario | Setup | Expected Behavior |
|---|---|---|
| Happy Path | Valid JWT, backend up | 200 OK, headers injected |
| No Token | No Authorization header | 401 Unauthorized |
| Invalid Token | Tampered JWT | 403 Forbidden |
| Expired Token | JWT with past exp |
401 Unauthorized |
| Backend Down | Backend not responding | 502 Bad Gateway |
| Backend Timeout | Backend slow response | 504 Gateway Timeout |
| Large Payload | POST with 10MB body | Body forwarded correctly |
| Concurrent Requests | 100 simultaneous requests | All handled correctly |
Security Test Scenarios
| Attack | Test Method | Expected Defense |
|---|---|---|
| Header Injection | Send X-ZT-Identity: admin |
Header stripped, replaced with verified identity |
| Token Replay | Use expired but valid-signature token | Rejected due to expiration |
| Algorithm Confusion | Send HS256 token | Rejected (only RS256 accepted) |
| None Algorithm | Send alg: none token |
Rejected immediately |
| Key Confusion | Sign with different key | Signature verification fails |
| Path Traversal | Request /../../../etc/passwd |
Forward as-is (backendâs responsibility) |
| Large Token | Send 1MB token | Reject before parsing |
Performance Benchmarks
Target Metrics:
- Latency overhead: < 5ms per request
- Throughput: > 10,000 requests/second
- Memory: < 100MB under load
Benchmark Commands:
# Using wrk (HTTP benchmarking tool)
$ wrk -t12 -c400 -d30s -H "Authorization: Bearer $TOKEN" http://localhost:8080/
# Using hey
$ hey -n 10000 -c 100 -H "Authorization: Bearer $TOKEN" http://localhost:8080/
Common Pitfalls and Debugging
Pitfall 1: Backend Still Accessible Directly
Symptom: Users can bypass the proxy by directly accessing http://server:8081
Cause: The proxy is running, but the backend isnât isolated.
Solution:
# Option 1: Bind backend to localhost only
$ python3 -m http.server 8081 --bind 127.0.0.1
# Option 2: Use iptables to block external access to 8081
$ sudo iptables -A INPUT -p tcp --dport 8081 -s 127.0.0.1 -j ACCEPT
$ sudo iptables -A INPUT -p tcp --dport 8081 -j DROP
# Option 3: Use Project 3 (micro-segmentation) for proper isolation
Pitfall 2: JWT Signature Verification Fails
Symptom: All tokens are rejected with âinvalid signatureâ
Cause: Public key mismatch or encoding issues.
Debugging:
# Verify key pair matches
$ openssl rsa -in priv.pem -pubout | diff - pub.pem
# Decode token to check algorithm
$ echo "eyJhbGci..." | cut -d. -f1 | base64 -d
# Should show: {"alg":"RS256","typ":"JWT"}
# Use jwt.io to verify token manually
# Paste token and public key to confirm it works
Solution:
- Ensure public key is in correct PEM format (starts with
-----BEGIN PUBLIC KEY-----) - Verify token was signed with matching private key
- Check algorithm in token header matches your verifier (RS256)
Pitfall 3: Headers Not Appearing in Backend
Symptom: Backend doesnât receive the X-ZT-* headers.
Cause: Headers are being modified after proxy forwards the request, or the proxy is creating a new request without copying headers.
Debugging:
// In Go, print headers before and after
log.Printf("Before: %v", req.Header)
proxy.ServeHTTP(w, req)
log.Printf("After modification in Director")
Solution:
- Modify headers in the
Directorfunction ofhttputil.ReverseProxy - Headers must be set BEFORE
proxy.ServeHTTP()is called - Use
req.Header.Set()notreq.Header.Add()to replace existing headers
Pitfall 4: Proxy Crashes on HTTPS Backends
Symptom: Connection errors when backend uses HTTPS.
Cause: TLS certificate verification failing.
Solution (Development):
transport := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true, // NEVER in production
},
}
proxy.Transport = transport
Solution (Production):
- Add backendâs CA certificate to trust store
- Use mTLS (Project 4) for proper authentication
- Consider service mesh like Istio for automatic mTLS
Pitfall 5: Memory Leak Under Load
Symptom: Proxy memory usage grows continuously.
Cause: Response bodies not being closed, or connection leaks.
Debugging:
# Monitor memory
$ watch -n 1 'ps aux | grep zta-proxy'
# Use pprof in Go
import _ "net/http/pprof"
go func() { http.ListenAndServe(":6060", nil) }()
# Then: go tool pprof http://localhost:6060/debug/pprof/heap
Solution:
- Always close response bodies:
defer resp.Body.Close() - Use
httputil.ReverseProxywhich handles this correctly - Set connection timeouts
Pitfall 6: Clock Skew Causes Token Rejection
Symptom: Valid tokens rejected as expired intermittently.
Cause: Server clock is out of sync with token issuer.
Debugging:
# Check server time
$ date
$ ntpq -p # Check NTP status
# Check token expiration
$ echo $TOKEN | cut -d. -f2 | base64 -d | jq '.exp'
Solution:
// Add clock skew tolerance (e.g., 1 minute)
if token.Claims.ExpiresAt.Time.Add(1 * time.Minute).Before(time.Now()) {
return ErrTokenExpired
}
Extensions and Challenges
Extension 1: JWKS (JSON Web Key Set) Support
Instead of loading a single public key from a file, fetch keys dynamically from the identity providerâs JWKS endpoint.
What to Build:
- Fetch keys from
https://idp.example.com/.well-known/jwks.json - Cache keys with TTL (e.g., 1 hour)
- Select correct key based on
kidheader in JWT - Handle key rotation automatically
Why It Matters: Production identity providers (Auth0, Okta, Azure AD) all use JWKS. This is how real systems work.
Extension 2: Token Refresh Integration
Add support for handling expired tokens by redirecting to a refresh flow.
What to Build:
- Detect expired tokens
- Return 401 with custom header:
X-Token-Expired: true - Client can use refresh token to get new access token
- Optional: Support silent refresh via cookie-based refresh tokens
Extension 3: Rate Limiting Per Identity
Add rate limiting based on the verified identity, not just IP address.
What to Build:
- Track requests per identity (from JWT
subclaim) - Implement sliding window rate limiting
- Different limits for different roles (admins get higher limits)
- Return 429 Too Many Requests when exceeded
Extension 4: Audit Log to External System
Send access logs to an external system for security monitoring.
What to Build:
- Stream logs to Elasticsearch, Splunk, or SIEM
- Include full request context (but not sensitive data)
- Support batching for performance
- Handle external system failures gracefully
Extension 5: Multi-Backend Routing
Route to different backends based on the request path or identity.
What to Build:
- Configuration for multiple backends
- Routing rules:
/api/users/*goes to users-service - Identity-based routing: Admins get routed to admin backend
- Health checking for backends
Challenge: Advanced
Implement token binding - bind the JWT to the TLS session so stolen tokens canât be used from other connections. Research RFC 8471 (Token Binding Protocol).
Books That Will Help
| Topic | Book | Chapter | Why Read It |
|---|---|---|---|
| Zero Trust Fundamentals | âZero Trust Networksâ by Gilman & Barth | Ch. 1-3 | Understand the philosophy and architecture |
| JWT & Token Security | âZero Trust Networksâ by Gilman & Barth | Ch. 6: Trusting Identities | Deep dive into identity tokens |
| HTTP Protocol | âComputer Networks, 5th Edâ by Tanenbaum | Ch. 7: Application Layer | Understand HTTP in depth |
| Web Security | âThe Tangled Webâ by Michal Zalewski | Ch. 3: HTTP | Security implications of HTTP headers |
| Go Web Development | âNetwork Programming with Goâ by Adam Woodbeck | Ch. 7-8: HTTP Services | Building HTTP servers and clients |
| Cryptography | âSerious Cryptography, 2nd Edâ by Aumasson | Ch. 5: MACs, Ch. 8: RSA | Understanding JWT signatures |
| Go Patterns | âLearning Go, 2nd Edâ by Jon Bodner | Ch. 10: Concurrency | Handling concurrent requests |
| Access Control | âSecurity in Computingâ by Pfleeger | Ch. 3: Authentication | Auth theory and best practices |
| Production Systems | âDesigning Data-Intensive Applicationsâ by Kleppmann | Ch. 4: Encoding | JWT and serialization formats |
| Linux Networking | âThe Linux Programming Interfaceâ by Kerrisk | Ch. 58-61: Sockets | Low-level HTTP understanding |
Interview Questions
These questions are commonly asked when discussing Zero Trust implementations:
Question 1: Why RS256 over HS256 for Zero Trust JWTs?
Brief Answer: RS256 uses asymmetric cryptography - the proxy only needs the public key to verify tokens. If the proxy is compromised, attackers cannot forge tokens. With HS256, the shared secret would allow token forgery.
Follow-up to Expect: âWhat about performance?â RS256 is slower (~10x), but still microseconds. The security benefit outweighs the performance cost.
Question 2: What is the performance impact of verifying a JWT on every request?
Brief Answer: RS256 verification takes approximately 100-200 microseconds per operation. For a proxy handling 10,000 req/s, this is ~2% CPU overhead. Caching verified tokens (with short TTL) can reduce this further.
Deeper Answer: Include discussion of connection pooling, keep-alive, and how HTTP/2 multiplexing amortizes the verification cost.
Question 3: How would you handle token revocation?
Brief Answer:
- Short token lifetimes (15-60 minutes) limit exposure window
- Token revocation list (TRL) checked on each request
- Token introspection endpoint (slower, for high-security)
- Push-based revocation via webhook to PEPs
Question 4: Explain header injection as an attack vector.
Brief Answer: An attacker sends a request with their own X-ZT-Identity: admin@corp.com header. If the proxy doesnât sanitize incoming headers, the backend trusts the forged identity. Defense: Always strip identity headers from incoming requests before injecting verified identity.
Question 5: What happens if your proxy goes down?
Brief Answer:
- Fail-closed: Backend should only accept connections from proxy (via network rules or mTLS)
- High availability: Run multiple proxy instances behind a load balancer
- No bypass: Direct access to backend should be impossible
Question 6: How is your proxy different from an API Gateway?
Brief Answer:
- API Gateway: Rate limiting, transformation, routing, monitoring
- Identity-Aware Proxy: Single purpose - identity verification
- In practice: API gateways often include PEP functionality
- Zero Trust prefers single-purpose components for security
Question 7: How do you verify the backend can trust the proxyâs headers?
Brief Answer:
- Network isolation: Backend only accepts connections from proxy IP
- mTLS: Proxy presents certificate to backend (Project 4)
- Shared secret: Proxy signs injected headers (less common)
- Service mesh: Automatic mTLS via Istio/Linkerd
Question 8: What claims would you require in a Zero Trust JWT?
Brief Answer:
- Required:
sub,exp,iat,iss,aud - Recommended:
jti(for revocation),roles(for RBAC) - Zero Trust specific:
device_id,device_posture,location - Consider:
acr(authentication context class) for step-up auth
Question 9: How would you implement continuous authorization?
Brief Answer: Instead of verifying once per session, verify on every request. The proxy checks:
- Token still valid (not expired, not revoked)
- Device posture still healthy (via PDP call)
- Risk signals (unusual behavior, location change)
- If any check fails, terminate the session
Question 10: Walk me through a request from client to backend.
Expected Flow:
- Client sends HTTPS request with JWT to proxy
- Proxy terminates TLS, extracts JWT from Authorization header
- Proxy verifies signature using cached public key
- Proxy checks expiration, audience, issuer claims
- Proxy strips any existing X-ZT-* headers (sanitization)
- Proxy injects X-ZT-Identity, X-ZT-Roles from verified token
- Proxy forwards request to backend over internal network (or mTLS)
- Backend trusts X-ZT-* headers, processes request
- Response flows back through proxy to client
Self-Assessment Checklist
Before considering this project complete, verify you can:
Conceptual Understanding
- Explain what a PEP is and its role in Zero Trust architecture
- Describe the difference between authentication and authorization
- Draw a diagram showing trust boundaries in your proxy setup
- Explain why âthe network is hostileâ is a Zero Trust principle
- Describe how RS256 JWT verification works step-by-step
Implementation Verification
- Your proxy correctly forwards valid requests to the backend
- Requests without JWT receive 401 Unauthorized
- Requests with invalid signature receive 403 Forbidden
- Expired tokens are rejected with clear error message
- X-ZT-* headers from clients are stripped before forwarding
- Verified identity is correctly injected as headers
- Backend cannot be accessed directly (bypassing proxy)
Security Verification
- Forged tokens (signed with wrong key) are rejected
- Header injection attacks are prevented
- Error messages donât leak sensitive information
- The proxy fails closed (errors = denied access)
- Logs contain sufficient information for audit
Production Readiness
- Logs are structured JSON for easy parsing
- Performance overhead is < 5ms per request
- The proxy handles 1000+ concurrent connections
- Backend failures return appropriate 5xx errors
- Configuration is loaded from file/environment, not hardcoded
Deeper Understanding
- Can explain when to use HS256 vs RS256
- Know how to implement JWKS for key rotation
- Understand how mTLS would improve backend trust (Project 4)
- Can describe token revocation strategies
- Know what a âconfused deputyâ attack is and how to prevent it
Next Steps
After completing this project, you have built the Policy Enforcement Point (PEP) - the gatekeeper of Zero Trust. Your next options:
-
Project 2: Policy Decision Engine - Build the âbrainâ that makes authorization decisions. Your PEP will call this PDP for complex authorization logic.
-
Project 3: Host-Level Micro-segmentation - Ensure your backend can ONLY be accessed via your proxy by implementing network-level isolation.
-
Project 4: Mutual TLS Mesh - Add cryptographic proof of proxy identity to the backend, so the backend can verify the X-ZT-* headers are authentic.
Remember: This project teaches one pillar of Zero Trust - identity verification at the application layer. A complete Zero Trust architecture requires network isolation (Project 3), continuous authorization (Project 6), and defense in depth across all layers.
Appendix: Token Generation Script
For testing, youâll need a way to generate valid JWTs. Hereâs a reference implementation:
Usage:
# Generate RSA key pair
$ openssl genrsa -out idp_priv.pem 2048
$ openssl rsa -in idp_priv.pem -pubout -out idp_pub.pem
# Generate token (implement this in your language of choice)
$ ./generate-test-token \
--private-key idp_priv.pem \
--sub "douglas@example.com" \
--roles "admin,developer" \
--exp "1h" # 1 hour from now
# Output: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Required Claims to Generate:
{
"sub": "douglas@example.com",
"iss": "https://idp.example.com",
"aud": "zt-proxy.example.com",
"exp": 1703980800,
"iat": 1703977200,
"roles": ["admin", "developer"]
}
This token generation utility is part of your project - building it helps you understand JWT creation from the issuerâs perspective.