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>
58 lines
1.9 KiB
Go
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}
|
|
}
|