This episode secures gRPC: setting up certificates and TLS on both server and client, mutual TLS for two-way authentication, and modern integration with JWT, OAuth2, and token-based auth sent via metadata.

So far all our gRPC connections have been plaintext — safe for learning, dangerous for production. Anyone on the same network could read the payloads. Episode 11 closes that gap with TLS and, for stricter needs, mutual TLS (mTLS).
Beyond encryption, we also cover identity authentication: verifying who the caller is. Two main approaches are covered: transport credentials (mTLS) and per-call credentials (JWT, OAuth2, tokens). They're often used together — TLS encrypts, tokens authorize.
For development, create a CA, server certificate, and client certificate with openssl:
openssl req -x509 -newkey rsa:2048 -nodes \
-keyout ca.key -out ca.crt -days 365 \
-subj "/CN=belajar-grpc-ca"
openssl req -newkey rsa:2048 -nodes \
-keyout server.key -out server.csr \
-subj "/CN=localhost"
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out server.crt -days 365These three commands create a Certificate Authority (CA), a server certificate request for localhost, and sign it with the CA. openssl x509 -req signs the certificate — exactly the process public CAs perform on the internet.
The server loads the certificate and private key, then creates the credentials:
creds, err := credentials.NewServerTLSFromFile("server.crt", "server.key")
if err != nil {
log.Fatal(err)
}
s := grpc.NewServer(grpc.Creds(creds))
pb.RegisterCatalogServiceServer(s, &catalogServer{})credentials.NewServerTLSFromFile("server.crt", "server.key") reads the certificate-key pair and produces the credentials grpc.NewServer needs.
The client must use TLS and trust our CA:
ca := x509.NewCertPool()
pem, _ := os.ReadFile("ca.crt")
ca.AppendCertsFromPEM(pem)
creds := credentials.NewClientTLSFromCert(ca, "localhost")
conn, _ := grpc.NewClient("localhost:50051",
grpc.WithTransportCredentials(creds))credentials.NewClientTLSFromCert(ca, "localhost") creates credentials that verify the server's certificate against our CA. Without this CA trust, the handshake fails — that's TLS doing its job.
In mTLS, it's not just the server that proves its identity; the client must also present a certificate. The server is modified to request and verify the client's certificate:
cert, _ := tls.LoadX509KeyPair("server.crt", "server.key")
ca := x509.NewCertPool()
pem, _ := os.ReadFile("ca.crt")
ca.AppendCertsFromPEM(pem)
tlsCfg := &tls.Config{
Certificates: []tls.Certificate{cert},
ClientCAs: ca,
ClientAuth: tls.RequireAndVerifyClientCert,
}
creds := credentials.NewTLS(tlsCfg)
s := grpc.NewServer(grpc.Creds(creds))The ClientAuth: tls.RequireAndVerifyClientCert setting forces the client to prove its identity with a certificate signed by the same CA. Only clients with a valid private key can connect.
The client loads its own certificate in addition to trusting the CA:
cert, _ := tls.LoadX509KeyPair("client.crt", "client.key")
creds := credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: ca,
})
conn, _ := grpc.NewClient("localhost:50051",
grpc.WithTransportCredentials(creds))credentials.NewTLS(&tls.Config{...}) gives the client a certificate to send to the server. mTLS is ideal for managed internal service-to-service communication.
Per-call credentials are sent via metadata as the authorization header:
ctx := metadata.AppendToOutgoingContext(
ctx, "authorization", "Bearer " + token,
)
res, err := client.GetProduct(ctx, &pb.ProductId{Id: "p-001"})The server extracts and verifies the token in an interceptor (episode 6):
claims := jwt.MapClaims{}
_, err := jwt.ParseWithClaims(bearer, claims, func(t *jwt.Token) (any, error) {
return publicKey, nil
})
if err != nil {
return nil, status.Error(codes.Unauthenticated, "token tidak valid")
}jwt.ParseWithClaims(...) verifies the JWT signature. The claims inside the token — like sub and scope — become the basis for authorization in episode 12.
For scenarios where the client acts on behalf of a user, use OAuth2: the client obtains an access token from the authorization server, then sends it on every call. gRPC provides credentials.OAuth to wrap the token and refresh it automatically:
perRPC := oauth.NewOauthAccess(
&oauth2.Token{AccessToken: token, TokenType: "Bearer"},
)
conn, _ := grpc.NewClient("localhost:50051",
grpc.WithTransportCredentials(creds),
grpc.WithPerRPCCredentials(perRPC),
)grpc.WithPerRPCCredentials(perRPC) makes the client add the token to every RPC automatically. The server just validates the token against the authorization server.
Key takeaways:
GRPC_TLS env from episode 7 is the doorway.In episode 12 next, we cover authorization, auditing, and security best practices — method-level authorization, policy enforcement, audit logging and protection of sensitive metadata, plus rate limiting, circuit breaking, and protection against DoS attacks. Your encrypted connections will now be equipped with comprehensive access control.