Skip to content

Add auth, security headers & a gateway

The transport servers are composed from small building blocks: authentication and security-header middleware from transport/http (and AuthInterceptor from transport/grpc), stitched together with a go/transit Chain, plus an optional grpc-gateway.

Authenticate HTTP requests

AuthMiddleware verifies each request against a go/authn verifier and stores the identity in the request context. It is an ordinary transit Middleware, so it composes into a Chain:

import (
    "gitlab.com/phpboyscout/go/authn"
    transithttp "gitlab.com/phpboyscout/go/transit/http"
    transporthttp "gitlab.com/phpboyscout/go/transport/http"
)

auth, err := transporthttp.AuthMiddleware(
    transporthttp.WithBearerVerifier(jwtVerifier),   // authn.Verifier
    transporthttp.WithAuthLogger(log),
)
if err != nil {
    return err
}

chain := transithttp.NewChain(auth, transporthttp.SecurityHeadersMiddleware())
srv, _ := transporthttp.Register(ctx, "api", controller, log, handler, settings,
    transporthttp.WithTLSPair(pair), transporthttp.WithMiddleware(chain))

On failure it writes a generic 401/403 with the credential redacted from logs. Read the verified identity downstream with transporthttp.IdentityFromContext(ctx).

Add security headers

SecurityHeadersMiddleware sets a secure baseline (X-Content-Type-Options, X-Frame-Options, Referrer-Policy, and optionally HSTS/CSP). Tune it with options:

sec := transporthttp.SecurityHeadersMiddleware(
    transporthttp.WithHSTS(365*24*time.Hour, true, true),
    transporthttp.WithContentSecurityPolicy("default-src 'self'"),
    transporthttp.WithFrameOptions("DENY"),
)

Authenticate gRPC calls

transport/grpc mirrors the HTTP auth surface as an interceptor:

import (
    transitgrpc "gitlab.com/phpboyscout/go/transit/grpc"
    transportgrpc "gitlab.com/phpboyscout/go/transport/grpc"
)

ic, err := transportgrpc.AuthInterceptor(
    transportgrpc.WithGRPCBearerVerifier(jwtVerifier),
    transportgrpc.WithGRPCMethodSkipper(func(m string) bool { return m == "/admin.v1.Admin/Ping" }),
)
if err != nil {
    return err
}

srv, err := transportgrpc.Register("rpc", controller, log, settings,
    transportgrpc.WithTLSPair(pair),
    transportgrpc.WithInterceptors(transitgrpc.NewInterceptorChain(ic)),
)

WithInterceptors is a registration option, so it goes to Register. NewServer builds a bare server and returns an error if handed one. The skipper adds exemptions on top of the health and reflection services, which the interceptor already skips whatever you pass.

Require client certificates (mTLS)

Server-side mTLS is config-driven end to end through the go/tls Pair: set ClientCAs (PEM CA files client certificates must chain to) and ClientAuth (request, verify-if-given, or require-verify) on the pair you pass to Register via WithTLSPair, and pair it with the mTLS verifier on the auth middleware/interceptor:

pair := gtls.Pair{
    Enabled:    true,
    Cert:       "/etc/certs/server.pem",
    Key:        "/etc/certs/server-key.pem",
    ClientCAs:  []string{"/etc/certs/client-ca.pem"},
    ClientAuth: gtls.ClientAuthRequireVerify,
}

// HTTP: the handler sees non-empty r.TLS.VerifiedChains, and WithMTLSVerifier
// authenticates the request from them.
auth, _ := transporthttp.AuthMiddleware(transporthttp.WithMTLSVerifier(authn.NewMTLSVerifier()))

// gRPC: WithGRPCMTLSVerifier sees the verified chains via peer.FromContext.
ic, _ := transportgrpc.AuthInterceptor(transportgrpc.WithGRPCMTLSVerifier(authn.NewMTLSVerifier()))

Both managed paths honour a caller-supplied server TLS config (transporthttp.WithServerTLSConfig / transportgrpc.WithServerTLSConfig) by merging the pair into it rather than replacing it: the pair contributes its certificate (and, on the gRPC listener, ALPN h2), while everything the caller set (MinVersion, verification hooks, a client-certificate policy) survives. Precedence: a config that already carries ClientCAs or a non-zero ClientAuth wins over the pair's ClientCAs/ClientAuth fields.

Serve TLS from an in-memory certificate

A gtls.Pair describes a certificate by file path. When the certificate is minted in memory and never written to disk (a self-signed fallback used when the local CA cannot be installed into the system trust store, an in-memory ACME/autocert certificate, a short-lived issuer, or a test), pass it with WithServedCertificate and leave the pair disabled:

cert, _ := someIssuer.Mint() // *tls.Certificate, held in RAM

_, err := transporthttp.Register(
    ctx, "studio", controller, logger, handler,
    transporthttp.ServerSettings{Port: 8443},
    transporthttp.WithServedCertificate(cert),     // serve TLS from RAM, no pair
)

The server serves over TLS even though no pair is enabled, so there is no need to persist a throwaway certificate to disk to satisfy the pair shape. The certificate is appended to the server's TLS config, so a config supplied via WithServerTLSConfig keeps its MinVersion, verification hooks, and any client-certificate policy; the passed *tls.Certificate is not mutated. A certificate placed directly on a WithServerTLSConfig is likewise served: populated certificate material is treated as TLS intent.

Expose a gRPC service over REST with the gateway

transport/gateway wraps a gRPC connection in a grpc-gateway http.Handler. Give it a connection (dial the gRPC server with go/grpcclient or transport/grpc.DialLocal) and your generated registration function:

import transportgateway "gitlab.com/phpboyscout/go/transport/gateway"

conn, _ := transportgrpc.DialLocal(grpcSettings, grpcPair)
handler, err := transportgateway.New(ctx, conn,
    func(ctx context.Context, mux *runtime.ServeMux, c *grpc.ClientConn) error {
        return widgetv1.RegisterWidgetServiceHandler(ctx, mux, c)
    },
    transportgateway.WithMiddleware(chain), // optional transit Chain over the REST handler
)

The gateway handler is a plain http.Handler, so serve it through transport/http like any other. The whole stack composes.

When the gRPC server requires client certificates, WithDialOptions is the escape hatch for the gateway's outbound dial: pass grpc.WithTransportCredentials(credentials.NewTLS(cfg)) with a client *tls.Config carrying the gateway's client certificate and the CA pool that signed the server's certificate. An explicit transport-credentials option overrides the automatically selected transport security.

Where the full option lists live

This page composes the pieces; the exhaustive tables (every option, its default, and what happens when a value is wrong) are in the reference:

  • HTTP server reference: AuthMiddleware options, credential precedence, the exact 401/403 responses, and the security-header defaults.
  • gRPC server reference: AuthInterceptor options, and which methods skip authentication whatever you configure.
  • TLS settings reference: the gtls.Pair fields, the ClientAuth modes, and which TLS setting wins when two disagree.
  • Gateway reference: what New and Register each own, including who closes the gRPC connection.

Two constraints that bite here and are stated in full under What transport does not do: gRPC reflection cannot be authenticated, and an enabled TLS pair must not be combined with your own grpc.Creds.