Skip to content

Confirm a connection is actually encrypted

Dial returns the same *grpc.ClientConn whether it built insecure credentials or TLS ones, and it logs nothing either way. Nothing in the type tells you which you got. Since a caller-supplied dial option can also override the credentials Dial derived, "we set TLS.Enabled" is not proof that the wire is encrypted.

Ask the connection instead.

Read the peer's auth info on a call

Pass grpc.Peer as a call option and inspect AuthInfo after the RPC returns. A TLS connection yields a credentials.TLSInfo; an insecure one does not.

import (
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
    "google.golang.org/grpc/peer"
)

var pr peer.Peer

resp, err := client.Check(ctx, req, grpc.Peer(&pr))
if err != nil {
    return err
}

info, ok := pr.AuthInfo.(credentials.TLSInfo)
if !ok {
    return fmt.Errorf("connection to %s is not encrypted (auth info %T)", pr.Addr, pr.AuthInfo)
}

log.Info("connection secured",
    "version", info.State.Version,
    "cipher", tls.CipherSuiteName(info.State.CipherSuite),
    "peer", info.State.PeerCertificates[0].Subject.CommonName,
)

Against a plaintext server the type assertion fails and pr.AuthInfo prints as insecure.info. Against a TLS server you get the negotiated version and suite — for example TLS 1.3 (0x0304) with TLS_AES_128_GCM_SHA256.

The peer is only populated once the RPC completes, which is the point: it reports what the transport actually negotiated, not what the Target asked for.

Why the negotiated cipher may not be in the go/tls list

go/tls's hardened config names six TLS 1.2 AEAD suites, but Go applies CipherSuites to TLS 1.2 handshakes only. When both ends support TLS 1.3 — the usual case — the connection negotiates a TLS 1.3 suite from Go's own fixed set and the list is not consulted. Seeing TLS_AES_128_GCM_SHA256 is correct and stronger, not a misconfiguration.

Assert it once, at startup

Rather than checking on every call, make it a startup gate: dial, issue one cheap RPC (the gRPC health check is ideal), assert on the auth info, and refuse to start if a production target came back unencrypted.

func assertEncrypted(ctx context.Context, conn *grpc.ClientConn) error {
    var pr peer.Peer

    _, err := grpc_health_v1.NewHealthClient(conn).Check(
        ctx, &grpc_health_v1.HealthCheckRequest{}, grpc.Peer(&pr),
    )
    if err != nil {
        return err
    }

    if _, ok := pr.AuthInfo.(credentials.TLSInfo); !ok {
        return fmt.Errorf("refusing to use unencrypted connection to %s", pr.Addr)
    }

    return nil
}

This also flushes out the lazy-connection surprise described in Dial does not connect to anything: if the server is unreachable or its certificate does not verify, you find out at startup rather than on the first real request.