Remove kubecertauthority pkg
All of its functionality was refactored to move elsewhere or to not be needed anymore by previous commits
This commit is contained in:
parent
69137fb6b9
commit
3f06be2246
@ -1,262 +0,0 @@
|
|||||||
// 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"
|
|
||||||
|
|
||||||
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"
|
|
||||||
|
|
||||||
"go.pinniped.dev/internal/certauthority"
|
|
||||||
"go.pinniped.dev/internal/constable"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ErrNoKubeCertAgentPod is returned when no kube-cert-agent pod is found on the cluster.
|
|
||||||
const ErrNoKubeCertAgentPod = constable.Error("did not find kube-cert-agent 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// AgentInfo is a data object that holds the fields necessary for a CA to communicate with an agent
|
|
||||||
// pod.
|
|
||||||
type AgentInfo struct {
|
|
||||||
// Namespace is the namespace in which the agent pod is running.
|
|
||||||
Namespace string
|
|
||||||
// LabelSelector is a label selector (e.g., "label-key=label=value") that can be used to filter
|
|
||||||
// the agent pods.
|
|
||||||
LabelSelector string
|
|
||||||
// CertPathAnnotation is the annotation used by the agent pod to indicate the path to the CA cert
|
|
||||||
// inside the pod.
|
|
||||||
CertPathAnnotation string
|
|
||||||
// KeyPathAnnotation is the annotation used by the agent pod to indicate the path to the CA key
|
|
||||||
// inside the pod.
|
|
||||||
KeyPathAnnotation string
|
|
||||||
}
|
|
||||||
|
|
||||||
type CA struct {
|
|
||||||
agentInfo *AgentInfo
|
|
||||||
|
|
||||||
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.
|
|
||||||
//
|
|
||||||
// The CA will try to read (via cat(1)) the kube API server's private key from an agent pod located
|
|
||||||
// via the provided agentInfo.
|
|
||||||
func New(
|
|
||||||
agentInfo *AgentInfo,
|
|
||||||
kubeClient kubernetes.Interface,
|
|
||||||
podCommandExecutor PodCommandExecutor,
|
|
||||||
tick <-chan time.Time,
|
|
||||||
onSuccessfulRefresh SuccessCallback,
|
|
||||||
onFailedRefresh FailureCallback,
|
|
||||||
) (*CA, ShutdownFunc) {
|
|
||||||
signer, err := createSignerWithAPIServerSecret(agentInfo, 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{
|
|
||||||
agentInfo: agentInfo,
|
|
||||||
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(
|
|
||||||
agentInfo *AgentInfo,
|
|
||||||
kubeClient kubernetes.Interface,
|
|
||||||
podCommandExecutor PodCommandExecutor,
|
|
||||||
) (signer, error) {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
pod, err := findCertAgentPod(ctx, kubeClient, agentInfo.Namespace, agentInfo.LabelSelector)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
certPath, keyPath := getKeypairFilePaths(pod, agentInfo)
|
|
||||||
|
|
||||||
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.agentInfo,
|
|
||||||
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 findCertAgentPod(ctx context.Context, kubeClient kubernetes.Interface, namespace, labelSelector string) (*v1.Pod, error) {
|
|
||||||
pods, err := kubeClient.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{
|
|
||||||
LabelSelector: labelSelector,
|
|
||||||
FieldSelector: "status.phase=Running",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("could not check for kube-cert-agent pod: %w", err)
|
|
||||||
}
|
|
||||||
for _, pod := range pods.Items {
|
|
||||||
return &pod, nil
|
|
||||||
}
|
|
||||||
return nil, ErrNoKubeCertAgentPod
|
|
||||||
}
|
|
||||||
|
|
||||||
func getKeypairFilePaths(pod *v1.Pod, agentInfo *AgentInfo) (string, string) {
|
|
||||||
annotations := pod.Annotations
|
|
||||||
if annotations == nil {
|
|
||||||
annotations = make(map[string]string)
|
|
||||||
}
|
|
||||||
|
|
||||||
certPath, ok := annotations[agentInfo.CertPathAnnotation]
|
|
||||||
if !ok {
|
|
||||||
certPath = k8sAPIServerCACertPEMDefaultPath
|
|
||||||
}
|
|
||||||
|
|
||||||
keyPath, ok := annotations[agentInfo.KeyPathAnnotation]
|
|
||||||
if !ok {
|
|
||||||
keyPath = k8sAPIServerCAKeyPEMDefaultPath
|
|
||||||
}
|
|
||||||
|
|
||||||
return certPath, keyPath
|
|
||||||
}
|
|
@ -1,426 +0,0 @@
|
|||||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package kubecertauthority
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/x509"
|
|
||||||
"crypto/x509/pkix"
|
|
||||||
"encoding/pem"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"sync"
|
|
||||||
"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"
|
|
||||||
|
|
||||||
"go.pinniped.dev/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
|
|
||||||
}
|
|
||||||
|
|
||||||
type callbackRecorder struct {
|
|
||||||
numberOfTimesSuccessCalled int
|
|
||||||
numberOfTimesFailureCalled int
|
|
||||||
failureErrors []error
|
|
||||||
mutex sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *callbackRecorder) OnSuccess() {
|
|
||||||
c.mutex.Lock()
|
|
||||||
defer c.mutex.Unlock()
|
|
||||||
c.numberOfTimesSuccessCalled++
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *callbackRecorder) OnFailure(err error) {
|
|
||||||
c.mutex.Lock()
|
|
||||||
defer c.mutex.Unlock()
|
|
||||||
c.numberOfTimesFailureCalled++
|
|
||||||
c.failureErrors = append(c.failureErrors, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *callbackRecorder) NumberOfTimesSuccessCalled() int {
|
|
||||||
c.mutex.Lock()
|
|
||||||
defer c.mutex.Unlock()
|
|
||||||
return c.numberOfTimesSuccessCalled
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *callbackRecorder) NumberOfTimesFailureCalled() int {
|
|
||||||
c.mutex.Lock()
|
|
||||||
defer c.mutex.Unlock()
|
|
||||||
return c.numberOfTimesFailureCalled
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *callbackRecorder) FailureErrors() []error {
|
|
||||||
c.mutex.Lock()
|
|
||||||
defer c.mutex.Unlock()
|
|
||||||
var errs = make([]error, len(c.failureErrors))
|
|
||||||
copy(errs, c.failureErrors)
|
|
||||||
return errs
|
|
||||||
}
|
|
||||||
|
|
||||||
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 callbacks *callbackRecorder
|
|
||||||
var logger *testutil.TranscriptLogger
|
|
||||||
var agentInfo AgentInfo
|
|
||||||
|
|
||||||
var requireInitialFailureLogMessage = func(specificErrorMessage string) {
|
|
||||||
r.Len(logger.Transcript(), 1)
|
|
||||||
r.Equal(
|
|
||||||
fmt.Sprintf("could not initially fetch the API server's signing key: %s\n", specificErrorMessage),
|
|
||||||
logger.Transcript()[0].Message,
|
|
||||||
)
|
|
||||||
r.Equal(logger.Transcript()[0].Level, "error")
|
|
||||||
}
|
|
||||||
|
|
||||||
var requireNotCapableOfIssuingCerts = func(subject *CA) {
|
|
||||||
certPEM, keyPEM, err := subject.IssuePEM(
|
|
||||||
pkix.Name{CommonName: "Test Server"},
|
|
||||||
[]string{"example.com"},
|
|
||||||
10*time.Minute,
|
|
||||||
)
|
|
||||||
r.Nil(certPEM)
|
|
||||||
r.Nil(keyPEM)
|
|
||||||
r.EqualError(err, "this cluster is not currently capable of issuing certificates")
|
|
||||||
}
|
|
||||||
|
|
||||||
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")
|
|
||||||
|
|
||||||
agentInfo = AgentInfo{
|
|
||||||
Namespace: "some-agent-namespace",
|
|
||||||
LabelSelector: "some-label-key=some-label-value",
|
|
||||||
CertPathAnnotation: "some-cert-path-annotation",
|
|
||||||
KeyPathAnnotation: "some-key-path-annotation",
|
|
||||||
}
|
|
||||||
fakePod = &corev1.Pod{
|
|
||||||
TypeMeta: metav1.TypeMeta{},
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Name: "fake-pod",
|
|
||||||
Namespace: agentInfo.Namespace,
|
|
||||||
Labels: map[string]string{
|
|
||||||
"some-label-key": "some-label-value",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Spec: corev1.PodSpec{
|
|
||||||
Containers: []corev1.Container{{Name: "some-agent-container-name"}},
|
|
||||||
},
|
|
||||||
Status: corev1.PodStatus{
|
|
||||||
Phase: corev1.PodRunning,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
kubeAPIClient = kubernetesfake.NewSimpleClientset()
|
|
||||||
|
|
||||||
fakeExecutor = &fakePodExecutor{
|
|
||||||
resultsToReturn: []string{
|
|
||||||
fakeCertPEM,
|
|
||||||
fakeKeyPEM,
|
|
||||||
fakeCert2PEM,
|
|
||||||
fakeKey2PEM,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
callbacks = &callbackRecorder{}
|
|
||||||
|
|
||||||
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 agent 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 := New(&agentInfo, kubeAPIClient, fakeExecutor, fakeTicker, callbacks.OnSuccess, callbacks.OnFailure)
|
|
||||||
defer shutdownFunc()
|
|
||||||
|
|
||||||
r.Equal(2, fakeExecutor.callCount)
|
|
||||||
|
|
||||||
r.Equal(agentInfo.Namespace, fakeExecutor.calledWithPodNamespace[0])
|
|
||||||
r.Equal("fake-pod", fakeExecutor.calledWithPodName[0])
|
|
||||||
r.Equal([]string{"cat", "/etc/kubernetes/ca/ca.pem"}, fakeExecutor.calledWithCommandAndArgs[0])
|
|
||||||
|
|
||||||
r.Equal(agentInfo.Namespace, fakeExecutor.calledWithPodNamespace[1])
|
|
||||||
r.Equal("fake-pod", fakeExecutor.calledWithPodName[1])
|
|
||||||
r.Equal([]string{"cat", "/etc/kubernetes/ca/ca.key"}, fakeExecutor.calledWithCommandAndArgs[1])
|
|
||||||
|
|
||||||
r.Equal(1, callbacks.NumberOfTimesSuccessCalled())
|
|
||||||
r.Equal(0, callbacks.NumberOfTimesFailureCalled())
|
|
||||||
|
|
||||||
// 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), 6*time.Minute)
|
|
||||||
validCert.RequireMatchesPrivateKey(string(keyPEM))
|
|
||||||
|
|
||||||
// Tick the timer and wait for another refresh loop to complete.
|
|
||||||
fakeTicker <- time.Now()
|
|
||||||
|
|
||||||
// Eventually it starts issuing certs using the new signing key.
|
|
||||||
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)
|
|
||||||
|
|
||||||
r.Equal(2, callbacks.NumberOfTimesSuccessCalled())
|
|
||||||
r.Equal(0, callbacks.NumberOfTimesFailureCalled())
|
|
||||||
|
|
||||||
validCert2 := testutil.ValidateCertificate(t, fakeCert2PEM, secondCertPEM)
|
|
||||||
validCert2.RequireDNSName("example.com")
|
|
||||||
validCert2.RequireLifetime(time.Now(), time.Now().Add(15*time.Minute), 6*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 := New(&agentInfo, kubeAPIClient, fakeExecutor, fakeTicker, callbacks.OnSuccess, callbacks.OnFailure)
|
|
||||||
defer shutdownFunc()
|
|
||||||
r.Equal(2, fakeExecutor.callCount)
|
|
||||||
r.Equal(1, callbacks.NumberOfTimesSuccessCalled())
|
|
||||||
r.Equal(0, callbacks.NumberOfTimesFailureCalled())
|
|
||||||
|
|
||||||
// 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")
|
|
||||||
|
|
||||||
r.Equal(1, callbacks.NumberOfTimesSuccessCalled())
|
|
||||||
r.Equal(1, callbacks.NumberOfTimesFailureCalled())
|
|
||||||
r.EqualError(callbacks.FailureErrors()[0], "some exec 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 fail the first time but subsequently returns the API server's keypair", func() {
|
|
||||||
it.Before(func() {
|
|
||||||
fakeExecutor.errorsToReturn = []error{fmt.Errorf("some exec error"), nil, nil}
|
|
||||||
fakeExecutor.resultsToReturn = []string{"", fakeCertPEM, fakeKeyPEM}
|
|
||||||
})
|
|
||||||
|
|
||||||
it("logs an error message and fails to issue certs until it can get the API server's keypair", func() {
|
|
||||||
fakeTicker := make(chan time.Time)
|
|
||||||
|
|
||||||
subject, shutdownFunc := New(&agentInfo, kubeAPIClient, fakeExecutor, fakeTicker, callbacks.OnSuccess, callbacks.OnFailure)
|
|
||||||
defer shutdownFunc()
|
|
||||||
r.Equal(1, fakeExecutor.callCount)
|
|
||||||
r.Equal(0, callbacks.NumberOfTimesSuccessCalled())
|
|
||||||
r.Equal(1, callbacks.NumberOfTimesFailureCalled())
|
|
||||||
r.EqualError(callbacks.FailureErrors()[0], "some exec error")
|
|
||||||
|
|
||||||
requireInitialFailureLogMessage("some exec error")
|
|
||||||
requireNotCapableOfIssuingCerts(subject)
|
|
||||||
|
|
||||||
// Tick the timer and wait for another refresh loop to complete.
|
|
||||||
fakeTicker <- time.Now()
|
|
||||||
|
|
||||||
// Wait until it can start to issue certs, and then validate the issued cert.
|
|
||||||
var certPEM, keyPEM []byte
|
|
||||||
r.Eventually(func() bool {
|
|
||||||
var err error
|
|
||||||
certPEM, keyPEM, err = subject.IssuePEM(
|
|
||||||
pkix.Name{CommonName: "Test Server"},
|
|
||||||
[]string{"example.com"},
|
|
||||||
10*time.Minute,
|
|
||||||
)
|
|
||||||
return err == nil
|
|
||||||
}, 5*time.Second, 10*time.Millisecond)
|
|
||||||
validCert := testutil.ValidateCertificate(t, fakeCertPEM, string(certPEM))
|
|
||||||
validCert.RequireDNSName("example.com")
|
|
||||||
validCert.RequireLifetime(time.Now().Add(-5*time.Minute), time.Now().Add(10*time.Minute), 1*time.Minute)
|
|
||||||
validCert.RequireMatchesPrivateKey(string(keyPEM))
|
|
||||||
|
|
||||||
r.Equal(1, callbacks.NumberOfTimesSuccessCalled())
|
|
||||||
r.Equal(1, callbacks.NumberOfTimesFailureCalled())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
when("the exec commands succeed but return garbage", func() {
|
|
||||||
it.Before(func() {
|
|
||||||
fakeExecutor.resultsToReturn = []string{"not a cert", "not a private key"}
|
|
||||||
})
|
|
||||||
|
|
||||||
it("returns a CA who cannot issue certs", func() {
|
|
||||||
subject, shutdownFunc := New(&agentInfo, kubeAPIClient, fakeExecutor, neverTicker, callbacks.OnSuccess, callbacks.OnFailure)
|
|
||||||
defer shutdownFunc()
|
|
||||||
requireInitialFailureLogMessage("could not load CA: tls: failed to find any PEM data in certificate input")
|
|
||||||
requireNotCapableOfIssuingCerts(subject)
|
|
||||||
r.Equal(0, callbacks.NumberOfTimesSuccessCalled())
|
|
||||||
r.Equal(1, callbacks.NumberOfTimesFailureCalled())
|
|
||||||
r.EqualError(callbacks.FailureErrors()[0], "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 a CA who cannot issue certs", func() {
|
|
||||||
subject, shutdownFunc := New(&agentInfo, kubeAPIClient, fakeExecutor, neverTicker, callbacks.OnSuccess, callbacks.OnFailure)
|
|
||||||
defer shutdownFunc()
|
|
||||||
requireInitialFailureLogMessage("some error")
|
|
||||||
requireNotCapableOfIssuingCerts(subject)
|
|
||||||
r.Equal(0, callbacks.NumberOfTimesSuccessCalled())
|
|
||||||
r.Equal(1, callbacks.NumberOfTimesFailureCalled())
|
|
||||||
r.EqualError(callbacks.FailureErrors()[0], "some error")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
when("the second exec command returns an error", func() {
|
|
||||||
it.Before(func() {
|
|
||||||
fakeExecutor.errorsToReturn = []error{nil, fmt.Errorf("some error")}
|
|
||||||
})
|
|
||||||
|
|
||||||
it("returns a CA who cannot issue certs", func() {
|
|
||||||
subject, shutdownFunc := New(&agentInfo, kubeAPIClient, fakeExecutor, neverTicker, callbacks.OnSuccess, callbacks.OnFailure)
|
|
||||||
defer shutdownFunc()
|
|
||||||
requireInitialFailureLogMessage("some error")
|
|
||||||
requireNotCapableOfIssuingCerts(subject)
|
|
||||||
r.Equal(0, callbacks.NumberOfTimesSuccessCalled())
|
|
||||||
r.Equal(1, callbacks.NumberOfTimesFailureCalled())
|
|
||||||
r.EqualError(callbacks.FailureErrors()[0], "some error")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
when("the agent pod is found with non-default CLI flag values", func() {
|
|
||||||
it.Before(func() {
|
|
||||||
fakePod.Annotations = make(map[string]string)
|
|
||||||
fakePod.Annotations[agentInfo.CertPathAnnotation] = "/etc/kubernetes/ca/non-default.pem"
|
|
||||||
fakePod.Annotations[agentInfo.KeyPathAnnotation] = "/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 := New(&agentInfo, kubeAPIClient, fakeExecutor, neverTicker, callbacks.OnSuccess, callbacks.OnFailure)
|
|
||||||
defer shutdownFunc()
|
|
||||||
|
|
||||||
r.Equal(2, fakeExecutor.callCount)
|
|
||||||
|
|
||||||
r.Equal(agentInfo.Namespace, 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(agentInfo.Namespace, 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 agent pod is not found", func() {
|
|
||||||
it("returns an error", func() {
|
|
||||||
subject, shutdownFunc := New(&agentInfo, kubeAPIClient, fakeExecutor, neverTicker, callbacks.OnSuccess, callbacks.OnFailure)
|
|
||||||
defer shutdownFunc()
|
|
||||||
requireInitialFailureLogMessage("did not find kube-cert-agent pod")
|
|
||||||
requireNotCapableOfIssuingCerts(subject)
|
|
||||||
r.Equal(0, callbacks.NumberOfTimesSuccessCalled())
|
|
||||||
r.Equal(1, callbacks.NumberOfTimesFailureCalled())
|
|
||||||
r.EqualError(callbacks.FailureErrors()[0], "did not find kube-cert-agent pod")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}, spec.Sequential(), spec.Report(report.Terminal{}))
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
-----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-----
|
|
@ -1,27 +0,0 @@
|
|||||||
-----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-----
|
|
@ -1,17 +0,0 @@
|
|||||||
-----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-----
|
|
@ -1,27 +0,0 @@
|
|||||||
-----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-----
|
|
Loading…
Reference in New Issue
Block a user