Chapter 29 — Hands-on: end-to-end authorization for SecureShop
Brief
Assemble Part 5 into one working system: SPIFFE identities for every service, mTLS + audience-scoped token exchange across three hops, a ReBAC policy engine enforcing object-level authorization consistently, RLS-backed tenant isolation in the database, and resilience controls — then attack it from a compromised service and prove the blast radius is minimal.
Setup
* kind cluster + a mesh (Istio or Linkerd) + SPIRE (or the mesh's
built-in SPIFFE).
* Services (any language): bff -> orders-api -> {catalog-api,
payments-api}, plus a data-service fronting PostgreSQL.
* OpenFGA (or SpiceDB) for the authz model.
* Keycloak (or ory/hydra) for OIDC + token exchange.
* PostgreSQL with RLS; Redis for rate limiting.
* Two tenants, a handful of users with different relationships
(owner, shared_viewer, tenant agent, tenant admin).
Tasks
1. IDENTITY (Ch 23)
[ ] SPIRE registration entries for every service (ns + sa selectors).
[ ] Strict mTLS in the mesh; AuthorizationPolicies so each service
only accepts its real callers by SPIFFE principal.
[ ] NetworkPolicy default-deny as the defense-in-depth backstop.
2. TOKEN PROPAGATION (Ch 24)
[ ] User logs in -> access token (aud=bff).
[ ] Each hop does RFC 8693 token exchange: down-scoped, aud = the
exact next service, exp ~60s, `act` chain recorded.
[ ] Every service validates sig/iss/exp/AUD==self; identity from the
token, never a header.
3. AUTHORIZATION (Ch 25)
[ ] Model SecureShop's order/invoice/tenant relations in OpenFGA.
[ ] A PEP in each service: check(subject=token.sub, relation,
object) before acting; 403 -> 404.
[ ] Keep tuples in sync from domain events (order.shared,
agent.added, ...).
[ ] Use a consistency token on security-sensitive reads.
4. TENANT DATA ISOLATION (Ch 27)
[ ] RLS + FORCE on orders/invoices; app role is non-owner,
non-superuser, no BYPASSRLS.
[ ] Pool sets `SET LOCAL app.tenant_id` per transaction from the
verified claim.
[ ] A "cross-tenant" test suite: every read path, run as tenant B,
assert zero tenant-A rows. A canary tenant + sentinel alert.
[ ] (bonus) per-tenant column encryption for invoice PII.
5. RESILIENCE (Ch 28)
[ ] Timeouts + bounded jittered retries + a retry budget on every
hop; a circuit breaker to payments-api.
[ ] Rate limits (per user/tenant/endpoint) + per-tenant quotas.
[ ] Reports/exports on a bulkheaded worker pool, async for big jobs.
6. ATTACK IT (document each)
[ ] Compromise catalog-api (assume RCE). From it:
- call payments-api with catalog-api's token -> 401 (aud).
- call the DB service directly -> mTLS identity is catalog-api,
not data-service's allowed callers -> refused; even if
reached, RLS scopes to the current tenant and PEP denies.
- forge `X-Tenant-Id`/`X-User-Id` -> ignored (identity from
token).
- read another tenant's order via the API -> OpenFGA check
denies; RLS would too.
- replay a captured token after 90s -> expired.
- retry-storm payments-api -> retry budget + circuit breaker
contain it.
[ ] Compromise the bff. Show it can only act for the currently
authenticated user, within exchanged scopes.
[ ] Forget a `WHERE tenant_id` in a new data-service query -> RLS
keeps it tenant-scoped; the cross-tenant test still fails the
build.
Deliverable
~/sec-lab/adv/reports/p5-authz.md: the request-flow diagram with identity,
token, and authz check at every hop; the OpenFGA model + representative
check/deny results; the RLS config and the cross-tenant test output; the
resilience settings; and the attack log showing each attempt blocked with the
specific control that stopped it. Plus: "the two controls doing the most work,
and what happens if each is removed".
Definition of done
- Every inter-service call is mTLS with a verified SPIFFE identity and an audience-scoped, short-lived token.
- Object-level authorization is enforced by one shared model, called by a PEP in every service; deny returns 404.
- Tenant isolation is structural (RLS + FORCE + non-privileged role);
"as tenant B" tests pass for every read path; a forgotten
WHEREis harmless. - A compromised
catalog-apicannot reach payments, cross tenants, forge identity, replay tokens, or storm downstreams. - Resilience controls (timeouts, retry budget, breaker, rate limits, quotas, bulkhead) are in place and demonstrated.
Rubric (/100)
SPIFFE identity + mesh authz + NetworkPolicy .... 15
Token exchange (scoped, aud-checked, act chain) . 20
ReBAC model + PEP in every service, consistent .. 25
Structural tenant isolation + cross-tenant tests 20
Resilience controls demonstrated ............... 10
Attack log: every attempt blocked, control named 5
Report ....................................... 5
Further reading
- Book: Microservices Security in Action (Siriwardena & Dias) — this chapter is essentially its capstone.
- Guides: OWASP "Microservices Security Cheat Sheet"; the OpenFGA "Modeling" guide worked end-to-end; Istio "Security" tasks.
- Reference: Google BeyondProd; the Zanzibar paper; RFC 8693.
End of Part 5 — Milestone check
- Every service can answer "who is calling me?" with a verified SPIFFE identity, obtained with no bootstrap secret
- I propagate end-user context across hops with audience-scoped, short-lived
exchanged tokens, and every service checks
aud - I have a single authorization model (ReBAC/Zanzibar-style) enforced by a PEP in every service, with consistency handled
- Tenant data isolation is structural (RLS + per-tenant keys + per-store
scoping), not a
WHEREclause, and I have "as tenant B" tests - I treat availability as security: rate limits, quotas, timeouts, retry budgets, circuit breakers, bulkheads, and complexity-input limits
- I built end-to-end authz for SecureShop and proved a compromised service has minimal blast radius