Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable mTLS for server certificates (stable-4.0) #13705

Merged
merged 8 commits into from
Jul 11, 2024
2 changes: 1 addition & 1 deletion lxd-agent/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func authenticate(r *http.Request, cert *x509.Certificate) bool {
clientCerts := map[string]x509.Certificate{"0": *cert}

for _, cert := range r.TLS.PeerCertificates {
trusted, _ := util.CheckTrustState(*cert, clientCerts, nil, false)
trusted, _ := util.CheckMutualTLS(*cert, clientCerts)
if trusted {
return true
}
Expand Down
9 changes: 9 additions & 0 deletions lxd/certificates.go
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,16 @@ func certificatesPost(d *Daemon, r *http.Request) response.Response {
// added when in CA mode.
return response.BadRequest(fmt.Errorf("No client certificate provided"))
}

cert = r.TLS.PeerCertificates[len(r.TLS.PeerCertificates)-1]
networkCert := d.endpoints.NetworkCert()
if networkCert.CA() != nil {
// If we are in CA mode, we only allow adding certificates that are signed by the CA.
trusted, _, _ := util.CheckCASignature(*cert, networkCert)
if !trusted {
return response.Forbidden(fmt.Errorf("The certificate is not trusted by the CA or has been revoked"))
}
}

remoteHost, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions lxd/cluster/tls.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,13 @@ func tlsCheckCert(r *http.Request, networkCert *shared.CertInfo, serverCert *sha
// member before the database is available. It also allows us to switch the server certificate to
// the network certificate during cluster upgrade to per-server certificates, and it be trusted.
trustedServerCert, _ := x509.ParseCertificate(serverCert.KeyPair().Certificate[0])
trusted, _ := util.CheckTrustState(*i, map[string]x509.Certificate{serverCert.Fingerprint(): *trustedServerCert}, networkCert, false)
trusted, _ := util.CheckMutualTLS(*i, map[string]x509.Certificate{serverCert.Fingerprint(): *trustedServerCert})
if trusted {
return true
}

// Check the trusted server certficates list provided.
trusted, _ = util.CheckTrustState(*i, trustedCerts[db.CertificateTypeServer], networkCert, false)
trusted, _ = util.CheckMutualTLS(*i, trustedCerts[db.CertificateTypeServer])
if trusted {
return true
}
Expand Down
15 changes: 13 additions & 2 deletions lxd/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ func (d *Daemon) Authenticate(w http.ResponseWriter, r *http.Request) (bool, str
// Allow internal cluster traffic by checking against the trusted certfificates.
if r.TLS != nil {
for _, i := range r.TLS.PeerCertificates {
trusted, _ := util.CheckTrustState(*i, trustedCerts[db.CertificateTypeServer], d.endpoints.NetworkCert(), false)
trusted, _ := util.CheckMutualTLS(*i, trustedCerts[db.CertificateTypeServer])
if trusted {
return true, "", "cluster", nil
}
Expand Down Expand Up @@ -368,8 +368,19 @@ func (d *Daemon) Authenticate(w http.ResponseWriter, r *http.Request) (bool, str
return false, "", "", err
}

networkCert := d.endpoints.NetworkCert()
checkCertificateSignature := networkCert.CA() != nil
for _, i := range r.TLS.PeerCertificates {
trusted, username := util.CheckTrustState(*i, trustedCerts[db.CertificateTypeClient], d.endpoints.NetworkCert(), trustCACertificates)
if checkCertificateSignature {
trusted, _, username := util.CheckCASignature(*i, networkCert)
if !trusted {
return false, "", "", nil
} else if trustCACertificates {
return true, username, "tls", nil
}
}

trusted, username := util.CheckMutualTLS(*i, trustedCerts[db.CertificateTypeClient])
if trusted {
return true, username, "tls", nil
}
Expand Down
87 changes: 62 additions & 25 deletions lxd/util/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
log "gopkg.in/inconshreveable/log15.v2"

"github.com/canonical/lxd/shared"
"github.com/canonical/lxd/shared/api"
"github.com/canonical/lxd/shared/logger"
)

Expand Down Expand Up @@ -148,40 +149,76 @@ type ContextAwareRequest interface {
WithContext(ctx context.Context) *http.Request
}

// CheckTrustState checks whether the given client certificate is trusted
// (i.e. it has a valid time span and it belongs to the given list of trusted
// certificates).
func CheckTrustState(cert x509.Certificate, trustedCerts map[string]x509.Certificate, networkCert *shared.CertInfo, trustCACertificates bool) (bool, string) {
// Extra validity check (should have been caught by TLS stack)
if time.Now().Before(cert.NotBefore) || time.Now().After(cert.NotAfter) {
return false, ""
// certificateInDate returns an error if the current time is before the certificates "not before", or after the
// certificates "not after".
func certificateInDate(cert x509.Certificate) error {
now := time.Now()
if now.Before(cert.NotBefore) {
return api.StatusErrorf(http.StatusUnauthorized, "Certificate is not yet valid")
}

if networkCert != nil && trustCACertificates {
ca := networkCert.CA()
if now.After(cert.NotAfter) {
return api.StatusErrorf(http.StatusUnauthorized, "Certificate has expired")
}

if ca != nil && cert.CheckSignatureFrom(ca) == nil {
// Check whether the certificate has been revoked.
crl := networkCert.CRL()
return nil
}

if crl != nil {
for _, revoked := range crl.TBSCertList.RevokedCertificates {
if cert.SerialNumber.Cmp(revoked.SerialNumber) == 0 {
return false, "" // Certificate is revoked, so not trusted anymore.
}
}
}
// CheckCASignature returns whether the certificate is signed by the CA, whether the certificate has been revoked, and the
// certificate fingerprint.
func CheckCASignature(cert x509.Certificate, networkCert *shared.CertInfo) (trusted bool, revoked bool, fingerprint string) {
err := certificateInDate(cert)
if err != nil {
return false, false, ""
}

if networkCert == nil {
logger.Error("Failed to check certificate has been signed by the CA, no network certificate provided")
return false, false, ""
}

ca := networkCert.CA()
if ca == nil {
logger.Error("Failed to check certificate has been signed by the CA, no CA defined on network certificate")
return false, false, ""
}

// Certificate not revoked, so trust it as is signed by CA cert.
return true, shared.CertFingerprint(&cert)
err = cert.CheckSignatureFrom(ca)
if err != nil {
// Certificate not signed by CA.
return false, false, ""
}

crl := networkCert.CRL()
if crl == nil {
// No revokation list entries to check.
return true, false, shared.CertFingerprint(&cert)
}

for _, revoked := range crl.TBSCertList.RevokedCertificates {
if cert.SerialNumber.Cmp(revoked.SerialNumber) == 0 {
// Certificate has been revoked
return false, true, ""
}
}

// Check whether client certificate is in trust store.
for k, v := range trustedCerts {
// Certificate not revoked.
return true, false, shared.CertFingerprint(&cert)
}

// CheckMutualTLS checks whether the given certificate is valid and is present in the given trustedCerts map.
// Returns true if the certificate is trusted, and the fingerprint of the certificate.
func CheckMutualTLS(cert x509.Certificate, trustedCerts map[string]x509.Certificate) (bool, string) {
err := certificateInDate(cert)
if err != nil {
return false, ""
}

// Check whether client certificate is in the map of trusted certs.
for fingerprint, v := range trustedCerts {
if bytes.Compare(cert.Raw, v.Raw) == 0 {
logger.Debug("Matched trusted cert", log.Ctx{"fingerprint": k, "subject": v.Subject})
return true, k
logger.Debug("Matched trusted cert", log.Ctx{"fingerprint": fingerprint, "subject": v.Subject})
return true, fingerprint
}
}

Expand Down
2 changes: 1 addition & 1 deletion lxd/util/net.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ func ServerTLSConfig(cert *shared.CertInfo) *tls.Config {
config.RootCAs = pool
config.ClientCAs = pool

logger.Infof("LXD is in CA mode, only CA-signed certificates will be allowed")
logger.Infof("LXD is in CA mode, only CA-signed client certificates will be allowed")
}

config.BuildNameToCertificate()
Expand Down
6 changes: 6 additions & 0 deletions shared/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,12 @@ func GetTLSConfigMem(tlsClientCert string, tlsClientKey string, tlsClientCA stri
}

tlsConfig.Certificates = []tls.Certificate{cert}
tlsConfig.GetClientCertificate = func(info *tls.CertificateRequestInfo) (*tls.Certificate, error) {
// GetClientCertificate is called if not nil instead of performing the default selection of an appropriate
// certificate from the `Certificates` list. We only have one-key pair to send, and we always want to send it
// because this is what uniquely identifies the caller to the server.
return &cert, nil
}
}

var tlsRemoteCert *x509.Certificate
Expand Down
Loading
Loading