cd686ffdf3
This change updates the TLS config used by all pinniped components. There are no configuration knobs associated with this change. Thus this change tightens our static defaults. There are four TLS config levels: 1. Secure (TLS 1.3 only) 2. Default (TLS 1.2+ best ciphers that are well supported) 3. Default LDAP (TLS 1.2+ with less good ciphers) 4. Legacy (currently unused, TLS 1.2+ with all non-broken ciphers) Highlights per component: 1. pinniped CLI - uses "secure" config against KAS - uses "default" for all other connections 2. concierge - uses "secure" config as an aggregated API server - uses "default" config as a impersonation proxy API server - uses "secure" config against KAS - uses "default" config for JWT authenticater (mostly, see code) - no changes to webhook authenticater (see code) 3. supervisor - uses "default" config as a server - uses "secure" config against KAS - uses "default" config against OIDC IDPs - uses "default LDAP" config against LDAP IDPs Signed-off-by: Monis Khan <mok@vmware.com>
242 lines
7.7 KiB
Go
242 lines
7.7 KiB
Go
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package dynamiccert
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"net"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"k8s.io/apimachinery/pkg/util/wait"
|
|
"k8s.io/apiserver/pkg/server/dynamiccertificates"
|
|
"k8s.io/apiserver/pkg/storage/names"
|
|
|
|
"go.pinniped.dev/internal/certauthority"
|
|
"go.pinniped.dev/internal/crypto/ptls"
|
|
"go.pinniped.dev/test/testlib"
|
|
)
|
|
|
|
func TestProviderWithDynamicServingCertificateController(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
f func(t *testing.T, ca Provider, certKey Private) (wantClientCASubjects [][]byte, wantCerts []tls.Certificate)
|
|
}{
|
|
{
|
|
name: "no-op leave everything alone",
|
|
f: func(t *testing.T, ca Provider, certKey Private) ([][]byte, []tls.Certificate) {
|
|
pool := x509.NewCertPool()
|
|
ok := pool.AppendCertsFromPEM(ca.CurrentCABundleContent())
|
|
require.True(t, ok, "should have valid non-empty CA bundle")
|
|
|
|
certPEM, keyPEM := certKey.CurrentCertKeyContent()
|
|
cert, err := tls.X509KeyPair(certPEM, keyPEM)
|
|
require.NoError(t, err)
|
|
|
|
return pool.Subjects(), []tls.Certificate{cert}
|
|
},
|
|
},
|
|
{
|
|
name: "unset the CA",
|
|
f: func(t *testing.T, ca Provider, certKey Private) ([][]byte, []tls.Certificate) {
|
|
ca.UnsetCertKeyContent()
|
|
|
|
certPEM, keyPEM := certKey.CurrentCertKeyContent()
|
|
cert, err := tls.X509KeyPair(certPEM, keyPEM)
|
|
require.NoError(t, err)
|
|
|
|
return nil, []tls.Certificate{cert}
|
|
},
|
|
},
|
|
{
|
|
name: "unset the serving cert - still serves the old content",
|
|
f: func(t *testing.T, ca Provider, certKey Private) ([][]byte, []tls.Certificate) {
|
|
pool := x509.NewCertPool()
|
|
ok := pool.AppendCertsFromPEM(ca.CurrentCABundleContent())
|
|
require.True(t, ok, "should have valid non-empty CA bundle")
|
|
|
|
certPEM, keyPEM := certKey.CurrentCertKeyContent()
|
|
cert, err := tls.X509KeyPair(certPEM, keyPEM)
|
|
require.NoError(t, err)
|
|
|
|
certKey.UnsetCertKeyContent()
|
|
|
|
return pool.Subjects(), []tls.Certificate{cert}
|
|
},
|
|
},
|
|
{
|
|
name: "change to a new CA",
|
|
f: func(t *testing.T, ca Provider, certKey Private) ([][]byte, []tls.Certificate) {
|
|
// use unique names for all CAs to make sure the pool subjects are different
|
|
newCA, err := certauthority.New(names.SimpleNameGenerator.GenerateName("new-ca"), time.Hour)
|
|
require.NoError(t, err)
|
|
caKey, err := newCA.PrivateKeyToPEM()
|
|
require.NoError(t, err)
|
|
err = ca.SetCertKeyContent(newCA.Bundle(), caKey)
|
|
require.NoError(t, err)
|
|
|
|
certPEM, keyPEM := certKey.CurrentCertKeyContent()
|
|
cert, err := tls.X509KeyPair(certPEM, keyPEM)
|
|
require.NoError(t, err)
|
|
|
|
return newCA.Pool().Subjects(), []tls.Certificate{cert}
|
|
},
|
|
},
|
|
{
|
|
name: "change to new serving cert",
|
|
f: func(t *testing.T, ca Provider, certKey Private) ([][]byte, []tls.Certificate) {
|
|
// use unique names for all CAs to make sure the pool subjects are different
|
|
newCA, err := certauthority.New(names.SimpleNameGenerator.GenerateName("new-ca"), time.Hour)
|
|
require.NoError(t, err)
|
|
|
|
certPEM, keyPEM, err := newCA.IssueServerCertPEM(nil, []net.IP{net.ParseIP("127.0.0.2")}, time.Hour)
|
|
require.NoError(t, err)
|
|
|
|
err = certKey.SetCertKeyContent(certPEM, keyPEM)
|
|
require.NoError(t, err)
|
|
|
|
cert, err := tls.X509KeyPair(certPEM, keyPEM)
|
|
require.NoError(t, err)
|
|
|
|
pool := x509.NewCertPool()
|
|
ok := pool.AppendCertsFromPEM(ca.CurrentCABundleContent())
|
|
require.True(t, ok, "should have valid non-empty CA bundle")
|
|
|
|
return pool.Subjects(), []tls.Certificate{cert}
|
|
},
|
|
},
|
|
{
|
|
name: "change both CA and serving cert",
|
|
f: func(t *testing.T, ca Provider, certKey Private) ([][]byte, []tls.Certificate) {
|
|
// use unique names for all CAs to make sure the pool subjects are different
|
|
newCA, err := certauthority.New(names.SimpleNameGenerator.GenerateName("new-ca"), time.Hour)
|
|
require.NoError(t, err)
|
|
|
|
certPEM, keyPEM, err := newCA.IssueServerCertPEM(nil, []net.IP{net.ParseIP("127.0.0.3")}, time.Hour)
|
|
require.NoError(t, err)
|
|
|
|
err = certKey.SetCertKeyContent(certPEM, keyPEM)
|
|
require.NoError(t, err)
|
|
|
|
cert, err := tls.X509KeyPair(certPEM, keyPEM)
|
|
require.NoError(t, err)
|
|
|
|
// use unique names for all CAs to make sure the pool subjects are different
|
|
newOtherCA, err := certauthority.New(names.SimpleNameGenerator.GenerateName("new-other-ca"), time.Hour)
|
|
require.NoError(t, err)
|
|
caKey, err := newOtherCA.PrivateKeyToPEM()
|
|
require.NoError(t, err)
|
|
err = ca.SetCertKeyContent(newOtherCA.Bundle(), caKey)
|
|
require.NoError(t, err)
|
|
|
|
return newOtherCA.Pool().Subjects(), []tls.Certificate{cert}
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// use unique names for all CAs to make sure the pool subjects are different
|
|
ca, err := certauthority.New(names.SimpleNameGenerator.GenerateName("ca"), time.Hour)
|
|
require.NoError(t, err)
|
|
caKey, err := ca.PrivateKeyToPEM()
|
|
require.NoError(t, err)
|
|
caContent := NewCA("ca")
|
|
err = caContent.SetCertKeyContent(ca.Bundle(), caKey)
|
|
require.NoError(t, err)
|
|
|
|
cert, key, err := ca.IssueServerCertPEM(nil, []net.IP{net.ParseIP("127.0.0.1")}, time.Hour)
|
|
require.NoError(t, err)
|
|
certKeyContent := NewServingCert("cert-key")
|
|
err = certKeyContent.SetCertKeyContent(cert, key)
|
|
require.NoError(t, err)
|
|
|
|
tlsConfig := ptls.Default(nil)
|
|
tlsConfig.ClientAuth = tls.RequestClientCert
|
|
|
|
dynamicCertificateController := dynamiccertificates.NewDynamicServingCertificateController(
|
|
tlsConfig,
|
|
caContent,
|
|
certKeyContent,
|
|
nil, // we do not care about SNI
|
|
nil, // we do not care about events
|
|
)
|
|
|
|
caContent.AddListener(dynamicCertificateController)
|
|
certKeyContent.AddListener(dynamicCertificateController)
|
|
|
|
err = dynamicCertificateController.RunOnce()
|
|
require.NoError(t, err)
|
|
|
|
stopCh := make(chan struct{})
|
|
defer close(stopCh)
|
|
go dynamicCertificateController.Run(1, stopCh)
|
|
|
|
tlsConfig.GetConfigForClient = dynamicCertificateController.GetConfigForClient
|
|
|
|
wantClientCASubjects, wantCerts := tt.f(t, caContent, certKeyContent)
|
|
|
|
var lastTLSConfig *tls.Config
|
|
|
|
// it will take some time for the controller to catch up
|
|
err = wait.PollImmediate(time.Second, 30*time.Second, func() (bool, error) {
|
|
actualTLSConfig, err := tlsConfig.GetConfigForClient(&tls.ClientHelloInfo{ServerName: "force-standard-sni"})
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
lastTLSConfig = actualTLSConfig
|
|
|
|
return reflect.DeepEqual(wantClientCASubjects, poolSubjects(actualTLSConfig.ClientCAs)) &&
|
|
reflect.DeepEqual(wantCerts, actualTLSConfig.Certificates), nil
|
|
})
|
|
|
|
if err != nil && lastTLSConfig != nil {
|
|
// for debugging failures
|
|
t.Log("diff between client CAs:\n", cmp.Diff(
|
|
testlib.Sdump(wantClientCASubjects),
|
|
testlib.Sdump(poolSubjects(lastTLSConfig.ClientCAs)),
|
|
))
|
|
t.Log("diff between serving certs:\n", cmp.Diff(
|
|
testlib.Sdump(wantCerts),
|
|
testlib.Sdump(lastTLSConfig.Certificates),
|
|
))
|
|
}
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func poolSubjects(pool *x509.CertPool) [][]byte {
|
|
if pool == nil {
|
|
return nil
|
|
}
|
|
return pool.Subjects()
|
|
}
|
|
|
|
func TestNewServingCert(t *testing.T) {
|
|
got := NewServingCert("")
|
|
|
|
ok1 := assert.Implements(fakeT{}, (*Private)(nil), got)
|
|
ok2 := assert.Implements(fakeT{}, (*Public)(nil), got)
|
|
ok3 := assert.Implements(fakeT{}, (*Provider)(nil), got)
|
|
|
|
require.True(t, ok1, "NewServingCert must implement Private")
|
|
require.False(t, ok2, "NewServingCert must not implement Public")
|
|
require.False(t, ok3, "NewServingCert must not implement Provider")
|
|
}
|
|
|
|
type fakeT struct{}
|
|
|
|
func (fakeT) Errorf(string, ...interface{}) {}
|