diff --git a/cmd/webhook/BUILD.bazel b/cmd/webhook/BUILD.bazel index 97d43e060..82717275f 100644 --- a/cmd/webhook/BUILD.bazel +++ b/cmd/webhook/BUILD.bazel @@ -21,10 +21,9 @@ go_library( importpath = "github.com/jetstack/cert-manager/cmd/webhook", visibility = ["//visibility:private"], deps = [ - "//pkg/logs:go_default_library", - "//pkg/webhook:go_default_library", - "//pkg/webhook/handlers:go_default_library", - "//pkg/webhook/server:go_default_library", + "//cmd/webhook/app:go_default_library", + "//cmd/webhook/app/options:go_default_library", + "@com_github_spf13_pflag//:go_default_library", "@io_k8s_klog//:go_default_library", "@io_k8s_klog//klogr:go_default_library", ], @@ -47,7 +46,10 @@ filegroup( filegroup( name = "all-srcs", - srcs = [":package-srcs"], + srcs = [ + ":package-srcs", + "//cmd/webhook/app:all-srcs", + ], tags = ["automanaged"], visibility = ["//visibility:public"], ) diff --git a/cmd/webhook/app/BUILD.bazel b/cmd/webhook/app/BUILD.bazel new file mode 100644 index 000000000..493ec121d --- /dev/null +++ b/cmd/webhook/app/BUILD.bazel @@ -0,0 +1,34 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["webhook.go"], + importpath = "github.com/jetstack/cert-manager/cmd/webhook/app", + visibility = ["//visibility:public"], + deps = [ + "//cmd/webhook/app/options:go_default_library", + "//pkg/logs:go_default_library", + "//pkg/webhook:go_default_library", + "//pkg/webhook/handlers:go_default_library", + "//pkg/webhook/server:go_default_library", + "@com_github_go_logr_logr//:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [ + ":package-srcs", + "//cmd/webhook/app/options:all-srcs", + "//cmd/webhook/app/testing:all-srcs", + ], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/cmd/webhook/app/options/BUILD.bazel b/cmd/webhook/app/options/BUILD.bazel new file mode 100644 index 000000000..82af29a98 --- /dev/null +++ b/cmd/webhook/app/options/BUILD.bazel @@ -0,0 +1,23 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["options.go"], + importpath = "github.com/jetstack/cert-manager/cmd/webhook/app/options", + visibility = ["//visibility:public"], + deps = ["@com_github_spf13_pflag//:go_default_library"], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/cmd/webhook/app/options/options.go b/cmd/webhook/app/options/options.go new file mode 100644 index 000000000..98f63dabd --- /dev/null +++ b/cmd/webhook/app/options/options.go @@ -0,0 +1,53 @@ +/* +Copyright 2020 The Jetstack cert-manager contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package options + +import ( + "github.com/spf13/pflag" +) + +type WebhookOptions struct { + ListenPort int + HealthzPort int + + TLSCertFile string + TLSKeyFile string + TLSCipherSuites []string +} + +func (o *WebhookOptions) AddFlags(fs *pflag.FlagSet) { + // TODO: rename secure-port to listen-port + fs.IntVar(&o.ListenPort, "secure-port", 6443, "port number to listen on for secure TLS connections") + fs.IntVar(&o.HealthzPort, "healthz-port", 6080, "port number to listen on for insecure healthz connections") + fs.StringVar(&o.TLSCertFile, "tls-cert-file", "", "path to the file containing the TLS certificate to serve with") + fs.StringVar(&o.TLSKeyFile, "tls-private-key-file", "", "path to the file containing the TLS private key to serve with") + fs.StringSliceVar(&o.TLSCipherSuites, "tls-cipher-suites", defaultCipherSuites, "comma separated list of TLS 1.2 cipher suites to use (TLS 1.3 cipher suites are not configurable)") +} + +var ( + defaultCipherSuites = []string{ + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_RSA_WITH_AES_128_GCM_SHA256", + "TLS_RSA_WITH_AES_256_GCM_SHA384", + "TLS_RSA_WITH_AES_128_CBC_SHA", + "TLS_RSA_WITH_AES_256_CBC_SHA", + } +) diff --git a/cmd/webhook/app/testing/BUILD.bazel b/cmd/webhook/app/testing/BUILD.bazel new file mode 100644 index 000000000..74f26ee44 --- /dev/null +++ b/cmd/webhook/app/testing/BUILD.bazel @@ -0,0 +1,29 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["testwebhook.go"], + importpath = "github.com/jetstack/cert-manager/cmd/webhook/app/testing", + visibility = ["//visibility:public"], + deps = [ + "//cmd/webhook/app:go_default_library", + "//cmd/webhook/app/options:go_default_library", + "//pkg/logs:go_default_library", + "//pkg/util/pki:go_default_library", + "@com_github_spf13_pflag//:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/cmd/webhook/app/testing/testwebhook.go b/cmd/webhook/app/testing/testwebhook.go new file mode 100644 index 000000000..069e92e0b --- /dev/null +++ b/cmd/webhook/app/testing/testwebhook.go @@ -0,0 +1,172 @@ +/* +Copyright 2020 The Jetstack cert-manager contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package testing + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "fmt" + "io/ioutil" + "math/big" + "net" + "os" + "path/filepath" + "testing" + "time" + + "github.com/spf13/pflag" + + "github.com/jetstack/cert-manager/cmd/webhook/app" + "github.com/jetstack/cert-manager/cmd/webhook/app/options" + logf "github.com/jetstack/cert-manager/pkg/logs" + "github.com/jetstack/cert-manager/pkg/util/pki" +) + +var log = logf.Log.WithName("webhook-server-test") + +type StopFunc func() + +type ServerOptions struct { + // URL is the base path/URL that the webhook server can be accessed on. + // This is typically of the form: https://127.0.0.1:12345. + URL string + + // CAPEM is PEM data containing the CA used to validate connections to the + // webhook. + // If `--tls-cert-file` or `--tls-private-key-file` are explicitly provided + // as flags, this field will be empty. + CAPEM []byte +} + +func StartWebhookServer(t *testing.T, args []string) (ServerOptions, StopFunc) { + // Allow user to override options using flags + opts := &options.WebhookOptions{} + fs := pflag.NewFlagSet("testset", pflag.ExitOnError) + opts.AddFlags(fs) + // Parse the arguments passed in into the WebhookOptions struct + fs.Parse(args) + + var caPEM []byte + tempDir, err := ioutil.TempDir("", "webhook-tls-") + if err != nil { + t.Fatal(err) + } + if opts.TLSKeyFile == "" && opts.TLSCertFile == "" { + // Generate a CA and serving certificate + ca, certificatePEM, privateKeyPEM, err := generateTLSAssets() + if err != nil { + t.Fatalf("failed to generate PKI assets: %v", err) + } + + caPEM = ca + if err := ioutil.WriteFile(filepath.Join(tempDir, "tls.crt"), certificatePEM, 0644); err != nil { + t.Fatal(err) + } + if err := ioutil.WriteFile(filepath.Join(tempDir, "tls.key"), privateKeyPEM, 0644); err != nil { + t.Fatal(err) + } + + opts.TLSKeyFile = filepath.Join(tempDir, "tls.key") + opts.TLSCertFile = filepath.Join(tempDir, "tls.crt") + } + + stopCh := make(chan struct{}) + go func() { + if err := app.RunServer(log, *opts, stopCh); err != nil { + t.Fatalf("error running webhook server: %v", err) + } + }() + + return ServerOptions{ + URL: fmt.Sprintf("https://127.0.0.1:%d", opts.ListenPort), + CAPEM: caPEM, + }, func() { + close(stopCh) + if err := os.RemoveAll(tempDir); err != nil { + t.Fatal(err) + } + } +} + +func generateTLSAssets() (caPEM, certificatePEM, privateKeyPEM []byte, err error) { + caPK, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, nil, nil, err + } + rootCA := &x509.Certificate{ + Version: 3, + BasicConstraintsValid: true, + SerialNumber: big.NewInt(1658), + PublicKeyAlgorithm: x509.RSA, + Subject: pkix.Name{ + CommonName: "testing-ca", + }, + NotBefore: time.Now().Add(-1 * time.Hour), + NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + IsCA: true, + } + rootCADER, err := x509.CreateCertificate(rand.Reader, rootCA, rootCA, caPK.Public(), caPK) + if err != nil { + return nil, nil, nil, err + } + rootCA, err = x509.ParseCertificate(rootCADER) + if err != nil { + return nil, nil, nil, err + } + servingCert := &x509.Certificate{ + Version: 3, + BasicConstraintsValid: true, + SerialNumber: big.NewInt(1659), + PublicKeyAlgorithm: x509.RSA, + DNSNames: []string{"localhost"}, + IPAddresses: []net.IP{{127, 0, 0, 1}}, + NotBefore: time.Now().Add(-1 * time.Hour), + NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + } + servingPK, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, nil, nil, err + } + servingDER, err := x509.CreateCertificate(rand.Reader, servingCert, rootCA, servingPK.Public(), caPK) + if err != nil { + return nil, nil, nil, err + } + servingCert, err = x509.ParseCertificate(servingDER) + if err != nil { + return nil, nil, nil, err + } + + // encoding PKI data to PEM + privateKeyPEM, err = pki.EncodePKCS8PrivateKey(servingPK) + if err != nil { + return nil, nil, nil, err + } + caPEM, err = pki.EncodeX509(rootCA) + if err != nil { + return nil, nil, nil, err + } + certificatePEM, err = pki.EncodeX509(servingCert) + if err != nil { + return nil, nil, nil, err + } + return +} diff --git a/cmd/webhook/app/webhook.go b/cmd/webhook/app/webhook.go new file mode 100644 index 000000000..98e3e5cd5 --- /dev/null +++ b/cmd/webhook/app/webhook.go @@ -0,0 +1,63 @@ +/* +Copyright 2020 The Jetstack cert-manager contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package app + +import ( + "fmt" + + "github.com/go-logr/logr" + + "github.com/jetstack/cert-manager/cmd/webhook/app/options" + "github.com/jetstack/cert-manager/pkg/logs" + "github.com/jetstack/cert-manager/pkg/webhook" + "github.com/jetstack/cert-manager/pkg/webhook/handlers" + "github.com/jetstack/cert-manager/pkg/webhook/server" +) + +var validationHook handlers.ValidatingAdmissionHook = handlers.NewRegistryBackedValidator(logs.Log, webhook.Scheme, webhook.ValidationRegistry) +var mutationHook handlers.MutatingAdmissionHook = handlers.NewSchemeBackedDefaulter(logs.Log, webhook.Scheme) +var conversionHook handlers.ConversionHook = handlers.NewSchemeBackedConverter(logs.Log, webhook.Scheme) + +func RunServer(log logr.Logger, opts options.WebhookOptions, stopCh <-chan struct{}) error { + var source server.CertificateSource + if opts.TLSCertFile == "" || opts.TLSKeyFile == "" { + log.Info("warning: serving insecurely as tls certificate data not provided") + } else { + log.Info("enabling TLS as certificate file flags specified") + source = &server.FileCertificateSource{ + CertPath: opts.TLSCertFile, + KeyPath: opts.TLSKeyFile, + Log: log, + } + } + + srv := server.Server{ + ListenAddr: fmt.Sprintf(":%d", opts.ListenPort), + HealthzAddr: fmt.Sprintf(":%d", opts.HealthzPort), + EnablePprof: true, + CertificateSource: source, + CipherSuites: opts.TLSCipherSuites, + ValidationWebhook: validationHook, + MutationWebhook: mutationHook, + ConversionWebhook: conversionHook, + Log: log, + } + if err := srv.Run(stopCh); err != nil { + return err + } + return nil +} diff --git a/cmd/webhook/main.go b/cmd/webhook/main.go index a928ad05d..f7fd4376f 100644 --- a/cmd/webhook/main.go +++ b/cmd/webhook/main.go @@ -17,89 +17,31 @@ limitations under the License. package main import ( - "flag" - "fmt" + goflag "flag" "os" "os/signal" - "strings" "syscall" + "github.com/spf13/pflag" "k8s.io/klog" "k8s.io/klog/klogr" - "github.com/jetstack/cert-manager/pkg/logs" - "github.com/jetstack/cert-manager/pkg/webhook" - "github.com/jetstack/cert-manager/pkg/webhook/handlers" - "github.com/jetstack/cert-manager/pkg/webhook/server" + "github.com/jetstack/cert-manager/cmd/webhook/app" + "github.com/jetstack/cert-manager/cmd/webhook/app/options" ) -const ( - defaultCipherSuites = "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256," + - "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384," + - "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305," + - "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA," + - "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA," + - "TLS_RSA_WITH_AES_128_GCM_SHA256," + - "TLS_RSA_WITH_AES_256_GCM_SHA384," + - "TLS_RSA_WITH_AES_128_CBC_SHA," + - "TLS_RSA_WITH_AES_256_CBC_SHA" -) - -var ( - securePort int - healthzPort int - tlsCertFile string - tlsKeyFile string - tlsCipherSuites string -) - -func init() { - flag.IntVar(&healthzPort, "healthz-port", 6080, "port number to listen on for insecure healthz connections") - flag.IntVar(&securePort, "secure-port", 6443, "port number to listen on for secure TLS connections") - flag.StringVar(&tlsCertFile, "tls-cert-file", "", "path to the file containing the TLS certificate to serve with") - flag.StringVar(&tlsKeyFile, "tls-private-key-file", "", "path to the file containing the TLS private key to serve with") - flag.StringVar(&tlsCipherSuites, "tls-cipher-suites", defaultCipherSuites, "comma separated list of TLS 1.2 cipher suites to use (TLS 1.3 cipher suites are not configurable)") -} - -var validationHook handlers.ValidatingAdmissionHook = handlers.NewRegistryBackedValidator(logs.Log, webhook.Scheme, webhook.ValidationRegistry) -var mutationHook handlers.MutatingAdmissionHook = handlers.NewSchemeBackedDefaulter(logs.Log, webhook.Scheme) -var conversionHook handlers.ConversionHook = handlers.NewSchemeBackedConverter(logs.Log, webhook.Scheme) - func main() { - klog.InitFlags(flag.CommandLine) - flag.Parse() + gofs := &goflag.FlagSet{} + klog.InitFlags(gofs) + pflag.CommandLine.AddGoFlagSet(gofs) + opts := &options.WebhookOptions{} + opts.AddFlags(pflag.CommandLine) + pflag.Parse() log := klogr.New() stopCh := setupSignalHandler() - var source server.CertificateSource - if tlsCertFile == "" || tlsKeyFile == "" { - log.Info("warning: serving insecurely as tls certificate data not provided") - } else { - log.Info("enabling TLS as certificate file flags specified") - source = &server.FileCertificateSource{ - CertPath: tlsCertFile, - KeyPath: tlsKeyFile, - Log: log, - } - } - var cipherSuites []string - if len(tlsCipherSuites) > 0 { - cipherSuites = strings.Split(tlsCipherSuites, ",") - } - - srv := server.Server{ - ListenAddr: fmt.Sprintf(":%d", securePort), - HealthzAddr: fmt.Sprintf(":%d", healthzPort), - EnablePprof: true, - CertificateSource: source, - CipherSuites: cipherSuites, - ValidationWebhook: validationHook, - MutationWebhook: mutationHook, - ConversionWebhook: conversionHook, - Log: log, - } - if err := srv.Run(stopCh); err != nil { + if err := app.RunServer(log, *opts, stopCh); err != nil { log.Error(err, "error running server") os.Exit(1) }