Implement all optional methods in dynamic certs provider

Signed-off-by: Monis Khan <mok@vmware.com>
This commit is contained in:
Monis Khan 2021-03-11 16:20:25 -05:00
parent 78fdc59d2d
commit 2d28d1da19
No known key found for this signature in database
GPG Key ID: 52C90ADA01B269B8
13 changed files with 268 additions and 111 deletions

View File

@ -355,7 +355,7 @@ func run() error {
kubeinformers.WithNamespace(namespace), kubeinformers.WithNamespace(namespace),
) )
dynamicCertProvider := dynamiccert.New() dynamicCertProvider := dynamiccert.New("local-user-authenticator-tls-serving-certificate")
startControllers(ctx, dynamicCertProvider, client.Kubernetes, kubeInformers) startControllers(ctx, dynamicCertProvider, client.Kubernetes, kubeInformers)
plog.Debug("controllers are ready") plog.Debug("controllers are ready")

View File

@ -473,8 +473,9 @@ func newCertProvider(t *testing.T) (dynamiccert.Provider, []byte, string) {
certPEM, keyPEM, err := certauthority.ToPEM(cert) certPEM, keyPEM, err := certauthority.ToPEM(cert)
require.NoError(t, err) require.NoError(t, err)
certProvider := dynamiccert.New() certProvider := dynamiccert.New(t.Name())
certProvider.Set(certPEM, keyPEM) err = certProvider.SetCertKeyContent(certPEM, keyPEM)
require.NoError(t, err)
return certProvider, ca.Bundle(), serverName return certProvider, ca.Bundle(), serverName
} }

View File

@ -17,7 +17,7 @@ import (
func TestCAIssuePEM(t *testing.T) { func TestCAIssuePEM(t *testing.T) {
t.Parallel() t.Parallel()
provider := dynamiccert.New() provider := dynamiccert.New(t.Name())
ca := New(provider) ca := New(provider)
goodCACrtPEM0, goodCAKeyPEM0, err := testutil.CreateCertificate( goodCACrtPEM0, goodCAKeyPEM0, err := testutil.CreateCertificate(
@ -44,12 +44,12 @@ func TestCAIssuePEM(t *testing.T) {
{ {
name: "only cert", name: "only cert",
caCrtPEM: goodCACrtPEM0, caCrtPEM: goodCACrtPEM0,
wantError: "could not load CA: tls: failed to find any PEM data in key input", wantError: "TestCAIssuePEM: attempt to set invalid key pair: tls: failed to find any PEM data in key input",
}, },
{ {
name: "only key", name: "only key",
caKeyPEM: goodCAKeyPEM0, caKeyPEM: goodCAKeyPEM0,
wantError: "could not load CA: tls: failed to find any PEM data in certificate input", wantError: "TestCAIssuePEM: attempt to set invalid key pair: tls: failed to find any PEM data in certificate input",
}, },
{ {
name: "new cert+key", name: "new cert+key",
@ -68,19 +68,19 @@ func TestCAIssuePEM(t *testing.T) {
name: "bad cert", name: "bad cert",
caCrtPEM: []byte("this is not a cert"), caCrtPEM: []byte("this is not a cert"),
caKeyPEM: goodCAKeyPEM0, caKeyPEM: goodCAKeyPEM0,
wantError: "could not load CA: tls: failed to find any PEM data in certificate input", wantError: "TestCAIssuePEM: attempt to set invalid key pair: tls: failed to find any PEM data in certificate input",
}, },
{ {
name: "bad key", name: "bad key",
caCrtPEM: goodCACrtPEM0, caCrtPEM: goodCACrtPEM0,
caKeyPEM: []byte("this is not a key"), caKeyPEM: []byte("this is not a key"),
wantError: "could not load CA: tls: failed to find any PEM data in key input", wantError: "TestCAIssuePEM: attempt to set invalid key pair: tls: failed to find any PEM data in key input",
}, },
{ {
name: "mismatch cert+key", name: "mismatch cert+key",
caCrtPEM: goodCACrtPEM0, caCrtPEM: goodCACrtPEM0,
caKeyPEM: goodCAKeyPEM1, caKeyPEM: goodCAKeyPEM1,
wantError: "could not load CA: tls: private key does not match public key", wantError: "TestCAIssuePEM: attempt to set invalid key pair: tls: private key does not match public key",
}, },
{ {
name: "good cert+key again", name: "good cert+key again",
@ -94,17 +94,7 @@ func TestCAIssuePEM(t *testing.T) {
// Can't run these steps in parallel, because each one depends on the previous steps being // Can't run these steps in parallel, because each one depends on the previous steps being
// run. // run.
if step.caCrtPEM != nil || step.caKeyPEM != nil { crtPEM, keyPEM, err := issuePEM(provider, ca, step.caCrtPEM, step.caKeyPEM)
provider.Set(step.caCrtPEM, step.caKeyPEM)
}
crtPEM, keyPEM, err := ca.IssuePEM(
pkix.Name{
CommonName: "some-common-name",
},
[]string{"some-dns-name", "some-other-dns-name"},
time.Hour*24,
)
if step.wantError != "" { if step.wantError != "" {
require.EqualError(t, err, step.wantError) require.EqualError(t, err, step.wantError)
@ -126,3 +116,21 @@ func TestCAIssuePEM(t *testing.T) {
}) })
} }
} }
func issuePEM(provider dynamiccert.Provider, ca *CA, caCrt, caKey []byte) ([]byte, []byte, error) {
// if setting fails, look at that error
if caCrt != nil || caKey != nil {
if err := provider.SetCertKeyContent(caCrt, caKey); err != nil {
return nil, nil, err
}
}
// otherwise check to see if their is an issuing error
return ca.IssuePEM(
pkix.Name{
CommonName: "some-common-name",
},
[]string{"some-dns-name", "some-other-dns-name"},
time.Hour*24,
)
}

View File

@ -12,11 +12,10 @@ import (
"strings" "strings"
"time" "time"
"k8s.io/apimachinery/pkg/util/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/endpoints/request"
@ -27,6 +26,7 @@ import (
"k8s.io/client-go/transport" "k8s.io/client-go/transport"
"go.pinniped.dev/internal/constable" "go.pinniped.dev/internal/constable"
"go.pinniped.dev/internal/dynamiccert"
"go.pinniped.dev/internal/httputil/securityheader" "go.pinniped.dev/internal/httputil/securityheader"
"go.pinniped.dev/internal/kubeclient" "go.pinniped.dev/internal/kubeclient"
"go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/plog"
@ -39,22 +39,22 @@ import (
// Instead, call the factory function again to get a new start function. // Instead, call the factory function again to get a new start function.
type FactoryFunc func( type FactoryFunc func(
port int, port int,
dynamicCertProvider dynamiccertificates.CertKeyContentProvider, dynamicCertProvider dynamiccert.Private,
impersonationProxySignerCA dynamiccertificates.CAContentProvider, impersonationProxySignerCA dynamiccert.Public,
) (func(stopCh <-chan struct{}) error, error) ) (func(stopCh <-chan struct{}) error, error)
func New( func New(
port int, port int,
dynamicCertProvider dynamiccertificates.CertKeyContentProvider, // TODO: we need to check those optional interfaces and see what we need to implement dynamicCertProvider dynamiccert.Private,
impersonationProxySignerCA dynamiccertificates.CAContentProvider, // TODO: we need to check those optional interfaces and see what we need to implement impersonationProxySignerCA dynamiccert.Public,
) (func(stopCh <-chan struct{}) error, error) { ) (func(stopCh <-chan struct{}) error, error) {
return newInternal(port, dynamicCertProvider, impersonationProxySignerCA, nil, nil) return newInternal(port, dynamicCertProvider, impersonationProxySignerCA, nil, nil)
} }
func newInternal( //nolint:funlen // yeah, it's kind of long. func newInternal( //nolint:funlen // yeah, it's kind of long.
port int, port int,
dynamicCertProvider dynamiccertificates.CertKeyContentProvider, dynamicCertProvider dynamiccert.Private,
impersonationProxySignerCA dynamiccertificates.CAContentProvider, impersonationProxySignerCA dynamiccert.Public,
clientOpts []kubeclient.Option, // for unit testing, should always be nil in production clientOpts []kubeclient.Option, // for unit testing, should always be nil in production
recOpts func(*genericoptions.RecommendedOptions), // for unit testing, should always be nil in production recOpts func(*genericoptions.RecommendedOptions), // for unit testing, should always be nil in production
) (func(stopCh <-chan struct{}) error, error) { ) (func(stopCh <-chan struct{}) error, error) {

View File

@ -18,7 +18,6 @@ import (
"k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/features" "k8s.io/apiserver/pkg/features"
"k8s.io/apiserver/pkg/server/dynamiccertificates"
genericoptions "k8s.io/apiserver/pkg/server/options" genericoptions "k8s.io/apiserver/pkg/server/options"
utilfeature "k8s.io/apiserver/pkg/util/feature" utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
@ -26,6 +25,7 @@ import (
featuregatetesting "k8s.io/component-base/featuregate/testing" featuregatetesting "k8s.io/component-base/featuregate/testing"
"go.pinniped.dev/internal/certauthority" "go.pinniped.dev/internal/certauthority"
"go.pinniped.dev/internal/dynamiccert"
"go.pinniped.dev/internal/kubeclient" "go.pinniped.dev/internal/kubeclient"
"go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil"
) )
@ -35,11 +35,16 @@ func TestNew(t *testing.T) {
ca, err := certauthority.New(pkix.Name{CommonName: "ca"}, time.Hour) ca, err := certauthority.New(pkix.Name{CommonName: "ca"}, time.Hour)
require.NoError(t, err) require.NoError(t, err)
caKey, err := ca.PrivateKeyToPEM()
require.NoError(t, err)
caContent := dynamiccert.New("ca")
err = caContent.SetCertKeyContent(ca.Bundle(), caKey)
require.NoError(t, err)
cert, key, err := ca.IssuePEM(pkix.Name{CommonName: "example.com"}, []string{"example.com"}, time.Hour) cert, key, err := ca.IssuePEM(pkix.Name{CommonName: "example.com"}, []string{"example.com"}, time.Hour)
require.NoError(t, err) require.NoError(t, err)
certKeyContent, err := dynamiccertificates.NewStaticCertKeyContent("cert-key", cert, key) certKeyContent := dynamiccert.New("cert-key")
require.NoError(t, err) err = certKeyContent.SetCertKeyContent(cert, key)
caContent, err := dynamiccertificates.NewStaticCAContent("ca", ca.Bundle())
require.NoError(t, err) require.NoError(t, err)
// Punch out just enough stuff to make New actually run without error. // Punch out just enough stuff to make New actually run without error.

View File

@ -114,15 +114,15 @@ func (a *App) runServer(ctx context.Context) error {
// is stored in a k8s Secret. Therefore it also effectively acting as // is stored in a k8s Secret. Therefore it also effectively acting as
// an in-memory cache of what is stored in the k8s Secret, helping to // an in-memory cache of what is stored in the k8s Secret, helping to
// keep incoming requests fast. // keep incoming requests fast.
dynamicServingCertProvider := dynamiccert.New() dynamicServingCertProvider := dynamiccert.New("concierge-serving-cert")
// This cert provider will be used to provide the Kube signing key to the // This cert provider will be used to provide the Kube signing key to the
// cert issuer used to issue certs to Pinniped clients wishing to login. // cert issuer used to issue certs to Pinniped clients wishing to login.
dynamicSigningCertProvider := dynamiccert.New() dynamicSigningCertProvider := dynamiccert.New("concierge-kube-signing-cert")
// This cert provider will be used to provide the impersonation proxy signing key to the // This cert provider will be used to provide the impersonation proxy signing key to the
// cert issuer used to issue certs to Pinniped clients wishing to login. // cert issuer used to issue certs to Pinniped clients wishing to login.
impersonationProxySigningCertProvider := dynamiccert.New() impersonationProxySigningCertProvider := dynamiccert.New("impersonation-proxy-signing-cert")
// Get the "real" name of the login concierge API group (i.e., the API group name with the // Get the "real" name of the login concierge API group (i.e., the API group name with the
// injected suffix). // injected suffix).

View File

@ -57,12 +57,15 @@ func (c *certsObserverController) Sync(_ controllerlib.Context) error {
if notFound { if notFound {
klog.Info("certsObserverController Sync found that the secret does not exist yet or was deleted") klog.Info("certsObserverController Sync found that the secret does not exist yet or was deleted")
// The secret does not exist yet or was deleted. // The secret does not exist yet or was deleted.
c.dynamicCertProvider.Set(nil, nil) c.dynamicCertProvider.UnsetCertKeyContent()
return nil return nil
} }
// Mutate the in-memory cert provider to update with the latest cert values. // Mutate the in-memory cert provider to update with the latest cert values.
c.dynamicCertProvider.Set(certSecret.Data[TLSCertificateChainSecretKey], certSecret.Data[tlsPrivateKeySecretKey]) if err := c.dynamicCertProvider.SetCertKeyContent(certSecret.Data[TLSCertificateChainSecretKey], certSecret.Data[tlsPrivateKeySecretKey]); err != nil {
return fmt.Errorf("failed to set serving cert/key content from secret %s/%s: %w", c.namespace, c.certsSecretResourceName, err)
}
klog.Info("certsObserverController Sync updated certs in the dynamic cert provider") klog.Info("certsObserverController Sync updated certs in the dynamic cert provider")
return nil return nil
} }

View File

@ -5,7 +5,9 @@ package apicerts
import ( import (
"context" "context"
"strings"
"testing" "testing"
"time"
"github.com/sclevine/spec" "github.com/sclevine/spec"
"github.com/sclevine/spec/report" "github.com/sclevine/spec/report"
@ -94,6 +96,7 @@ func TestObserverControllerInformerFilters(t *testing.T) {
} }
func TestObserverControllerSync(t *testing.T) { func TestObserverControllerSync(t *testing.T) {
name := t.Name()
spec.Run(t, "Sync", func(t *testing.T, when spec.G, it spec.S) { spec.Run(t, "Sync", func(t *testing.T, when spec.G, it spec.S) {
const installedInNamespace = "some-namespace" const installedInNamespace = "some-namespace"
const certsSecretResourceName = "some-resource-name" const certsSecretResourceName = "some-resource-name"
@ -142,7 +145,7 @@ func TestObserverControllerSync(t *testing.T) {
kubeInformerClient = kubernetesfake.NewSimpleClientset() kubeInformerClient = kubernetesfake.NewSimpleClientset()
kubeInformers = kubeinformers.NewSharedInformerFactory(kubeInformerClient, 0) kubeInformers = kubeinformers.NewSharedInformerFactory(kubeInformerClient, 0)
dynamicCertProvider = dynamiccert.New() dynamicCertProvider = dynamiccert.New(name)
}) })
it.After(func() { it.After(func() {
@ -160,7 +163,14 @@ func TestObserverControllerSync(t *testing.T) {
err := kubeInformerClient.Tracker().Add(unrelatedSecret) err := kubeInformerClient.Tracker().Add(unrelatedSecret)
r.NoError(err) r.NoError(err)
dynamicCertProvider.Set([]byte("some cert"), []byte("some private key")) crt, key, err := testutil.CreateCertificate(
time.Now().Add(-time.Hour),
time.Now().Add(time.Hour),
)
require.NoError(t, err)
err = dynamicCertProvider.SetCertKeyContent(crt, key)
r.NoError(err)
}) })
it("sets the dynamicCertProvider's cert and key to nil", func() { it("sets the dynamicCertProvider's cert and key to nil", func() {
@ -176,6 +186,12 @@ func TestObserverControllerSync(t *testing.T) {
when("there is a serving cert Secret with the expected keys already in the installation namespace", func() { when("there is a serving cert Secret with the expected keys already in the installation namespace", func() {
it.Before(func() { it.Before(func() {
crt, key, err := testutil.CreateCertificate(
time.Now().Add(-time.Hour),
time.Now().Add(time.Hour),
)
require.NoError(t, err)
apiServingCertSecret := &corev1.Secret{ apiServingCertSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: certsSecretResourceName, Name: certsSecretResourceName,
@ -183,24 +199,29 @@ func TestObserverControllerSync(t *testing.T) {
}, },
Data: map[string][]byte{ Data: map[string][]byte{
"caCertificate": []byte("fake cert"), "caCertificate": []byte("fake cert"),
"tlsPrivateKey": []byte("fake private key"), "tlsPrivateKey": key,
"tlsCertificateChain": []byte("fake cert chain"), "tlsCertificateChain": crt,
}, },
} }
err := kubeInformerClient.Tracker().Add(apiServingCertSecret) err = kubeInformerClient.Tracker().Add(apiServingCertSecret)
r.NoError(err) r.NoError(err)
dynamicCertProvider.Set(nil, nil) dynamicCertProvider.UnsetCertKeyContent()
}) })
it("updates the dynamicCertProvider's cert and key", func() { it("updates the dynamicCertProvider's cert and key", func() {
startInformersAndController() startInformersAndController()
actualCertChain, actualKey := dynamicCertProvider.CurrentCertKeyContent()
r.Nil(actualCertChain)
r.Nil(actualKey)
err := controllerlib.TestSync(t, subject, *syncContext) err := controllerlib.TestSync(t, subject, *syncContext)
r.NoError(err) r.NoError(err)
actualCertChain, actualKey := dynamicCertProvider.CurrentCertKeyContent() actualCertChain, actualKey = dynamicCertProvider.CurrentCertKeyContent()
r.Equal("fake cert chain", string(actualCertChain)) r.True(strings.HasPrefix(string(actualCertChain), `-----BEGIN CERTIFICATE-----`), "not a cert:\n%s", string(actualCertChain))
r.Equal("fake private key", string(actualKey)) r.True(strings.HasPrefix(string(actualKey), `-----BEGIN PRIVATE KEY-----`), "not a key:\n%s", string(actualKey))
}) })
}) })
@ -216,17 +237,22 @@ func TestObserverControllerSync(t *testing.T) {
err := kubeInformerClient.Tracker().Add(apiServingCertSecret) err := kubeInformerClient.Tracker().Add(apiServingCertSecret)
r.NoError(err) r.NoError(err)
dynamicCertProvider.Set(nil, nil) dynamicCertProvider.UnsetCertKeyContent()
}) })
it("set the missing values in the dynamicCertProvider as nil", func() { it("returns an error and does not change the dynamicCertProvider", func() {
startInformersAndController() startInformersAndController()
err := controllerlib.TestSync(t, subject, *syncContext)
r.NoError(err)
actualCertChain, actualKey := dynamicCertProvider.CurrentCertKeyContent() actualCertChain, actualKey := dynamicCertProvider.CurrentCertKeyContent()
r.Nil(actualCertChain) r.Nil(actualCertChain)
r.Nil(actualKey) r.Nil(actualKey)
err := controllerlib.TestSync(t, subject, *syncContext)
r.EqualError(err, "failed to set serving cert/key content from secret some-namespace/some-resource-name: TestObserverControllerSync: attempt to set invalid key pair: tls: failed to find any PEM data in certificate input")
actualCertChain, actualKey = dynamicCertProvider.CurrentCertKeyContent()
r.Nil(actualCertChain)
r.Nil(actualKey)
}) })
}) })
}, spec.Parallel(), spec.Report(report.Terminal{})) }, spec.Parallel(), spec.Report(report.Terminal{}))

View File

@ -117,7 +117,7 @@ func NewImpersonatorConfigController(
clock: clock, clock: clock,
impersonationSigningCertProvider: impersonationSigningCertProvider, impersonationSigningCertProvider: impersonationSigningCertProvider,
impersonatorFunc: impersonatorFunc, impersonatorFunc: impersonatorFunc,
tlsServingCertDynamicCertProvider: dynamiccert.New(), tlsServingCertDynamicCertProvider: dynamiccert.New("impersonation-proxy-serving-cert"),
}, },
}, },
withInformer( withInformer(
@ -238,7 +238,7 @@ func (c *impersonatorConfigController) doSync(syncCtx controllerlib.Context) (*v
nameInfo, err := c.findDesiredTLSCertificateName(config) nameInfo, err := c.findDesiredTLSCertificateName(config)
if err != nil { if err != nil {
// Unexpected error while determining the name that should go into the certs, so clear any existing certs. // Unexpected error while determining the name that should go into the certs, so clear any existing certs.
c.tlsServingCertDynamicCertProvider.Set(nil, nil) c.tlsServingCertDynamicCertProvider.UnsetCertKeyContent()
return nil, err return nil, err
} }
@ -371,7 +371,7 @@ func (c *impersonatorConfigController) ensureImpersonatorIsStarted(syncCtx contr
startImpersonatorFunc, err := c.impersonatorFunc( startImpersonatorFunc, err := c.impersonatorFunc(
impersonationProxyPort, impersonationProxyPort,
c.tlsServingCertDynamicCertProvider, c.tlsServingCertDynamicCertProvider,
dynamiccert.NewCAProvider(c.impersonationSigningCertProvider), c.impersonationSigningCertProvider,
) )
if err != nil { if err != nil {
return err return err
@ -750,16 +750,17 @@ func (c *impersonatorConfigController) createNewTLSSecret(ctx context.Context, c
func (c *impersonatorConfigController) loadTLSCertFromSecret(tlsSecret *v1.Secret) error { func (c *impersonatorConfigController) loadTLSCertFromSecret(tlsSecret *v1.Secret) error {
certPEM := tlsSecret.Data[v1.TLSCertKey] certPEM := tlsSecret.Data[v1.TLSCertKey]
keyPEM := tlsSecret.Data[v1.TLSPrivateKeyKey] keyPEM := tlsSecret.Data[v1.TLSPrivateKeyKey]
_, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil { if err := c.tlsServingCertDynamicCertProvider.SetCertKeyContent(certPEM, keyPEM); err != nil {
c.tlsServingCertDynamicCertProvider.Set(nil, nil) c.tlsServingCertDynamicCertProvider.UnsetCertKeyContent()
return fmt.Errorf("could not parse TLS cert PEM data from Secret: %w", err) return fmt.Errorf("could not parse TLS cert PEM data from Secret: %w", err)
} }
plog.Info("Loading TLS certificates for impersonation proxy", plog.Info("Loading TLS certificates for impersonation proxy",
"certPEM", string(certPEM), "certPEM", string(certPEM),
"secret", c.tlsSecretName, "secret", c.tlsSecretName,
"namespace", c.namespace) "namespace", c.namespace)
c.tlsServingCertDynamicCertProvider.Set(certPEM, keyPEM)
return nil return nil
} }
@ -779,7 +780,7 @@ func (c *impersonatorConfigController) ensureTLSSecretIsRemoved(ctx context.Cont
return err return err
} }
c.tlsServingCertDynamicCertProvider.Set(nil, nil) c.tlsServingCertDynamicCertProvider.UnsetCertKeyContent()
return nil return nil
} }
@ -798,8 +799,8 @@ func (c *impersonatorConfigController) loadSignerCA(status v1alpha1.StrategyStat
certPEM := signingCertSecret.Data[apicerts.CACertificateSecretKey] certPEM := signingCertSecret.Data[apicerts.CACertificateSecretKey]
keyPEM := signingCertSecret.Data[apicerts.CACertificatePrivateKeySecretKey] keyPEM := signingCertSecret.Data[apicerts.CACertificatePrivateKeySecretKey]
_, err = tls.X509KeyPair(certPEM, keyPEM)
if err != nil { if err := c.impersonationSigningCertProvider.SetCertKeyContent(certPEM, keyPEM); err != nil {
return fmt.Errorf("could not load the impersonator's credential signing secret: %w", err) return fmt.Errorf("could not load the impersonator's credential signing secret: %w", err)
} }
@ -807,13 +808,13 @@ func (c *impersonatorConfigController) loadSignerCA(status v1alpha1.StrategyStat
"certPEM", string(certPEM), "certPEM", string(certPEM),
"fromSecret", c.impersonationSignerSecretName, "fromSecret", c.impersonationSignerSecretName,
"namespace", c.namespace) "namespace", c.namespace)
c.impersonationSigningCertProvider.Set(certPEM, keyPEM)
return nil return nil
} }
func (c *impersonatorConfigController) clearSignerCA() { func (c *impersonatorConfigController) clearSignerCA() {
plog.Info("Clearing credential signing certificate for impersonation proxy") plog.Info("Clearing credential signing certificate for impersonation proxy")
c.impersonationSigningCertProvider.Set(nil, nil) c.impersonationSigningCertProvider.UnsetCertKeyContent()
} }
func (c *impersonatorConfigController) doSyncResult(nameInfo *certNameInfo, config *impersonator.Config, ca *certauthority.CA) *v1alpha1.CredentialIssuerStrategy { func (c *impersonatorConfigController) doSyncResult(nameInfo *certNameInfo, config *impersonator.Config, ca *certauthority.CA) *v1alpha1.CredentialIssuerStrategy {

View File

@ -30,7 +30,6 @@ import (
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/clock" "k8s.io/apimachinery/pkg/util/clock"
"k8s.io/apiserver/pkg/server/dynamiccertificates"
kubeinformers "k8s.io/client-go/informers" kubeinformers "k8s.io/client-go/informers"
kubernetesfake "k8s.io/client-go/kubernetes/fake" kubernetesfake "k8s.io/client-go/kubernetes/fake"
coretesting "k8s.io/client-go/testing" coretesting "k8s.io/client-go/testing"
@ -266,6 +265,7 @@ func TestImpersonatorConfigControllerOptions(t *testing.T) {
} }
func TestImpersonatorConfigControllerSync(t *testing.T) { func TestImpersonatorConfigControllerSync(t *testing.T) {
name := t.Name()
spec.Run(t, "Sync", func(t *testing.T, when spec.G, it spec.S) { spec.Run(t, "Sync", func(t *testing.T, when spec.G, it spec.S) {
const installedInNamespace = "some-namespace" const installedInNamespace = "some-namespace"
const configMapResourceName = "some-configmap-resource-name" const configMapResourceName = "some-configmap-resource-name"
@ -306,8 +306,8 @@ func TestImpersonatorConfigControllerSync(t *testing.T) {
var impersonatorFunc = func( var impersonatorFunc = func(
port int, port int,
dynamicCertProvider dynamiccertificates.CertKeyContentProvider, dynamicCertProvider dynamiccert.Private,
impersonationProxySignerCAProvider dynamiccertificates.CAContentProvider, impersonationProxySignerCAProvider dynamiccert.Public,
) (func(stopCh <-chan struct{}) error, error) { ) (func(stopCh <-chan struct{}) error, error) {
impersonatorFuncWasCalled++ impersonatorFuncWasCalled++
r.Equal(8444, port) r.Equal(8444, port)
@ -972,7 +972,7 @@ func TestImpersonatorConfigControllerSync(t *testing.T) {
kubeAPIClient = kubernetesfake.NewSimpleClientset() kubeAPIClient = kubernetesfake.NewSimpleClientset()
pinnipedAPIClient = pinnipedfake.NewSimpleClientset() pinnipedAPIClient = pinnipedfake.NewSimpleClientset()
frozenNow = time.Date(2021, time.March, 2, 7, 42, 0, 0, time.Local) frozenNow = time.Date(2021, time.March, 2, 7, 42, 0, 0, time.Local)
signingCertProvider = dynamiccert.New() signingCertProvider = dynamiccert.New(name)
ca := newCA() ca := newCA()
signingCACertPEM = ca.Bundle() signingCACertPEM = ca.Bundle()
@ -2408,7 +2408,7 @@ func TestImpersonatorConfigControllerSync(t *testing.T) {
it("returns the error", func() { it("returns the error", func() {
startInformersAndController() startInformersAndController()
errString := `could not load the impersonator's credential signing secret: tls: failed to find any PEM data in certificate input` errString := `could not load the impersonator's credential signing secret: TestImpersonatorConfigControllerSync: attempt to set invalid key pair: tls: failed to find any PEM data in certificate input`
r.EqualError(runControllerSync(), errString) r.EqualError(runControllerSync(), errString)
requireCredentialIssuer(newErrorStrategy(errString)) requireCredentialIssuer(newErrorStrategy(errString))
requireSigningCertProviderIsEmpty() requireSigningCertProviderIsEmpty()
@ -2423,7 +2423,7 @@ func TestImpersonatorConfigControllerSync(t *testing.T) {
it("returns the error", func() { it("returns the error", func() {
startInformersAndController() startInformersAndController()
errString := `could not load the impersonator's credential signing secret: tls: failed to find any PEM data in certificate input` errString := `could not load the impersonator's credential signing secret: TestImpersonatorConfigControllerSync: attempt to set invalid key pair: tls: failed to find any PEM data in certificate input`
r.EqualError(runControllerSync(), errString) r.EqualError(runControllerSync(), errString)
requireCredentialIssuer(newErrorStrategy(errString)) requireCredentialIssuer(newErrorStrategy(errString))
requireSigningCertProviderIsEmpty() requireSigningCertProviderIsEmpty()
@ -2459,7 +2459,7 @@ func TestImpersonatorConfigControllerSync(t *testing.T) {
addSecretToTrackers(updatedSigner, kubeInformerClient) addSecretToTrackers(updatedSigner, kubeInformerClient)
waitForObjectToAppearInInformer(updatedSigner, kubeInformers.Core().V1().Secrets()) waitForObjectToAppearInInformer(updatedSigner, kubeInformers.Core().V1().Secrets())
errString := `could not load the impersonator's credential signing secret: tls: failed to find any PEM data in certificate input` errString := `could not load the impersonator's credential signing secret: TestImpersonatorConfigControllerSync: attempt to set invalid key pair: tls: failed to find any PEM data in certificate input`
r.EqualError(runControllerSync(), errString) r.EqualError(runControllerSync(), errString)
requireCredentialIssuer(newErrorStrategy(errString)) requireCredentialIssuer(newErrorStrategy(errString))
requireSigningCertProviderIsEmpty() requireSigningCertProviderIsEmpty()

View File

@ -11,9 +11,9 @@ import (
k8serrors "k8s.io/apimachinery/pkg/api/errors" k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/clock" "k8s.io/apimachinery/pkg/util/clock"
"k8s.io/apimachinery/pkg/util/errors"
corev1informers "k8s.io/client-go/informers/core/v1" corev1informers "k8s.io/client-go/informers/core/v1"
"k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd"
"k8s.io/klog/v2"
configv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1" configv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1"
pinnipedclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned" pinnipedclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned"
@ -119,8 +119,7 @@ func (c *execerController) Sync(ctx controllerlib.Context) error {
c.pinnipedAPIClient, c.pinnipedAPIClient,
strategyError(c.clock, err), strategyError(c.clock, err),
) )
klog.ErrorS(strategyResultUpdateErr, "could not create or update CredentialIssuer with strategy success") return newAggregate(err, strategyResultUpdateErr)
return err
} }
keyPEM, err := c.podCommandExecutor.Exec(agentPod.Namespace, agentPod.Name, "cat", keyPath) keyPEM, err := c.podCommandExecutor.Exec(agentPod.Namespace, agentPod.Name, "cat", keyPath)
@ -132,11 +131,20 @@ func (c *execerController) Sync(ctx controllerlib.Context) error {
c.pinnipedAPIClient, c.pinnipedAPIClient,
strategyError(c.clock, err), strategyError(c.clock, err),
) )
klog.ErrorS(strategyResultUpdateErr, "could not create or update CredentialIssuer with strategy success") return newAggregate(err, strategyResultUpdateErr)
return err
} }
c.dynamicCertProvider.Set([]byte(certPEM), []byte(keyPEM)) if err := c.dynamicCertProvider.SetCertKeyContent([]byte(certPEM), []byte(keyPEM)); err != nil {
err = fmt.Errorf("failed to set signing cert/key content from agent pod %s/%s: %w", agentPod.Namespace, agentPod.Name, err)
strategyResultUpdateErr := issuerconfig.UpdateStrategy(
ctx.Context,
c.credentialIssuerLocationConfig.Name,
c.credentialIssuerLabels,
c.pinnipedAPIClient,
strategyError(c.clock, err),
)
return newAggregate(err, strategyResultUpdateErr)
}
apiInfo, err := c.getTokenCredentialRequestAPIInfo() apiInfo, err := c.getTokenCredentialRequestAPIInfo()
if err != nil { if err != nil {
@ -153,8 +161,7 @@ func (c *execerController) Sync(ctx controllerlib.Context) error {
LastUpdateTime: metav1.NewTime(c.clock.Now()), LastUpdateTime: metav1.NewTime(c.clock.Now()),
}, },
) )
klog.ErrorS(strategyResultUpdateErr, "could not create or update CredentialIssuer with strategy success") return newAggregate(err, strategyResultUpdateErr)
return err
} }
return issuerconfig.UpdateStrategy( return issuerconfig.UpdateStrategy(
@ -219,3 +226,7 @@ func (c *execerController) getKeypairFilePaths(pod *v1.Pod) (string, string) {
return certPath, keyPath return certPath, keyPath
} }
func newAggregate(errs ...error) error {
return errors.NewAggregate(errs)
}

View File

@ -132,6 +132,7 @@ func (s *fakePodExecutor) Exec(podNamespace string, podName string, commandAndAr
} }
func TestManagerControllerSync(t *testing.T) { func TestManagerControllerSync(t *testing.T) {
name := t.Name()
spec.Run(t, "Sync", func(t *testing.T, when spec.G, it spec.S) { spec.Run(t, "Sync", func(t *testing.T, when spec.G, it spec.S) {
const agentPodNamespace = "some-namespace" const agentPodNamespace = "some-namespace"
const agentPodName = "some-agent-pod-name-123" const agentPodName = "some-agent-pod-name-123"
@ -139,8 +140,6 @@ func TestManagerControllerSync(t *testing.T) {
const keyPathAnnotationName = "kube-cert-agent.pinniped.dev/key-path" const keyPathAnnotationName = "kube-cert-agent.pinniped.dev/key-path"
const fakeCertPath = "/some/cert/path" const fakeCertPath = "/some/cert/path"
const fakeKeyPath = "/some/key/path" const fakeKeyPath = "/some/key/path"
const defaultDynamicCertProviderCert = "initial-cert"
const defaultDynamicCertProviderKey = "initial-key"
const credentialIssuerResourceName = "ci-resource-name" const credentialIssuerResourceName = "ci-resource-name"
var r *require.Assertions var r *require.Assertions
@ -159,6 +158,8 @@ func TestManagerControllerSync(t *testing.T) {
var fakeCertPEM, fakeKeyPEM string var fakeCertPEM, fakeKeyPEM string
var credentialIssuerGVR schema.GroupVersionResource var credentialIssuerGVR schema.GroupVersionResource
var frozenNow time.Time var frozenNow time.Time
var defaultDynamicCertProviderCert string
var defaultDynamicCertProviderKey string
// Defer starting the informers until the last possible moment so that the // Defer starting the informers until the last possible moment so that the
// nested Before's can keep adding things to the informer caches. // nested Before's can keep adding things to the informer caches.
@ -228,14 +229,23 @@ func TestManagerControllerSync(t *testing.T) {
it.Before(func() { it.Before(func() {
r = require.New(t) r = require.New(t)
crt, key, err := testutil.CreateCertificate(
time.Now().Add(-time.Hour),
time.Now().Add(time.Hour),
)
require.NoError(t, err)
defaultDynamicCertProviderCert = string(crt)
defaultDynamicCertProviderKey = string(key)
cancelContext, cancelContextCancelFunc = context.WithCancel(context.Background()) cancelContext, cancelContextCancelFunc = context.WithCancel(context.Background())
pinnipedAPIClient = pinnipedfake.NewSimpleClientset() pinnipedAPIClient = pinnipedfake.NewSimpleClientset()
kubeClientset = kubernetesfake.NewSimpleClientset() kubeClientset = kubernetesfake.NewSimpleClientset()
kubeInformerFactory = kubeinformers.NewSharedInformerFactory(kubeClientset, 0) kubeInformerFactory = kubeinformers.NewSharedInformerFactory(kubeClientset, 0)
fakeExecutor = &fakePodExecutor{r: r} fakeExecutor = &fakePodExecutor{r: r}
frozenNow = time.Date(2020, time.September, 23, 7, 42, 0, 0, time.Local) frozenNow = time.Date(2020, time.September, 23, 7, 42, 0, 0, time.Local)
dynamicCertProvider = dynamiccert.New() dynamicCertProvider = dynamiccert.New(name)
dynamicCertProvider.Set([]byte(defaultDynamicCertProviderCert), []byte(defaultDynamicCertProviderKey)) err = dynamicCertProvider.SetCertKeyContent([]byte(defaultDynamicCertProviderCert), []byte(defaultDynamicCertProviderKey))
r.NoError(err)
loadFile := func(filename string) string { loadFile := func(filename string) string {
bytes, err := ioutil.ReadFile(filename) bytes, err := ioutil.ReadFile(filename)
@ -669,6 +679,55 @@ func TestManagerControllerSync(t *testing.T) {
r.Equal([]coretesting.Action{expectedGetAction, expectedCreateAction, expectedUpdateAction}, pinnipedAPIClient.Actions()) r.Equal([]coretesting.Action{expectedGetAction, expectedCreateAction, expectedUpdateAction}, pinnipedAPIClient.Actions())
}) })
}) })
when("the third resulting pod exec has invalid key data", func() {
var keyParseErrorMessage string
it.Before(func() {
keyParseErrorMessage = "failed to set signing cert/key content from agent pod some-namespace/some-agent-pod-name-123: TestManagerControllerSync: attempt to set invalid key pair: tls: failed to find any PEM data in key input"
fakeExecutor.errorsToReturn = []error{nil, nil}
fakeExecutor.resultsToReturn = []string{fakeCertPEM, ""}
startInformersAndController()
})
it("does not update the dynamic certificates provider", func() {
r.EqualError(controllerlib.TestSync(t, subject, *syncContext), keyParseErrorMessage)
requireDynamicCertProviderHasDefaultValues()
})
it("creates or updates the the CredentialIssuer status field with an error", func() {
r.EqualError(controllerlib.TestSync(t, subject, *syncContext), keyParseErrorMessage)
expectedCreateCredentialIssuer := &configv1alpha1.CredentialIssuer{
TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{
Name: credentialIssuerResourceName,
},
}
expectedCredentialIssuer := &configv1alpha1.CredentialIssuer{
TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{
Name: credentialIssuerResourceName,
},
Status: configv1alpha1.CredentialIssuerStatus{
Strategies: []configv1alpha1.CredentialIssuerStrategy{
{
Type: configv1alpha1.KubeClusterSigningCertificateStrategyType,
Status: configv1alpha1.ErrorStrategyStatus,
Reason: configv1alpha1.CouldNotFetchKeyStrategyReason,
Message: keyParseErrorMessage,
LastUpdateTime: metav1.NewTime(frozenNow),
},
},
},
}
expectedGetAction := coretesting.NewRootGetAction(credentialIssuerGVR, credentialIssuerResourceName)
expectedCreateAction := coretesting.NewRootCreateAction(credentialIssuerGVR, expectedCreateCredentialIssuer)
expectedUpdateAction := coretesting.NewRootUpdateSubresourceAction(credentialIssuerGVR, "status", expectedCredentialIssuer)
r.Equal([]coretesting.Action{expectedGetAction, expectedCreateAction, expectedUpdateAction}, pinnipedAPIClient.Actions())
})
})
}) })
}, spec.Parallel(), spec.Report(report.Terminal{})) }, spec.Parallel(), spec.Report(report.Terminal{}))
} }

View File

@ -4,69 +4,112 @@
package dynamiccert package dynamiccert
import ( import (
"crypto/tls"
"crypto/x509" "crypto/x509"
"fmt"
"sync" "sync"
"k8s.io/apiserver/pkg/server/dynamiccertificates" "k8s.io/apiserver/pkg/server/dynamiccertificates"
) )
// Provider provides a getter, CurrentCertKeyContent(), and a setter, Set(), for a PEM-formatted
// certificate and matching key.
type Provider interface { type Provider interface {
Private
Public
}
type Private interface {
dynamiccertificates.CertKeyContentProvider dynamiccertificates.CertKeyContentProvider
// TODO dynamiccertificates.Notifier SetCertKeyContent(certPEM, keyPEM []byte) error
// TODO dynamiccertificates.ControllerRunner ??? UnsetCertKeyContent()
Set(certPEM, keyPEM []byte)
notifier
}
type Public interface {
dynamiccertificates.CAContentProvider
notifier
}
type notifier interface {
dynamiccertificates.Notifier
dynamiccertificates.ControllerRunner // we do not need this today, but it could grow and change in the future
} }
type provider struct { type provider struct {
name string
// mutex guards all the fields below it
mutex sync.RWMutex
certPEM []byte certPEM []byte
keyPEM []byte keyPEM []byte
mutex sync.RWMutex listeners []dynamiccertificates.Listener
} }
// New returns an empty Provider. The returned Provider is thread-safe. // New returns an empty Provider. The returned Provider is thread-safe.
func New() Provider { func New(name string) Provider {
return &provider{} return &provider{name: name}
}
func (p *provider) Set(certPEM, keyPEM []byte) {
p.mutex.Lock() // acquire a write lock
defer p.mutex.Unlock()
p.certPEM = certPEM
p.keyPEM = keyPEM
} }
func (p *provider) Name() string { func (p *provider) Name() string {
return "DynamicCertProvider" return p.name // constant after struct initialization and thus does not need locking
} }
func (p *provider) CurrentCertKeyContent() (cert []byte, key []byte) { func (p *provider) CurrentCertKeyContent() (cert []byte, key []byte) {
p.mutex.RLock() // acquire a read lock p.mutex.RLock()
defer p.mutex.RUnlock() defer p.mutex.RUnlock()
return p.certPEM, p.keyPEM return p.certPEM, p.keyPEM
} }
func NewCAProvider(delegate dynamiccertificates.CertKeyContentProvider) dynamiccertificates.CAContentProvider { func (p *provider) SetCertKeyContent(certPEM, keyPEM []byte) error {
return &caContentProvider{delegate: delegate} // always make sure that we have valid PEM data, otherwise
// dynamiccertificates.NewUnionCAContentProvider.VerifyOptions will panic
if _, err := tls.X509KeyPair(certPEM, keyPEM); err != nil {
return fmt.Errorf("%s: attempt to set invalid key pair: %w", p.name, err)
}
p.setCertKeyContent(certPEM, keyPEM)
return nil
} }
type caContentProvider struct { func (p *provider) UnsetCertKeyContent() {
delegate dynamiccertificates.CertKeyContentProvider p.setCertKeyContent(nil, nil)
} }
func (c *caContentProvider) Name() string { func (p *provider) setCertKeyContent(certPEM, keyPEM []byte) {
return "DynamicCAProvider" p.mutex.Lock()
defer p.mutex.Unlock()
p.certPEM = certPEM
p.keyPEM = keyPEM
for _, listener := range p.listeners {
listener.Enqueue()
}
} }
func (c *caContentProvider) CurrentCABundleContent() []byte { func (p *provider) CurrentCABundleContent() []byte {
ca, _ := c.delegate.CurrentCertKeyContent() ca, _ := p.CurrentCertKeyContent()
return ca return ca
} }
func (c *caContentProvider) VerifyOptions() (x509.VerifyOptions, bool) { func (p *provider) VerifyOptions() (x509.VerifyOptions, bool) {
return x509.VerifyOptions{}, false // assume we are unioned via dynamiccertificates.NewUnionCAContentProvider return x509.VerifyOptions{}, false // assume we are unioned via dynamiccertificates.NewUnionCAContentProvider
} }
// TODO look at both the serving side union struct and the ca side union struct for all optional interfaces func (p *provider) AddListener(listener dynamiccertificates.Listener) {
// and then implement everything that makes sense for us to implement p.mutex.Lock()
defer p.mutex.Unlock()
p.listeners = append(p.listeners, listener)
}
func (p *provider) RunOnce() error {
return nil // no-op, but we want to make sure to stay in sync with dynamiccertificates.ControllerRunner
}
func (p *provider) Run(workers int, stopCh <-chan struct{}) {
// no-op, but we want to make sure to stay in sync with dynamiccertificates.ControllerRunner
}