Microservices Security Patterns

July 23, 2018

Monolithic applications have a single security boundary. Microservices have many. Each service needs authentication, authorization, and secure communication. The attack surface is larger, and security patterns are different.

Here’s how to secure microservices architectures.

Authentication Patterns

API Gateway Authentication

Centralize authentication at the gateway:

Client → API Gateway → Microservices
           ↓
    Authenticate
    Validate token
    Add identity headers
# Kong/nginx configuration
routes:
  - path: /api/*
    plugins:
      - name: jwt
        config:
          key_claim_name: kid
      - name: request-transformer
        config:
          add:
            headers:
              - X-User-ID:$(jwt.sub)
              - X-User-Role:$(jwt.role)

Pros:

Cons:

Token-Based Service Authentication

Services validate tokens independently:

func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := extractToken(r)
        if token == "" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }

        claims, err := validateJWT(token)
        if err != nil {
            http.Error(w, "Invalid token", http.StatusUnauthorized)
            return
        }

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

Pros:

Cons:

Service-to-Service Authentication

Internal services need identity:

mTLS (Mutual TLS):

# Istio PeerAuthentication
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
spec:
  mtls:
    mode: STRICT

All traffic encrypted and authenticated. Services have cryptographic identity.

Service Account Tokens:

// Service gets token for calling other services
func getServiceToken() string {
    claims := jwt.MapClaims{
        "iss": "order-service",
        "sub": "order-service",
        "aud": "payment-service",
        "exp": time.Now().Add(5 * time.Minute).Unix(),
    }
    token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
    return token.SignedString(servicePrivateKey)
}

Authorization Patterns

Centralized Authorization Service

Dedicated service makes authorization decisions:

Service → Authorization Service → Decision
            ↓
    Policy Engine (OPA)
            ↓
        Policies
// Check authorization
func isAuthorized(ctx context.Context, action, resource string) bool {
    userID := getUserID(ctx)

    resp, err := authzClient.Check(ctx, &CheckRequest{
        Subject:  userID,
        Action:   action,
        Resource: resource,
    })

    return err == nil && resp.Allowed
}

Pros:

Cons:

Embedded Authorization

Policies evaluated within services:

// Open Policy Agent embedded
var policy = `
package authz

default allow = false

allow {
    input.user.role == "admin"
}

allow {
    input.user.id == input.resource.owner_id
    input.action == "read"
}
`

func isAuthorized(user User, action string, resource Resource) bool {
    input := map[string]interface{}{
        "user":     user,
        "action":   action,
        "resource": resource,
    }

    result, _ := rego.New(
        rego.Query("data.authz.allow"),
        rego.Module("authz.rego", policy),
    ).Eval(ctx, rego.EvalInput(input))

    return result[0].Expressions[0].Value.(bool)
}

Pros:

Cons:

Token-Embedded Permissions

Include permissions in JWT:

{
  "sub": "user_123",
  "roles": ["user", "premium"],
  "permissions": ["read:orders", "write:orders", "read:products"],
  "tenant_id": "tenant_456"
}
func hasPermission(ctx context.Context, required string) bool {
    claims := getClaims(ctx)
    for _, perm := range claims.Permissions {
        if perm == required {
            return true
        }
    }
    return false
}

Pros:

Cons:

Secure Communication

Service Mesh Encryption

Let infrastructure handle encryption:

# Istio - encrypt all internal traffic
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: production
spec:
  mtls:
    mode: STRICT
---
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: order-service-policy
spec:
  selector:
    matchLabels:
      app: order-service
  rules:
  - from:
    - source:
        principals: ["cluster.local/ns/production/sa/api-gateway"]
    to:
    - operation:
        methods: ["GET", "POST"]
        paths: ["/orders/*"]

Benefits:

Application-Level Encryption

Encrypt sensitive data end-to-end:

// Encrypt before sending
func sendToPaymentService(card CardDetails) error {
    encrypted, err := encrypt(card, paymentServicePublicKey)
    if err != nil {
        return err
    }
    return paymentClient.ProcessCard(encrypted)
}

// Decrypt in payment service
func ProcessCard(encrypted []byte) error {
    card, err := decrypt(encrypted, privateKey)
    if err != nil {
        return err
    }
    return processCard(card)
}

Defense in depth: even if network is compromised, data is protected.

Secrets Management

Don’t Hardcode Secrets

Never:

// Never do this
var apiKey = "sk_live_abc123"
var dbPassword = "production_password"

External Secrets Management

Use HashiCorp Vault or similar:

// Vault client
func getSecret(path string) (string, error) {
    secret, err := vaultClient.Logical().Read(path)
    if err != nil {
        return "", err
    }
    return secret.Data["value"].(string), nil
}

func connectDB() (*sql.DB, error) {
    password, err := getSecret("secret/data/db/password")
    if err != nil {
        return nil, err
    }
    return sql.Open("postgres", fmt.Sprintf(
        "host=db user=app password=%s dbname=app",
        password,
    ))
}

Short-Lived Credentials

Prefer dynamic secrets:

// Vault generates temporary database credentials
creds, err := vaultClient.Logical().Read("database/creds/app-role")
username := creds.Data["username"].(string)
password := creds.Data["password"].(string)
// Credentials auto-expire after TTL

Kubernetes Secrets

Inject secrets via environment or volume:

apiVersion: v1
kind: Pod
spec:
  containers:
  - name: app
    envFrom:
    - secretRef:
        name: app-secrets
    volumeMounts:
    - name: secrets
      mountPath: /etc/secrets
      readOnly: true
  volumes:
  - name: secrets
    secret:
      secretName: app-secrets

Defense in Depth

Network Policies

Restrict traffic between services:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: order-service
spec:
  podSelector:
    matchLabels:
      app: order-service
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: api-gateway
    ports:
    - port: 8080
  egress:
  - to:
    - podSelector:
        matchLabels:
          app: payment-service
    ports:
    - port: 8080
  - to:
    - podSelector:
        matchLabels:
          app: database
    ports:
    - port: 5432

Default deny, explicit allow.

Input Validation

Every service validates input:

type OrderRequest struct {
    UserID    string  `json:"user_id" validate:"required,uuid"`
    ProductID string  `json:"product_id" validate:"required,uuid"`
    Quantity  int     `json:"quantity" validate:"required,min=1,max=100"`
    Amount    float64 `json:"amount" validate:"required,min=0.01"`
}

func createOrder(r *http.Request) (*Order, error) {
    var req OrderRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        return nil, err
    }

    if err := validate.Struct(req); err != nil {
        return nil, err
    }

    // Trust nothing, validate everything
    return processOrder(req)
}

Least Privilege

Services get minimum required permissions:

# Kubernetes ServiceAccount with minimal RBAC
apiVersion: v1
kind: ServiceAccount
metadata:
  name: order-service
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: order-service
rules:
- apiGroups: [""]
  resources: ["configmaps"]
  resourceNames: ["order-service-config"]
  verbs: ["get"]
---
# Database user with minimal permissions
GRANT SELECT, INSERT, UPDATE ON orders TO order_service;
GRANT SELECT ON products TO order_service;
-- No DELETE, no other tables

Audit Logging

Log security-relevant events:

func auditLog(ctx context.Context, event AuditEvent) {
    logger.WithFields(log.Fields{
        "event_type": event.Type,
        "actor":      event.Actor,
        "resource":   event.Resource,
        "action":     event.Action,
        "outcome":    event.Outcome,
        "timestamp":  time.Now(),
        "request_id": getRequestID(ctx),
        "client_ip":  event.ClientIP,
    }).Info("Audit event")
}

// Log all authorization decisions
func authorize(ctx context.Context, action, resource string) bool {
    allowed := checkPermission(ctx, action, resource)
    auditLog(ctx, AuditEvent{
        Type:     "authorization",
        Actor:    getUserID(ctx),
        Resource: resource,
        Action:   action,
        Outcome:  allowed,
    })
    return allowed
}

Key Takeaways

Microservices security requires more effort than monoliths, but the patterns exist. Apply them consistently across all services.