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

Fix gap in validation of pre-existing certificates when switching to PKI mode #1458

Merged
merged 3 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion cmd/incusd/certificates.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,11 @@ func certificatesGet(d *Daemon, r *http.Request) response.Response {

body := []string{}

trustedCertificates := d.getTrustedCertificates()
trustedCertificates, err := d.getTrustedCertificates()
if err != nil {
return response.SmartError(err)
}

for _, certs := range trustedCertificates {
for _, cert := range certs {
fingerprint := localtls.CertFingerprint(&cert)
Expand Down
45 changes: 42 additions & 3 deletions cmd/incusd/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -429,8 +429,44 @@ func (d *Daemon) checkTrustedClient(r *http.Request) error {
}

// getTrustedCertificates returns trusted certificates key on DB type and fingerprint.
func (d *Daemon) getTrustedCertificates() map[certificate.Type]map[string]x509.Certificate {
return d.clientCerts.GetCertificates()
//
// When in PKI mode, this also filters out any non-server certificate which isn't issued by the PKI.
func (d *Daemon) getTrustedCertificates() (map[certificate.Type]map[string]x509.Certificate, error) {
certs := d.clientCerts.GetCertificates()

// If not in PKI mode, return all certificates.
if !util.PathExists(internalUtil.VarPath("server.ca")) {
return certs, nil
}

// If in PKI mode, filter certificates that aren't trusted by the CA.
ca, err := localtls.ReadCert(internalUtil.VarPath("server.ca"))
if err != nil {
return nil, err
}

certPool := x509.NewCertPool()
certPool.AddCert(ca)

for certType, certEntries := range certs {
if certType == certificate.TypeServer {
continue
}

for name, entry := range certEntries {
_, err := entry.Verify(x509.VerifyOptions{
Roots: certPool,
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
})

if err != nil {
// Skip certificates that aren't signed by the PKI.
delete(certs[certType], name)
}
}
}

return certs, nil
}

// Authenticate validates an incoming http Request
Expand All @@ -441,7 +477,10 @@ func (d *Daemon) getTrustedCertificates() map[certificate.Type]map[string]x509.C
// Returns whether trusted or not, the username (or certificate fingerprint) of the trusted client, and the type of
// client that has been authenticated (cluster, unix, or tls).
func (d *Daemon) Authenticate(w http.ResponseWriter, r *http.Request) (bool, string, string, error) {
trustedCerts := d.getTrustedCertificates()
trustedCerts, err := d.getTrustedCertificates()
if err != nil {
return false, "", "", err
}

// Allow internal cluster traffic by checking against the trusted certfificates.
if r.TLS != nil {
Expand Down
12 changes: 10 additions & 2 deletions internal/server/cluster/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,18 @@ func setDqliteVersionHeader(request *http.Request) {
// These handlers might return 404, either because this server is a
// non-clustered member not available over the network or because it is not a
// database node part of the dqlite cluster.
func (g *Gateway) HandlerFuncs(heartbeatHandler HeartbeatHandler, trustedCerts func() map[certificate.Type]map[string]x509.Certificate) map[string]http.HandlerFunc {
func (g *Gateway) HandlerFuncs(heartbeatHandler HeartbeatHandler, trustedCerts func() (map[certificate.Type]map[string]x509.Certificate, error)) map[string]http.HandlerFunc {
database := func(w http.ResponseWriter, r *http.Request) {
g.lock.RLock()
if !tlsCheckCert(r, g.networkCert, g.state().ServerCert(), trustedCerts()) {

certs, err := trustedCerts()
if err != nil {
g.lock.RUnlock()
http.Error(w, "403 invalid client certificate", http.StatusForbidden)
return
}

if !tlsCheckCert(r, g.networkCert, g.state().ServerCert(), certs) {
g.lock.RUnlock()
http.Error(w, "403 invalid client certificate", http.StatusForbidden)
return
Expand Down
16 changes: 4 additions & 12 deletions internal/server/cluster/gateway_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ import (
localtls "github.com/lxc/incus/v6/shared/tls"
)

func trustedCerts() (map[certificate.Type]map[string]x509.Certificate, error) {
return nil, nil
}

// Basic creation and shutdown. By default, the gateway runs an in-memory gRPC
// server.
func TestGateway_Single(t *testing.T) {
Expand All @@ -37,10 +41,6 @@ func TestGateway_Single(t *testing.T) {
gateway := newGateway(t, node, cert, s)
defer func() { _ = gateway.Shutdown() }()

trustedCerts := func() map[certificate.Type]map[string]x509.Certificate {
return nil
}

handlerFuncs := gateway.HandlerFuncs(nil, trustedCerts)
assert.Len(t, handlerFuncs, 1)
for endpoint, f := range handlerFuncs {
Expand Down Expand Up @@ -101,10 +101,6 @@ func TestGateway_SingleWithNetworkAddress(t *testing.T) {
gateway := newGateway(t, node, cert, s)
defer func() { _ = gateway.Shutdown() }()

trustedCerts := func() map[certificate.Type]map[string]x509.Certificate {
return nil
}

for path, handler := range gateway.HandlerFuncs(nil, trustedCerts) {
mux.HandleFunc(path, handler)
}
Expand Down Expand Up @@ -146,10 +142,6 @@ func TestGateway_NetworkAuth(t *testing.T) {
gateway := newGateway(t, node, cert, s)
defer func() { _ = gateway.Shutdown() }()

trustedCerts := func() map[certificate.Type]map[string]x509.Certificate {
return nil
}

for path, handler := range gateway.HandlerFuncs(nil, trustedCerts) {
mux.HandleFunc(path, handler)
}
Expand Down
6 changes: 0 additions & 6 deletions internal/server/cluster/heartbeat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package cluster_test

import (
"context"
"crypto/x509"
"net/http"
"net/http/httptest"
"testing"
Expand All @@ -12,7 +11,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/lxc/incus/v6/internal/server/certificate"
"github.com/lxc/incus/v6/internal/server/cluster"
clusterConfig "github.com/lxc/incus/v6/internal/server/cluster/config"
"github.com/lxc/incus/v6/internal/server/db"
Expand Down Expand Up @@ -214,10 +212,6 @@ func (f *heartbeatFixture) node() (*state.State, *cluster.Gateway, string) {
mux := http.NewServeMux()
server := newServer(serverCert, mux)

trustedCerts := func() map[certificate.Type]map[string]x509.Certificate {
return nil
}

for path, handler := range gateway.HandlerFuncs(nil, trustedCerts) {
mux.HandleFunc(path, handler)
}
Expand Down
8 changes: 2 additions & 6 deletions internal/server/cluster/membership_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,10 +139,6 @@ func TestBootstrap(t *testing.T) {
// The cluster certificate is in place.
assert.True(t, util.PathExists(filepath.Join(state.OS.VarDir, "cluster.crt")))

trustedCerts := func() map[certificate.Type]map[string]x509.Certificate {
return nil
}

// The dqlite driver is now exposed over the network.
for path, handler := range gateway.HandlerFuncs(nil, trustedCerts) {
mux.HandleFunc(path, handler)
Expand Down Expand Up @@ -297,12 +293,12 @@ func TestJoin(t *testing.T) {
altServerCert := localtls.TestingAltKeyPair()
trustedAltServerCert, _ := x509.ParseCertificate(altServerCert.KeyPair().Certificate[0])

trustedCerts := func() map[certificate.Type]map[string]x509.Certificate {
trustedCerts := func() (map[certificate.Type]map[string]x509.Certificate, error) {
return map[certificate.Type]map[string]x509.Certificate{
certificate.TypeServer: {
altServerCert.Fingerprint(): *trustedAltServerCert,
},
}
}, nil
}

for path, handler := range targetGateway.HandlerFuncs(nil, trustedCerts) {
Expand Down
6 changes: 0 additions & 6 deletions internal/server/cluster/upgrade_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package cluster_test

import (
"context"
"crypto/x509"
"errors"
"fmt"
"io/fs"
Expand All @@ -18,7 +17,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/lxc/incus/v6/internal/server/certificate"
"github.com/lxc/incus/v6/internal/server/cluster"
"github.com/lxc/incus/v6/internal/server/db"
"github.com/lxc/incus/v6/internal/server/node"
Expand Down Expand Up @@ -160,10 +158,6 @@ func TestUpgradeMembersWithoutRole(t *testing.T) {
gateway := newGateway(t, state.DB.Node, serverCert, state)
defer func() { _ = gateway.Shutdown() }()

trustedCerts := func() map[certificate.Type]map[string]x509.Certificate {
return nil
}

for path, handler := range gateway.HandlerFuncs(nil, trustedCerts) {
mux.HandleFunc(path, handler)
}
Expand Down
21 changes: 18 additions & 3 deletions test/suites/pki.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,35 @@ test_pki() {
fi
)

# Setup the daemon.
# Setup the daemon in normal mode
INCUS5_DIR=$(mktemp -d -p "${TEST_DIR}" XXX)
chmod +x "${INCUS5_DIR}"
cp "${TEST_DIR}/pki/keys/ca.crt" "${INCUS5_DIR}/server.ca"
cp "${TEST_DIR}/pki/keys/crl.pem" "${INCUS5_DIR}/ca.crl"
spawn_incus "${INCUS5_DIR}" true
INCUS5_ADDR=$(cat "${INCUS5_DIR}/incus.addr")

# Generate, trust and test a client certificate
openssl req -x509 -newkey rsa:4096 -sha384 -keyout "${INCUS_CONF}/simple-client.key" -nodes -out "${INCUS_CONF}/simple-client.crt" -days 1 -subj "/CN=test.local"
INCUS_DIR="${INCUS5_DIR}" incus config trust add-certificate "${INCUS_CONF}/simple-client.crt"
INCUS_DIR="${INCUS5_DIR}" incus config set user.test foo
curl -k -s --cert "${INCUS_CONF}/simple-client.crt" --key "${INCUS_CONF}/simple-client.key" "https://${INCUS5_ADDR}/1.0" | grep -q "user.test.*foo" || false

# Restart the daemon in PKI mode
shutdown_incus "${INCUS5_DIR}"
cp "${TEST_DIR}/pki/keys/ca.crt" "${INCUS5_DIR}/server.ca"
cp "${TEST_DIR}/pki/keys/crl.pem" "${INCUS5_DIR}/ca.crl"
respawn_incus "${INCUS5_DIR}" true

# Setup the client.
INC5_DIR=$(mktemp -d -p "${TEST_DIR}" XXX)
cp "${TEST_DIR}/pki/keys/incus-client.crt" "${INC5_DIR}/client.crt"
cp "${TEST_DIR}/pki/keys/incus-client.key" "${INC5_DIR}/client.key"
cp "${TEST_DIR}/pki/keys/ca.crt" "${INC5_DIR}/client.ca"

# Re-test the regular client certificate
curl -k -s --cert "${INCUS_CONF}/simple-client.crt" --key "${INCUS_CONF}/simple-client.key" "https://${INCUS5_ADDR}/1.0" incus/1.0 | grep -q "user.test.*foo" && false
fingerprint="$(INCUS_DIR="${INCUS5_DIR}" incus config trust list -cf -fcsv)"
INCUS_DIR="${INCUS5_DIR}" incus config trust remove "${fingerprint}"

# Confirm that a valid client certificate works.
(
set -e
Expand Down
Loading