---
title: "gRPC Security: TLS, mTLS, Authentication Interceptors, and Token-Based Access Control"
description: "Securing gRPC services with TLS encryption, mutual TLS authentication, per-RPC credentials, interceptor-based auth patterns, and integration with Kubernetes service mesh and cert-manager."
url: https://agent-zone.ai/knowledge/security/grpc-security/
section: knowledge
date: 2026-02-22
categories: ["security"]
tags: ["grpc","tls","mtls","authentication","interceptors","protobuf","api-security"]
skills: ["grpc-tls-configuration","grpc-authentication","interceptor-patterns","service-security"]
tools: ["grpc","protobuf","openssl","cert-manager","istio","envoy"]
levels: ["intermediate"]
word_count: 1258
formats:
  json: https://agent-zone.ai/knowledge/security/grpc-security/index.json
  html: https://agent-zone.ai/knowledge/security/grpc-security/?format=html
  api: https://api.agent-zone.ai/api/v1/knowledge/search?q=gRPC+Security%3A+TLS%2C+mTLS%2C+Authentication+Interceptors%2C+and+Token-Based+Access+Control
---


# gRPC Security

gRPC uses HTTP/2 as its transport, which means TLS is not just a security feature — it is a practical necessity. Many load balancers, proxies, and clients expect HTTP/2 over TLS (h2) rather than plaintext HTTP/2 (h2c). Securing gRPC means configuring TLS correctly, authenticating clients, authorizing RPCs, and handling the gRPC-specific gotchas that do not exist with REST APIs.

## gRPC Over TLS

### Server-Side TLS in Go

```go
import (
    "crypto/tls"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
)

func main() {
    cert, err := tls.LoadX509KeyPair("server-cert.pem", "server-key.pem")
    if err != nil {
        log.Fatal(err)
    }

    tlsConfig := &tls.Config{
        Certificates: []tls.Certificate{cert},
        MinVersion:   tls.VersionTLS13,
    }

    creds := credentials.NewTLS(tlsConfig)
    server := grpc.NewServer(grpc.Creds(creds))

    pb.RegisterMyServiceServer(server, &myService{})

    lis, _ := net.Listen("tcp", ":50051")
    server.Serve(lis)
}
```

### Client-Side TLS in Go

```go
import (
    "crypto/x509"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
)

func main() {
    // For public CAs (Let's Encrypt, etc.), use system cert pool
    creds := credentials.NewTLS(&tls.Config{
        MinVersion: tls.VersionTLS13,
    })

    // For internal CAs, load the CA cert explicitly
    caCert, _ := os.ReadFile("ca-cert.pem")
    certPool := x509.NewCertPool()
    certPool.AppendCertsFromPEM(caCert)
    creds = credentials.NewTLS(&tls.Config{
        RootCAs:    certPool,
        MinVersion: tls.VersionTLS13,
    })

    conn, err := grpc.NewClient("api.internal:50051",
        grpc.WithTransportCredentials(creds),
    )
    defer conn.Close()

    client := pb.NewMyServiceClient(conn)
}
```

### TLS in Python

```python
import grpc

# Server
server_credentials = grpc.ssl_server_credentials(
    [(open("server-key.pem", "rb").read(), open("server-cert.pem", "rb").read())]
)
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
pb_grpc.add_MyServiceServicer_to_server(MyService(), server)
server.add_secure_port("[::]:50051", server_credentials)
server.start()

# Client
ca_cert = open("ca-cert.pem", "rb").read()
channel_credentials = grpc.ssl_channel_credentials(root_certificates=ca_cert)
channel = grpc.secure_channel("api.internal:50051", channel_credentials)
client = pb_grpc.MyServiceStub(channel)
```

## Mutual TLS for gRPC

mTLS is the strongest authentication model for service-to-service gRPC. Each service has a certificate, and both sides verify each other.

### mTLS Server in Go

```go
caCert, _ := os.ReadFile("ca-cert.pem")
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)

serverCert, _ := tls.LoadX509KeyPair("server-cert.pem", "server-key.pem")

tlsConfig := &tls.Config{
    Certificates: []tls.Certificate{serverCert},
    ClientAuth:   tls.RequireAndVerifyClientCert,
    ClientCAs:    caCertPool,
    MinVersion:   tls.VersionTLS13,
}

creds := credentials.NewTLS(tlsConfig)
server := grpc.NewServer(grpc.Creds(creds))
```

### mTLS Client in Go

```go
caCert, _ := os.ReadFile("ca-cert.pem")
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)

clientCert, _ := tls.LoadX509KeyPair("client-cert.pem", "client-key.pem")

tlsConfig := &tls.Config{
    Certificates: []tls.Certificate{clientCert},
    RootCAs:      caCertPool,
    MinVersion:   tls.VersionTLS13,
}

creds := credentials.NewTLS(tlsConfig)
conn, _ := grpc.NewClient("api.internal:50051",
    grpc.WithTransportCredentials(creds),
)
```

### mTLS in Python

```python
# Server with client cert verification
server_credentials = grpc.ssl_server_credentials(
    [(open("server-key.pem", "rb").read(), open("server-cert.pem", "rb").read())],
    root_certificates=open("ca-cert.pem", "rb").read(),
    require_client_auth=True,
)

# Client with cert
channel_credentials = grpc.ssl_channel_credentials(
    root_certificates=open("ca-cert.pem", "rb").read(),
    private_key=open("client-key.pem", "rb").read(),
    certificate_chain=open("client-cert.pem", "rb").read(),
)
channel = grpc.secure_channel("api.internal:50051", channel_credentials)
```

### Extracting Client Identity from mTLS

In mTLS, the client certificate contains the identity. Extract it in your RPC handlers:

```go
import "google.golang.org/grpc/peer"
import "google.golang.org/grpc/credentials"

func (s *myService) GetData(ctx context.Context, req *pb.Request) (*pb.Response, error) {
    p, ok := peer.FromContext(ctx)
    if !ok {
        return nil, status.Error(codes.Unauthenticated, "no peer info")
    }

    tlsInfo, ok := p.AuthInfo.(credentials.TLSInfo)
    if !ok || len(tlsInfo.State.PeerCertificates) == 0 {
        return nil, status.Error(codes.Unauthenticated, "no client certificate")
    }

    clientCert := tlsInfo.State.PeerCertificates[0]
    clientID := clientCert.Subject.CommonName
    // Use clientID for authorization decisions
}
```

## Token-Based Authentication

For cases where mTLS is impractical (external clients, mobile apps, browser-based gRPC-Web), use token-based authentication via gRPC metadata.

### Per-RPC Credentials

gRPC has a built-in mechanism for attaching credentials to every RPC:

```go
// Client: attach token to every RPC
type tokenAuth struct {
    token string
}

func (t tokenAuth) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
    return map[string]string{
        "authorization": "Bearer " + t.token,
    }, nil
}

func (t tokenAuth) RequireTransportSecurity() bool {
    return true // Refuse to send tokens over plaintext
}

conn, _ := grpc.NewClient("api.example.com:443",
    grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})),
    grpc.WithPerRPCCredentials(tokenAuth{token: "eyJhbGci..."}),
)
```

`RequireTransportSecurity() bool` returning `true` ensures the token is never sent over an unencrypted connection. This is a critical safety net.

### Server-Side Token Validation

Use a unary interceptor to validate tokens before the RPC handler runs:

```go
func authInterceptor(
    ctx context.Context,
    req interface{},
    info *grpc.UnaryServerInfo,
    handler grpc.UnaryHandler,
) (interface{}, error) {
    // Skip auth for health checks
    if info.FullMethod == "/grpc.health.v1.Health/Check" {
        return handler(ctx, req)
    }

    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return nil, status.Error(codes.Unauthenticated, "no metadata")
    }

    authHeader := md.Get("authorization")
    if len(authHeader) == 0 {
        return nil, status.Error(codes.Unauthenticated, "no authorization header")
    }

    token := strings.TrimPrefix(authHeader[0], "Bearer ")
    claims, err := validateJWT(token)
    if err != nil {
        return nil, status.Error(codes.Unauthenticated, "invalid token")
    }

    // Add claims to context for use in handler
    ctx = context.WithValue(ctx, "claims", claims)
    return handler(ctx, req)
}

server := grpc.NewServer(
    grpc.Creds(creds),
    grpc.UnaryInterceptor(authInterceptor),
)
```

### Streaming Interceptor

Streaming RPCs need a separate interceptor:

```go
func streamAuthInterceptor(
    srv interface{},
    ss grpc.ServerStream,
    info *grpc.StreamServerInfo,
    handler grpc.StreamHandler,
) error {
    md, ok := metadata.FromIncomingContext(ss.Context())
    if !ok {
        return status.Error(codes.Unauthenticated, "no metadata")
    }

    authHeader := md.Get("authorization")
    if len(authHeader) == 0 {
        return status.Error(codes.Unauthenticated, "no authorization header")
    }

    token := strings.TrimPrefix(authHeader[0], "Bearer ")
    if _, err := validateJWT(token); err != nil {
        return status.Error(codes.Unauthenticated, "invalid token")
    }

    return handler(srv, ss)
}

server := grpc.NewServer(
    grpc.Creds(creds),
    grpc.UnaryInterceptor(authInterceptor),
    grpc.StreamInterceptor(streamAuthInterceptor),
)
```

## Authorization: Per-Method Access Control

Authentication proves who the caller is. Authorization decides what they can do. Implement per-method authorization in the interceptor:

```go
// Method-level RBAC
var methodPermissions = map[string][]string{
    "/myapp.v1.UserService/GetUser":    {"user:read", "admin"},
    "/myapp.v1.UserService/DeleteUser": {"admin"},
    "/myapp.v1.DataService/Export":     {"data:export", "admin"},
}

func authzInterceptor(
    ctx context.Context,
    req interface{},
    info *grpc.UnaryServerInfo,
    handler grpc.UnaryHandler,
) (interface{}, error) {
    claims := ctx.Value("claims").(*Claims)
    requiredPerms := methodPermissions[info.FullMethod]

    if !hasAnyPermission(claims.Roles, requiredPerms) {
        return nil, status.Errorf(codes.PermissionDenied,
            "method %s requires one of %v", info.FullMethod, requiredPerms)
    }

    return handler(ctx, req)
}
```

Chain multiple interceptors using `grpc.ChainUnaryInterceptor`:

```go
server := grpc.NewServer(
    grpc.Creds(creds),
    grpc.ChainUnaryInterceptor(
        loggingInterceptor,
        authInterceptor,
        authzInterceptor,
        rateLimitInterceptor,
    ),
)
```

## gRPC Security in Kubernetes

### Service Mesh (Istio / Linkerd)

The simplest path to mTLS for gRPC in Kubernetes is a service mesh. Istio and Linkerd inject sidecar proxies that handle TLS transparently:

```yaml
# Istio: enable strict mTLS for a namespace
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
  name: default
  namespace: my-app
spec:
  mtls:
    mode: STRICT
```

With this, all gRPC traffic between services in `my-app` is encrypted with mTLS. Applications use plaintext gRPC internally — the sidecar handles encryption. This is operationally simpler than configuring TLS in every application, but adds sidecar resource overhead and debugging complexity.

### cert-manager for Application-Level TLS

If you prefer application-level TLS without a service mesh:

```yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: grpc-server-cert
  namespace: my-app
spec:
  secretName: grpc-server-tls
  duration: 720h
  renewBefore: 168h
  issuerRef:
    name: internal-ca-issuer
    kind: ClusterIssuer
  dnsNames:
    - grpc-server
    - grpc-server.my-app.svc
    - grpc-server.my-app.svc.cluster.local
  usages:
    - server auth
    - client auth
```

Mount the secret and configure the gRPC server to read certificates from the mounted path. Implement certificate file watching to pick up renewals without restart.

### Health Checks

gRPC has a standard health checking protocol. Kubernetes probes need to reach it:

```yaml
containers:
  - name: grpc-server
    ports:
      - containerPort: 50051
    livenessProbe:
      grpc:
        port: 50051
    readinessProbe:
      grpc:
        port: 50051
```

Kubernetes native gRPC probes (available since 1.24) work over plaintext. If your gRPC server requires TLS, use `grpc_health_probe` with TLS flags or expose a separate HTTP health endpoint.

## gRPC-Web and Browser Clients

Browsers cannot make native gRPC calls (no HTTP/2 trailers support). gRPC-Web is a protocol variant that works over HTTP/1.1 and HTTP/2 through a proxy.

Security considerations for gRPC-Web:

- **Always terminate TLS at the proxy.** Envoy or grpc-web-proxy sits between the browser and the gRPC backend.
- **Use CORS headers.** Browsers enforce CORS. Configure allowed origins on the proxy.
- **Token auth, not mTLS.** Browsers cannot present client certificates reliably. Use JWT or OAuth2 tokens in metadata.

```yaml
# Envoy filter for gRPC-Web
http_filters:
  - name: envoy.filters.http.grpc_web
  - name: envoy.filters.http.cors
    typed_config:
      "@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors
  - name: envoy.filters.http.router
```

## Common Mistakes

1. **Using plaintext gRPC (h2c) between services without a service mesh.** If there is no sidecar encrypting traffic, your RPCs and metadata (including auth tokens) are visible to anything on the network.
2. **Not implementing `RequireTransportSecurity()` on per-RPC credentials.** Without this, a misconfigured client silently sends tokens over plaintext connections.
3. **Skipping auth on health check endpoints.** Health probes need to reach `/grpc.health.v1.Health/Check` without credentials. Explicitly exclude health endpoints from auth interceptors.
4. **Using the same interceptor for unary and streaming RPCs.** Unary and streaming interceptors have different signatures. A unary-only interceptor does not protect streaming RPCs. Register both.
5. **Not validating the full method path in authorization.** gRPC methods are `/<package>.<service>/<method>`. A typo in the authorization map silently allows access. Log unauthorized access attempts to catch misconfigurations.

