From 68520495d6d11be5f43bc501db9e9eed23f7067e Mon Sep 17 00:00:00 2001 From: Justin Barrick Date: Tue, 19 Mar 2019 01:04:30 -0700 Subject: [PATCH 1/4] Support TLS and mutual TLS to verify upstream. --- README.md | 9 ++++++++ cmd/kubehook/kubehook.go | 48 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 19a4bde..388c196 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,15 @@ Flags: --kubecfg-template=KUBECFG-TEMPLATE A kubecfg file containing clusters to populate with a user and contexts. + --client-ca=CLIENT-CA If set, enables mutual TLS and specifies the path to CA file + to use when validating client connections. + --client-ca-subject=CLIENT-CA-SUBJECT + If set, requires that the client CA matches the provided + subject (requires --client-ca). + --tls-cert=TLS-CERT If set, enables TLS and specifies the path to TLS + certificate to use for HTTPS server (requires --tls-key). + --tls-key=TLS-KEY Path to TLS key to use for HTTPS server (requires + --tls-cert). Args: Secret for JWT HMAC signature and verification. diff --git a/cmd/kubehook/kubehook.go b/cmd/kubehook/kubehook.go index 089e0cb..a025dc5 100644 --- a/cmd/kubehook/kubehook.go +++ b/cmd/kubehook/kubehook.go @@ -18,7 +18,10 @@ package main import ( "context" + "crypto/tls" + "crypto/x509" "fmt" + "io/ioutil" "net/http" "os" "os/signal" @@ -72,6 +75,10 @@ func main() { groupHeaderDelim = app.Flag("group-header-delimiter", "Delimiter separating group names in the group-header.").Default(handlers.DefaultGroupHeaderDelimiter).String() maxlife = app.Flag("max-lifetime", "Maximum allowed JWT lifetime, in Go's time.ParseDuration format.").Default(jwt.DefaultMaxLifetime.String()).Duration() template = app.Flag("kubecfg-template", "A kubecfg file containing clusters to populate with a user and contexts.").ExistingFile() + clientCA = app.Flag("client-ca", "If set, enables mutual TLS and specifies the path to CA file to use when validating client connections.").String() + clientCASubject = app.Flag("client-ca-subject", "If set, requires that the client CA matches the provided subject (requires --client-ca).").String() + tlsCert = app.Flag("tls-cert", "If set, enables TLS and specifies the path to TLS certificate to use for HTTPS server (requires --tls-key).").String() + tlsKey = app.Flag("tls-key", "Path to TLS key to use for HTTPS server (requires --tls-cert).").String() secret = app.Arg("secret", "Secret for JWT HMAC signature and verification.").Required().Envar(envVarName(app.Name, "secret")).String() ) @@ -89,7 +96,37 @@ func main() { kingpin.FatalIfError(err, "cannot create JWT authenticator") r := httprouter.New() - s := &http.Server{Addr: *listen, Handler: logRequests(r, log)} + + tlsConfig := &tls.Config{} + + if *clientCASubject != "" { + tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + err := fmt.Errorf("No verified certificates") + for _, chain := range verifiedChains { + certificate := chain[0] + err = certificate.VerifyHostname(*clientCASubject) + if err == nil { + return nil + } + } + return err + } + } + + if *clientCA != "" { + clientCACert, err := ioutil.ReadFile(*clientCA) + kingpin.FatalIfError(err, "unable to open client CA file") + + clientCertPool := x509.NewCertPool() + clientCertPool.AppendCertsFromPEM(clientCACert) + + tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert + tlsConfig.ClientCAs = clientCertPool + + tlsConfig.BuildNameToCertificate() + } + + s := &http.Server{Addr: *listen, Handler: logRequests(r, log), TLSConfig: tlsConfig,} ctx, cancel := context.WithTimeout(context.Background(), *grace) done := make(chan struct{}) @@ -132,7 +169,14 @@ func main() { r.HandlerFunc("GET", "/kubecfg", handlers.NotImplemented()) } - log.Info("shutdown", zap.Error(s.ListenAndServe())) + if *tlsCert != "" && *tlsKey != "" { + err = s.ListenAndServeTLS(*tlsCert, *tlsKey) + } else { + err = s.ListenAndServe() + } + + log.Info("shutdown", zap.Error(err)) + <-done cancel() } From 98cdaaf2bb575debbf8f312c63652abb9b6abf27 Mon Sep 17 00:00:00 2001 From: Justin Barrick Date: Tue, 19 Mar 2019 18:37:14 -0700 Subject: [PATCH 2/4] address linter issues --- cmd/kubehook/kubehook.go | 91 ++++++++++++++++++++++++---------------- 1 file changed, 56 insertions(+), 35 deletions(-) diff --git a/cmd/kubehook/kubehook.go b/cmd/kubehook/kubehook.go index a025dc5..d858de8 100644 --- a/cmd/kubehook/kubehook.go +++ b/cmd/kubehook/kubehook.go @@ -63,6 +63,59 @@ func envVarName(app, arg string) string { return strings.Replace(strings.ToUpper(fmt.Sprintf("%s_%s", app, arg)), "-", "_", -1) } +func listenAndServe(s *http.Server, tlsCert, tlsKey string) error { + var err error + + if tlsCert != "" && tlsKey != "" { + err = s.ListenAndServeTLS(tlsCert, tlsKey) + } else { + err = s.ListenAndServe() + } + + return err +} + +func makeTLSConfig(clientCA, clientCASubject string) (*tls.Config, error) { + tlsConfig := &tls.Config{} + + if clientCASubject != "" { + tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + err := fmt.Errorf("No verified certificates") + + for _, chain := range verifiedChains { + certificate := chain[0] + err = certificate.VerifyHostname(clientCASubject) + if err == nil { + return nil + } + } + + return err + } + } + + if clientCA != "" { + var clientCACert []byte + + // Suppress linter warning related to file inclusion since we are + // attempting to load a CA file specified by the user. + clientCACert, err := ioutil.ReadFile(clientCA) // nolint: gosec + if err != nil { + return nil, err + } + + clientCertPool := x509.NewCertPool() + clientCertPool.AppendCertsFromPEM(clientCACert) + + tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert + tlsConfig.ClientCAs = clientCertPool + + tlsConfig.BuildNameToCertificate() + } + + return tlsConfig, nil +} + func main() { var ( app = kingpin.New(filepath.Base(os.Args[0]), "Authenticates Kubernetes users via JWT tokens.").DefaultEnvars() @@ -97,34 +150,8 @@ func main() { r := httprouter.New() - tlsConfig := &tls.Config{} - - if *clientCASubject != "" { - tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { - err := fmt.Errorf("No verified certificates") - for _, chain := range verifiedChains { - certificate := chain[0] - err = certificate.VerifyHostname(*clientCASubject) - if err == nil { - return nil - } - } - return err - } - } - - if *clientCA != "" { - clientCACert, err := ioutil.ReadFile(*clientCA) - kingpin.FatalIfError(err, "unable to open client CA file") - - clientCertPool := x509.NewCertPool() - clientCertPool.AppendCertsFromPEM(clientCACert) - - tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert - tlsConfig.ClientCAs = clientCertPool - - tlsConfig.BuildNameToCertificate() - } + tlsConfig, err := makeTLSConfig(*clientCA, *clientCASubject) + kingpin.FatalIfError(err, "initializing TLS configuration") s := &http.Server{Addr: *listen, Handler: logRequests(r, log), TLSConfig: tlsConfig,} @@ -169,13 +196,7 @@ func main() { r.HandlerFunc("GET", "/kubecfg", handlers.NotImplemented()) } - if *tlsCert != "" && *tlsKey != "" { - err = s.ListenAndServeTLS(*tlsCert, *tlsKey) - } else { - err = s.ListenAndServe() - } - - log.Info("shutdown", zap.Error(err)) + log.Info("shutdown", zap.Error(listenAndServe(s, *tlsCert, *tlsKey))) <-done cancel() From c73f0eb5672481fdbcf447263f165a82a7e2b153 Mon Sep 17 00:00:00 2001 From: Justin Barrick Date: Thu, 21 Mar 2019 23:58:17 -0700 Subject: [PATCH 3/4] Automatically reload certificates when they change. --- cmd/kubehook/kubehook.go | 20 ++++++++++++++------ glide.lock | 10 +++++++--- glide.yaml | 2 ++ 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/cmd/kubehook/kubehook.go b/cmd/kubehook/kubehook.go index d858de8..6410530 100644 --- a/cmd/kubehook/kubehook.go +++ b/cmd/kubehook/kubehook.go @@ -36,6 +36,7 @@ import ( "github.com/planetlabs/kubehook/handlers/kubecfg" _ "github.com/planetlabs/kubehook/statik" + "github.com/dyson/certman" "github.com/julienschmidt/httprouter" "github.com/rakyll/statik/fs" "go.uber.org/zap" @@ -64,15 +65,22 @@ func envVarName(app, arg string) string { } func listenAndServe(s *http.Server, tlsCert, tlsKey string) error { - var err error - if tlsCert != "" && tlsKey != "" { - err = s.ListenAndServeTLS(tlsCert, tlsKey) - } else { - err = s.ListenAndServe() + cm, err := certman.New(tlsCert, tlsKey) + if err != nil { + return err + } + + if err := cm.Watch(); err != nil { + return err + } + + s.TLSConfig.GetCertificate = cm.GetCertificate + + return s.ListenAndServeTLS("", "") } - return err + return s.ListenAndServe() } func makeTLSConfig(clientCA, clientCASubject string) (*tls.Config, error) { diff --git a/glide.lock b/glide.lock index 0c6b8a4..e6dbed4 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: 53e0159422e4c98935ecc64cd263d7eef3778cd08d84a568bfe64a48b860770b -updated: 2018-01-16T19:18:20.162427-08:00 +hash: 8ebb802f15f29dce03c48982b8c6d869cd3ec0624a86ebe889bdddd12c99f91b +updated: 2019-03-21T23:30:27.211202977-07:00 imports: - name: github.com/alecthomas/template version: a0175ee3bccc567396460bf5acd36800cb10c49c @@ -9,10 +9,14 @@ imports: version: 2efee857e7cfd4f3d0138cc3cbb1b4966962b93a - name: github.com/dgrijalva/jwt-go version: dbeaa9332f19a944acb5736b4456cfcc02140e29 +- name: github.com/dyson/certman + version: 90625714c2e968d4e3c49c1c15a3f3f497a388ff - name: github.com/emicklei/go-restful version: ff4f55a206334ef123e4f79bbf348980da81ca46 subpackages: - log +- name: github.com/fsnotify/fsnotify + version: 1485a34d5d5723fea214f5710708e19a831720e4 - name: github.com/ghodss/yaml version: 73d445a93680fa1a78ae23a5839bad48f32ba1ee - name: github.com/go-openapi/jsonpointer @@ -69,7 +73,7 @@ imports: - name: github.com/spf13/pflag version: 9ff6c6923cfffbcd502984b8e0c80539a94968b7 - name: go.uber.org/atomic - version: 8474b86a5a6f79c443ce4b2992817ff32cf208b8 + version: 1ea20fb1cbb1cc08cbd0d913a96dead89aa18289 - name: go.uber.org/multierr version: 3c4937480c32f4c13a875a1829af76c98ca3d40a - name: go.uber.org/zap diff --git a/glide.yaml b/glide.yaml index 6569275..918997c 100644 --- a/glide.yaml +++ b/glide.yaml @@ -25,6 +25,8 @@ import: subpackages: - tools/clientcmd - tools/clientcmd/api +- package: github.com/dyson/certman + version: ~0.2.1 testImport: - package: github.com/go-test/deep version: v1.0.0 From 626255f623a30cef2bab807ddc659de39541c8dd Mon Sep 17 00:00:00 2001 From: Justin Barrick Date: Mon, 25 Mar 2019 23:13:43 -0700 Subject: [PATCH 4/4] Address review comments --- cmd/kubehook/kubehook.go | 49 +++++++++++++++++----------------------- 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/cmd/kubehook/kubehook.go b/cmd/kubehook/kubehook.go index 6410530..3df13f4 100644 --- a/cmd/kubehook/kubehook.go +++ b/cmd/kubehook/kubehook.go @@ -38,6 +38,7 @@ import ( "github.com/dyson/certman" "github.com/julienschmidt/httprouter" + "github.com/pkg/errors" "github.com/rakyll/statik/fs" "go.uber.org/zap" kingpin "gopkg.in/alecthomas/kingpin.v2" @@ -83,37 +84,22 @@ func listenAndServe(s *http.Server, tlsCert, tlsKey string) error { return s.ListenAndServe() } -func makeTLSConfig(clientCA, clientCASubject string) (*tls.Config, error) { +func makeTLSConfig(clientCA []byte, clientCASubject string) *tls.Config { tlsConfig := &tls.Config{} if clientCASubject != "" { tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { - err := fmt.Errorf("No verified certificates") - - for _, chain := range verifiedChains { - certificate := chain[0] - err = certificate.VerifyHostname(clientCASubject) - if err == nil { - return nil - } + if len(verifiedChains) == 0 || len(verifiedChains[0]) == 0 { + return errors.New("client did not present any TLS certificates") } - return err + return verifiedChains[0][0].VerifyHostname(clientCASubject) } } - if clientCA != "" { - var clientCACert []byte - - // Suppress linter warning related to file inclusion since we are - // attempting to load a CA file specified by the user. - clientCACert, err := ioutil.ReadFile(clientCA) // nolint: gosec - if err != nil { - return nil, err - } - + if len(clientCA) > 0 { clientCertPool := x509.NewCertPool() - clientCertPool.AppendCertsFromPEM(clientCACert) + clientCertPool.AppendCertsFromPEM(clientCA) tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert tlsConfig.ClientCAs = clientCertPool @@ -121,7 +107,7 @@ func makeTLSConfig(clientCA, clientCASubject string) (*tls.Config, error) { tlsConfig.BuildNameToCertificate() } - return tlsConfig, nil + return tlsConfig } func main() { @@ -136,10 +122,10 @@ func main() { groupHeaderDelim = app.Flag("group-header-delimiter", "Delimiter separating group names in the group-header.").Default(handlers.DefaultGroupHeaderDelimiter).String() maxlife = app.Flag("max-lifetime", "Maximum allowed JWT lifetime, in Go's time.ParseDuration format.").Default(jwt.DefaultMaxLifetime.String()).Duration() template = app.Flag("kubecfg-template", "A kubecfg file containing clusters to populate with a user and contexts.").ExistingFile() - clientCA = app.Flag("client-ca", "If set, enables mutual TLS and specifies the path to CA file to use when validating client connections.").String() + clientCA = app.Flag("client-ca", "If set, enables mutual TLS and specifies the path to CA file to use when validating client connections.").File() clientCASubject = app.Flag("client-ca-subject", "If set, requires that the client CA matches the provided subject (requires --client-ca).").String() - tlsCert = app.Flag("tls-cert", "If set, enables TLS and specifies the path to TLS certificate to use for HTTPS server (requires --tls-key).").String() - tlsKey = app.Flag("tls-key", "Path to TLS key to use for HTTPS server (requires --tls-cert).").String() + tlsCert = app.Flag("tls-cert", "If set, enables TLS and specifies the path to TLS certificate to use for HTTPS server (requires --tls-key).").ExistingFile() + tlsKey = app.Flag("tls-key", "Path to TLS key to use for HTTPS server (requires --tls-cert).").ExistingFile() secret = app.Arg("secret", "Secret for JWT HMAC signature and verification.").Required().Envar(envVarName(app.Name, "secret")).String() ) @@ -158,10 +144,17 @@ func main() { r := httprouter.New() - tlsConfig, err := makeTLSConfig(*clientCA, *clientCASubject) - kingpin.FatalIfError(err, "initializing TLS configuration") + var clientCACert []byte + if *clientCA != nil { + clientCACert, err = ioutil.ReadAll(*clientCA) + kingpin.FatalIfError(err, "cannot load client CA certificate file") + } - s := &http.Server{Addr: *listen, Handler: logRequests(r, log), TLSConfig: tlsConfig,} + s := &http.Server{ + Addr: *listen, + Handler: logRequests(r, log), + TLSConfig: makeTLSConfig(clientCACert, *clientCASubject), + } ctx, cancel := context.WithTimeout(context.Background(), *grace) done := make(chan struct{})