Skip to content

Commit 2203d80

Browse files
authored
Merge pull request #1458 from stgraber/tls
Fix gap in validation of pre-existing certificates when switching to PKI mode
2 parents ccdf48a + 7ec93a3 commit 2203d80

File tree

8 files changed

+81
-39
lines changed

8 files changed

+81
-39
lines changed

cmd/incusd/certificates.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,11 @@ func certificatesGet(d *Daemon, r *http.Request) response.Response {
177177

178178
body := []string{}
179179

180-
trustedCertificates := d.getTrustedCertificates()
180+
trustedCertificates, err := d.getTrustedCertificates()
181+
if err != nil {
182+
return response.SmartError(err)
183+
}
184+
181185
for _, certs := range trustedCertificates {
182186
for _, cert := range certs {
183187
fingerprint := localtls.CertFingerprint(&cert)

cmd/incusd/daemon.go

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -429,8 +429,44 @@ func (d *Daemon) checkTrustedClient(r *http.Request) error {
429429
}
430430

431431
// getTrustedCertificates returns trusted certificates key on DB type and fingerprint.
432-
func (d *Daemon) getTrustedCertificates() map[certificate.Type]map[string]x509.Certificate {
433-
return d.clientCerts.GetCertificates()
432+
//
433+
// When in PKI mode, this also filters out any non-server certificate which isn't issued by the PKI.
434+
func (d *Daemon) getTrustedCertificates() (map[certificate.Type]map[string]x509.Certificate, error) {
435+
certs := d.clientCerts.GetCertificates()
436+
437+
// If not in PKI mode, return all certificates.
438+
if !util.PathExists(internalUtil.VarPath("server.ca")) {
439+
return certs, nil
440+
}
441+
442+
// If in PKI mode, filter certificates that aren't trusted by the CA.
443+
ca, err := localtls.ReadCert(internalUtil.VarPath("server.ca"))
444+
if err != nil {
445+
return nil, err
446+
}
447+
448+
certPool := x509.NewCertPool()
449+
certPool.AddCert(ca)
450+
451+
for certType, certEntries := range certs {
452+
if certType == certificate.TypeServer {
453+
continue
454+
}
455+
456+
for name, entry := range certEntries {
457+
_, err := entry.Verify(x509.VerifyOptions{
458+
Roots: certPool,
459+
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
460+
})
461+
462+
if err != nil {
463+
// Skip certificates that aren't signed by the PKI.
464+
delete(certs[certType], name)
465+
}
466+
}
467+
}
468+
469+
return certs, nil
434470
}
435471

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

446485
// Allow internal cluster traffic by checking against the trusted certfificates.
447486
if r.TLS != nil {

internal/server/cluster/gateway.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,10 +154,18 @@ func setDqliteVersionHeader(request *http.Request) {
154154
// These handlers might return 404, either because this server is a
155155
// non-clustered member not available over the network or because it is not a
156156
// database node part of the dqlite cluster.
157-
func (g *Gateway) HandlerFuncs(heartbeatHandler HeartbeatHandler, trustedCerts func() map[certificate.Type]map[string]x509.Certificate) map[string]http.HandlerFunc {
157+
func (g *Gateway) HandlerFuncs(heartbeatHandler HeartbeatHandler, trustedCerts func() (map[certificate.Type]map[string]x509.Certificate, error)) map[string]http.HandlerFunc {
158158
database := func(w http.ResponseWriter, r *http.Request) {
159159
g.lock.RLock()
160-
if !tlsCheckCert(r, g.networkCert, g.state().ServerCert(), trustedCerts()) {
160+
161+
certs, err := trustedCerts()
162+
if err != nil {
163+
g.lock.RUnlock()
164+
http.Error(w, "403 invalid client certificate", http.StatusForbidden)
165+
return
166+
}
167+
168+
if !tlsCheckCert(r, g.networkCert, g.state().ServerCert(), certs) {
161169
g.lock.RUnlock()
162170
http.Error(w, "403 invalid client certificate", http.StatusForbidden)
163171
return

internal/server/cluster/gateway_test.go

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ import (
2222
localtls "github.com/lxc/incus/v6/shared/tls"
2323
)
2424

25+
func trustedCerts() (map[certificate.Type]map[string]x509.Certificate, error) {
26+
return nil, nil
27+
}
28+
2529
// Basic creation and shutdown. By default, the gateway runs an in-memory gRPC
2630
// server.
2731
func TestGateway_Single(t *testing.T) {
@@ -37,10 +41,6 @@ func TestGateway_Single(t *testing.T) {
3741
gateway := newGateway(t, node, cert, s)
3842
defer func() { _ = gateway.Shutdown() }()
3943

40-
trustedCerts := func() map[certificate.Type]map[string]x509.Certificate {
41-
return nil
42-
}
43-
4444
handlerFuncs := gateway.HandlerFuncs(nil, trustedCerts)
4545
assert.Len(t, handlerFuncs, 1)
4646
for endpoint, f := range handlerFuncs {
@@ -101,10 +101,6 @@ func TestGateway_SingleWithNetworkAddress(t *testing.T) {
101101
gateway := newGateway(t, node, cert, s)
102102
defer func() { _ = gateway.Shutdown() }()
103103

104-
trustedCerts := func() map[certificate.Type]map[string]x509.Certificate {
105-
return nil
106-
}
107-
108104
for path, handler := range gateway.HandlerFuncs(nil, trustedCerts) {
109105
mux.HandleFunc(path, handler)
110106
}
@@ -146,10 +142,6 @@ func TestGateway_NetworkAuth(t *testing.T) {
146142
gateway := newGateway(t, node, cert, s)
147143
defer func() { _ = gateway.Shutdown() }()
148144

149-
trustedCerts := func() map[certificate.Type]map[string]x509.Certificate {
150-
return nil
151-
}
152-
153145
for path, handler := range gateway.HandlerFuncs(nil, trustedCerts) {
154146
mux.HandleFunc(path, handler)
155147
}

internal/server/cluster/heartbeat_test.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package cluster_test
22

33
import (
44
"context"
5-
"crypto/x509"
65
"net/http"
76
"net/http/httptest"
87
"testing"
@@ -12,7 +11,6 @@ import (
1211
"github.com/stretchr/testify/assert"
1312
"github.com/stretchr/testify/require"
1413

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

217-
trustedCerts := func() map[certificate.Type]map[string]x509.Certificate {
218-
return nil
219-
}
220-
221215
for path, handler := range gateway.HandlerFuncs(nil, trustedCerts) {
222216
mux.HandleFunc(path, handler)
223217
}

internal/server/cluster/membership_test.go

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -139,10 +139,6 @@ func TestBootstrap(t *testing.T) {
139139
// The cluster certificate is in place.
140140
assert.True(t, util.PathExists(filepath.Join(state.OS.VarDir, "cluster.crt")))
141141

142-
trustedCerts := func() map[certificate.Type]map[string]x509.Certificate {
143-
return nil
144-
}
145-
146142
// The dqlite driver is now exposed over the network.
147143
for path, handler := range gateway.HandlerFuncs(nil, trustedCerts) {
148144
mux.HandleFunc(path, handler)
@@ -297,12 +293,12 @@ func TestJoin(t *testing.T) {
297293
altServerCert := localtls.TestingAltKeyPair()
298294
trustedAltServerCert, _ := x509.ParseCertificate(altServerCert.KeyPair().Certificate[0])
299295

300-
trustedCerts := func() map[certificate.Type]map[string]x509.Certificate {
296+
trustedCerts := func() (map[certificate.Type]map[string]x509.Certificate, error) {
301297
return map[certificate.Type]map[string]x509.Certificate{
302298
certificate.TypeServer: {
303299
altServerCert.Fingerprint(): *trustedAltServerCert,
304300
},
305-
}
301+
}, nil
306302
}
307303

308304
for path, handler := range targetGateway.HandlerFuncs(nil, trustedCerts) {

internal/server/cluster/upgrade_test.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package cluster_test
22

33
import (
44
"context"
5-
"crypto/x509"
65
"errors"
76
"fmt"
87
"io/fs"
@@ -18,7 +17,6 @@ import (
1817
"github.com/stretchr/testify/assert"
1918
"github.com/stretchr/testify/require"
2019

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

163-
trustedCerts := func() map[certificate.Type]map[string]x509.Certificate {
164-
return nil
165-
}
166-
167161
for path, handler := range gateway.HandlerFuncs(nil, trustedCerts) {
168162
mux.HandleFunc(path, handler)
169163
}

test/suites/pki.sh

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,35 @@ test_pki() {
3636
fi
3737
)
3838

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

45+
# Generate, trust and test a client certificate
46+
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"
47+
INCUS_DIR="${INCUS5_DIR}" incus config trust add-certificate "${INCUS_CONF}/simple-client.crt"
48+
INCUS_DIR="${INCUS5_DIR}" incus config set user.test foo
49+
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
50+
51+
# Restart the daemon in PKI mode
52+
shutdown_incus "${INCUS5_DIR}"
53+
cp "${TEST_DIR}/pki/keys/ca.crt" "${INCUS5_DIR}/server.ca"
54+
cp "${TEST_DIR}/pki/keys/crl.pem" "${INCUS5_DIR}/ca.crl"
55+
respawn_incus "${INCUS5_DIR}" true
56+
4757
# Setup the client.
4858
INC5_DIR=$(mktemp -d -p "${TEST_DIR}" XXX)
4959
cp "${TEST_DIR}/pki/keys/incus-client.crt" "${INC5_DIR}/client.crt"
5060
cp "${TEST_DIR}/pki/keys/incus-client.key" "${INC5_DIR}/client.key"
5161
cp "${TEST_DIR}/pki/keys/ca.crt" "${INC5_DIR}/client.ca"
5262

63+
# Re-test the regular client certificate
64+
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
65+
fingerprint="$(INCUS_DIR="${INCUS5_DIR}" incus config trust list -cf -fcsv)"
66+
INCUS_DIR="${INCUS5_DIR}" incus config trust remove "${fingerprint}"
67+
5368
# Confirm that a valid client certificate works.
5469
(
5570
set -e

0 commit comments

Comments
 (0)