mirror of
https://github.com/vmware-tanzu/pinniped.git
synced 2026-01-07 14:05:50 +00:00
Switch back to an exec-based approach to grab the controller-manager CA. (#65)
This switches us back to an approach where we use the Pod "exec" API to grab the keys we need, rather than forcing our code to run on the control plane node. It will help us fail gracefully (or dynamically switch to alternate implementations) when the cluster is not self-hosted. Signed-off-by: Matt Moyer <moyerm@vmware.com> Co-authored-by: Ryan Richard <richardry@vmware.com>
This commit is contained in:
@@ -62,8 +62,8 @@ func secureEnv() env {
|
||||
var ErrInvalidCACertificate = fmt.Errorf("invalid CA certificate")
|
||||
|
||||
// Load a certificate authority from an existing certificate and private key (in PEM format).
|
||||
func Load(certPath string, keyPath string) (*CA, error) {
|
||||
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
|
||||
func Load(certPEM string, keyPEM string) (*CA, error) {
|
||||
cert, err := tls.X509KeyPair([]byte(certPEM), []byte(keyPEM))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not load CA: %w", err)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"crypto/x509/pkix"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -19,6 +20,19 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func loadFromFiles(t *testing.T, certPath string, keyPath string) (*CA, error) {
|
||||
t.Helper()
|
||||
|
||||
certPEM, err := ioutil.ReadFile(certPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
keyPEM, err := ioutil.ReadFile(keyPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
ca, err := Load(string(certPEM), string(keyPEM))
|
||||
return ca, err
|
||||
}
|
||||
|
||||
func TestLoad(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -26,30 +40,6 @@ func TestLoad(t *testing.T) {
|
||||
keyPath string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "missing cert",
|
||||
certPath: "./testdata/cert-does-not-exist",
|
||||
keyPath: "./testdata/test.key",
|
||||
wantErr: "could not load CA: open ./testdata/cert-does-not-exist: no such file or directory",
|
||||
},
|
||||
{
|
||||
name: "empty cert",
|
||||
certPath: "./testdata/empty",
|
||||
keyPath: "./testdata/test.key",
|
||||
wantErr: "could not load CA: tls: failed to find any PEM data in certificate input",
|
||||
},
|
||||
{
|
||||
name: "invalid cert",
|
||||
certPath: "./testdata/invalid",
|
||||
keyPath: "./testdata/test.key",
|
||||
wantErr: "could not load CA: tls: failed to find any PEM data in certificate input",
|
||||
},
|
||||
{
|
||||
name: "missing key",
|
||||
certPath: "./testdata/test.crt",
|
||||
keyPath: "./testdata/key-does-not-exist",
|
||||
wantErr: "could not load CA: open ./testdata/key-does-not-exist: no such file or directory",
|
||||
},
|
||||
{
|
||||
name: "empty key",
|
||||
certPath: "./testdata/test.crt",
|
||||
@@ -83,7 +73,7 @@ func TestLoad(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ca, err := Load(tt.certPath, tt.keyPath)
|
||||
ca, err := loadFromFiles(t, tt.certPath, tt.keyPath)
|
||||
if tt.wantErr != "" {
|
||||
require.EqualError(t, err, tt.wantErr)
|
||||
return
|
||||
@@ -202,7 +192,7 @@ func (e *errSigner) Sign(_ io.Reader, _ []byte, _ crypto.SignerOpts) ([]byte, er
|
||||
func TestIssue(t *testing.T) {
|
||||
now := time.Date(2020, 7, 10, 12, 41, 12, 1234, time.UTC)
|
||||
|
||||
realCA, err := Load("./testdata/test.crt", "./testdata/test.key")
|
||||
realCA, err := loadFromFiles(t, "./testdata/test.crt", "./testdata/test.key")
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
@@ -302,7 +292,7 @@ func TestIssue(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIssuePEM(t *testing.T) {
|
||||
realCA, err := Load("./testdata/test.crt", "./testdata/test.key")
|
||||
realCA, err := loadFromFiles(t, "./testdata/test.crt", "./testdata/test.key")
|
||||
require.NoError(t, err)
|
||||
|
||||
certPEM, keyPEM, err := realCA.IssuePEM(pkix.Name{CommonName: "Test Server"}, []string{"example.com"}, 10*time.Minute)
|
||||
|
||||
209
internal/certauthority/kubecertauthority/kubecertauthority.go
Normal file
209
internal/certauthority/kubecertauthority/kubecertauthority.go
Normal file
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
Copyright 2020 VMware, Inc.
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
// Package kubecertauthority implements a signer backed by the kubernetes controller-manager signing
|
||||
// keys (accessed via the kubernetes Exec API).
|
||||
package kubecertauthority
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/x509/pkix"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/deprecated/scheme"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/remotecommand"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"github.com/suzerain-io/placeholder-name/internal/certauthority"
|
||||
"github.com/suzerain-io/placeholder-name/internal/constable"
|
||||
)
|
||||
|
||||
// ErrNoKubeControllerManagerPod is returned when no kube-controller-manager pod is found on the cluster.
|
||||
const ErrNoKubeControllerManagerPod = constable.Error("did not find kube-controller-manager pod")
|
||||
|
||||
const k8sAPIServerCACertPEMDefaultPath = "/etc/kubernetes/ca/ca.pem"
|
||||
const k8sAPIServerCAKeyPEMDefaultPath = "/etc/kubernetes/ca/ca.key"
|
||||
|
||||
type signer interface {
|
||||
IssuePEM(subject pkix.Name, dnsNames []string, ttl time.Duration) ([]byte, []byte, error)
|
||||
}
|
||||
|
||||
type PodCommandExecutor interface {
|
||||
Exec(podNamespace string, podName string, commandAndArgs ...string) (stdoutResult string, err error)
|
||||
}
|
||||
|
||||
type kubeClientPodCommandExecutor struct {
|
||||
kubeConfig *restclient.Config
|
||||
kubeClient kubernetes.Interface
|
||||
}
|
||||
|
||||
func NewPodCommandExecutor(kubeConfig *restclient.Config, kubeClient kubernetes.Interface) PodCommandExecutor {
|
||||
return &kubeClientPodCommandExecutor{kubeConfig: kubeConfig, kubeClient: kubeClient}
|
||||
}
|
||||
|
||||
func (s *kubeClientPodCommandExecutor) Exec(podNamespace string, podName string, commandAndArgs ...string) (string, error) {
|
||||
request := s.kubeClient.
|
||||
CoreV1().
|
||||
RESTClient().
|
||||
Post().
|
||||
Namespace(podNamespace).
|
||||
Resource("pods").
|
||||
Name(podName).
|
||||
SubResource("exec").
|
||||
VersionedParams(&v1.PodExecOptions{
|
||||
Stdin: false,
|
||||
Stdout: true,
|
||||
Stderr: false,
|
||||
TTY: false,
|
||||
Command: commandAndArgs,
|
||||
}, scheme.ParameterCodec)
|
||||
|
||||
executor, err := remotecommand.NewSPDYExecutor(s.kubeConfig, "POST", request.URL())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var stdoutBuf bytes.Buffer
|
||||
if err := executor.Stream(remotecommand.StreamOptions{Stdout: &stdoutBuf}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return stdoutBuf.String(), nil
|
||||
}
|
||||
|
||||
type CA struct {
|
||||
kubeClient kubernetes.Interface
|
||||
podCommandExecutor PodCommandExecutor
|
||||
|
||||
shutdown, done chan struct{}
|
||||
|
||||
lock sync.RWMutex
|
||||
activeSigner signer
|
||||
}
|
||||
|
||||
type ShutdownFunc func()
|
||||
|
||||
// New creates a new instance of a CA which is has loaded the kube API server's private key
|
||||
// and is ready to issue certs, or an error. When successful, it also starts a goroutine
|
||||
// to periodically reload the kube API server's private key in case it changed, and returns
|
||||
// a function that can be used to shut down that goroutine.
|
||||
func New(kubeClient kubernetes.Interface, podCommandExecutor PodCommandExecutor, tick <-chan time.Time) (*CA, ShutdownFunc, error) {
|
||||
signer, err := createSignerWithAPIServerSecret(kubeClient, podCommandExecutor)
|
||||
if err != nil {
|
||||
// The initial load failed, so give up
|
||||
return nil, nil, err
|
||||
}
|
||||
result := &CA{
|
||||
kubeClient: kubeClient,
|
||||
podCommandExecutor: podCommandExecutor,
|
||||
activeSigner: signer,
|
||||
shutdown: make(chan struct{}),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
go result.refreshLoop(tick)
|
||||
return result, result.shutdownRefresh, nil
|
||||
}
|
||||
|
||||
func createSignerWithAPIServerSecret(kubeClient kubernetes.Interface, podCommandExecutor PodCommandExecutor) (signer, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
pod, err := findControllerManagerPod(ctx, kubeClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
certPath, keyPath := getKeypairFilePaths(pod)
|
||||
|
||||
certPEM, err := podCommandExecutor.Exec(pod.Namespace, pod.Name, "cat", certPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keyPEM, err := podCommandExecutor.Exec(pod.Namespace, pod.Name, "cat", keyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return certauthority.Load(certPEM, keyPEM)
|
||||
}
|
||||
|
||||
func (c *CA) refreshLoop(tick <-chan time.Time) {
|
||||
for {
|
||||
select {
|
||||
case <-c.shutdown:
|
||||
close(c.done)
|
||||
return
|
||||
case <-tick:
|
||||
c.updateSigner()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CA) updateSigner() {
|
||||
newSigner, err := createSignerWithAPIServerSecret(c.kubeClient, c.podCommandExecutor)
|
||||
if err != nil {
|
||||
klog.Errorf("could not create signer with API server secret: %s", err)
|
||||
return
|
||||
}
|
||||
c.lock.Lock()
|
||||
c.activeSigner = newSigner
|
||||
c.lock.Unlock()
|
||||
}
|
||||
|
||||
func (c *CA) shutdownRefresh() {
|
||||
close(c.shutdown)
|
||||
<-c.done
|
||||
}
|
||||
|
||||
// IssuePEM issues a new server certificate for the given identity and duration, returning it as a pair of
|
||||
// PEM-formatted byte slices for the certificate and private key.
|
||||
func (c *CA) IssuePEM(subject pkix.Name, dnsNames []string, ttl time.Duration) ([]byte, []byte, error) {
|
||||
c.lock.RLock()
|
||||
signer := c.activeSigner
|
||||
c.lock.RUnlock()
|
||||
|
||||
return signer.IssuePEM(subject, dnsNames, ttl)
|
||||
}
|
||||
|
||||
func findControllerManagerPod(ctx context.Context, kubeClient kubernetes.Interface) (*v1.Pod, error) {
|
||||
pods, err := kubeClient.CoreV1().Pods("kube-system").List(ctx, metav1.ListOptions{
|
||||
LabelSelector: "component=kube-controller-manager",
|
||||
FieldSelector: "status.phase=Running",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not check for kube-controller-manager pod: %w", err)
|
||||
}
|
||||
for _, pod := range pods.Items {
|
||||
return &pod, nil
|
||||
}
|
||||
return nil, ErrNoKubeControllerManagerPod
|
||||
}
|
||||
|
||||
func getKeypairFilePaths(pod *v1.Pod) (string, string) {
|
||||
certPath := getContainerArgByName(pod, "cluster-signing-cert-file", k8sAPIServerCACertPEMDefaultPath)
|
||||
keyPath := getContainerArgByName(pod, "cluster-signing-key-file", k8sAPIServerCAKeyPEMDefaultPath)
|
||||
return certPath, keyPath
|
||||
}
|
||||
|
||||
func getContainerArgByName(pod *v1.Pod, name string, defaultValue string) string {
|
||||
for _, container := range pod.Spec.Containers {
|
||||
flagset := pflag.NewFlagSet("", pflag.ContinueOnError)
|
||||
flagset.ParseErrorsWhitelist = pflag.ParseErrorsWhitelist{UnknownFlags: true}
|
||||
var val string
|
||||
flagset.StringVar(&val, name, "", "")
|
||||
_ = flagset.Parse(append(container.Command, container.Args...))
|
||||
if val != "" {
|
||||
return val
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
/*
|
||||
Copyright 2020 VMware, Inc.
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package kubecertauthority
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sclevine/spec"
|
||||
"github.com/sclevine/spec/report"
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
kubernetesfake "k8s.io/client-go/kubernetes/fake"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"github.com/suzerain-io/placeholder-name/internal/testutil"
|
||||
)
|
||||
|
||||
type fakePodExecutor struct {
|
||||
resultsToReturn []string
|
||||
errorsToReturn []error
|
||||
|
||||
calledWithPodName []string
|
||||
calledWithPodNamespace []string
|
||||
calledWithCommandAndArgs [][]string
|
||||
|
||||
callCount int
|
||||
}
|
||||
|
||||
func (s *fakePodExecutor) Exec(podNamespace string, podName string, commandAndArgs ...string) (string, error) {
|
||||
s.calledWithPodNamespace = append(s.calledWithPodNamespace, podNamespace)
|
||||
s.calledWithPodName = append(s.calledWithPodName, podName)
|
||||
s.calledWithCommandAndArgs = append(s.calledWithCommandAndArgs, commandAndArgs)
|
||||
result := s.resultsToReturn[s.callCount]
|
||||
var err error = nil
|
||||
if s.errorsToReturn != nil {
|
||||
err = s.errorsToReturn[s.callCount]
|
||||
}
|
||||
s.callCount++
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func TestCA(t *testing.T) {
|
||||
spec.Run(t, "CA", func(t *testing.T, when spec.G, it spec.S) {
|
||||
var r *require.Assertions
|
||||
var fakeCertPEM, fakeKeyPEM string
|
||||
var fakeCert2PEM, fakeKey2PEM string
|
||||
var fakePod *corev1.Pod
|
||||
var kubeAPIClient *kubernetesfake.Clientset
|
||||
var fakeExecutor *fakePodExecutor
|
||||
var neverTicker <-chan time.Time
|
||||
|
||||
var logger *testutil.TranscriptLogger
|
||||
|
||||
it.Before(func() {
|
||||
r = require.New(t)
|
||||
|
||||
loadFile := func(filename string) string {
|
||||
bytes, err := ioutil.ReadFile(filename)
|
||||
r.NoError(err)
|
||||
return string(bytes)
|
||||
}
|
||||
fakeCertPEM = loadFile("./testdata/test.crt")
|
||||
fakeKeyPEM = loadFile("./testdata/test.key")
|
||||
fakeCert2PEM = loadFile("./testdata/test2.crt")
|
||||
fakeKey2PEM = loadFile("./testdata/test2.key")
|
||||
|
||||
fakePod = &corev1.Pod{
|
||||
TypeMeta: metav1.TypeMeta{},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "fake-pod",
|
||||
Namespace: "kube-system",
|
||||
Labels: map[string]string{"component": "kube-controller-manager"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{Name: "kube-controller-manager"}},
|
||||
},
|
||||
Status: corev1.PodStatus{
|
||||
Phase: "Running",
|
||||
},
|
||||
}
|
||||
|
||||
kubeAPIClient = kubernetesfake.NewSimpleClientset()
|
||||
|
||||
fakeExecutor = &fakePodExecutor{
|
||||
resultsToReturn: []string{
|
||||
fakeCertPEM,
|
||||
fakeKeyPEM,
|
||||
fakeCert2PEM,
|
||||
fakeKey2PEM,
|
||||
},
|
||||
}
|
||||
|
||||
logger = testutil.NewTranscriptLogger(t)
|
||||
klog.SetLogger(logger) // this is unfortunately a global logger, so can't run these tests in parallel :(
|
||||
})
|
||||
|
||||
it.After(func() {
|
||||
klog.SetLogger(nil)
|
||||
})
|
||||
|
||||
when("the kube-controller-manager pod is found with default CLI flag values", func() {
|
||||
it.Before(func() {
|
||||
err := kubeAPIClient.Tracker().Add(fakePod)
|
||||
r.NoError(err)
|
||||
})
|
||||
|
||||
when("the exec commands return the API server's keypair", func() {
|
||||
it("finds the API server's signing key and uses it to issue certificates", func() {
|
||||
fakeTicker := make(chan time.Time)
|
||||
|
||||
subject, shutdownFunc, err := New(kubeAPIClient, fakeExecutor, fakeTicker)
|
||||
r.NoError(err)
|
||||
r.NotNil(shutdownFunc)
|
||||
defer shutdownFunc()
|
||||
|
||||
r.Equal(2, fakeExecutor.callCount)
|
||||
|
||||
r.Equal("kube-system", fakeExecutor.calledWithPodNamespace[0])
|
||||
r.Equal("fake-pod", fakeExecutor.calledWithPodName[0])
|
||||
r.Equal([]string{"cat", "/etc/kubernetes/ca/ca.pem"}, fakeExecutor.calledWithCommandAndArgs[0])
|
||||
|
||||
r.Equal("kube-system", fakeExecutor.calledWithPodNamespace[1])
|
||||
r.Equal("fake-pod", fakeExecutor.calledWithPodName[1])
|
||||
r.Equal([]string{"cat", "/etc/kubernetes/ca/ca.key"}, fakeExecutor.calledWithCommandAndArgs[1])
|
||||
|
||||
// Validate that we can issue a certificate signed by the original API server CA.
|
||||
certPEM, keyPEM, err := subject.IssuePEM(
|
||||
pkix.Name{CommonName: "Test Server"},
|
||||
[]string{"example.com"},
|
||||
10*time.Minute,
|
||||
)
|
||||
r.NoError(err)
|
||||
validCert := testutil.ValidateCertificate(t, fakeCertPEM, string(certPEM))
|
||||
validCert.RequireDNSName("example.com")
|
||||
validCert.RequireLifetime(time.Now(), time.Now().Add(10*time.Minute), 2*time.Minute)
|
||||
validCert.RequireMatchesPrivateKey(string(keyPEM))
|
||||
|
||||
// Tick the timer and wait for another refresh loop to complete.
|
||||
fakeTicker <- time.Now()
|
||||
|
||||
var secondCertPEM, secondKeyPEM string
|
||||
r.Eventually(func() bool {
|
||||
certPEM, keyPEM, err := subject.IssuePEM(
|
||||
pkix.Name{CommonName: "Test Server"},
|
||||
[]string{"example.com"},
|
||||
10*time.Minute,
|
||||
)
|
||||
r.NoError(err)
|
||||
secondCertPEM = string(certPEM)
|
||||
secondKeyPEM = string(keyPEM)
|
||||
|
||||
block, _ := pem.Decode(certPEM)
|
||||
require.NotNil(t, block)
|
||||
parsed, err := x509.ParseCertificate(block.Bytes)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Validate the created cert using the second API server CA.
|
||||
roots := x509.NewCertPool()
|
||||
require.True(t, roots.AppendCertsFromPEM([]byte(fakeCert2PEM)))
|
||||
opts := x509.VerifyOptions{Roots: roots}
|
||||
_, err = parsed.Verify(opts)
|
||||
return err == nil
|
||||
}, 5*time.Second, 100*time.Millisecond)
|
||||
|
||||
validCert2 := testutil.ValidateCertificate(t, fakeCert2PEM, secondCertPEM)
|
||||
validCert2.RequireDNSName("example.com")
|
||||
validCert2.RequireLifetime(time.Now(), time.Now().Add(10*time.Minute), 2*time.Minute)
|
||||
validCert2.RequireMatchesPrivateKey(secondKeyPEM)
|
||||
})
|
||||
})
|
||||
|
||||
when("the exec commands return the API server's keypair the first time but subsequently fails", func() {
|
||||
it.Before(func() {
|
||||
fakeExecutor.errorsToReturn = []error{nil, nil, fmt.Errorf("some exec error")}
|
||||
})
|
||||
|
||||
it("logs an error message", func() {
|
||||
fakeTicker := make(chan time.Time)
|
||||
|
||||
subject, shutdownFunc, err := New(kubeAPIClient, fakeExecutor, fakeTicker)
|
||||
r.NoError(err)
|
||||
r.NotNil(shutdownFunc)
|
||||
defer shutdownFunc()
|
||||
r.Equal(2, fakeExecutor.callCount)
|
||||
|
||||
// Tick the timer and wait for another refresh loop to complete.
|
||||
fakeTicker <- time.Now()
|
||||
|
||||
// Wait for there to be a log output and require that it matches our expectation.
|
||||
r.Eventually(func() bool { return len(logger.Transcript()) >= 1 }, 5*time.Second, 10*time.Millisecond)
|
||||
r.Contains(logger.Transcript()[0].Message, "could not create signer with API server secret: some exec error")
|
||||
r.Equal(logger.Transcript()[0].Level, "error")
|
||||
|
||||
// Validate that we can still issue a certificate signed by the original API server CA.
|
||||
certPEM, _, err := subject.IssuePEM(
|
||||
pkix.Name{CommonName: "Test Server"},
|
||||
[]string{"example.com"},
|
||||
10*time.Minute,
|
||||
)
|
||||
r.NoError(err)
|
||||
testutil.ValidateCertificate(t, fakeCertPEM, string(certPEM))
|
||||
})
|
||||
})
|
||||
|
||||
when("the exec commands succeed but return garbage", func() {
|
||||
it.Before(func() {
|
||||
fakeExecutor.resultsToReturn = []string{"not a cert", "not a private key"}
|
||||
})
|
||||
|
||||
it("returns an error", func() {
|
||||
subject, shutdownFunc, err := New(kubeAPIClient, fakeExecutor, neverTicker)
|
||||
r.Nil(subject)
|
||||
r.Nil(shutdownFunc)
|
||||
r.EqualError(err, "could not load CA: tls: failed to find any PEM data in certificate input")
|
||||
})
|
||||
})
|
||||
|
||||
when("the first exec command returns an error", func() {
|
||||
it.Before(func() {
|
||||
fakeExecutor.errorsToReturn = []error{fmt.Errorf("some error"), nil}
|
||||
})
|
||||
|
||||
it("returns an error", func() {
|
||||
subject, shutdownFunc, err := New(kubeAPIClient, fakeExecutor, neverTicker)
|
||||
r.Nil(subject)
|
||||
r.Nil(shutdownFunc)
|
||||
r.EqualError(err, "some error")
|
||||
})
|
||||
})
|
||||
|
||||
when("the second exec command returns an error", func() {
|
||||
it.Before(func() {
|
||||
fakeExecutor.errorsToReturn = []error{nil, fmt.Errorf("some error")}
|
||||
})
|
||||
|
||||
it("returns an error", func() {
|
||||
subject, shutdownFunc, err := New(kubeAPIClient, fakeExecutor, neverTicker)
|
||||
r.Nil(subject)
|
||||
r.Nil(shutdownFunc)
|
||||
r.EqualError(err, "some error")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
when("the kube-controller-manager pod is found with non-default CLI flag values", func() {
|
||||
it.Before(func() {
|
||||
fakePod.Spec.Containers[0].Command = []string{
|
||||
"kube-controller-manager",
|
||||
"--cluster-signing-cert-file=/etc/kubernetes/ca/non-default.pem",
|
||||
}
|
||||
fakePod.Spec.Containers[0].Args = []string{
|
||||
"--cluster-signing-key-file=/etc/kubernetes/ca/non-default.key",
|
||||
}
|
||||
err := kubeAPIClient.Tracker().Add(fakePod)
|
||||
r.NoError(err)
|
||||
})
|
||||
|
||||
it("finds the API server's signing key and uses it to issue certificates", func() {
|
||||
_, shutdownFunc, err := New(kubeAPIClient, fakeExecutor, neverTicker)
|
||||
r.NoError(err)
|
||||
r.NotNil(shutdownFunc)
|
||||
defer shutdownFunc()
|
||||
|
||||
r.Equal(2, fakeExecutor.callCount)
|
||||
|
||||
r.Equal("kube-system", fakeExecutor.calledWithPodNamespace[0])
|
||||
r.Equal("fake-pod", fakeExecutor.calledWithPodName[0])
|
||||
r.Equal([]string{"cat", "/etc/kubernetes/ca/non-default.pem"}, fakeExecutor.calledWithCommandAndArgs[0])
|
||||
|
||||
r.Equal("kube-system", fakeExecutor.calledWithPodNamespace[1])
|
||||
r.Equal("fake-pod", fakeExecutor.calledWithPodName[1])
|
||||
r.Equal([]string{"cat", "/etc/kubernetes/ca/non-default.key"}, fakeExecutor.calledWithCommandAndArgs[1])
|
||||
})
|
||||
})
|
||||
|
||||
when("the kube-controller-manager pod is found with non-default CLI flag values separated by spaces", func() {
|
||||
it.Before(func() {
|
||||
fakePod.Spec.Containers[0].Command = []string{
|
||||
"kube-controller-manager",
|
||||
"--cluster-signing-cert-file", "/etc/kubernetes/ca/non-default.pem",
|
||||
"--cluster-signing-key-file", "/etc/kubernetes/ca/non-default.key",
|
||||
"--foo=bar",
|
||||
}
|
||||
err := kubeAPIClient.Tracker().Add(fakePod)
|
||||
r.NoError(err)
|
||||
})
|
||||
|
||||
it("finds the API server's signing key and uses it to issue certificates", func() {
|
||||
_, shutdownFunc, err := New(kubeAPIClient, fakeExecutor, neverTicker)
|
||||
r.NoError(err)
|
||||
r.NotNil(shutdownFunc)
|
||||
defer shutdownFunc()
|
||||
|
||||
r.Equal(2, fakeExecutor.callCount)
|
||||
|
||||
r.Equal("kube-system", fakeExecutor.calledWithPodNamespace[0])
|
||||
r.Equal("fake-pod", fakeExecutor.calledWithPodName[0])
|
||||
r.Equal([]string{"cat", "/etc/kubernetes/ca/non-default.pem"}, fakeExecutor.calledWithCommandAndArgs[0])
|
||||
|
||||
r.Equal("kube-system", fakeExecutor.calledWithPodNamespace[1])
|
||||
r.Equal("fake-pod", fakeExecutor.calledWithPodName[1])
|
||||
r.Equal([]string{"cat", "/etc/kubernetes/ca/non-default.key"}, fakeExecutor.calledWithCommandAndArgs[1])
|
||||
})
|
||||
})
|
||||
|
||||
when("the kube-controller-manager pod is not found", func() {
|
||||
it("returns an error", func() {
|
||||
subject, shutdownFunc, err := New(kubeAPIClient, fakeExecutor, neverTicker)
|
||||
r.Nil(subject)
|
||||
r.Nil(shutdownFunc)
|
||||
r.True(errors.Is(err, ErrNoKubeControllerManagerPod))
|
||||
})
|
||||
})
|
||||
}, spec.Report(report.Terminal{}))
|
||||
}
|
||||
17
internal/certauthority/kubecertauthority/testdata/test.crt
vendored
Normal file
17
internal/certauthority/kubecertauthority/testdata/test.crt
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICyDCCAbCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwprdWJl
|
||||
cm5ldGVzMB4XDTIwMDcyNTIxMDQxOFoXDTMwMDcyMzIxMDQxOFowFTETMBEGA1UE
|
||||
AxMKa3ViZXJuZXRlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL3K
|
||||
hYv2gIQ1Dwzh2cWMid+ofAnvLIfV2Xv61vTLGprUI+XUqB4/gtf6X6UNn0Lett2n
|
||||
d8p4wy7hw73hU/ggdvmWJvqBrSjc3JGfy+kj66fKXX+PTlbL7QbwiRvcSqIXIWlV
|
||||
lHHxECWrED8jCulw/NVqfook/h5iNUCT9yswSJr/0fImiVnoTlIoEYG2eCNejZ5c
|
||||
g39uD3ZTqd9ZxWwSLLnI+2kpJnZBPcd1ZQ8AQqzDgZtYRCqacn5gckQUKZWKQlxo
|
||||
Eft6g1XHJouAWAZw7hEtk0v8rG0/eKF7wamxFi6BFVlbjWBsB4T9rApbdBWTKeCJ
|
||||
Hv8fv5RMFSzpT3uzTO8CAwEAAaMjMCEwDgYDVR0PAQH/BAQDAgKkMA8GA1UdEwEB
|
||||
/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBACh5RhbxqJe+Z/gc17cZhKNmdiwu
|
||||
I2pLp3QBfwvN+Wbmajzw/7rYhY0d8JYVTJzXSCPWi6UAKxAtXOLF8WIIf9i39n6R
|
||||
uKOBGW14FzzGyRJiD3qaG/JTvEW+SLhwl68Ndr5LHSnbugAqq31abcQy6Zl9v5A8
|
||||
JKC97Lj/Sn8rj7opKy4W3oq7NCQsAb0zh4IllRF6UvSnJySfsg7xdXHHpxYDHtOS
|
||||
XcOu5ySUIZTgFe9RfeUZlGZ5xn0ckMlQ7qW2Wx1q0OVWw5us4NtkGqKrHG4Tn1X7
|
||||
uwo/Yytn5sDxrDv1/oii6AZOCsTPre4oD3wz4nmVzCVJcgrqH4Q24hT8WNg=
|
||||
-----END CERTIFICATE-----
|
||||
27
internal/certauthority/kubecertauthority/testdata/test.key
vendored
Normal file
27
internal/certauthority/kubecertauthority/testdata/test.key
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEogIBAAKCAQEAvcqFi/aAhDUPDOHZxYyJ36h8Ce8sh9XZe/rW9MsamtQj5dSo
|
||||
Hj+C1/pfpQ2fQt623ad3ynjDLuHDveFT+CB2+ZYm+oGtKNzckZ/L6SPrp8pdf49O
|
||||
VsvtBvCJG9xKohchaVWUcfEQJasQPyMK6XD81Wp+iiT+HmI1QJP3KzBImv/R8iaJ
|
||||
WehOUigRgbZ4I16NnlyDf24PdlOp31nFbBIsucj7aSkmdkE9x3VlDwBCrMOBm1hE
|
||||
KppyfmByRBQplYpCXGgR+3qDVccmi4BYBnDuES2TS/ysbT94oXvBqbEWLoEVWVuN
|
||||
YGwHhP2sClt0FZMp4Ike/x+/lEwVLOlPe7NM7wIDAQABAoIBAFC1tUEmHNUcM0BJ
|
||||
M3D9KQzB+63F1mwVlx1QOOV1EeVR3co5Ox1R6PSr9sycFGQ9jgqI0zp5TJe9Tp6L
|
||||
GkhklfPh1MWnK9o6wlnzWKXWrrp2Jni+mpPyuOPAmq4Maniv2XeP+0bROwqpyojv
|
||||
AA7yC7M+TH226ZJGNVs3EV9+cwHml0yuzBfIJn/rv/w2g+WRKM/MC0S7k2d8bRlA
|
||||
NycKVGAGBhKTltjoVYOeh6aHEpSjK8zfaePjo5dYJvoVIli60YCgcJOU/8jXT+Np
|
||||
1Fm7tRvAtj3pUp0Sqdaf2RUzh9jfJp2VFCHuSJ6TPqArOyQojtMcTHF0TiW7xrHP
|
||||
xOCRIAECgYEAwGBPU7vdthMJBg+ORUoGQQaItTeJvQwIqJvbKD2osp4jhS1dGZBw
|
||||
W30GKEc/gd8JNtOq9BBnMicPF7hktuy+bSPv41XPud67rSSO7Tsw20C10gFRq06B
|
||||
zIJWFAUqK3IkvVc3VDmtSLSDox4QZ/BdqaMlQ5y5JCsC5kThmkZFlO8CgYEA/I9X
|
||||
YHi6RioMJE1fqOHJL4DDjlezmcuRrD7fE5InKbtJZ2JhGYOX/C0KXnHTOWTCDxxN
|
||||
FBvpvD6Xv5o3PhB9Z6k2fqvJ4GS8urkG/KU4xcC+bak+9ava8oaiSqG16zD9NH2P
|
||||
jJ60NrbLl1J0pU9fiwuFVUKJ4hDZOfN9RqYdyAECgYAVwo8WhJiGgM6zfcz073OX
|
||||
pVqPTPHqjVLpZ3+5pIfRdGvGI6R1QM5EuvaYVb7MPOM47WZX5wcVOC/P2g6iVlMP
|
||||
21HGIC2384a9BfaYxOo40q/+SiHnw6CQ9mkwKIllkqqvNA9RGpkMMUb2i28For2l
|
||||
c4vCgxa6DZdtXns6TRqPxwKBgCfY5cxOv/T6BVhk7MbUeM2J31DB/ZAyUhV/Bess
|
||||
kAlBh19MYk2IOZ6L7KriApV3lDaWHIMjtEkDByYvyq98Io0MYZCywfMpca10K+oI
|
||||
l2B7/I+IuGpCZxUEsO5dfTpSTGDPvqpND9niFVUWqVi7oTNq6ep9yQtl5SADjqxq
|
||||
4SABAoGAIm0hUg1wtcS46cGLy6PIkPM5tocTSghtz4vFsuk/i4QA9GBoBO2gH6ty
|
||||
+kJHmeaXt2dmgySp0QAWit5UlceEumB0NXnAdJZQxeGSFSyYkDWhwXd8wDceKo/1
|
||||
LfCU6Dk8IN/SsppVUWXQ2rlORvxlrHeCio8o0kS9Yiu55WMYg4g=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
17
internal/certauthority/kubecertauthority/testdata/test2.crt
vendored
Normal file
17
internal/certauthority/kubecertauthority/testdata/test2.crt
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICyDCCAbCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwprdWJl
|
||||
cm5ldGVzMB4XDTIwMDgxODE2NDEzNloXDTMwMDgxNjE2NDEzNlowFTETMBEGA1UE
|
||||
AxMKa3ViZXJuZXRlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALH7
|
||||
C2JpttDi3mxpD4bd+BZucCrS8XF2YwqYAr42HePp++PBnlUFqWmtPc9/bmo+7+7z
|
||||
iAAlnAV0pJWP+HR/PskX8MRcFAA1HoXLa37Q4SuBBQG+JE+AeaOObmQYaCFv55ej
|
||||
UF4+JIoQOdlbYEMYSI07el0cxQL4Io/CHJ3p7AtNElxjDuMK4B9W8NiCse3p7Uf+
|
||||
Qje4we1TYOfcpAM0jpBPHK9vCBCpX+j52S5DUTRVIk9kye3lCDmWOXH/fhj/aJTM
|
||||
1MP/hThbl2wIbFuv1bpa0kXNZs8xB63dtqROQ+lCghDmuayRmzwXl2PX6IgFFcjV
|
||||
yAgjXrZqjihs+mY8eT0CAwEAAaMjMCEwDgYDVR0PAQH/BAQDAgKkMA8GA1UdEwEB
|
||||
/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAE+Saqk2EyuIx1rxFWrOwpTi5q/B
|
||||
p/TwEtrmrFIRVPnGeBnhyfbGXPDMkzIY1mEvztu8H+pm5RPyhQYLsuwzYiYMQyxX
|
||||
yL9VvO7uydn7+3zX7oknQ5qAvN3nmItNyOKw3MRIKGsySNuTQ5JPtU/ufGlEivbK
|
||||
vNaDBqjKrBvwhIKMdV9/xYSyeBhSSWr/6W1tAk+XbHhQH1M78rdwGN5SI75L4FGu
|
||||
13kn/W2n8pE17TAY88B1YGKhsLSvf8KrFNYv+UUmzh2WstECKSlnbrSM+boMlGJn
|
||||
XahE8M23fieB+SaenQdOezrY4GAnXQ3qToDlhdYAOkWhcGDct47VRM93whY=
|
||||
-----END CERTIFICATE-----
|
||||
27
internal/certauthority/kubecertauthority/testdata/test2.key
vendored
Normal file
27
internal/certauthority/kubecertauthority/testdata/test2.key
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEogIBAAKCAQEAsfsLYmm20OLebGkPht34Fm5wKtLxcXZjCpgCvjYd4+n748Ge
|
||||
VQWpaa09z39uaj7v7vOIACWcBXSklY/4dH8+yRfwxFwUADUehctrftDhK4EFAb4k
|
||||
T4B5o45uZBhoIW/nl6NQXj4kihA52VtgQxhIjTt6XRzFAvgij8IcnensC00SXGMO
|
||||
4wrgH1bw2IKx7entR/5CN7jB7VNg59ykAzSOkE8cr28IEKlf6PnZLkNRNFUiT2TJ
|
||||
7eUIOZY5cf9+GP9olMzUw/+FOFuXbAhsW6/VulrSRc1mzzEHrd22pE5D6UKCEOa5
|
||||
rJGbPBeXY9foiAUVyNXICCNetmqOKGz6Zjx5PQIDAQABAoIBAD06klYO7De8dKxz
|
||||
EEZjgn+lCq2Q2EMiaTwxw2/QikPoMSHPcDrrsbaLROJngoLGmCBqY3U5ew1dbWmO
|
||||
l/jr9ZuUwt2ql67il1eL/bUpAu3GewR4d2FqX25nB48j3l7ycof2RSXG1ycwIdam
|
||||
2tz6M6tytMvno9c7qhguvU2ONghEreXG3YYLdf9l97aB+p6GdXhwty22b7tAVwp1
|
||||
GKn79kVYgmL86lph9hBPqtHuG1LHZUiFodr2iWXSu3H/265OD58a33ZO3iyfFI0s
|
||||
PPy87ZN0r+1hGpoKKkDe63udOYgAG6xmIea/1Pdn9Eg87tueoeC7XcUpdaCJlKaF
|
||||
tqCusEECgYEA60rWyXxTFKJ4QdVaqXoWMA4cQkT73RxznSKwkN/Svk8TVv+p5s5Y
|
||||
oYKN4qyMzxvQzu+QNWpd1yTveCmmEynz457ELpGtidtiJdm7xZMdMGrU02eCL9mZ
|
||||
ERbtfAkbEAKvN8D73fWyzghKv4dgcQptmsqZlYYc4vpwHveK+/N5lukCgYEAwaT3
|
||||
iMTWCv7Vp87iKrzNUAH4iBWlazwbE+EDEnHVw26Y82fhgEgxiU2UvFSaIVhGpaCz
|
||||
MYSXSdRcQTHgCoJLPfWHUHTJPqf36KfAJfdaxxjzTTbNYjUOkdcUD1bcNrm0yjoY
|
||||
nR4zK1FPw86ODMYtBpfkyL7ZX8G1v5pRL/6/gzUCgYBzgwQ7Wmu3H6QGPeYKecNW
|
||||
yDabWh6ECKnBpPwlw5xEjbGi7lTM2NSuRde+RpPCQZebYATeFGAJdTqTNW8wzVHM
|
||||
l28cpawal7dxeZkzf+u+j1P4jUJel2cL+sOQNzAwBgFbT8TWzP6BI5T+vklcdZAl
|
||||
g/0uaO7Zh7Vvnnt/AaLZsQKBgGfbHzuGPjoFdPecOKatPfxUIkRyP5bk1KzzuF8T
|
||||
GI/JaFTbeREBJzg5mLTtNwD9RF6ecpzzPOTG9Xet1Tgtq0cewSUAjdKB6a8pESAL
|
||||
qu8vTYYzBzJNvHOxg7u6XT8omHMBd6QEx3LLGFmvFXZ6bzmjC3wzB4iY7u5FSJfS
|
||||
LEqlAoGAb0rbJ85vrJopbx8lzhJjyaJfM8+A3oQg1K3TphHrcgkUc8qx8QEosziM
|
||||
wzYKSBlXd2bxMibyd0mTEMNl4/BqofaKoqof9gBIbkamwXOO8s7IgKxQAfr1R/z8
|
||||
tHBW/g0QWPB+qtaVDtHwyQLlxjx8HD7atIo8d/do9ruwVaf+r6g=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
Reference in New Issue
Block a user