From 16659204a0dbf13349408737ba0f0dc496209ab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Peliz=C3=A4us?= Date: Fri, 16 Oct 2026 18:26:52 +0200 Subject: [PATCH 1/3] api/services_auth: Add GetPeerCertificate helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julian Pelizäus Inspired-by: Mark Laing --- api/services_auth.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/api/services_auth.go b/api/services_auth.go index 4f0b3c652..dd31ee118 100644 --- a/api/services_auth.go +++ b/api/services_auth.go @@ -1,11 +1,13 @@ package api import ( + "crypto/x509" "errors" "fmt" "net/http" "github.com/canonical/lxd/lxd/util" + "github.com/canonical/lxd/shared/api" "github.com/canonical/lxd/shared/logger" "github.com/canonical/lxd/shared/trust" "github.com/canonical/microcluster/v3/microcluster/types" @@ -16,6 +18,12 @@ import ( // endpointHandler is just a convenience for writing clean return types. type endpointHandler func(types.State, *http.Request) types.Response +// ErrNoCertificate is returned if the request does not contain a certificate. +var ErrNoCertificate = errors.New("No certificate present in request") + +// ErrNoTLS is returned if the request does not have a TLS connection. +var ErrNoTLS = errors.New("No TLS connection state") + // authHandlerMTLS ensures a request has been authenticated using mTLS. func authHandlerMTLS(sh *service.Handler, f endpointHandler) endpointHandler { return func(s types.State, r *http.Request) types.Response { @@ -89,3 +97,16 @@ func authHandlerHMAC(sh *service.Handler, f endpointHandler) endpointHandler { return f(s, r) } } + +// GetPeerCertificate returns the first peer certificate in the request TLS connection states peer certificate list. +func GetPeerCertificate(r *http.Request) (*x509.Certificate, error) { + if r.TLS == nil { + return nil, api.StatusErrorf(http.StatusBadRequest, "%w", ErrNoTLS) + } + + if len(r.TLS.PeerCertificates) == 0 { + return nil, api.StatusErrorf(http.StatusBadRequest, "%w", ErrNoCertificate) + } + + return r.TLS.PeerCertificates[0], nil +} From 47d226e3c4c704946b6c7fc0e5a2885772610cd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Peliz=C3=A4us?= Date: Fri, 16 Oct 2026 18:27:59 +0200 Subject: [PATCH 2/3] api/serivces_auth: Use peer certificate helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes https://github.com/canonical/lxd/security/advisories/GHSA-99jp-jwmc-65p4. Signed-off-by: Julian Pelizäus --- api/services_auth.go | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/api/services_auth.go b/api/services_auth.go index dd31ee118..2a45ed234 100644 --- a/api/services_auth.go +++ b/api/services_auth.go @@ -34,22 +34,24 @@ func authHandlerMTLS(sh *service.Handler, f endpointHandler) endpointHandler { } // Use certificate based authentication between cluster members. - if r.TLS != nil { - trustedCerts := s.Truststore().RemoteCertificatesNative() - for _, cert := range r.TLS.PeerCertificates { - // First evaluate the permanent turst store. - trusted, _ := util.CheckMutualTLS(*cert, trustedCerts) - if trusted { - return f(s, r) - } + trustedCerts := s.Truststore().RemoteCertificatesNative() - // Second evaluate the temporary trust store. - // This is the fallback during the forming of the cluster. - trusted, _ = util.CheckMutualTLS(*cert, sh.TemporaryTrustStore()) - if trusted { - return f(s, r) - } - } + peerCertificate, err := GetPeerCertificate(r) + if err != nil { + return types.BadRequest(err) + } + + // First evaluate the permanent trust store. + trusted, _ := util.CheckMutualTLS(*peerCertificate, trustedCerts) + if trusted { + return f(s, r) + } + + // Second evaluate the temporary trust store. + // This is the fallback during the forming of the cluster. + trusted, _ = util.CheckMutualTLS(*peerCertificate, sh.TemporaryTrustStore()) + if trusted { + return f(s, r) } return types.Forbidden(errors.New("Failed to authenticate using mTLS")) From 1edf9059a179aea581b86bb860298bb0b83073aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Peliz=C3=A4us?= Date: Fri, 16 Oct 2026 18:29:12 +0200 Subject: [PATCH 3/3] cmd/microcloudd: Implement OnAuthenticate hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes https://github.com/canonical/lxd/security/advisories/GHSA-99jp-jwmc-65p4. Signed-off-by: Julian Pelizäus --- cmd/microcloudd/main.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/cmd/microcloudd/main.go b/cmd/microcloudd/main.go index 68f111f2c..3bb618c95 100644 --- a/cmd/microcloudd/main.go +++ b/cmd/microcloudd/main.go @@ -3,6 +3,8 @@ package main import ( "context" + "crypto/x509" + "errors" "fmt" "net/http" "os" @@ -267,6 +269,26 @@ func (c *cmdDaemon) run(cmd *cobra.Command, args []string) error { return nil }, + OnAuthenticate: func(ctx context.Context, s microTypes.State, r *http.Request, trustedCerts map[string]x509.Certificate) (bool, error) { + peerCertificate, err := api.GetPeerCertificate(r) + if err != nil { + // During explicit trust establishment there is no peer certificate as the members use HMAC authentication. + // Therefore return false (unauthenticated) without an error. + // This gives MicroCloud's HMAC handler a chance to handle the request in the next step. + if errors.Is(err, api.ErrNoCertificate) { + return false, nil + } + + return false, err + } + + trusted, fingerprint := util.CheckMutualTLS(*peerCertificate, trustedCerts) + if trusted { + logger.Debug("Authenticated request", logger.Ctx{"fingerprint": fingerprint}) + } + + return trusted, nil + }, }, ExtensionServers: map[string]microTypes.Server{