Files
llmgw/internal/control/agentsvc/identity.go
T
Ilker ManapandClaude Opus 5 1e3cafdc86 Add a private CA and mutual TLS for the agent stream
A node's identity was whatever the agent said it was. With mutual TLS the
control plane compares the client certificate's common name against the
claimed node id and refuses the connection when they disagree, so identity
becomes something a node proves rather than something it picks.

A public authority cannot supply these certificates: Let's Encrypt and its
peers stopped issuing the clientAuth usage, and Go verifies client chains
against it. Hence internal/tlsconf/ca.go, a small authority whose key lives
on one machine and whose root is trusted everywhere, and `llmgw ctl cert`
over it so issuing is reproducible instead of remembered openssl.

The server certificate's hosts matter more than they look: hostname
verification reads subject alternative names and ignores the common name
entirely, so a certificate without the address agents dial — bare IP
included — is rejected no matter what its subject says. There is a test for
exactly that, because it is the failure everyone hits once.

requireIdentity stays wired to false until the configuration lands, so this
commit changes no running behaviour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 07:28:56 +02:00

58 lines
1.9 KiB
Go

package agentsvc
import (
"context"
"crypto/x509"
"net/http"
)
// peerKey carries the verified client identity through to the stream handler.
//
// connect's BidiStream exposes the peer's address but not its certificate, so
// the identity is lifted out of the TLS state by middleware and put on the
// context before the connect handler ever sees the request.
type peerKey struct{}
// PeerIdentity is who the other end proved itself to be.
type PeerIdentity struct {
// CommonName is the certificate's subject CN, which is the node id a node
// is allowed to claim.
CommonName string
// Verified is false when the connection carried no client certificate,
// which only happens when TLS is switched off.
Verified bool
}
// WithPeerIdentity records a verified identity on the context.
func WithPeerIdentity(ctx context.Context, id PeerIdentity) context.Context {
return context.WithValue(ctx, peerKey{}, id)
}
// peerFrom reads the identity a request was authenticated with.
func peerFrom(ctx context.Context) PeerIdentity {
id, _ := ctx.Value(peerKey{}).(PeerIdentity)
return id
}
// AuthMiddleware lifts the client certificate's common name onto the request
// context.
//
// The certificate chain has already been verified by the TLS handshake — this
// only makes the result reachable from the handler. Without a certificate the
// identity is marked unverified and the handler decides what to do, which keeps
// the insecure development path working without a second code path through the
// service itself.
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var id PeerIdentity
if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {
id = identityOf(r.TLS.PeerCertificates[0])
}
next.ServeHTTP(w, r.WithContext(WithPeerIdentity(r.Context(), id)))
})
}
func identityOf(cert *x509.Certificate) PeerIdentity {
return PeerIdentity{CommonName: cert.Subject.CommonName, Verified: true}
}