gRPC authentication

Secure gRPC connections in Go by configuring TLS credentials using the grpc/credentials package for both clients and servers.

The handshake before the message

You build a gRPC service. It compiles cleanly. The client calls the server. The response arrives in milliseconds. You push it to a shared network and suddenly strangers can invoke your methods. The pipe is open. The data flows. Nobody asked for permission. gRPC gives you a fast, binary protocol over HTTP/2, but speed means nothing if you do not control who gets to send messages. Authentication is the bouncer at the door. It decides whether a request gets processed or gets dropped before it touches your business logic.

Two layers of trust

gRPC splits security into two distinct layers. Transport credentials protect the wire. They wrap the connection in TLS so eavesdroppers see only encrypted noise. Application credentials protect the caller. They attach a token, API key, or client certificate to each request so the server knows exactly who is asking. You can use one layer, the other, or both. Most production systems use TLS for the pipe and tokens or mutual TLS for the identity. The google.golang.org/grpc/credentials package handles the cryptographic heavy lifting. You just need to know which knob to turn and where to attach it.

Transport credentials run once during the connection handshake. Application credentials run on every single RPC. That difference shapes how you design your service.

Securing the client connection

The client initiates the handshake. You tell gRPC to expect a TLS server and optionally provide a root certificate to verify the server's identity. The standard library handles certificate chains, expiration checks, and hostname validation automatically.

package main

import (
	"log"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
)

// NewSecureClient dials a gRPC server with TLS transport credentials.
func NewSecureClient(target string) (*grpc.ClientConn, error) {
	// Load system root certificates automatically.
	// Passing nil tells gRPC to trust the OS certificate store.
	creds := credentials.NewClientTLSFromCert(nil, "")

	// Dial the server with transport credentials attached to the connection.
	// The connection blocks until the TLS handshake completes.
	conn, err := grpc.Dial(target, grpc.WithTransportCredentials(creds))
	if err != nil {
		return nil, err
	}

	return conn, nil
}

func main() {
	conn, err := NewSecureClient("localhost:50051")
	if err != nil {
		log.Fatalf("connection failed: %v", err)
	}
	defer conn.Close()
}

The nil argument tells gRPC to trust whatever root certificates your operating system provides. The empty string disables strict hostname verification, which is fine for local development but dangerous in production. Replace "" with your server's domain name when you deploy.

Walking through the dial

When grpc.Dial runs, gRPC opens a TCP socket to the target address. It immediately starts an HTTP/2 preface exchange. The transport credentials kick in next. gRPC performs a standard TLS handshake using the Go crypto/tls package under the hood. The server presents its certificate. The client validates the chain against the root CA you provided. If validation passes, gRPC upgrades the socket to an encrypted HTTP/2 stream. The connection is now ready to multiplex multiple RPCs over a single TCP socket.

If the server's certificate is self-signed or issued by an internal CA, the client will reject it. The runtime returns a connection error like x509: certificate signed by unknown authority. You fix this by loading your internal CA into a x509.CertPool and passing it to credentials.NewClientTLSFromCert instead of nil.

Securing the server side

The server listens on a port and waits for incoming TCP connections. It must present a certificate and private key to prove its identity. The setup mirrors the client but flips the direction of trust.

package main

import (
	"log"
	"net"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
)

// StartSecureServer creates a gRPC server with TLS and begins listening.
func StartSecureServer(addr string) error {
	// Load the server certificate and private key from disk.
	// Both files must be readable by the running process.
	creds, err := credentials.NewServerTLSFromFile("server.crt", "server.key")
	if err != nil {
		return err
	}

	// Open a TCP listener on the specified address.
	lis, err := net.Listen("tcp", addr)
	if err != nil {
		return err
	}

	// Create the gRPC server and attach the transport credentials.
	// Every incoming connection will be forced through TLS.
	s := grpc.NewServer(grpc.Creds(creds))

	// Register your generated service implementation here.
	// pb.RegisterYourServiceServer(s, &YourServer{})

	// Serve blocks until the listener is closed or an unrecoverable error occurs.
	return s.Serve(lis)
}

The grpc.Creds option attaches the TLS configuration to the server. gRPC will reject any plain HTTP/2 connection attempt. Clients that try to dial without transport credentials will get a protocol mismatch error immediately.

Adding caller identity with tokens

Transport credentials secure the pipe. They do not tell the server who the caller is. If multiple services share the same network, you need application credentials. gRPC handles this through PerRPCCredentials. You implement an interface that returns a map of key-value pairs, and gRPC injects them into the HTTP/2 metadata for every request.

package main

import (
	"context"

	"google.golang.org/grpc/credentials"
)

// TokenCreds implements credentials.PerRPCCredentials for bearer token auth.
type TokenCreds struct {
	token string
}

// GetRequestMetadata returns the token as HTTP/2 metadata.
// The context parameter lets you inspect cancellation or deadlines.
func (t *TokenCreds) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
	// Return the token in the standard Authorization header format.
	// gRPC will attach this to every outgoing RPC on the connection.
	return map[string]string{
		"authorization": "Bearer " + t.token,
	}, nil
}

// RequireTransportSecurity tells gRPC to only send credentials over TLS.
// This prevents accidental token leakage over unencrypted connections.
func (t *TokenCreds) RequireTransportSecurity() bool {
	return true
}

You attach this to the client dial options with grpc.WithPerRPCCredentials(&TokenCreds{token: "my-secret"}). On the server side, you extract the token from the incoming context using metadata.FromIncomingContext(ctx). The context flows through your entire call stack. By convention, context.Context always goes as the first parameter, conventionally named ctx. Functions that take a context should respect cancellation and deadlines. If the client cancels the request, the server stops processing immediately.

Where the pipe breaks

Authentication failures rarely crash your program. They return errors that bubble up through your call chain. The Go community accepts verbose error handling because it makes the unhappy path visible. You will see if err != nil { return err } everywhere. That boilerplate is intentional. It forces you to acknowledge failure modes instead of hiding them.

Common failure points include certificate expiration, hostname mismatches, and missing metadata. If your server certificate expires, the TLS handshake fails before any gRPC code runs. The client logs tls: certificate has expired or is not yet valid. If you forget to pass grpc.WithPerRPCCredentials, the server receives an empty metadata map. Your interceptor or handler must check for the token and return a gRPC status error like codes.Unauthenticated.

Goroutine leaks happen when the goroutine waits on a channel that never gets closed. Always have a cancellation path. If you spawn a background goroutine to refresh tokens or monitor connection health, tie its lifecycle to a context. When the parent context cancels, the goroutine exits cleanly. The worst goroutine bug is the one that never logs.

Another subtle trap is passing pointers to strings for configuration values. Do not pass a *string. Strings are already cheap to pass by value. The compiler will copy the header, which is three machine words. Pointers add indirection without saving memory.

Choosing your authentication strategy

Use TLS transport credentials when you need to encrypt traffic between services on an untrusted network. Use mutual TLS when both sides must verify each other's identity and you want to avoid managing tokens. Use PerRPCCredentials with bearer tokens when you need fine-grained caller identity, rate limiting per user, or integration with external identity providers. Use plain HTTP/2 without credentials only for local development or internal services that sit behind a strict network firewall. Trust the type system and the credential interfaces. Do not roll your own encryption.

Where to go next