eab5c2b86b
Signed-off-by: Andrew Keesler <akeesler@vmware.com>
234 lines
7.1 KiB
Go
234 lines
7.1 KiB
Go
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
|
// 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/pinniped/internal/certauthority"
|
|
"github.com/suzerain-io/pinniped/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 ErrIncapableOfIssuingCertificates = constable.Error("this cluster is not currently capable of issuing certificates")
|
|
|
|
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{}
|
|
|
|
onSuccessfulRefresh SuccessCallback
|
|
onFailedRefresh FailureCallback
|
|
|
|
lock sync.RWMutex
|
|
activeSigner signer
|
|
}
|
|
|
|
type ShutdownFunc func()
|
|
type SuccessCallback func()
|
|
type FailureCallback func(error)
|
|
|
|
// New creates a new instance of a CA. It tries to load the kube API server's private key
|
|
// immediately. If that succeeds then it calls the success callback and it is ready to issue certs.
|
|
// When it fails to get the kube API server's private key, then it calls the failure callback and
|
|
// it will try again on the next tick. It starts a goroutine to periodically reload the kube
|
|
// API server's private key in case it failed previously or case the key has changed. It returns
|
|
// a function that can be used to shut down that goroutine. Future attempts made by that goroutine
|
|
// to get the key will also result in success or failure callbacks.
|
|
func New(
|
|
kubeClient kubernetes.Interface,
|
|
podCommandExecutor PodCommandExecutor,
|
|
tick <-chan time.Time,
|
|
onSuccessfulRefresh SuccessCallback,
|
|
onFailedRefresh FailureCallback,
|
|
) (*CA, ShutdownFunc) {
|
|
signer, err := createSignerWithAPIServerSecret(kubeClient, podCommandExecutor)
|
|
if err != nil {
|
|
klog.Errorf("could not initially fetch the API server's signing key: %s", err)
|
|
signer = nil
|
|
onFailedRefresh(err)
|
|
} else {
|
|
onSuccessfulRefresh()
|
|
}
|
|
result := &CA{
|
|
kubeClient: kubeClient,
|
|
podCommandExecutor: podCommandExecutor,
|
|
shutdown: make(chan struct{}),
|
|
done: make(chan struct{}),
|
|
onSuccessfulRefresh: onSuccessfulRefresh,
|
|
onFailedRefresh: onFailedRefresh,
|
|
activeSigner: signer,
|
|
}
|
|
go result.refreshLoop(tick)
|
|
return result, result.shutdownRefresh
|
|
}
|
|
|
|
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)
|
|
c.onFailedRefresh(err)
|
|
return
|
|
}
|
|
c.lock.Lock()
|
|
c.activeSigner = newSigner
|
|
c.lock.Unlock()
|
|
c.onSuccessfulRefresh()
|
|
}
|
|
|
|
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()
|
|
|
|
if signer == nil {
|
|
return nil, nil, ErrIncapableOfIssuingCertificates
|
|
}
|
|
|
|
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
|
|
}
|