diff --git a/lxd-agent/server.go b/lxd-agent/server.go index a6739ba4f370..c7c5111f7848 100644 --- a/lxd-agent/server.go +++ b/lxd-agent/server.go @@ -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 } diff --git a/lxd/certificates.go b/lxd/certificates.go index ea0379a9db97..051043d9ba69 100644 --- a/lxd/certificates.go +++ b/lxd/certificates.go @@ -621,6 +621,14 @@ func certificatesPost(d *Daemon, r *http.Request) response.Response { } 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")) + } + } } else { return response.BadRequest(fmt.Errorf("Can't use TLS data on non-TLS link")) } diff --git a/lxd/cluster/tls.go b/lxd/cluster/tls.go index ad02b5239202..77defbbdce34 100644 --- a/lxd/cluster/tls.go +++ b/lxd/cluster/tls.go @@ -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 } diff --git a/lxd/daemon.go b/lxd/daemon.go index db31d392c22d..ad64b13df3f6 100644 --- a/lxd/daemon.go +++ b/lxd/daemon.go @@ -298,7 +298,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, fingerprint := util.CheckTrustState(*i, trustedCerts[db.CertificateTypeServer], d.endpoints.NetworkCert(), false) + trusted, fingerprint := util.CheckMutualTLS(*i, trustedCerts[db.CertificateTypeServer]) if trusted { return true, fingerprint, "cluster", nil } @@ -370,8 +370,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 } diff --git a/lxd/util/http.go b/lxd/util/http.go index 0e88e5a9f0b8..0b285369d050 100644 --- a/lxd/util/http.go +++ b/lxd/util/http.go @@ -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" ) @@ -148,37 +149,72 @@ 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). -// Returns whether or not the certificate is trusted, and the fingerprint of the certificate. -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. + // 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": fingerprint, "subject": v.Subject}) diff --git a/lxd/util/net.go b/lxd/util/net.go index 4d5141716288..3052adaa1428 100644 --- a/lxd/util/net.go +++ b/lxd/util/net.go @@ -111,7 +111,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() diff --git a/shared/network.go b/shared/network.go index b681e29afcc6..a8bf64fba803 100644 --- a/shared/network.go +++ b/shared/network.go @@ -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 diff --git a/test/suites/pki.sh b/test/suites/pki.sh index ad6aec8d741d..fbaa124c5f6b 100644 --- a/test/suites/pki.sh +++ b/test/suites/pki.sh @@ -1,14 +1,21 @@ +# shellcheck disable=2031 test_pki() { if [ ! -d "/usr/share/easy-rsa/" ]; then echo "==> SKIP: The pki test requires easy-rsa to be installed" return fi + cert_fingerprint() { + openssl x509 -noout -fingerprint -sha256 -in "${1}" | sed 's/.*=//; s/://g; s/\(.*\)/\L\1/' + } + # Setup the PKI. cp -R /usr/share/easy-rsa "${TEST_DIR}/pki" ( set -e cd "${TEST_DIR}/pki" + export EASYRSA_KEY_SIZE=4096 + # shellcheck disable=SC1091 if [ -e pkitool ]; then . ./vars @@ -23,12 +30,13 @@ test_pki() { echo "lxd" | ./easyrsa build-ca nopass ./easyrsa gen-crl ./easyrsa build-client-full lxd-client nopass - ./easyrsa build-client-full lxd-client-revoked nopass + ./easyrsa build-client-full ca-trusted nopass + ./easyrsa build-client-full prior-revoked nopass mkdir keys cp pki/private/* keys/ cp pki/issued/* keys/ cp pki/ca.crt keys/ - echo "yes" | ./easyrsa revoke lxd-client-revoked + echo "yes" | ./easyrsa revoke prior-revoked ./easyrsa gen-crl cp pki/crl.pem keys/ fi @@ -37,65 +45,183 @@ test_pki() { # Setup the daemon. LXD5_DIR=$(mktemp -d -p "${TEST_DIR}" XXX) chmod +x "${LXD5_DIR}" - cp "${TEST_DIR}/pki/keys/ca.crt" "${LXD5_DIR}/server.ca" - cp "${TEST_DIR}/pki/keys/crl.pem" "${LXD5_DIR}/ca.crl" spawn_lxd "${LXD5_DIR}" true LXD5_ADDR=$(cat "${LXD5_DIR}/lxd.addr") - # Setup the client. + # Add a certificate to the trust store that is not signed by the CA before enabling CA mode. + lxc_remote remote add pki-lxd "${LXD5_ADDR}" --accept-certificate --password foo + + # Shutdown LXD. The CA certificate and revokation list must be present at start up to enable PKI. + shutdown_lxd "${LXD5_DIR}" + cp "${TEST_DIR}/pki/keys/ca.crt" "${LXD5_DIR}/server.ca" + cp "${TEST_DIR}/pki/keys/crl.pem" "${LXD5_DIR}/ca.crl" + respawn_lxd "${LXD5_DIR}" true + + # New tmp directory for lxc client config. LXC5_DIR=$(mktemp -d -p "${TEST_DIR}" XXX) - cp "${TEST_DIR}/pki/keys/lxd-client.crt" "${LXC5_DIR}/client.crt" - cp "${TEST_DIR}/pki/keys/lxd-client.key" "${LXC5_DIR}/client.key" - cp "${TEST_DIR}/pki/keys/ca.crt" "${LXC5_DIR}/client.ca" # Confirm that a valid client certificate works. ( set -e - export LXD_CONF=${LXC5_DIR} + # shellcheck disable=2030 + export LXD_CONF="${LXC5_DIR}" + + ### CA signed certificate with `core.trust_ca_certificates` disabled. + + # Set up the client config + cp "${TEST_DIR}/pki/keys/lxd-client.crt" "${LXD_CONF}/client.crt" + cp "${TEST_DIR}/pki/keys/lxd-client.key" "${LXD_CONF}/client.key" + cp "${TEST_DIR}/pki/keys/ca.crt" "${LXD_CONF}/client.ca" + cat "${LXD_CONF}/client.crt" "${LXD_CONF}/client.key" > "${LXD_CONF}/client.pem" - # Try adding remote using an incorrect password. - # This should fail, as if the certificate is unknown and password is wrong then no access should be allowed. + # Try adding remote using an incorrect password. This should fail even though the client certificate + # has been signed by the CA because `core.trust_ca_certificates` is not enabled. ! lxc_remote remote add pki-lxd "${LXD5_ADDR}" --accept-certificate --password=bar || false # Add remote using the correct password. # This should work because the client certificate is signed by the CA. - lxc_remote remote add pki-lxd "${LXD5_ADDR}" --accept-certificate --password=foo + lxc_remote remote add pki-lxd "${LXD5_ADDR}" --accept-certificate --password foo + + # Should have trust store entry because `core.trust_ca_certificates` is disabled. lxc_remote config trust ls pki-lxd: | grep lxd-client + + # Should be able to view server config + lxc_remote info pki-lxd: | grep -F 'core.https_address' + curl -s --cert "${LXD_CONF}/client.pem" --cacert "${LXD5_DIR}/server.crt" "https://${LXD5_ADDR}/1.0" | jq -e '.metadata.config."core.https_address"' + + # Revoke the client certificate + cd "${TEST_DIR}/pki" && "${TEST_DIR}/pki/easyrsa" --batch revoke lxd-client keyCompromise && "${TEST_DIR}/pki/easyrsa" gen-crl && cd - + + # Restart LXD with the revoked certificate in the CRL. + shutdown_lxd "${LXD5_DIR}" + cp "${TEST_DIR}/pki/pki/crl.pem" "${LXD5_DIR}/ca.crl" + respawn_lxd "${LXD5_DIR}" true + + # Revoked certificate no longer has access even though it is in the trust store. + lxc_remote info pki-lxd: | grep -F 'auth: untrusted' + ! lxc_remote ls pki-lxd: || false + [ "$(curl -s --cert "${LXD_CONF}/client.pem" --cacert "${LXD5_DIR}/server.crt" "https://${LXD5_ADDR}/1.0/instances" | jq -e -r '.error')" = "not authorized" ] + + # Remove cert from truststore. + fingerprint="$(cert_fingerprint "${LXD_CONF}/client.crt")" + LXD_DIR="${LXD5_DIR}" lxc config trust remove "${fingerprint}" lxc_remote remote remove pki-lxd - # Add remote using a CA-signed client certificate, and not providing a password. - # This should succeed and tests that the CA trust is working, as adding the client certificate to the trust - # store without a trust password would normally fail. + + ### CA signed certificate with `core.trust_ca_certificates` enabled. + + # Set up the client config + cp "${TEST_DIR}/pki/keys/ca-trusted.crt" "${LXD_CONF}/client.crt" + cp "${TEST_DIR}/pki/keys/ca-trusted.key" "${LXD_CONF}/client.key" + cat "${LXD_CONF}/client.crt" "${LXD_CONF}/client.key" > "${LXD_CONF}/client.pem" + + # Enable `core.trust_ca_certificates`. LXD_DIR=${LXD5_DIR} lxc config set core.trust_ca_certificates true + + # Add remote using a CA-signed client certificate, and not providing a password. + # This should succeed because `core.trust_ca_certificates` is enabled. lxc_remote remote add pki-lxd "${LXD5_ADDR}" --accept-certificate - lxc_remote config trust ls pki-lxd: | grep lxd-client + + # Client cert should not be present in trust store. + ! lxc_remote config trust ls pki-lxd: | grep ca-trusted || false + + # Remove remote lxc_remote remote remove pki-lxd - # Add remote using a CA-signed client certificate, and providing an incorrect password. + # Add the remote again using an incorrect password. # This should succeed as is the same as the test above but with an incorrect password rather than no password. lxc_remote remote add pki-lxd "${LXD5_ADDR}" --accept-certificate --password=bar - lxc_remote config trust ls pki-lxd: | grep lxd-client - lxc_remote remote remove pki-lxd + + # Client cert should not be present in trust store. + ! lxc_remote config trust ls pki-lxd: | grep ca-trusted || false + + # The certificate is trusted as root because `core.trust_ca_certificates` is enabled. + lxc_remote info pki-lxd: | grep -F 'core.https_address' + curl -s --cert "${LXD_CONF}/client.pem" --cacert "${LXD5_DIR}/server.crt" "https://${LXD5_ADDR}/1.0" | jq -e '.metadata.config."core.https_address"' + + # Unset `core.trust_ca_certificates` (this should work because the certificate is trusted as root as `core.trust_ca_certificates` is still enabled). + lxc_remote config unset pki-lxd: core.trust_ca_certificates + + # Check that we no longer have access. + lxc_remote info pki-lxd: | grep -F 'auth: untrusted' + ! lxc_remote ls pki-lxd: || false + [ "$(curl -s --cert "${LXD_CONF}/client.pem" --cacert "${LXD5_DIR}/server.crt" "https://${LXD5_ADDR}/1.0/instances" | jq -e -r '.error')" = "not authorized" ] + + # Re-enable `core.trust_ca_certificates`. + LXD_DIR=${LXD5_DIR} lxc config set core.trust_ca_certificates true + + # Revoke the client certificate + cd "${TEST_DIR}/pki" && "${TEST_DIR}/pki/easyrsa" --batch revoke ca-trusted keyCompromise && "${TEST_DIR}/pki/easyrsa" gen-crl && cd - + + # Restart LXD with the revoked certificate. + shutdown_lxd "${LXD5_DIR}" + cp "${TEST_DIR}/pki/pki/crl.pem" "${LXD5_DIR}/ca.crl" + respawn_lxd "${LXD5_DIR}" true + + # Check that we no longer have access (certificate was previously trusted, but is now revoked). + lxc_remote info pki-lxd: | grep -F 'auth: untrusted' + ! lxc_remote ls pki-lxd: || false + [ "$(curl -s --cert "${LXD_CONF}/client.pem" --cacert "${LXD5_DIR}/server.crt" "https://${LXD5_ADDR}/1.0/instances" | jq -e -r '.error')" = "not authorized" ] + + # Remove remote. + lxc remote remove pki-lxd + + ### CA signed certificate that has been revoked prior to connecting to LXD. + # `core.trust_ca_certificates` is currently enabled. # Replace the client certificate with a revoked certificate in the CRL. - cp "${TEST_DIR}/pki/keys/lxd-client-revoked.crt" "${LXC5_DIR}/client.crt" - cp "${TEST_DIR}/pki/keys/lxd-client-revoked.key" "${LXC5_DIR}/client.key" + cp "${TEST_DIR}/pki/keys/prior-revoked.crt" "${LXC5_DIR}/client.crt" + cp "${TEST_DIR}/pki/keys/prior-revoked.key" "${LXC5_DIR}/client.key" # Try adding a remote using a revoked client certificate, and the correct password. - # This should fail, as although revoked certificates can be added to the trust store, they will not be usable. - ! lxc_remote remote add pki-lxd "${LXD5_ADDR}" --accept-certificate --password=foo || false + # This should fail, and the revoked certificate should not be added to the trust store. + ! lxc_remote remote add pki-lxd "${LXD5_ADDR}" --accept-certificate --password foo || false + ! lxc config trust ls | grep prior-revoked || false # Try adding a remote using a revoked client certificate, and an incorrect password. # This should fail, as if the certificate is revoked and password is wrong then no access should be allowed. ! lxc_remote remote add pki-lxd "${LXD5_ADDR}" --accept-certificate --password=incorrect || false + + # Unset `core.trust_ca_certificates` and re-test, there should be no change in behaviour as the certificate is revoked. + LXD_DIR=${LXD5_DIR} lxc config unset core.trust_ca_certificates + + # Try adding a remote using a revoked client certificate, and the correct password. + # This should fail, and the revoked certificate should not be added to the trust store. + ! lxc_remote remote add pki-lxd "${LXD5_ADDR}" --accept-certificate --password foo || false + ! lxc config trust ls | grep prior-revoked || false + + # Try adding a remote using a revoked client certificate, and an incorrect password. + # This should fail, as if the certificate is revoked and password is wrong then no access should be allowed. + ! lxc_remote remote add pki-lxd "${LXD5_ADDR}" --accept-certificate --password=incorrect || false + + # Check we can't access anything with the revoked certificate. + [ "$(curl -s --cert "${LXD_CONF}/client.pem" --cacert "${LXD5_DIR}/server.crt" "https://${LXD5_ADDR}/1.0/instances" | jq -e -r '.error')" = "not authorized" ] ) - # Confirm that a normal, non-PKI certificate doesn't. - # As LXD_CONF is not set to LXC5_DIR where the CA signed client certs are, this will cause the lxc command to - # generate a new certificate that isn't trusted by the CA certificate and thus will not be allowed, even with a - # correct trust password. This is because the LXD TLS listener in CA mode will not consider a client cert that - # is not signed by the CA as valid. - ! lxc_remote remote add pki-lxd "${LXD5_ADDR}" --accept-certificate --password=foo || false + # Confirm that we cannot add a remote using a certificate that is not signed by the CA. + # Outside of the subshell above, `LXD_CONF` is not set to `LXD5_DIR` where the CA trusted certs are. + # Since we added a certificate to the trust store prior to enabling PKI, the certificates in current `LXD_CONF` are + # in the trust store, but not signed by the CA. So here we are checking that mTLS for a client does not work when CA + # mode is enabled. + ! lxc_remote remote add pki-lxd2 "${LXD5_ADDR}" --accept-certificate --password foo || false + + # Confirm that the certificate we added earlier cannot authenticate with LXD. + lxc_remote info pki-lxd: | grep -F 'auth: untrusted' + ! lxc_remote ls pki-lxd: || false + cat "${LXD_CONF}/client.crt" "${LXD_CONF}/client.key" > "${LXD_CONF}/client.pem" + [ "$(curl -s --cert "${LXD_CONF}/client.pem" --cacert "${LXD5_DIR}/server.crt" "https://${LXD5_ADDR}/1.0/instances" | jq -e -r '.error')" = "not authorized" ] + + # Trick LXD into thinking the client cert is a server certificate. + LXD_DIR="${LXD5_DIR}" lxd sql global "UPDATE certificates SET type = 2 WHERE fingerprint = '$(cert_fingerprint "${LXD_CONF}/client.crt")'" + shutdown_lxd "${LXD5_DIR}" + respawn_lxd "${LXD5_DIR}" true + + # Show that mTLS still works for server certificates. A server certificate has root access, so it can see server config. + lxc_remote info pki-lxd: | grep -F 'auth: trusted' + curl -s --cert "${LXD_CONF}/client.pem" --cacert "${LXD5_DIR}/server.crt" "https://${LXD5_ADDR}/1.0" | jq -e '.metadata.config."core.https_address"' + + rm "${LXD_CONF}/client.pem" + lxc remote rm pki-lxd kill_lxd "${LXD5_DIR}" }