Securing Server-to-Server Calls Between FastAPI and Django with JWTs and mTLS
Layer JWTs and mutual TLS for production-grade service-to-service authentication. Identity at the application layer, trust at transport—the infrastructure judgment senior engineers use.
Securing Server-to-Server Calls Between FastAPI and Django with JWTs and mTLS
Most security tutorials stop at "add a bearer token." Fine for user-facing auth, but when your FastAPI service calls your Django API—or vice versa—you're dealing with a different threat model. No human login. No browser cookie. Two machines talking, and you need to prove both identity and integrity on every request.
After watching service meshes get bolted on painfully after the fact, my recommendation is to layer two complementary mechanisms: JWTs for application-layer identity and mutual TLS (mTLS) for transport-layer trust. Neither alone is sufficient. Together, they're production-grade.
Why Both? The Layered Security Argument
JWTs answer: who is calling and what are they allowed to do? mTLS answers: is this actually the machine I think it is? A stolen JWT used from a compromised network segment is a real attack. mTLS closes that gap by requiring the caller to present a valid client certificate—one your CA issued—before the TLS handshake completes. The JWT then carries claims (service name, scopes, expiry) that the receiving service validates at the application layer.
Belt and suspenders.
Step 1: Set Up a Private Certificate Authority
Generate a private CA instead of per-service self-signed certs. This lets you rotate and revoke cleanly.
Store these in AWS Secrets Manager, HashiCorp Vault, or equivalent. Never commit them.
Step 2: Configure Django to Require Client Certificates
Django itself doesn't handle TLS; your WSGI/ASGI server does. With Gunicorn:
Extract and validate the client cert CN in middleware to confirm the caller's identity:
Register it in MIDDLEWARE before your auth middleware.
Step 3: Issue and Validate JWTs Between Services
Use RS256 with your CA's signing key. Here's the pattern with python-jose:
FastAPI side (issue before calling Django):
Make the request with both mTLS and JWT:
Django side (validate the JWT):
Return 401 if validation fails.
Step 4: Token Rotation and Operational Hygiene
A few production gotchas:
- Clock skew: Allow minor leeway (
options={"leeway": 10}in jose), but keep it under 30 seconds. - Cert rotation: Automate it. Expiring certs undetected is unnecessary pain. Use renewal jobs or cert-manager in Kubernetes.
- Short JWT expiry: Five minutes is reasonable for service-to-service. Longer expiry is just poor credential hygiene.
- Audit logging: Log
iss,sub, and request path for every validated call. You'll need it when debugging.
Putting It Together in Docker Compose
In production, use ECS secrets or AWS Parameter Store instead of volume mounts.
The Takeaway
mTLS proves the machine. JWTs prove application identity and carry scoped claims. Together they give you defense in depth without a full service mesh—often overkill until you're running dozens of services. Get this pattern solid, automate cert rotation, keep tokens short-lived, and you'll handle 95% of service-to-service security with code you can debug at 2am.
That's the goal: security you can reason about, not just security that looks good in a diagram.
Damian Hodgkiss
Senior Staff Engineer at Sumo Group, leading development of AppSumo marketplace. Technical solopreneur with 25+ years of experience building SaaS products.