Privacy Tools Guide

Secure Microservice Communication Patterns

In a microservice architecture, the network between services is as much an attack surface as the internet-facing edge. A compromised service can pivot to any other service on the same network unless you enforce authentication and authorization between them. This guide covers four patterns from basic to production-grade zero trust.

The Problem: Flat Internal Networks

In a default Kubernetes cluster or Docker network, all pods can reach all other pods on any port. If attacker code runs in any service — via a supply chain attack, SSRF, or RCE — they have unfettered access to every database, internal API, and message queue.

Without isolation:
[Auth Service] ──can reach──> [Payment Service] ──can reach──> [User DB]
                                                ──can reach──> [Config Service]
[Compromised Service] ──can reach──> ALL of the above

Pattern 1: mTLS (Mutual TLS)

mTLS requires both client and server to present certificates. A service without a valid certificate cannot connect to another service — even within the same cluster.

Manual mTLS with Go

// server.go — service requiring client certificate
package main

import (
    "crypto/tls"
    "crypto/x509"
    "net/http"
    "os"
    "log"
)

func main() {
    // Load CA that signed client certs
    caCert, _ := os.ReadFile("/certs/ca.crt")
    caCertPool := x509.NewCertPool()
    caCertPool.AppendCertsFromPEM(caCert)

    tlsConfig := &tls.Config{
        ClientAuth: tls.RequireAndVerifyClientCert,
        ClientCAs:  caCertPool,
        MinVersion: tls.VersionTLS13,
    }

    server := &http.Server{
        Addr:      ":8443",
        TLSConfig: tlsConfig,
        Handler:   http.HandlerFunc(handler),
    }

    log.Fatal(server.ListenAndServeTLS("/certs/server.crt", "/certs/server.key"))
}

func handler(w http.ResponseWriter, r *http.Request) {
    // Client identity from verified certificate
    clientCN := r.TLS.PeerCertificates[0].Subject.CommonName
    log.Printf("Authenticated client: %s", clientCN)
    w.Write([]byte("OK"))
}
// client.go — service presenting its certificate
package main

import (
    "crypto/tls"
    "crypto/x509"
    "net/http"
    "os"
)

func newMTLSClient() *http.Client {
    cert, _ := tls.LoadX509KeyPair("/certs/client.crt", "/certs/client.key")
    caCert, _ := os.ReadFile("/certs/ca.crt")
    caCertPool := x509.NewCertPool()
    caCertPool.AppendCertsFromPEM(caCert)

    return &http.Client{
        Transport: &http.Transport{
            TLSClientConfig: &tls.Config{
                Certificates: []tls.Certificate{cert},
                RootCAs:      caCertPool,
                MinVersion:   tls.VersionTLS13,
            },
        },
    }
}

Certificate issuance for mTLS: each service needs a cert. At scale, automate with cert-manager (Kubernetes) or SPIFFE/SPIRE.


Pattern 2: SPIFFE/SPIRE (Identity Framework)

SPIFFE (Secure Production Identity Framework for Everyone) assigns cryptographic identities to workloads, not to machines. A service running on pod X gets a SPIFFE ID like spiffe://cluster.local/ns/payments/sa/payment-service.

SPIRE is the reference implementation. It issues X.509 SVIDs (SPIFFE Verifiable Identity Documents) to workloads that automatically rotate every hour.

# Install SPIRE server and agent on Kubernetes
kubectl apply -f https://github.com/spiffe/spire/releases/latest/download/spire-server.yaml
kubectl apply -f https://github.com/spiffe/spire/releases/latest/download/spire-agent.yaml

# Register a workload
kubectl exec -n spire spire-server-0 -- \
  spire-server entry create \
  -spiffeID spiffe://example.org/ns/default/sa/payment-service \
  -parentID spiffe://example.org/ns/spire/sa/spire-agent \
  -selector k8s:ns:default \
  -selector k8s:sa:payment-service

Workloads fetch their SVID via the SPIFFE Workload API (Unix socket):

import "github.com/spiffe/go-spiffe/v2/workloadapi"

func getX509Source() *workloadapi.X509Source {
    ctx := context.Background()
    source, err := workloadapi.NewX509Source(ctx)
    if err != nil {
        log.Fatal(err)
    }
    return source
}

// Use SPIFFE-aware TLS — SVID rotates automatically
tlsConfig := tlsconfig.MTLSServerConfig(source, source,
    tlsconfig.AuthorizeID(spiffeid.RequireIDFromString(
        "spiffe://example.org/ns/default/sa/auth-service")))

Pattern 3: Service Mesh (Istio/Linkerd)

A service mesh injects a sidecar proxy (Envoy for Istio, ultra-lightweight proxy for Linkerd) into every pod. The sidecar handles mTLS transparently — application code does not need to implement TLS at all.

Istio setup:

# Install Istio
curl -L https://istio.io/downloadIstio | ISTIO_VERSION=1.21.0 sh -
istioctl install --set profile=default -y

# Enable mTLS globally (STRICT = reject plaintext)
kubectl apply -f - <<'EOF'
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: istio-system
spec:
  mtls:
    mode: STRICT
EOF

Authorization Policy — restrict which services can call which:

apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: payment-service-policy
  namespace: default
spec:
  selector:
    matchLabels:
      app: payment-service
  rules:
    - from:
        - source:
            principals:
              # Only auth-service and order-service can call payment-service
              - "cluster.local/ns/default/sa/auth-service"
              - "cluster.local/ns/default/sa/order-service"
      to:
        - operation:
            methods: ["POST"]
            paths: ["/payment/*"]

Pattern 4: Service-to-Service JWT

When mTLS infrastructure is too heavy, use short-lived JWTs for service identity. Each service has an asymmetric key pair; it signs JWTs and presents them to downstream services.

# service_auth.py — shared library for service-to-service JWT
import jwt, time, httpx
from pathlib import Path
from functools import lru_cache

SERVICE_NAME   = "auth-service"
PRIVATE_KEY    = Path("/run/secrets/service_private_key.pem").read_text()
PUBLIC_KEYS_URL = "http://internal-jwks:8080/.well-known/jwks.json"

def create_service_token(target_service: str) -> str:
    now = int(time.time())
    return jwt.encode({
        "iss": SERVICE_NAME,
        "sub": SERVICE_NAME,
        "aud": target_service,
        "iat": now,
        "exp": now + 300,   # 5-minute TTL
    }, PRIVATE_KEY, algorithm="RS256")

@lru_cache(maxsize=1, typed=False)
def get_public_keys() -> dict:
    return httpx.get(PUBLIC_KEYS_URL).json()

def verify_service_token(token: str, expected_issuer: str) -> dict:
    jwks = get_public_keys()
    # Find the key matching the token's kid header
    header = jwt.get_unverified_header(token)
    key = next(k for k in jwks["keys"] if k["kid"] == header["kid"])
    return jwt.decode(
        token,
        jwt.algorithms.RSAAlgorithm.from_jwk(key),
        algorithms=["RS256"],
        audience=SERVICE_NAME,
        issuer=expected_issuer,
    )

Kubernetes Network Policy

Regardless of which auth pattern you use, enforce network policy to restrict traffic at the pod level:

# network-policy.yaml — payment-service only accepts from auth and order
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: payment-service-netpol
  namespace: default
spec:
  podSelector:
    matchLabels:
      app: payment-service
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: auth-service
        - podSelector:
            matchLabels:
              app: order-service
      ports:
        - protocol: TCP
          port: 8443
  egress:
    - to:
        - podSelector:
            matchLabels:
              app: user-database
      ports:
        - protocol: TCP
          port: 5432
    - ports:
        - protocol: UDP
          port: 53   # DNS

Network policy is enforced by the CNI plugin (Calico, Cilium, Weave Net). It provides defense in depth even if the service-layer auth is bypassed.


Choosing the Right Pattern

Scenario Pattern
Greenfield Kubernetes cluster Istio or Linkerd (transparent mTLS)
Existing services, can’t add sidecars Manual mTLS or service-to-service JWT
Strict identity rotation requirements SPIFFE/SPIRE
Legacy non-containerized services mTLS with cert-manager or Vault PKI
Basic hardening for small teams Network Policy + service-to-service JWT

Start with Network Policy for immediate blast-radius reduction, then layer in mTLS as the authoritative authentication mechanism.



Built by theluckystrike — More at zovo.one