Security & Auth
Two questions, two layers
Section titled “Two questions, two layers”Security in gRPC answers two different questions, and it’s crucial not to conflate them:
- Is the connection private and is the server who it claims to be? → transport security (TLS).
- Who is the caller, and are they allowed to make this call? → call authentication (a token, usually in metadata).
gRPC models these as two kinds of credentials: channel credentials (the transport layer) and call credentials (per-RPC auth). You almost always want both.
flowchart TB
subgraph transport["Channel credentials — TLS"]
t["encrypts the connection
+ verifies server identity
(mTLS also verifies client)"]
end
subgraph call["Call credentials — per RPC"]
c["attaches a token in metadata
identifies the caller
checked by a server interceptor"]
end
transport --> call TLS: encrypt and identify the channel
Section titled “TLS: encrypt and identify the channel”By default a plaintext channel (insecure) is fine for a local demo and unacceptable in production. TLS encrypts the connection and lets the client verify the server’s certificate, so nobody can eavesdrop or impersonate the server.
mTLS (mutual TLS) goes one step further: the server also verifies the client’s certificate. This is the common pattern for service-to-service auth inside a mesh — both ends prove their identity with certificates, and no bearer token is needed for the machine identity.
Call credentials: who is calling
Section titled “Call credentials: who is calling”TLS proves the server’s identity and the machine’s, but often you also need the user’s identity — a bearer token (JWT, OAuth) sent per call. gRPC attaches this as call credentials: the client puts an authorization token in metadata on every RPC, and a server interceptor validates it and rejects bad ones with UNAUTHENTICATED.
Call credentials are per-RPC, so different calls on the same channel can carry different tokens (e.g. one channel, many end users).
Putting them together
Section titled “Putting them together”// Channel credentials: TLS. Call credentials: a token on every RPC.creds := credentials.NewClientTLSFromCert(certPool, "")perRPC := oauth.TokenSource{TokenSource: myTokenSource}
conn, _ := grpc.NewClient( "api.example.com:443", grpc.WithTransportCredentials(creds), // TLS grpc.WithPerRPCCredentials(perRPC), // token per call)# Channel credentials: TLS. Call credentials: a token per RPC.channel_creds = grpc.ssl_channel_credentials(root_certificates=root_certs)call_creds = grpc.access_token_call_credentials(access_token)composite = grpc.composite_channel_credentials(channel_creds, call_creds)
channel = grpc.secure_channel("api.example.com:443", composite)// TLS channel credentials + a per-call metadata token.const ssl = credentials.createSsl(rootCert);const callCreds = credentials.createFromMetadataGenerator((_, cb) => { const md = new Metadata(); md.set('authorization', `Bearer ${token}`); cb(null, md);});const combined = credentials.combineChannelCredentials(ssl, callCreds);const client = new ApiClient('api.example.com:443', combined);The mistake to avoid
Section titled “The mistake to avoid”Never send a bearer token over an insecure channel. gRPC enforces this: call credentials refuse to attach to a plaintext connection, precisely because a token sent in cleartext is a token anyone on the path can steal. Call credentials require channel credentials (TLS) underneath — the framework is protecting you from the most common gRPC security bug.