feat: Add additional kubernetes certs (#619)

Add support for supplying all of the necessary CA cert and key pairs for
kubeadm use.

Signed-off-by: Brad Beam <brad.beam@talos-systems.com>
This commit is contained in:
Brad Beam 2019-05-07 11:30:10 -05:00 committed by GitHub
parent 7676a31b20
commit 2c0ec43a0b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 186 additions and 115 deletions

View File

@ -44,14 +44,48 @@ func (k *Kubeadm) ID(data *userdata.UserData) string {
}
// PreFunc implements the Service interface.
// nolint: gocyclo
func (k *Kubeadm) PreFunc(data *userdata.UserData) (err error) {
if err = writeKubeadmConfig(data); err != nil {
return err
}
if data.IsBootstrap() {
if err = writeKubeadmPKIFiles(data.Security.Kubernetes.CA); err != nil {
return err
// Write out all certs we've been provided
certs := []struct {
Cert *x509.PEMEncodedCertificateAndKey
CertPath string
KeyPath string
}{
{
Cert: data.Security.Kubernetes.CA,
CertPath: constants.KubeadmCACert,
KeyPath: constants.KubeadmCAKey,
},
{
Cert: data.Security.Kubernetes.SA,
CertPath: constants.KubeadmSACert,
KeyPath: constants.KubeadmSAKey,
},
{
Cert: data.Security.Kubernetes.FrontProxy,
CertPath: constants.KubeadmFrontProxyCACert,
KeyPath: constants.KubeadmFrontProxyCAKey,
},
{
Cert: data.Security.Kubernetes.Etcd,
CertPath: constants.KubeadmEtcdCACert,
KeyPath: constants.KubeadmEtcdCAKey,
},
}
for _, cert := range certs {
if cert.Cert == nil {
continue
}
if err = writeKubeadmPKIFiles(cert.Cert, cert.CertPath, cert.KeyPath); err != nil {
return err
}
}
} else if data.IsControlPlane() {
if data.Services.Trustd == nil || data.Services.Trustd.BootstrapNode == "" {
@ -235,25 +269,25 @@ func writeKubeadmConfig(data *userdata.UserData) (err error) {
return nil
}
func writeKubeadmPKIFiles(data *x509.PEMEncodedCertificateAndKey) (err error) {
func writeKubeadmPKIFiles(data *x509.PEMEncodedCertificateAndKey, cert string, key string) (err error) {
if data == nil {
return nil
}
if data.Crt != nil {
if err = os.MkdirAll(path.Dir(constants.KubeadmCACert), 0600); err != nil {
if err = os.MkdirAll(path.Dir(cert), 0600); err != nil {
return err
}
if err = ioutil.WriteFile(constants.KubeadmCACert, data.Crt, 0400); err != nil {
return fmt.Errorf("write %s: %v", constants.KubeadmCACert, err)
if err = ioutil.WriteFile(cert, data.Crt, 0400); err != nil {
return fmt.Errorf("write %s: %v", cert, err)
}
}
if data.Key != nil {
if err = os.MkdirAll(path.Dir(constants.KubeadmCAKey), 0600); err != nil {
if err = os.MkdirAll(path.Dir(key), 0600); err != nil {
return err
}
if err = ioutil.WriteFile(constants.KubeadmCAKey, data.Key, 0400); err != nil {
return fmt.Errorf("write %s: %v", constants.KubeadmCAKey, err)
if err = ioutil.WriteFile(key, data.Key, 0400); err != nil {
return fmt.Errorf("write %s: %v", key, err)
}
}
return nil

View File

@ -4,7 +4,11 @@
package constants
import "github.com/containerd/containerd/defaults"
import (
"github.com/containerd/containerd/defaults"
"k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta1"
"k8s.io/kubernetes/cmd/kubeadm/app/constants"
)
const (
// KernelParamUserData is the kernel parameter name for specifying the URL
@ -63,10 +67,28 @@ const (
KubeadmConfig = "/etc/kubernetes/kubeadm-config.yaml"
// KubeadmCACert is the path to the root CA certificate.
KubeadmCACert = "/etc/kubernetes/pki/ca.crt"
KubeadmCACert = v1beta1.DefaultCertificatesDir + "/" + constants.CACertName
// KubeadmCAKey is the path to the root CA private key.
KubeadmCAKey = "/etc/kubernetes/pki/ca.key"
KubeadmCAKey = v1beta1.DefaultCertificatesDir + "/" + constants.CAKeyName
// KubeadmSACert is the path to the SA certificate.
KubeadmSACert = v1beta1.DefaultCertificatesDir + "/" + constants.ServiceAccountPublicKeyName
// KubeadmSAKey is the path to the SA private key.
KubeadmSAKey = v1beta1.DefaultCertificatesDir + "/" + constants.ServiceAccountPrivateKeyName
// KubeadmFrontProxyCACert is the path to the front proxy CA certificate.
KubeadmFrontProxyCACert = v1beta1.DefaultCertificatesDir + "/" + constants.FrontProxyCACertName
// KubeadmFrontProxyCAKey is the path to the front proxy CA private key.
KubeadmFrontProxyCAKey = v1beta1.DefaultCertificatesDir + "/" + constants.FrontProxyCAKeyName
// KubeadmEtcdCACert is the path to the etcd CA certificate.
KubeadmEtcdCACert = v1beta1.DefaultCertificatesDir + "/" + constants.EtcdCACertName
// KubeadmEtcdCAKey is the path to the etcd CA private key.
KubeadmEtcdCAKey = v1beta1.DefaultCertificatesDir + "/" + constants.EtcdCAKeyName
// KubernetesVersion is the enforced target version of the control plane.
KubernetesVersion = "v1.14.1"

View File

@ -5,18 +5,17 @@
package userdata
import (
"encoding/pem"
"strings"
"github.com/hashicorp/go-multierror"
"github.com/talos-systems/talos/pkg/crypto/x509"
"golang.org/x/xerrors"
)
// KubernetesSecurity represents the set of security options specific to
// Kubernetes.
type KubernetesSecurity struct {
CA *x509.PEMEncodedCertificateAndKey `yaml:"ca"`
CA *x509.PEMEncodedCertificateAndKey `yaml:"ca"`
SA *x509.PEMEncodedCertificateAndKey `yaml:"sa"`
FrontProxy *x509.PEMEncodedCertificateAndKey `yaml:"frontproxy"`
Etcd *x509.PEMEncodedCertificateAndKey `yaml:"etcd"`
}
// KubernetesSecurityCheck defines the function type for checks
@ -34,52 +33,28 @@ func (k *KubernetesSecurity) Validate(checks ...KubernetesSecurityCheck) error {
}
// CheckKubernetesCA verfies the KubernetesSecurity settings are valid
// nolint: dupl
func CheckKubernetesCA() KubernetesSecurityCheck {
return func(k *KubernetesSecurity) error {
var result *multierror.Error
// Verify the required sections are present
if k.CA == nil {
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.kubernetes.ca", "", ErrRequiredSection))
certs := []certTest{
{
Cert: k.CA,
Path: "security.kubernetes.ca",
Required: true,
},
{
Cert: k.SA,
Path: "security.kubernetes.sa",
},
{
Cert: k.FrontProxy,
Path: "security.kubernetes.frontproxy",
},
{
Cert: k.Etcd,
Path: "security.kubernetes.etcd",
},
}
// Bail early since we're already missing the required sections
if result.ErrorOrNil() != nil {
return result.ErrorOrNil()
}
if k.CA.Crt == nil {
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.kubernetes.ca.crt", "", ErrRequiredSection))
}
if k.CA.Key == nil {
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.kubernetes.ca.key", "", ErrRequiredSection))
}
// test if k.CA fields are present ( x509 package handles the b64 decode
// during yaml unmarshal, so we have the bytes if it was successful )
var block *pem.Block
block, _ = pem.Decode(k.CA.Crt)
// nolint: gocritic
if block == nil {
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.kubernetes.ca.crt", k.CA.Crt, ErrInvalidCert))
} else {
if block.Type != "CERTIFICATE" {
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.kubernetes.ca.crt", k.CA.Crt, ErrInvalidCertType))
}
}
block, _ = pem.Decode(k.CA.Key)
// nolint: gocritic
if block == nil {
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.kubernetes.ca.key", k.CA.Key, ErrInvalidCert))
} else {
if !strings.HasSuffix(block.Type, "PRIVATE KEY") {
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.kubernetes.ca.key", k.CA.Key, ErrInvalidCertType))
}
}
return result.ErrorOrNil()
return checkCertKeyPair(certs)
}
}

View File

@ -36,8 +36,22 @@ func (suite *validateSuite) TestValidateKubernetesSecurity() {
suite.Require().Error(err)
// Successful test
kube.CA.Crt = []byte("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----")
kube.CA.Key = []byte("-----BEGIN EC PRIVATE KEY-----\n-----END EC PRIVATE KEY-----")
kube.CA = &x509.PEMEncodedCertificateAndKey{
Crt: []byte("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"),
Key: []byte("-----BEGIN EC PRIVATE KEY-----\n-----END EC PRIVATE KEY-----"),
}
kube.SA = &x509.PEMEncodedCertificateAndKey{
Crt: []byte("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"),
Key: []byte("-----BEGIN EC PRIVATE KEY-----\n-----END EC PRIVATE KEY-----"),
}
kube.FrontProxy = &x509.PEMEncodedCertificateAndKey{
Crt: []byte("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"),
Key: []byte("-----BEGIN EC PRIVATE KEY-----\n-----END EC PRIVATE KEY-----"),
}
kube.Etcd = &x509.PEMEncodedCertificateAndKey{
Crt: []byte("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"),
Key: []byte("-----BEGIN EC PRIVATE KEY-----\n-----END EC PRIVATE KEY-----"),
}
err = kube.Validate(CheckKubernetesCA())
suite.Require().NoError(err)
}

View File

@ -5,12 +5,8 @@
package userdata
import (
"encoding/pem"
"strings"
"github.com/hashicorp/go-multierror"
"github.com/talos-systems/talos/pkg/crypto/x509"
"golang.org/x/xerrors"
)
// OSSecurity represents the set of security options specific to the OS.
@ -34,52 +30,16 @@ func (o *OSSecurity) Validate(checks ...OSSecurityCheck) error {
}
// CheckOSCA verfies the OSSecurity settings are valid
// nolint: dupl
func CheckOSCA() OSSecurityCheck {
return func(o *OSSecurity) error {
var result *multierror.Error
// Verify the required sections are present
if o.CA == nil {
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.os.ca", "", ErrRequiredSection))
certs := []certTest{
{
Cert: o.CA,
Path: "security.os.ca",
Required: true,
},
}
// Bail early since we're already missing the required sections
if result.ErrorOrNil() != nil {
return result.ErrorOrNil()
}
if o.CA.Crt == nil {
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.os.ca.crt", "", ErrRequiredSection))
}
if o.CA.Key == nil {
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.os.ca.key", "", ErrRequiredSection))
}
// test if o.CA fields are present ( x509 package handles the b64 decode
// during yaml unmarshal, so we have the bytes if it was successful )
var block *pem.Block
block, _ = pem.Decode(o.CA.Crt)
// nolint: gocritic
if block == nil {
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.os.ca.crt", o.CA.Crt, ErrInvalidCert))
} else {
if block.Type != "CERTIFICATE" {
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.os.ca.crt", o.CA.Crt, ErrInvalidCertType))
}
}
block, _ = pem.Decode(o.CA.Key)
// nolint: gocritic
if block == nil {
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.os.ca.key", o.CA.Key, ErrInvalidCert))
} else {
if !strings.HasSuffix(block.Type, "PRIVATE KEY") {
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", "security.os.ca.key", o.CA.Key, ErrInvalidCertType))
}
}
return result.ErrorOrNil()
return checkCertKeyPair(certs)
}
}

View File

@ -15,11 +15,13 @@ import (
"net/http"
"os"
"path"
"strings"
"time"
"github.com/hashicorp/go-multierror"
"github.com/talos-systems/talos/internal/pkg/net"
"github.com/talos-systems/talos/pkg/crypto/x509"
"golang.org/x/xerrors"
yaml "gopkg.in/yaml.v2"
)
@ -254,3 +256,58 @@ func Open(p string) (data *UserData, err error) {
return data, nil
}
type certTest struct {
Cert *x509.PEMEncodedCertificateAndKey
Path string
Required bool
}
// nolint: gocyclo
func checkCertKeyPair(certs []certTest) error {
var result *multierror.Error
for _, cert := range certs {
// Verify the required sections are present
if cert.Required && cert.Cert == nil {
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", cert.Path, "", ErrRequiredSection))
}
// Bail early since we're already missing the required sections
if result.ErrorOrNil() != nil {
continue
}
if cert.Cert.Crt == nil {
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", cert.Path+".crt", "", ErrRequiredSection))
}
if cert.Cert.Key == nil {
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", cert.Path+".key", "", ErrRequiredSection))
}
// test if CA fields are present ( x509 package handles the b64 decode
// during yaml unmarshal, so we have the bytes if it was successful )
var block *pem.Block
block, _ = pem.Decode(cert.Cert.Crt)
// nolint: gocritic
if block == nil {
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", cert.Path+".crt", cert.Cert.Crt, ErrInvalidCert))
} else {
if block.Type != "CERTIFICATE" {
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", cert.Path+".crt", cert.Cert.Crt, ErrInvalidCertType))
}
}
block, _ = pem.Decode(cert.Cert.Key)
// nolint: gocritic
if block == nil {
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", cert.Path+".key", cert.Cert.Key, ErrInvalidCert))
} else {
if !strings.HasSuffix(block.Type, "PRIVATE KEY") {
result = multierror.Append(result, xerrors.Errorf("[%s] %q: %w", cert.Path+".key", cert.Cert.Key, ErrInvalidCertType))
}
}
}
return result.ErrorOrNil()
}

View File

@ -72,15 +72,24 @@ const testConfig = `version: "1"
security:
os:
ca:
crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNMRENDQVk2Z0F3SUJBZ0lRTHhqVGhwbjB0SGpzZ25xMGlJV21HakFLQmdncWhrak9QUVFEQkRBak1RNHcKREFZRFZRUUtFd1ZVWVd4dmN6RVJNQThHQTFVRUF4TUlWR0ZzYjNNZ1EwRXdIaGNOTVRneE1qQTFNVFF6TWpFeApXaGNOTVRreE1qQTFNVFF6TWpFeFdqQWpNUTR3REFZRFZRUUtFd1ZVWVd4dmN6RVJNQThHQTFVRUF4TUlWR0ZzCmIzTWdRMEV3Z1pzd0VBWUhLb1pJemowQ0FRWUZLNEVFQUNNRGdZWUFCQUFRN0I4TWhxZG1IZDduOXVKT3RLTU8Kb0dOamJ0U3YxcHk1QUdPMTQzd3hsVkQ3RWpKK215KzZGQkk1T3ZEakdSYnFZbnNiWFdrWGorWG5qZmpPcnBYNwo2d0UwbEF6aEJwMzRnWENsUC92Z2FzWlk2dmJlYnUwSGdTQ2tLYW9IMlgrNDlBNWEvNHBhTURsSjBCSGlQa3plCmVIcHV0TkVHWitTYThEcHpFaitxdnp4SkxLTmhNRjh3RGdZRFZSMFBBUUgvQkFRREFnS2tNQjBHQTFVZEpRUVcKTUJRR0NDc0dBUVVGQndNQkJnZ3JCZ0VGQlFjREFqQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01CMEdBMVVkRGdRVwpCQlJwRDFubnBrSWJWSldYUC9YZDRIR0xRSEh2UERBS0JnZ3Foa2pPUFFRREJBT0Jpd0F3Z1ljQ1FVLzNkbm1ECi8wNmJXaHNVcE0zM2hUWnhwM3NTcDlKTHF6Zm5xUUMrTTBQYWwrMHc1ZzEyb0RuN0s3RnVZcnM1UGFlcy9MREIKa0N1Y3VvZmxESG82OFRrVUFrSUFob01MMXhXUVU3Z1UvODMyY1JuV3A2V0xnK3VWRGVWQno3YW1UK21rT1RYYgpqS2dpRCs5bmlEc3U3elFoS081NVFKZTFEU1JsQzQ1cTRJZ01NQzFnckE4PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1JSGNBZ0VCQkVJQjY1cmZJUmZ6NVRPR0VuWVVnL05iSWU0NFhVelFCNG9DWU9qYWY2WU83TktsSS9WTmE0aDYKUFJlY0Vsay9LZzZTK1pXWTB3MGwxSC9JTVFXNXhzSDVJQnlnQndZRks0RUVBQ09oZ1lrRGdZWUFCQUFRN0I4TQpocWRtSGQ3bjl1Sk90S01Pb0dOamJ0U3YxcHk1QUdPMTQzd3hsVkQ3RWpKK215KzZGQkk1T3ZEakdSYnFZbnNiClhXa1hqK1huamZqT3JwWDc2d0UwbEF6aEJwMzRnWENsUC92Z2FzWlk2dmJlYnUwSGdTQ2tLYW9IMlgrNDlBNWEKLzRwYU1EbEowQkhpUGt6ZWVIcHV0TkVHWitTYThEcHpFaitxdnp4SkxBPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=
crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIEVDIFBSSVZBVEUgS0VZLS0tLS0=
identity:
crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNZRENDQWNHZ0F3SUJBZ0lSQUl5cjIwVXNhaXpTOWZtK1FlOFZMSVV3Q2dZSUtvWkl6ajBFQXdRd0l6RU8KTUF3R0ExVUVDaE1GVkdGc2IzTXhFVEFQQmdOVkJBTVRDRlJoYkc5eklFTkJNQjRYRFRFNE1USXdOVEUwTXpVMQpObG9YRFRFNU1USXdOVEUwTXpVMU5sb3dKVEVPTUF3R0ExVUVDaE1GVkdGc2IzTXhFekFSQmdOVkJBTVRDbFJoCmJHOXpJRTV2WkdVd2dac3dFQVlIS29aSXpqMENBUVlGSzRFRUFDTURnWVlBQkFHTHl1aXFIcWpzdWhMcE1sZWcKL2pOWEdXYTdBN3pIRWRvSG9KTnNWcmRPTHFBUldlRjNULzkrRVM2aXRISXRBWU5ONis0THFITHQ0dkdmT1E0eAp3OUdpR2dEdmwrUlhQZzVibUVNby90ZmxRNHk0QVdTK3R2Z2F2MG9hZXlOSmh0YVk2Y0hrQ3RVR2tXcGRKMEVnCllFQ2xQcFhnbXRSMVVMSGQ1dTdKeGQ0NFp2R1hTNk9Ca0RDQmpUQU9CZ05WSFE4QkFmOEVCQU1DQmFBd0hRWUQKVlIwbEJCWXdGQVlJS3dZQkJRVUhBd0VHQ0NzR0FRVUZCd01DTUF3R0ExVWRFd0VCL3dRQ01BQXdId1lEVlIwagpCQmd3Rm9BVWFROVo1NlpDRzFTVmx6LzEzZUJ4aTBCeDd6d3dMUVlEVlIwUkJDWXdKSWNFQ21PQ2pJY0VDbU9DCmpZY0VDbU9Dam9jRWswdE8yWWNFazB0THZZY0VrMHRMaVRBS0JnZ3Foa2pPUFFRREJBT0JqQUF3Z1lnQ1FnRzcKdnU2MTBNcktLdytFMVBTaEtLMkJkZVV4RmRvYXRBV1U2L3Z6RjZ5MDZrci9XL0xvc04remtuSWRsT1FoZDVIeQpyMWh3Mm1tQXRtVjNKYTNkVFNYUExBSkNBZFYzRUlWaUtvb2JnTStySTl4S1dPbmRMS2hLK3VoUHdXZy9QNmx4ClNjcU1sMlo0WFhjZ3NudnppdDZBZFBMV3ZmUkN2WjF5NW9LTzVFc0IvOGU0Um1nbgotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1JSGNBZ0VCQkVJQWc0akVSbUhTM2RHajIyYVcwRlVFS0ZBNWEwazk5cHBmSjNnb2FTQll1aS9mRHZXb0YveUYKQnZUeHh4L0w2UFZhSmZ0UVFaZUVuTXVrU3IrL1FacnhPYTZnQndZRks0RUVBQ09oZ1lrRGdZWUFCQUdMeXVpcQpIcWpzdWhMcE1sZWcvak5YR1dhN0E3ekhFZG9Ib0pOc1ZyZE9McUFSV2VGM1QvOStFUzZpdEhJdEFZTk42KzRMCnFITHQ0dkdmT1E0eHc5R2lHZ0R2bCtSWFBnNWJtRU1vL3RmbFE0eTRBV1MrdHZnYXYwb2FleU5KaHRhWTZjSGsKQ3RVR2tXcGRKMEVnWUVDbFBwWGdtdFIxVUxIZDV1N0p4ZDQ0WnZHWFN3PT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=
crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIEVDIFBSSVZBVEUgS0VZLS0tLS0=
kubernetes:
ca:
crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZSVENDQXkyZ0F3SUJBZ0lRV3dqZWpSUldmc2VEaFl6UkRwcXlCekFOQmdrcWhraUc5dzBCQVFzRkFEQXQKTVJNd0VRWURWUVFLRXdwTGRXSmxjbTVsZEdWek1SWXdGQVlEVlFRREV3MUxkV0psY201bGRHVnpJRU5CTUI0WApEVEU0TVRJd05URTBNekl4TTFvWERURTVNVEl3TlRFME16SXhNMW93TFRFVE1CRUdBMVVFQ2hNS1MzVmlaWEp1ClpYUmxjekVXTUJRR0ExVUVBeE1OUzNWaVpYSnVaWFJsY3lCRFFUQ0NBaUl3RFFZSktvWklodmNOQVFFQkJRQUQKZ2dJUEFEQ0NBZ29DZ2dJQkFLWlh4aFRlNnc1MkRYS2NMSVR2VGM3ZTc2eWhpcG5Zb0tndVdtNWMzT0xqTVpkawpPTkxra0xKK01QNzR3QlpRaGlmaWFCZ0xhY0FobXMzVHZrZEMzRkFNSnpiRXJmd25nazhNQnBPMmtBRW9FNWFpCmxhNEJTYWlIZXcvWkZUamE2elljRXVLdExDa2ZwSEFvVU94NUtYQ2hGY1RmVUh4U1ZndUJXaWJYMERHNjltTkwKT1dhQVBqSEw1NWFsWC9TcDF0NHU4OXNIaDZmZVRFc2o0aCtSMlBVZUVkS3I3NHdRM21EWXB4Rkl1NzFvNUxOTgprKzNYRDNLZldJZGlYUTNnV1UwSXU4czVFYmkrVUg1OC9jRW1GUUpWNm8wQ1lhcnkzRVRGWEtYaS9RUXVrUEhOCjh3YkZlVWJaYktIcm5VU2JnNllpNDRVUTg2RUJxZm90b2JmSHZpZmsvUlY1amFSQko0cjhTakRTeFJyd1A2VXkKNlZISitaS29XVFB5M1kxYTFoL3VFTEhjaDliNFdJTUtWaW9BOTFrTjFmcytzMkN3NXJzV2x5eHpXRWpCdUpGLwpPTDVRR2ZLbW1sREdGT3N6MXVZSjhNcmxwVkp1b0NiQU8ra3JWWEhwWnhtcUpPS3JwWEVicUg1UHMwbUlCZkYzCk5wUElJMEFXbUd2bnBudzQxaDBUc0dmcHFjak9ZclM3TXE1RkYrSTNHbUM5OGV4Q1loYnFPNGVsK01WNk9PY3oKQ2dRenBTdDlTYUV4NHFKaG5TaWthbGNiR3BHYlFtNERyMFdvcnkxOTI2Wjl0WlhCRS91ZDJQR0Q2OFBKSi9NbgozUHhHbmNoUVdhbVRiaWpEay9QM3F4OWhuM0d1MEVaWSsvMkEzSGpRTGxKTkhtNzlhb1YyQkdEazRPNjNBZ01CCkFBR2pZVEJmTUE0R0ExVWREd0VCL3dRRUF3SUNwREFkQmdOVkhTVUVGakFVQmdnckJnRUZCUWNEQVFZSUt3WUIKQlFVSEF3SXdEd1lEVlIwVEFRSC9CQVV3QXdFQi96QWRCZ05WSFE0RUZnUVVQcW1iVk9oL3VkbkRscWxlVS9zVgpncTI0V2xRd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dJQkFKL0JYWU9JM2hJemxUSTlaSFlSclpGenlRR0wwbExaCklCYnB0UVhWN1Vla2x1RUJBbkRCU21VUytETmh6ZTJVZGlzUHdaV1oxZWtYNnRiT2ZwYVJGWlYwT1IxTm5UU2IKQmFQNUdyeW9JYXVhcjhVT1BkRkI4b0RsYWQrOHQwKzNZVFQvRyt5dC9tdlVReEJ1c3g3ckJLbE1nS1F3ejBqdQpEUi84Y1BuY0JSa2h6eExqRnJFY2REb1BDNi83MmJUTzF2S0VOczZOdVBtb1h0TzJVdkhUdlBrUWZVR0dyYzRVCjZEZjhjTTZBSnlDRnNIM0NUZUdxRGsvK3ZjQWFpcUZRNW9rZTRvOGxqYklqbkFGTytXWmhMMkg5UGMwRFlybEEKZVRUbUNxNitCa2dkRmJFZmpXdGY1NDJXdlJGRjZPSm9qcEsveUlZNlRjQTlSa0o1blY5UHFCcTVEdG1oeW9uNwpzalN0TWdxRWVUdDNVUC9ZMDI1eEN3T1orMU9halpMdjUwTHhkbVFQNkh4NkJrYzFscDZSSENxTTdQZXhhZlJSClZRODFmVVhURzl5dkNxTmdpdXdWYzloMkRsb25PUGdMaGNDS0ozYjE2NWE1TXFMMkdRZ3BOY254dC9wblE5SGUKeVFiekMwZFhDOXlKSWc2MTFWZ3lkMkJWVlFISWsvTU1lRmlEM2RWa1ZKSW95WU9tcy9nQmN4dHM2TzkwRnY5QwpxNUI2TUIvc016a2pLYnpXNFVPb2ltd2grVUtVRVVaUVhXbGs3UXN3U3NmT3Fidyt4ajRxUzl4MmFvRXdBSXM2Ckloa0NZZmRqZU1henFmV21valhQM1FNYTZsNXZuRlZMa29ZOTRIT285RDdjQi9pSGlMbXEzejRESGlYamU3RWQKVENlWG1NZ25CcnkzCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlKS1FJQkFBS0NBZ0VBcGxmR0ZON3JEbllOY3B3c2hPOU56dDd2cktHS21kaWdxQzVhYmx6YzR1TXhsMlE0CjB1U1FzbjR3L3ZqQUZsQ0dKK0pvR0F0cHdDR2F6ZE8rUjBMY1VBd25Oc1N0L0NlQ1R3d0drN2FRQVNnVGxxS1YKcmdGSnFJZDdEOWtWT05yck5od1M0cTBzS1Ira2NDaFE3SGtwY0tFVnhOOVFmRkpXQzRGYUp0ZlFNYnIyWTBzNQpab0ErTWN2bmxxVmY5S25XM2k3ejJ3ZUhwOTVNU3lQaUg1SFk5UjRSMHF2dmpCRGVZTmluRVVpN3ZXamtzMDJUCjdkY1BjcDlZaDJKZERlQlpUUWk3eXprUnVMNVFmbno5d1NZVkFsWHFqUUpocXZMY1JNVmNwZUw5QkM2UThjM3oKQnNWNVJ0bHNvZXVkUkp1RHBpTGpoUkR6b1FHcCtpMmh0OGUrSitUOUZYbU5wRUVuaXZ4S01OTEZHdkEvcFRMcApVY241a3FoWk0vTGRqVnJXSCs0UXNkeUgxdmhZZ3dwV0tnRDNXUTNWK3o2ellMRG11eGFYTEhOWVNNRzRrWDg0CnZsQVo4cWFhVU1ZVTZ6UFc1Z253eXVXbFVtNmdKc0E3NlN0VmNlbG5HYW9rNHF1bGNSdW9mayt6U1lnRjhYYzIKazhnalFCYVlhK2VtZkRqV0hST3daK21weU01aXRMc3lya1VYNGpjYVlMM3g3RUppRnVvN2g2WDR4WG80NXpNSwpCRE9sSzMxSm9USGlvbUdkS0tScVZ4c2FrWnRDYmdPdlJhaXZMWDNicG4yMWxjRVQrNTNZOFlQcnc4a244eWZjCi9FYWR5RkJacVpOdUtNT1Q4L2VySDJHZmNhN1FSbGo3L1lEY2VOQXVVazBlYnYxcWhYWUVZT1RnN3JjQ0F3RUEKQVFLQ0FnQWs0Y05PcjFxSTIwNENBblN3aU9yRW1wT2p3REdlQ1BVZU5TRGg1WDhvWTEyRWhyaytzV1VQM29ENApsNmpuaWJVbE5NTUZ5Y29KeXFtclIyNmlHRVlIRFpySVB2V2d1aFhmZHZnaVdsRTFvSEF2Yng4bTlLd2pUTjdSClZiMnAzSWhZZUFNTDlYK2NJcUx3SjFCQ0RsOU0xTHFoNnkzS1V1czNJOWdjVXErYnh5dUlPbzZnbG9Denc5VTIKaHJadTVoVjVNQW5ybjVESmZMV0gxNDMwbC83MTRsUHJWU29JOFZpTk9weFliYnNLdFM0NFAxUTBZUUhRMVZubwpzcnVWTHhBOXoxanNKMXd4alNrRjBxcmszNCs2ODlmTXFpR0RxTk9FQUFxWjBXbHVPdkR5WEVxdFRxMmtNdU11ClI3S1lHcThtOEFkb1lxalNJeTlRQkR3ZUpwQ1hwa1pxS1Btc1ozZVlGMXBOMHFFbWFaUUxFNHNDdjd5anpSd3oKSXpPS21DOS9UcFVOaE94Z0E1UkExUEpoWVZZYlVwc0k2bjEzOEVqSjY1cHpSTW55V01VZlZIQ0xPVGdUTFdmeApTRWNKOW1vZW00emwvclRxZWJmbm9WMm54elpQYTlCMnFmMlFVYXpBKzZxTDY4U2l5c2l5bXB4d2pzMWVjTTM2ClkzMDZUMFBrUkw1MFhiTDYvTk5uRGxpMmRrMlVHTzlUVTFMeDZsTlAvd2hPbzFqN1AzUm5OelFzRDB3aG5MakQKL05zUmFpRndzYVhKUU4rUzBRVEN1cTZWU1NYTjZaK3FmMWNsdkh3aXdvZERZbGRsR3lYUWN2VWErMmg3M3Jadgo2TW1nRm13Znc3OWhrS1B4UjdaOXdRL0JLeW1PMEVjS3FyWnlvYWpqUC9lZHRHMGFhUUtDQVFFQTJGcVk0VWo0CjN0QVJXcVRGQTRwYXFUNHlzZU14M0g0dFhTWm8xQ200cEFWc1pMNFEvYkRYNFZCTUNzUFRnVVdseWtJZlJJNEEKRC9zaHYvSGFHNkZRSTUvQUlza2ZaMWhYbzBUOGRzaHVYeUJkbUc0bk56cEpxYzF4UktyS0dsYTA3a2lDVHd0NwpncUxJNldmZHVaMHhZVTJtOFFPTTNWckpRVmpmM1lUczN0eitqY3RFRjJoYi93UDhJZzNVTCtpbmJOb0NNR3FZCm9kSWY4Wlk0VUdnV3NiYlA0K29CUUtnUHljcjRxWGUvayt6ZXJPaitoQ0hrV3NpWVhiay9hMm40V3FIWGxrUDUKaVRRbGtMTzQ1UXVxc2hkTWZ5Z2NDZ3JQUWNGNXZGQ01XTTJMaHJWSTBISmx6UjJCL2FLaERqZmxNY2l2VU1JdApISHJYbTJUcnRrN2Znd0tDQVFFQXhOTWFkVnB3SWsxVjBPT3RwYlNjVXlkWlAyNnlkUGl1a2NHcXhHSVcydVR2ClpqanNOQTY5UlpLbFhsVWNmVVdmY3dob1U1blcySzg4My9lOUZUanpZK0ZrTVc4VW5hN005OEdxTlhkQ09pSzAKUHcvK0huTU1DTlpyaVloVDR0RnFNYTdWdjhMNzBBRmxBMnB3MzEyR0FiSm1hRVF6ZkRNQ3RvUVRZdnVpRFhUcworSzJTaHVyQW5yUWRmNE81dkpwYXlzRkJJMFhOalo2WlpndU0wMDdhYm9ZcW4vS0V1cVVCTUh2OFQ1Q1pxM0ZZClBFYUtZZitrSWhmWnRjc3QzYWlXUE4zZ3dDQzFrNHBlaWtPMzk0ZlRqOVdmTVhZa0c1cUNIaVUwUFo5ZHU3QkMKYk1oS21IcHhCNklCS2FpaXgwTzd6cUxreXAvSGVHY0ZqbmI2Zml0NXZRS0NBUUFOK2p4cVFaYWlmbnJBaW1pWQpBL1k3Zk9NMWp1SUh4cmNUajRteU8wZk1nUFV4eFAzQUJnN01aYTJqL0diTHNUNDJ4UExVTVFCY0IvTjBQU0hFCkt6WE1OMlBvVzJvRitUVWdQVEs2VWRTZm5LMnZUVjZIT09MTmI1Smp4MHpyU3JMQnVqbUE5ZEx5NjZWalB1eWEKTTBlZmE2N2ZYMFZZZjZjRTY4TDZ3cjJ5NEVBcDFQbi9NU1RRWXVlRkk3T1RyTW8wUkJsa0cxN2xCWGcrMlYrbQpBak9GSTdSbW14V2RvYjN6WVlPVEgwTm5RU1JaczJ2T0NZcUJPdmh0QmF6Tk9ibHIwWXptRGxvdXZRbTRRWVF1CmVBUjlJUGcyTnRjbzV1M2c1NmovdTR4MXFFSGRZQlRtTXAzVkZKVHpWL0JqeE1TdjVMRSszR3lockdZRmlnMlkKWjV3VkFvSUJBUUNKNm1wbHhNTkNKZkoxOXJPY294dk80SW1XK3hndEFueEFRZlpJdVhyT1V4QzFEam5rbUZkagp3c2dCZTU0RTRaNUMycWRQdlVKZ2Z4b216YzgrYWJPU1QwTUhxSkJIbFU2bVpKdkI3eGtHNTM1cG1lazBxT1RMCjVJTmgwWmNHSGp0TktQYzU4WVRGVWRUWU9lTnhnSFVBQjJQZXQ3dGowZEthMVoxSUJJNmU0MG1SVHY2WGJ6TWQKZ1pRUjM5bHNJTFJ4MGt5MFRKc21HdlhlSFVYR1kwQ0hwWmVFa0VrVkI3MzNuVWxFNzB2ZWFBT3Q2TGZheDJqWgpRdUVZazVCRiszV2FuTlo1aWZIeHVrbFlsTzc1a2NGYXpoTkljMElGM1pSRFQwcDF5NC9BamxkZEpsT3UyRTBQClRpT0diMVh3VHJQd3JRK29NNkZ2NnJCb0RWS2t5ZzM1QW9JQkFRQ2VFdllVeG1iRlIwTXpHTTdyZklPNWdtbm4KUStyd1d6NkJYZFJCeFdxY2FlZ09helNXYmpnaDdFS0lvVmg0THh3Ykw3a0lCaUZmVzY3a0tONUxSTnYyUzRRdwp4QnhrcHFuZ2ZoSTdhbloyZ0JkWm9NK3JEcU8wK3A2YnFiWXRKZnZrOGJUdDJQZ1pyMWg5Q0Jmbm5uaHNpUE5HCk5xTW9BN2FQTnA5cmlTS05YNDlUS096YysvREg2SkdIQUZxM0x0TVBpK2U5L01Fb3hZUFFhTjR6QnhZdE1qb2QKVWd3dVR5b2pBS3JKZmd6eDVWbmdPdzlQeEVzL0FHemxqaWQ3RHlBamR1VXBZRVNSdzJUdnh3SU1QaFEvSDJvUQo5V3l5L0FNK1BXTVU0Y2pySXBBZ2JpdFQ4ODFOR2R6c0M5bFNubXFpTVZwNkxVNkx2SklTWXZyUWNZU3AKLS0tLS1FTkQgUlNBIFBSSVZBVEUgS0VZLS0tLS0K
crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIEVDIFBSSVZBVEUgS0VZLS0tLS0=
sa:
crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIEVDIFBSSVZBVEUgS0VZLS0tLS0=
frontproxy:
crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIEVDIFBSSVZBVEUgS0VZLS0tLS0=
etcd:
crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIEVDIFBSSVZBVEUgS0VZLS0tLS0=
networking:
os: {}
kubernetes: {}