Skip to main content
CoreSDK
Guides

Zero-Trust Microservices

Build a zero-trust service mesh where every internal service-to-service call is authenticated via short-lived JWTs, policies control which services can call which endpoints, and lateral movement is impossible. Go SDK.

Zero-Trust Microservices

What you'll build: A three-service mesh (Orders → Inventory → Payments) where every inter-service call carries a signed service JWT. No service trusts another based on network position alone. Rego policy controls which service can call which endpoint. A compromised Orders service cannot call Payments directly. Go SDK.

The Story

Shopstream runs an e-commerce platform. After a pen-test found that a compromised orders-service could directly call payments-service with no auth (both on the same VPC subnet), they adopted zero-trust. Now every service call is authenticated as if it came from the internet.

Zero-trust rules:

  • orders-service can call inventory-service — read-only
  • orders-service can call payments-service — only POST /charge with order_id header
  • inventory-service cannot call payments-service — ever
  • External user tokens are different from internal service tokens — same validator, different claims
  • Short-lived tokens (5 minute TTL) — compromise window is minimal

Architecture

User Browser
    │  user JWT (sub, roles, tenant_id)

┌─────────────────┐
│  orders-service  │  :8081
│  CoreSDK auth   │  ← validates user JWT
│       │         │
│       ▼         │
│  business logic │
│       │         │
│  service JWT    │  ← mints short-lived internal token
│       │         │
└───────┼─────────┘
        │                    ┌─────────────────────┐
        ├───────────────────►│  inventory-service  │ :8082
        │  svc JWT           │  CoreSDK auth       │
        │                    │  policy: orders→inv │
        │                    └─────────────────────┘

        │                    ┌─────────────────────┐
        └───────────────────►│  payments-service   │ :8083
           svc JWT           │  CoreSDK auth       │
           + order_id header │  policy: orders→pmt │
                             └─────────────────────┘

Prerequisites

go get github.com/coresdk-dev/sdk-go
go get github.com/golang-jwt/jwt/v5

Quickstart

git clone https://github.com/coresdk-dev/examples
cd examples/go/zero-trust-mesh
go run ./...

Code Walkthrough

Step 1 — Service identity JWT

Each service has a keypair. On startup it mints a short-lived service token:

package serviceauth

import (
    "time"
    "github.com/golang-jwt/jwt/v5"
)

type ServiceClaims struct {
    ServiceName string   `json:"service_name"`
    AllowedTargets []string `json:"allowed_targets"`
    jwt.RegisteredClaims
}

// MintServiceToken creates a 5-minute service-to-service JWT.
// In production: use a secrets manager or SPIFFE/SVID.
func MintServiceToken(serviceName string, targets []string, signingKey []byte) (string, error) {
    claims := ServiceClaims{
        ServiceName: serviceName,
        AllowedTargets: targets,
        RegisteredClaims: jwt.RegisteredClaims{
            Subject:   serviceName,
            Issuer:    "shopstream-internal",
            IssuedAt:  jwt.NewNumericDate(time.Now()),
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(5 * time.Minute)),
        },
    }
    return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(signingKey)
}

Step 2 — Every service validates callers

Each service runs the same CoreSDK middleware:

func ServiceAuthMiddleware(sdk *coresdk.SDK, allowedCallers []string) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")

            claims, err := sdk.Authorize(r.Context(), token)
            if err != nil {
                writeProblem(w, 401, "Unauthorized", "Service JWT required.")
                return
            }

            // Check caller is in the allowed list for this service
            callerService := claims.Subject
            allowed := false
            for _, a := range allowedCallers {
                if a == callerService {
                    allowed = true
                    break
                }
            }
            if !allowed {
                writeProblem(w, 403, "Forbidden",
                    fmt.Sprintf("Service '%s' is not permitted to call this endpoint.", callerService))
                return
            }

            ctx := context.WithValue(r.Context(), "caller_service", callerService)
            ctx = context.WithValue(ctx, "claims", claims)
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

Step 3 — Inventory service: only orders can call

// inventory-service/main.go
func main() {
    sdk, _ := coresdk.FromEnv()
    mux := http.NewServeMux()

    // Only orders-service can call inventory — enforced in middleware
    protected := ServiceAuthMiddleware(sdk, []string{"orders-service"})

    mux.Handle("/inventory/check", protected(http.HandlerFunc(checkInventory)))
    mux.Handle("/inventory/reserve", protected(http.HandlerFunc(reserveStock)))

    http.ListenAndServe(":8082", mux)
}

func checkInventory(w http.ResponseWriter, r *http.Request) {
    caller := r.Context().Value("caller_service").(string)
    productID := r.URL.Query().Get("product_id")

    // inventory-service trusts orders-service but still validates the data
    stock := db.GetStock(productID)
    log.Printf("inventory.check: caller=%s product=%s stock=%d", caller, productID, stock)

    json.NewEncoder(w).Encode(map[string]any{
        "product_id": productID,
        "available":  stock > 0,
        "quantity":   stock,
    })
}

Step 4 — Payments service: stricter rules via Rego

// payments-service/main.go
func main() {
    sdk, _ := coresdk.FromEnv()

    // Two layers: middleware (identity) + policy (intent)
    mux := http.NewServeMux()
    mux.HandleFunc("/charge", func(w http.ResponseWriter, r *http.Request) {
        token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
        claims, err := sdk.Authorize(r.Context(), token)
        if err != nil {
            writeProblem(w, 401, "Unauthorized", err.Error())
            return
        }

        // Policy: only orders-service can charge, and must provide order_id
        orderID := r.Header.Get("X-Order-ID")
        allowed, err := sdk.EvaluatePolicy(r.Context(), "payments.allow_charge", map[string]any{
            "caller_service": claims.Subject,
            "order_id":       orderID,
            "method":         r.Method,
            "amount_cents":   parseAmount(r),
        })
        if err != nil || !allowed {
            writeProblem(w, 403, "Forbidden",
                "Policy denied: only orders-service may charge with a valid order_id.")
            return
        }

        result := processCharge(orderID, parseAmount(r))
        json.NewEncoder(w).Encode(result)
    })

    http.ListenAndServe(":8083", mux)
}

policies/payments.rego:

package payments

default allow_charge := false

allow_charge if {
    input.caller_service == "orders-service"
    input.order_id != ""
    input.method == "POST"
    input.amount_cents > 0
    input.amount_cents <= 100_000_00  # $100,000 max per charge
}

Step 5 — Orders service: orchestrates the flow

// orders-service/main.go
func PlaceOrder(w http.ResponseWriter, r *http.Request) {
    // 1. Validate incoming user JWT
    userClaims := r.Context().Value("user_claims").(*coresdk.Claims)

    // 2. Mint a short-lived service token for downstream calls
    svcToken, err := serviceauth.MintServiceToken(
        "orders-service",
        []string{"inventory-service", "payments-service"},
        serviceSigningKey,
    )
    if err != nil {
        writeProblem(w, 500, "Internal Error", "Could not mint service token.")
        return
    }

    svcHeader := "Bearer " + svcToken

    // 3. Check inventory (orders → inventory, allowed by policy)
    inv, err := callService("http://inventory-service:8082/inventory/check?product_id="+productID,
        svcHeader, nil)
    if err != nil || !inv["available"].(bool) {
        writeProblem(w, 409, "Out of Stock", "Product is not available.")
        return
    }

    // 4. Reserve stock
    callService("http://inventory-service:8082/inventory/reserve",
        svcHeader, map[string]any{"product_id": productID, "qty": 1})

    // 5. Charge (orders → payments, allowed by policy with X-Order-ID)
    orderID := uuid.New().String()
    charge, err := callServiceWithHeader(
        "http://payments-service:8083/charge",
        svcHeader,
        "X-Order-ID", orderID,
        map[string]any{"amount_cents": req.AmountCents},
    )
    if err != nil {
        writeProblem(w, 402, "Payment Failed", "Charge was declined.")
        return
    }

    json.NewEncoder(w).Encode(map[string]any{
        "order_id": orderID,
        "status":   "confirmed",
        "charge":   charge,
        "user":     userClaims.Subject,
    })
}

What Zero-Trust Prevents

AttackHow it's blocked
Compromised inventory-service calls payments-serviceRego policy: only orders-service can charge
Stolen service token reused after 5 minJWT TTL: exp enforced by sidecar
orders-service calls payments-service without order_idRego: input.order_id != "" required
External actor spoofs service identityService signing key never leaves that service's pod
Network-adjacent service bypasses authEvery call validated — no implicit trust by subnet

Comparison: Network Trust vs Zero-Trust

# Before: network trust (VPC-only)
# Any service on the subnet can call any other service
curl http://payments-service/charge -d '{"amount":50000}'  # works! 😱

# After: zero-trust with CoreSDK
# Must carry a valid service JWT signed by orders-service's key
# Must pass Rego policy check
# Token expires in 5 minutes
curl http://payments-service/charge \
  -H "Authorization: Bearer <orders-service-jwt>" \
  -H "X-Order-ID: order-001" \
  -d '{"amount":50000}'  # only works from orders-service

Full Source

examples/go/zero-trust-mesh/

cd examples/go/zero-trust-mesh
go run ./...
# Starts all three services on :8081, :8082, :8083

On this page