Securing service-to-service traffic
Apply zero trust inside the system: authenticate users at the edge with tokens, authenticate services with mTLS, and authorize every call.
- Explain zero trust and why the internal network isn’t a safe zone
- Validate JWT access tokens: signature, expiry, audience and scopes
- Authenticate services with mutual TLS and authorize calls with least privilege
In the monolith, “inside the server” meant safe. With microservices, dozens of services talk over a network - and if an attacker compromises one (say, through a vulnerable library in Recommendations), a flat, trusting internal network lets them call Payments directly. Zero trust: never trust a call just because it comes from inside; authenticate and authorize every one.
- Users sign in once with an identity provider (OAuth 2.0 / OpenID Connect) and get an access token, usually a JWT. The gateway validates it, and services check what they need.
- Services prove their identity to each other with mutual TLS (mTLS): both sides present certificates, so Payments knows the caller really is Ordering - and the traffic is encrypted. Service meshes can issue and rotate these certificates automatically.
- Authorization with least privilege: Ordering may call
POST /chargeson Payments; Recommendations may not.
1// header
2{"alg": "RS256", "kid": "noodle-key-7"}
3// payload (claims)
4{"sub": "user-123", "iss": "https://auth.galacticnoodle.space", "aud": "noodle-api",
5 "exp": 1791000000, "scope": "orders:read orders:write"}
6// signature: RS256 over header.payload with the identity provider's private keyValidating a JWT means checking, in order: the signature (with the issuer’s public key - never trust an unsigned token or alg: none), the issuer (iss), the expiry (exp), the audience (aud - a token for another API must be rejected), and then the scopes the endpoint needs. The payload is only base64-encoded, not encrypted: anyone can read it, so never put secrets in it.
1import base64
2import json
3
4token = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyLTEyMyIsImF1ZCI6Im5vb2RsZS1hcGkiLCJleHAiOjE3OTEwMDAwMDAsInNjb3BlIjoib3JkZXJzOnJlYWQifQ.c2lnbmF0dXJl"
5header, payload, signature = token.split(".")
6claims = json.loads(base64.urlsafe_b64decode(payload + "=" * (-len(payload) % 4)))
7print(claims)
8print("can write orders:", "orders:write" in claims["scope"].split()){'sub': 'user-123', 'aud': 'noodle-api', 'exp': 1791000000, 'scope': 'orders:read'}
can write orders: FalseKey takeaways
Zero trust: the internal network isn’t safe; authenticate and authorize every call.
Users get tokens (OAuth 2.0/OIDC, JWTs); validate signature, issuer, expiry and audience, then scopes.
Services authenticate each other with mTLS, often managed by a service mesh.
Grant each service only the calls it needs, and keep secrets out of tokens, images and code.
Lesson quiz
7 questions · pass with 5 correct · up to 50 XP
Passing this quiz completes the lesson and keeps your streak going. Questions you miss come back in review sessions later.
Practice: simulate microservice patterns in Python
Build small Python simulations of the patterns - routers, sagas, outboxes, circuit breakers, traces - and run them against sample inputs. They run locally in your browser; no servers or containers needed.
Check token claims
The first input line is now audience required_scope. Each following line is a JWT. (Assume the gateway already verified each signature - this exercise checks the claims.) Decode each payload (base64url, padding stripped) and check, in order: aud must equal the audience, exp must be in the future (greater than now), and the space-separated scope must include the required scope.
Print user-123: allowed or user-9: denied (expired) with the first failing reason: wrong audience, expired or missing scope orders:write.
- Four tokens
Python runs in a sandboxed browser worker with a 60 second time limit. Its runtime loads from the Pyodide CDN; your code stays in this browser.
Enforce service-to-service policy
The input has policy rules caller -> callee METHOD path-prefix (method * allows any), ---, then calls caller -> callee METHOD path. A call is allowed if any rule matches: same caller and callee, method equal or *, and the path equals the prefix or starts with prefix + /. Everything else is denied (default deny). Print ALLOW ordering -> payments POST /charges/9 or DENY ..., then denied: N of M calls.
- Noodle policy
Python runs in a sandboxed browser worker with a 60 second time limit. Its runtime loads from the Pyodide CDN; your code stays in this browser.
Questions about this lesson
Stuck? Ask. Figured something out? Share it. Explaining is one of the best ways to learn.
Loading posts…