VPN vs Service Mesh: Network Security for Microservices

VPNs and service meshes both provide network security, but they operate at different layers and solve different problems. A VPN secures traffic between networks or devices at the network layer. A serv

Introduction#

VPNs and service meshes both provide network security, but they operate at different layers and solve different problems. A VPN secures traffic between networks or devices at the network layer. A service mesh secures and controls traffic between microservices at the application layer. Understanding when each is appropriate prevents over-engineering and under-securing your infrastructure.

VPN: Network-Layer Security#

1
2
3
4
5
6
7
8
9
10
11
12
13
VPN creates an encrypted tunnel between two endpoints.
Traffic within the tunnel is encrypted at the IP layer.
Use cases:
  - Engineer laptop → corporate network
  - Remote offices → data center
  - AWS VPC ↔ on-premise data center (Site-to-Site VPN)
  - External clients → private services (client VPN)

NOT designed for:
  - Service-to-service authentication within a cluster
  - Fine-grained traffic policies between microservices
  - Observability (traces, metrics, circuit breaking)
  - Canary deployments and A/B routing

Service Mesh: Application-Layer Traffic Management#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Service mesh: infrastructure layer for service-to-service communication
Implemented as sidecar proxies (Envoy, Linkerd-proxy) next to each service

Capabilities:
  - Mutual TLS (mTLS): cryptographic identity for every service
  - Traffic policies: circuit breaking, retries, timeouts
  - Observability: metrics, traces, logs for every call
  - Traffic routing: canary, A/B, weighted splits
  - Rate limiting and access control (AuthorizationPolicies)

Architecture:
  Service A (app + sidecar) → [mTLS] → Service B (app + sidecar)
  
  Control plane (Istiod / Linkerd control plane):
    - Issues certificates to sidecars (SVID / SPIFFE identity)
    - Pushes configuration to all proxies
    - Aggregates telemetry

Istio Setup#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# Install Istio
# istioctl install --set profile=production

# Enable sidecar injection in a namespace
kubectl label namespace production istio-injection=enabled

# Every pod in 'production' now gets an Envoy sidecar automatically
---
# PeerAuthentication: enforce mTLS for all services in namespace
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: production
spec:
  mtls:
    mode: STRICT  # reject plaintext connections
---
# AuthorizationPolicy: allow only api-service to call order-service
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: order-service-policy
  namespace: production
spec:
  selector:
    matchLabels:
      app: order-service
  rules:
  - from:
    - source:
        principals: ["cluster.local/ns/production/sa/api-service"]
    to:
    - operation:
        methods: ["POST", "GET"]
        paths: ["/api/orders*"]

Traffic Management with Istio#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# Canary deployment: route 10% of traffic to v2
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: order-service
spec:
  hosts:
  - order-service
  http:
  - match:
    - headers:
        x-canary:
          exact: "true"
    route:
    - destination:
        host: order-service
        subset: v2
  - route:
    - destination:
        host: order-service
        subset: v1
      weight: 90
    - destination:
        host: order-service
        subset: v2
      weight: 10
---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: order-service
spec:
  host: order-service
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2
  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 100
      http:
        http2MaxRequests: 1000
    outlierDetection:
      consecutiveErrors: 5
      interval: 30s
      baseEjectionTime: 30s

Linkerd: Simpler Alternative to Istio#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Install Linkerd (much lighter than Istio)
curl --proto '=https' --tlsv1.2 -sSfL https://run.linkerd.io/install | sh
linkerd install --crds | kubectl apply -f -
linkerd install | kubectl apply -f -

# Inject sidecar into existing deployment
kubectl get deploy -n production -o yaml \
  | linkerd inject - \
  | kubectl apply -f -

# Verify
linkerd check
linkerd viz install | kubectl apply -f -
linkerd viz dashboard &
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Linkerd ServiceProfile: circuit breaking and retries
apiVersion: linkerd.io/v1alpha2
kind: ServiceProfile
metadata:
  name: order-service.production.svc.cluster.local
  namespace: production
spec:
  routes:
  - name: POST /orders
    condition:
      method: POST
      pathRegex: /api/orders
    isRetryable: false  # don't retry POST (not idempotent)
    timeout: 5s

  - name: GET /orders
    condition:
      method: GET
      pathRegex: /api/orders/.*
    isRetryable: true
    timeout: 2s

Comparison#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Feature                  | VPN                | Service Mesh
-------------------------|--------------------|--------------------
Layer                    | Network (L3/L4)    | Application (L7)
Authentication           | IP-based           | Certificate identity (SPIFFE)
Encryption               | Network traffic    | Service-to-service (mTLS)
Observability            | None               | Full (metrics, traces, logs)
Traffic routing          | None               | Canary, A/B, weighted
Circuit breaking         | None               | Built-in
Retries/timeouts         | None               | Configurable per route
Complexity               | Low                | High
Overhead                 | Low                | ~1ms latency, ~50-100MB RAM/pod
Use case                 | Network perimeter  | Service-to-service control

When you need both:
  VPN: engineers connect to the cluster, external office connectivity
  Service mesh: services inside the cluster authenticate and communicate securely

Zero Trust with Service Mesh#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# Zero trust: default deny, explicit allow
# Every service must prove identity, no implicit trust from network location

# Step 1: Deny all by default
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: deny-all
  namespace: production
spec: {}  # empty spec = deny all

# Step 2: Explicitly allow specific paths
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: allow-api-to-orders
  namespace: production
spec:
  selector:
    matchLabels:
      app: order-service
  rules:
  - from:
    - source:
        principals: ["cluster.local/ns/production/sa/api-gateway"]
  - from:
    - source:
        principals: ["cluster.local/ns/production/sa/payment-service"]
    to:
    - operation:
        methods: ["GET"]
        paths: ["/api/orders/*/status"]  # payment service can only check status

Conclusion#

Use a VPN for network-layer security between networks and for developer access. Use a service mesh for service-to-service security, observability, and traffic management within a Kubernetes cluster. They are complementary, not competing. Istio provides the most features but adds significant operational complexity. Linkerd is the right choice for teams that want mTLS and observability without the full Istio feature set. Before adding a service mesh, ensure you genuinely need the capabilities — mTLS can also be implemented at the application layer, and not every microservice architecture requires a full mesh.

Contents