Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add kudo init --unsafe-self-signed-webhook-ca option #1459

Merged
merged 8 commits into from
Apr 16, 2020
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")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is more of a warning.. right? there is no value of a CA bundle without webhooks... but there is no reason we can't run that way. I'm ok with it... just seems unnecessary

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The webhook is populated with the CA bundle so it does need it. Also, most CLIs that I use fail in the presence of an invalid option. I think we should do the same here. It might save the user some surprises.

}

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