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:

  1. Implement a Policy Enforcement Point (PEP) - The core gatekeeper component of Zero Trust Architecture
  2. Build a transparent HTTP reverse proxy - Forward traffic while preserving headers, cookies, and request context
  3. Parse and validate JWT tokens cryptographically - Verify RS256 signatures using public key cryptography
  4. Perform secure header injection - Pass verified identity information to backend services safely
  5. Implement header sanitization - Prevent header injection attacks from malicious clients
  6. Design fail-closed security systems - Ensure that failures result in denied access, not open access
  7. 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:

  1. 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 Host header when a request passes through a proxy?
    • How do X-Forwarded-For and X-Forwarded-Proto headers work, and why are they important?
    • Book Reference: “Computer Networks, 5th Ed” by Tanenbaum - Ch. 7: Application Layer
  2. 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
  3. 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
  4. 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
  5. 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() and Add() 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
  6. 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 Authorization header - 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:

  1. Receive request (log: request received)
  2. Check for Authorization header (fail: 401)
  3. Extract Bearer token (fail: 401)
  4. Parse JWT structure (fail: 400)
  5. Verify signature with public key (fail: 403)
  6. Check expiration claim (fail: 401)
  7. Check audience claim (fail: 403)
  8. Check issuer claim (fail: 403)
  9. Strip all X-ZT-* headers from request
  10. Inject verified identity headers
  11. Forward to backend
  12. 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 client
  • httputil.ReverseProxy - Proxy implementation
  • github.com/golang-jwt/jwt/v5 - JWT parsing and validation
  • github.com/rs/zerolog - Structured logging

Python (FastAPI Alternative):

  • fastapi - HTTP framework
  • httpx - Async HTTP client
  • python-jose[cryptography] - JWT handling
  • structlog - Structured logging

Node.js Alternative:

  • http-proxy-middleware - Proxy implementation
  • jsonwebtoken - JWT handling
  • pino - 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:

  1. Create HTTP server on configurable port
  2. For each request, create a client request to the backend
  3. Copy headers from original request
  4. Forward the response back to the client
  5. 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:

  1. Check for Authorization header in request
  2. Validate format is Bearer <token>
  3. Split token on . into three parts
  4. Base64URL decode header and payload
  5. Parse JSON payload into struct
  6. 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 exp claim for expiration

Steps:

  1. Load public key from file at startup
  2. Parse PEM format into RSA public key
  3. For each request, verify signature:
    • Create signing input: base64(header) + "." + base64(payload)
    • Verify SHA256 signature with public key
  4. Check exp claim against current time
  5. 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-Identity from sub claim
  • Inject X-ZT-Roles from roles claim (if present)
  • Add X-Forwarded-For with client IP

Steps:

  1. Before validation, strip all X-ZT-* headers from request
  2. After validation, extract identity claims from payload
  3. Create new headers:
    • X-ZT-Identity: Subject from JWT
    • X-ZT-Roles: Comma-separated roles
    • X-ZT-Verified: “true”
    • X-ZT-Token-Exp: Expiration timestamp
  4. 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:

  1. Implement structured logger with JSON output
  2. Log every request with: timestamp, source IP, path, decision, identity
  3. Handle backend connection errors (return 502 Bad Gateway)
  4. Add request timeout handling (return 504 Gateway Timeout)
  5. 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 Director function of httputil.ReverseProxy
  • Headers must be set BEFORE proxy.ServeHTTP() is called
  • Use req.Header.Set() not req.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.ReverseProxy which 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 kid header 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 sub claim)
  • 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:

  1. Short token lifetimes (15-60 minutes) limit exposure window
  2. Token revocation list (TRL) checked on each request
  3. Token introspection endpoint (slower, for high-security)
  4. 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:

  1. Fail-closed: Backend should only accept connections from proxy (via network rules or mTLS)
  2. High availability: Run multiple proxy instances behind a load balancer
  3. 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:

  1. Network isolation: Backend only accepts connections from proxy IP
  2. mTLS: Proxy presents certificate to backend (Project 4)
  3. Shared secret: Proxy signs injected headers (less common)
  4. 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:

  1. Token still valid (not expired, not revoked)
  2. Device posture still healthy (via PDP call)
  3. Risk signals (unusual behavior, location change)
  4. If any check fails, terminate the session

Question 10: Walk me through a request from client to backend.

Expected Flow:

  1. Client sends HTTPS request with JWT to proxy
  2. Proxy terminates TLS, extracts JWT from Authorization header
  3. Proxy verifies signature using cached public key
  4. Proxy checks expiration, audience, issuer claims
  5. Proxy strips any existing X-ZT-* headers (sanitization)
  6. Proxy injects X-ZT-Identity, X-ZT-Roles from verified token
  7. Proxy forwards request to backend over internal network (or mTLS)
  8. Backend trusts X-ZT-* headers, processes request
  9. 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:

  1. Project 2: Policy Decision Engine - Build the “brain” that makes authorization decisions. Your PEP will call this PDP for complex authorization logic.

  2. Project 3: Host-Level Micro-segmentation - Ensure your backend can ONLY be accessed via your proxy by implementing network-level isolation.

  3. 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.