Skip to content

Commit

Permalink
Add kudo init --unsafe-self-signed-webhook-ca option (#1459)
Browse files Browse the repository at this point in the history
Summary:
added a new `kudo init ... --unsafe-self-signed-webhook-ca` option which can be used when installing KUDO with enabled instance admission webhook (`kudo init --webhook InstanceValidation`) to avoid the cert-manager dependency. When using this option a certificate signed by a self-signed CA is used by the webhook server. This option is meant to be used for local development, testing, and demos and is **not meant to be used in production.**

Signed-off-by: Aleksey Dukhovniy <alex.dukhovniy@googlemail.com>
Co-authored-by: alenkacz <avarkockova@mesosphere.com>
  • Loading branch information
Aleksey Dukhovniy and alenkacz committed Apr 16, 2020
1 parent 2476776 commit e6bae64
Show file tree
Hide file tree
Showing 11 changed files with 308 additions and 46 deletions.
39 changes: 23 additions & 16 deletions pkg/kudoctl/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,25 +51,28 @@ and finishes with success if KUDO is already installed.
kubectl kudo init --crd-only --dry-run --output yaml | kubectl delete -f -
# pass existing serviceaccount
kubectl kudo init --service-account testaccount
# install kudo with activated instance admission webhook
kubectl kudo init --webhook InstanceValidation
`
)

type initCmd struct {
out io.Writer
fs afero.Fs
image string
dryRun bool
output string
version string
ns string
serviceAccount string
wait bool
timeout int64
clientOnly bool
crdOnly bool
home kudohome.Home
client *kube.Client
webhooks string
out io.Writer
fs afero.Fs
image string
dryRun bool
output string
version string
ns string
serviceAccount string
wait bool
timeout int64
clientOnly bool
crdOnly bool
home kudohome.Home
client *kube.Client
webhooks string
selfSignedWebhookCA bool
}

func newInitCmd(fs afero.Fs, out io.Writer) *cobra.Command {
Expand Down Expand Up @@ -105,6 +108,7 @@ func newInitCmd(fs afero.Fs, out io.Writer) *cobra.Command {
f.Int64Var(&i.timeout, "wait-timeout", 300, "Wait timeout to be used")
f.StringVar(&i.webhooks, "webhook", "", "List of webhooks to install separated by commas (One of: InstanceValidation)")
f.StringVarP(&i.serviceAccount, "service-account", "", "", "Override for the default serviceAccount kudo-manager")
f.BoolVar(&i.selfSignedWebhookCA, "unsafe-self-signed-webhook-ca", false, "Use self-signed CA bundle (for testing only) for the webhooks")

return cmd
}
Expand All @@ -128,13 +132,16 @@ func (initCmd *initCmd) validate(flags *flag.FlagSet) error {
if initCmd.webhooks != "" && initCmd.webhooks != "InstanceValidation" {
return errors.New("webhooks can be only empty or contain a single string 'InstanceValidation'. No other webhooks supported")
}
if initCmd.webhooks == "" && initCmd.selfSignedWebhookCA {
return errors.New("self-signed CA bundle can only be used with webhooks option")
}

return nil
}

// run initializes local config and installs KUDO manager to Kubernetes cluster.
func (initCmd *initCmd) run() error {
opts := kudoinit.NewOptions(initCmd.version, initCmd.ns, initCmd.serviceAccount, webhooksArray(initCmd.webhooks))
opts := kudoinit.NewOptions(initCmd.version, initCmd.ns, initCmd.serviceAccount, webhooksArray(initCmd.webhooks), initCmd.selfSignedWebhookCA)
// if image provided switch to it.
if initCmd.image != "" {
opts.Image = initCmd.image
Expand Down
6 changes: 3 additions & 3 deletions pkg/kudoctl/cmd/init_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ func TestIntegInitWithNameSpace(t *testing.T) {

kudoControllerFound := false
for _, ss := range statefulsets.Items {
if ss.Name == "kudo-controller-manager" {
if ss.Name == kudoinit.DefaultManagerName {
kudoControllerFound = true
}
}
Expand Down Expand Up @@ -287,7 +287,7 @@ func TestIntegInitWithServiceAccount(t *testing.T) {

kudoControllerFound := false
for _, ss := range statefulsets.Items {
if ss.Name == "kudo-controller-manager" {
if ss.Name == kudoinit.DefaultManagerName {
kudoControllerFound = true
}
}
Expand Down Expand Up @@ -341,7 +341,7 @@ func TestNoErrorOnReInit(t *testing.T) {

func deleteInitObjects(client *testutils.RetryClient) {
crds := crd.NewInitializer()
prereqs := prereq.NewInitializer(kudoinit.NewOptions("", "", "", []string{}))
prereqs := prereq.NewInitializer(kudoinit.NewOptions("", "", "", []string{}, false))
deleteCRDs(crds.Resources(), client)
deletePrereq(prereqs.Resources(), client)
}
Expand Down
10 changes: 5 additions & 5 deletions pkg/kudoctl/kudoinit/manager/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,12 @@ func generateDeployment(opts kudoinit.Options) *appsv1.StatefulSet {
},
ObjectMeta: metav1.ObjectMeta{
Namespace: opts.Namespace,
Name: "kudo-controller-manager",
Name: kudoinit.DefaultManagerName,
Labels: managerLabels,
},
Spec: appsv1.StatefulSetSpec{
Selector: &metav1.LabelSelector{MatchLabels: managerLabels},
ServiceName: "kudo-controller-manager-service",
ServiceName: kudoinit.DefaultServiceName,
Template: v1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: managerLabels,
Expand All @@ -122,7 +122,7 @@ func generateDeployment(opts kudoinit.Options) *appsv1.StatefulSet {
Command: []string{"/root/manager"},
Env: []v1.EnvVar{
{Name: "POD_NAMESPACE", ValueFrom: &v1.EnvVarSource{FieldRef: &v1.ObjectFieldSelector{FieldPath: "metadata.namespace"}}},
{Name: "SECRET_NAME", Value: "kudo-webhook-server-secret"},
{Name: "SECRET_NAME", Value: kudoinit.DefaultSecretName},
{Name: "ENABLE_WEBHOOKS", Value: strconv.FormatBool(opts.HasWebhooksEnabled())},
},
Image: image,
Expand Down Expand Up @@ -154,7 +154,7 @@ func generateDeployment(opts kudoinit.Options) *appsv1.StatefulSet {
Name: "cert",
VolumeSource: v1.VolumeSource{
Secret: &v1.SecretVolumeSource{
SecretName: "kudo-webhook-server-secret",
SecretName: kudoinit.DefaultSecretName,
DefaultMode: &secretDefaultMode,
},
},
Expand All @@ -174,7 +174,7 @@ func generateService(opts kudoinit.Options) *v1.Service {
},
ObjectMeta: metav1.ObjectMeta{
Namespace: opts.Namespace,
Name: "kudo-controller-manager-service",
Name: kudoinit.DefaultServiceName,
Labels: managerLabels,
},
Spec: v1.ServiceSpec{
Expand Down
10 changes: 7 additions & 3 deletions pkg/kudoctl/kudoinit/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@ type Options struct {
TerminationGracePeriodSeconds int64
// Image defines the image to be used
Image string
// Enable validation
Webhooks []string
// List of enabled webhooks
Webhooks []string
// Using self-signed webhook CA bundle
SelfSignedWebhookCA bool

ServiceAccount string
}

func NewOptions(v string, ns string, sa string, webhooks []string) Options {
func NewOptions(v string, ns string, sa string, webhooks []string, selfSignedWebhookCA bool) Options {
if v == "" {
v = version.Get().GitVersion
}
Expand All @@ -39,6 +42,7 @@ func NewOptions(v string, ns string, sa string, webhooks []string) Options {
Image: fmt.Sprintf("kudobuilder/controller:v%v", v),
Webhooks: webhooks,
ServiceAccount: sa,
SelfSignedWebhookCA: selfSignedWebhookCA,
}
}

Expand Down
4 changes: 2 additions & 2 deletions pkg/kudoctl/kudoinit/prereq/namespace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
func TestPrereq_Fail_PreValidate_CustomNamespace(t *testing.T) {
client := getFakeClient()

init := NewInitializer(kudoinit.NewOptions("", "customNS", "", make([]string, 0)))
init := NewInitializer(kudoinit.NewOptions("", "customNS", "", make([]string, 0), false))

result := init.PreInstallVerify(client)

Expand All @@ -24,7 +24,7 @@ func TestPrereq_Ok_PreValidate_CustomNamespace(t *testing.T) {

mockGetNamespace(client, "customNS")

init := NewInitializer(kudoinit.NewOptions("", "customNS", "", make([]string, 0)))
init := NewInitializer(kudoinit.NewOptions("", "customNS", "", make([]string, 0), false))

result := init.PreInstallVerify(client)

Expand Down
2 changes: 1 addition & 1 deletion pkg/kudoctl/kudoinit/prereq/prereqs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func getFakeClient() *kube.Client {
func TestPrereq_Ok_PreValidate_DefaultOpts(t *testing.T) {
client := getFakeClient()

init := NewInitializer(kudoinit.NewOptions("", "", "", make([]string, 0)))
init := NewInitializer(kudoinit.NewOptions("", "", "", make([]string, 0), false))

result := init.PreInstallVerify(client)

Expand Down
6 changes: 3 additions & 3 deletions pkg/kudoctl/kudoinit/prereq/serviceaccount_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
func TestPrereq_Fail_PreValidate_CustomServiceAccount(t *testing.T) {
client := getFakeClient()

init := NewInitializer(kudoinit.NewOptions("", "", "customSA", make([]string, 0)))
init := NewInitializer(kudoinit.NewOptions("", "", "customSA", make([]string, 0), false))

result := init.PreInstallVerify(client)

Expand All @@ -26,7 +26,7 @@ func TestPrereq_Fail_PreValidate_CustomServiceAccount_MissingPermissions(t *test

mockListServiceAccounts(client, customSA)

init := NewInitializer(kudoinit.NewOptions("", "", customSA, make([]string, 0)))
init := NewInitializer(kudoinit.NewOptions("", "", customSA, make([]string, 0), false))

result := init.PreInstallVerify(client)

Expand All @@ -37,7 +37,7 @@ func TestPrereq_Ok_PreValidate_CustomServiceAccount(t *testing.T) {
client := getFakeClient()

customSA := "customSA"
opts := kudoinit.NewOptions("", "", customSA, make([]string, 0))
opts := kudoinit.NewOptions("", "", customSA, make([]string, 0), false)

mockListServiceAccounts(client, opts.ServiceAccount)
mockListClusterRoleBindings(client, opts)
Expand Down
153 changes: 153 additions & 0 deletions pkg/kudoctl/kudoinit/prereq/tinyca.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package prereq

// This is a slightly modified version of controller-runtime/pkg/internal/testing/integration/internal/tinyca.go
// package which is sadly internal and can't be used directly. All the methods here are supposed to be FOR TESTING ONLY.
// This package is used to provide self-signed CA along with a CA signed server certificate (and key) for services running
// inside the cluster. This is IN NO WAY a generic certificate generation solution as it is tailored towards testing and demos.
// Generated server certificate is valid 1 week which is generous enough for testing and demos.
// More information: https://kubernetes.io/blog/2019/03/21/a-guide-to-kubernetes-admission-controllers/

import (
"crypto"
crand "crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"math/big"
"time"

certutil "k8s.io/client-go/util/cert"
)

var (
rsaKeySize = 2048 // a decent number, as of 2019
bigOne = big.NewInt(1)
)

// CertPair is a private key and certificate for use for client auth, as a CA, or serving.
type CertPair struct {
Key crypto.Signer
Cert *x509.Certificate
}

// CertBytes returns the PEM-encoded version of the certificate for this pair.
func (k CertPair) CertBytes() []byte {
return pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: k.Cert.Raw,
})
}

// AsBytes encodes key-pair in the appropriate formats for on-disk storage (PEM and PKCS8, respectively).
func (k CertPair) AsBytes() (cert []byte, key []byte, err error) {
cert = k.CertBytes()

rawKeyData, err := x509.MarshalPKCS8PrivateKey(k.Key)
if err != nil {
return nil, nil, fmt.Errorf("unable to encode private key: %v", err)
}

key = pem.EncodeToMemory(&pem.Block{
Type: "PRIVATE KEY",
Bytes: rawKeyData,
})

return cert, key, nil
}

// TinyCA supports signing serving certs and client-certs for services
// and can be used as an auth mechanism with tests.
type TinyCA struct {
CA CertPair
CN string
Service string
Namespace string
nextSerial *big.Int
}

// newPrivateKey generates a new private key of a relatively sane size (see rsaKeySize)
func newPrivateKey() (crypto.Signer, error) {
return rsa.GenerateKey(crand.Reader, rsaKeySize)
}

func NewTinyCA(svc, ns string) (*TinyCA, error) {
caPrivateKey, err := newPrivateKey()
if err != nil {
return nil, fmt.Errorf("unable to generate private key for CA: %v", err)
}
cn := fmt.Sprintf("%s.%s.svc", svc, ns)
caCfg := certutil.Config{CommonName: cn}
caCert, err := certutil.NewSelfSignedCACert(caCfg, caPrivateKey)
if err != nil {
return nil, fmt.Errorf("unable to generate certificate for CA: %v", err)
}

return &TinyCA{
CA: CertPair{Key: caPrivateKey, Cert: caCert},
CN: cn,
Service: svc,
Namespace: ns,
nextSerial: big.NewInt(1),
}, nil
}

func (ca *TinyCA) makeCert(cfg certutil.Config) (CertPair, error) {
now := time.Now()

key, err := newPrivateKey()
if err != nil {
return CertPair{}, fmt.Errorf("unable to create private key: %v", err)
}

serial := new(big.Int).Set(ca.nextSerial)
ca.nextSerial.Add(ca.nextSerial, bigOne)

template := x509.Certificate{
Subject: pkix.Name{CommonName: cfg.CommonName, Organization: cfg.Organization},
DNSNames: cfg.AltNames.DNSNames,
IPAddresses: cfg.AltNames.IPs,
SerialNumber: serial,

KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: cfg.Usages,

// technically not necessary for testing, but let's set anyway just in case.
NotBefore: now.UTC(),
// 1 week is just long enough for a long-term test, but not too long that anyone would
// try to use this seriously.
NotAfter: now.Add(168 * time.Hour).UTC(),
}

certRaw, err := x509.CreateCertificate(crand.Reader, &template, ca.CA.Cert, key.Public(), ca.CA.Key)
if err != nil {
return CertPair{}, fmt.Errorf("unable to create certificate: %v", err)
}

cert, err := x509.ParseCertificate(certRaw)
if err != nil {
return CertPair{}, fmt.Errorf("generated invalid certificate, could not parse: %v", err)
}

return CertPair{
Key: key,
Cert: cert,
}, nil
}

// NewServingCert returns a new CertPair for a serving HTTPS for a service. DNSNames are generated from the passed
// service and namespace
func (ca *TinyCA) NewServingCert() (CertPair, error) {
return ca.makeCert(certutil.Config{
CommonName: ca.CN,
AltNames: certutil.AltNames{
DNSNames: []string{
ca.Service,
fmt.Sprintf("%s.%s", ca.Service, ca.Namespace),
fmt.Sprintf("%s.%s.svc", ca.Service, ca.Namespace),
},
},
Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
})
}
Loading

0 comments on commit e6bae64

Please sign in to comment.