From e76a351d7cc8a48e0abeb47d8e8213188e70d6f2 Mon Sep 17 00:00:00 2001 From: candysmurf <77.ears@gmail.com> Date: Wed, 2 Aug 2017 14:09:40 -0700 Subject: [PATCH] SDI-2730: Add SSL/TLC capability to snap-cli --- README.md | 92 ++++++++++++++++++++++++++++++++++++++++++++++- glide.yaml | 21 ----------- main.go | 26 +++++++++++++- snaptel/common.go | 29 ++++++++++++++- snaptel/plugin.go | 51 +++++++++++++++++++++++++- snaptel/watch.go | 12 +++++-- 6 files changed, 204 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index bcbd51b..94675b7 100644 --- a/README.md +++ b/README.md @@ -300,4 +300,94 @@ $ snaptel -p plugin list $ snaptel -p metric list $ snaptel -p plugin load /opt/snap/plugins/snap-plugin-collector-mock1 $ snaptel -p task create -t mock-file.yml -``` \ No newline at end of file +``` + +### Secure GRPC plugins +Snap supports TLS for GRPC plugins. Referring to [secure plugin communication](https://github.com/intelsdi-x/snap/blob/master/docs/SECURE_PLUGIN_COMMUNICATION.md) for details. How to setup TLS on both server and client? The [Setup TLS Certificates](https://github.com/intelsdi-x/snap/blob/master/docs/SETUP_TLS_CERTIFICATES.md) has everything. + +#### Sample Use Cases + +| Flag | Description | +| ------ | ------ | +| tls-cert | TLS client certificate | +| tls-key | TLS client private key | +| ca-cert-paths | TLS client CA certificates | +| plugin-cert | TLS server certificate | +| plugin-key | TLS server private key | +| plugin-ca-certs | TLS server CA certificates | + +##### Case 1: Start `snapteld` with TLS certs + +Snap is a client for all GRPC plugins. Note that Snap loads CA certificates from your OS certificate trust store if it's not specified. + +```sh +$snapteld -t 0 -l 1 --tls-cert snaptest-cli.crt --tls-key snaptest-cli.key --ca-cert-paths snaptest-ca.crt +``` +##### Case 1: Run `snaptel` + +```sh +▶ snaptel plugin load --plugin-cert snaptest-srv.crt --plugin-key snaptest-srv.key --plugin-ca-certs snaptest-ca.crt ../snap-plugin-lib-go/rand-collector +Error: Both plugin certification and key are mandatory. The request has to use HTTPS +Usage: load [--plugin-cert= --plugin-key= --plugin-ca-certs=] +``` + +> :collision: Urgh! Loading a secured GRPC plugin has to use HTTPS + +```sh +▶ snaptel --url https://localhost:8181 plugin load --plugin-cert snaptest-srv.crt --plugin-key snaptest-srv.key --plugin-ca-certs snaptest-ca.crt ../snap-plugin-lib-go/rand-collector +Error: Error: Post https://localhost:8181/v2/plugins: http: server gave HTTP response to HTTPS client +Usage: load [--plugin-cert= --plugin-key= --plugin-ca-certs=] +``` + +> :collision: The server was not started using HTTPs + +##### Case 2: Start `snapteld` with TLS certs and HTTPS +Snap only requires the server certificate verificate for HTTPS. + +```sh +▶ snapteld -t 0 -l 1 --rest-https --rest-cert snaphttps-srv.crt --rest-key snaphttps-srv.key --tls-cert snaptest-cli.crt --tls-key snaptest-cli.key --ca-cert-paths snaptest-ca.crt +``` + +> :white_check_mark: using this setting to start `snapteld` for a seured GRPC plugin communication. + +##### Case 2: Run `snaptel` + +```sh +▶ snaptel --url https://localhost:8181 plugin load --plugin-cert snaptest-srv.crt --plugin-key snaptest-srv.key --plugin-ca-certs snaptest-ca.crt ../snap-plugin-lib-go/rand-collector +Error: Error: Post https://localhost:8181/v2/plugins: x509: certificate signed by unknown authority +Usage: load [--plugin-cert= --plugin-key= --plugin-ca-certs=] +``` + +> :collision: Urgh! HTTPS does not have a trusted CA. There is no way to specify a CA using a flag for HTTPS currently. Putting the trusted CA in your OS trust store in production. Using --insecure flag for your testing convenience. + +```sh +▶ snaptel --url https://localhost:8181 --insecure plugin load --plugin-cert snaptest-srv.crt --plugin-key snaptest-srv.key --plugin-ca-certs snaptest-ca.crt ../snap-plugin-lib-go/rand-collector +Plugin loaded +Name: test-rand-collector +Version: 1 +Type: collector +Signed: false +Loaded Time: Wed, 02 Aug 2017 15:23:09 PDT +``` + +>:white_check_mark: The secured GRPC plugin loaded! You may omit the `plugin-ca-certs` flag if it's in the trust store of your OS/App. + +Only loading a GRPC plugin requires TLS certs. Not any other commands. + +```sh +▶ snaptel --url https://localhost:8181 --insecure plugin list +NAME VERSION TYPE SIGNED STATUS LOADED TIME +test-rand-collector 1 collector false loaded Wed, 02 Aug 2017 15:23:09 PDT +``` + +##### Case 3: Caveat +Starting `snapteld` same as case 2. Loading a non GRPC plugin. + +```sh +▶ snaptel --url https://localhost:8181 --insecure plugin load --plugin-cert snaptest-srv.crt --plugin-key snaptest-srv.key --plugin-ca-certs snaptest-ca.crt ../snap/build/darwin/x86_64/plugins/snap-plugin-collector-mock1 +Error: secure framework can't connect to insecure plugin; plugin_name: mock +Usage: load [--plugin-cert= --plugin-key= --plugin-ca-certs=] +``` + +>:collision: Urgh! Currently, no TLS is available for non-grpc plugins. Restarting `snapteld` without TLS to load non-grpc plugins. + diff --git a/glide.yaml b/glide.yaml index 73aaf2d..906b9d5 100644 --- a/glide.yaml +++ b/glide.yaml @@ -15,27 +15,8 @@ import: version: c7477ad8e330bef55bf1ebe300cf8aa67c492d1b - package: github.com/ghodss/yaml version: c3eb24aeea63668ebdac08d2e252f20df8b6b1ae -- package: github.com/golang/protobuf - version: 888eb0692c857ec880338addf316bd662d5e630e - subpackages: - - proto -- package: github.com/hashicorp/go-msgpack - version: fa3f63826f7c23912c15263591e65d54d080b458 - subpackages: - - codec -- package: github.com/hashicorp/memberlist - version: a93fbd426dd831f5a66db3adc6a5ffa6f44cc60a -- package: github.com/intelsdi-x/gomit -- package: github.com/julienschmidt/httprouter - version: 8c199fb6259ffc1af525cc3ad52ee60ba8359669 -- package: github.com/pborman/uuid - version: ca53cad383cad2479bbba7f7a1a05797ec1386e4 - package: github.com/robfig/cron version: 32d9c273155a0506d27cf73dd1246e86a470997e -- package: github.com/vrischmann/jsonutil - version: 694784f9315ee9fc763c1d30f28753cba21307aa -- package: github.com/xeipuuv/gojsonschema - version: d3178baac32433047aa76f07317f84fbe2be6cda - package: golang.org/x/crypto version: aedad9a179ec1ea11b7064c57cbc6dc30d7724ec subpackages: @@ -47,8 +28,6 @@ import: - context - trace - http2 -- package: google.golang.org/grpc - version: 0032a855ba5c8a3c8e0d71c2deef354b70af1584 - package: gopkg.in/yaml.v2 version: c1cd2254a6dd314c9d73c338c12688c9325d85c6 - package: github.com/intelsdi-x/snap-client-go/client diff --git a/main.go b/main.go index 7f33a15..c39203d 100644 --- a/main.go +++ b/main.go @@ -20,11 +20,14 @@ limitations under the License. package main import ( + "crypto/tls" "fmt" + "net/http" "net/url" "os" "sort" + openapiclient "github.com/go-openapi/runtime/client" "github.com/golang/glog" "github.com/intelsdi-x/snap-cli/snaptel" "github.com/intelsdi-x/snap-client-go/client" @@ -35,6 +38,10 @@ var ( gitversion string ) +type tlsClientOptions struct { + insecureSkipVerify bool +} + func main() { app := cli.NewApp() app.Name = "snaptel" @@ -66,13 +73,30 @@ func beforeAction(ctx *cli.Context) error { glog.Fatal(err) } - c := client.NewHTTPClientWithConfig(nil, &client.TransportConfig{Host: u.Host, BasePath: snaptel.FlAPIVer.Value, Schemes: []string{u.Scheme}}) + tlcOpts := tlsClientOptions{insecureSkipVerify: ctx.Bool("insecure")} + tlcClient := tlsClient(tlcOpts) + rt := openapiclient.NewWithClient(u.Host, snaptel.FlAPIVer.Value, []string{u.Scheme}, tlcClient) + c := client.New(rt, nil) snaptel.SetClient(c) + snaptel.SetScheme(u.Scheme) snaptel.SetAuthInfo(snaptel.BasicAuth(ctx)) return nil } +// tlsClient creates a http.Client +func tlsClient(opts tlsClientOptions) *http.Client { + transport := tlsTransport(opts) + return &http.Client{Transport: transport} +} + +func tlsTransport(opts tlsClientOptions) http.RoundTripper { + cfg := &tls.Config{} + cfg.InsecureSkipVerify = opts.insecureSkipVerify + cfg.BuildNameToCertificate() + return &http.Transport{TLSClientConfig: cfg} +} + // ByCommand contains array of CLI commands. type ByCommand []cli.Command diff --git a/snaptel/common.go b/snaptel/common.go index 7577b25..df6d058 100644 --- a/snaptel/common.go +++ b/snaptel/common.go @@ -23,6 +23,7 @@ import ( "encoding/json" "fmt" "io/ioutil" + "strings" "golang.org/x/crypto/ssh/terminal" @@ -39,6 +40,7 @@ var ( client *snapClient.Snap authInfoWriter runtime.ClientAuthInfoWriter password string + scheme string ) // UsageError defines the error message and CLI context @@ -61,7 +63,7 @@ func newUsageError(s string, ctx *cli.Context) UsageError { return UsageError{s, ctx} } -// SetClient provides a way to set the private snapClient in this package. +// SetClient sets the private HTTP Client in this package. func SetClient(cl *snapClient.Snap) { client = cl } @@ -71,6 +73,11 @@ func SetAuthInfo(aw runtime.ClientAuthInfoWriter) { authInfoWriter = aw } +// SetScheme sets the request protocol. +func SetScheme(s string) { + scheme = s +} + // GetFirstChar gets the first character of a giving string. func GetFirstChar(s string) string { firstChar := "" @@ -142,6 +149,10 @@ func getErrorDetail(err error, ctx *cli.Context) error { case *tasks.UpdateTaskStateUnauthorized: return newUsageError(fmt.Sprintf("%v", err.(*tasks.UpdateTaskStateUnauthorized).Payload.Message), ctx) default: + // this is a hack + if strings.Contains(err.Error(), "tls: oversized record") || strings.Contains(err.Error(), "malformed HTTP response") { + return newUsageError(extractError(err.Error()), ctx) + } return newUsageError(fmt.Sprintf("Error: %v", err), ctx) } } @@ -210,3 +221,19 @@ func BasicAuth(ctx *cli.Context) runtime.ClientAuthInfoWriter { } return nil } + +// extractError is a hack for SSL/TLS handshake error. +func extractError(m string) string { + ts := strings.Split(m, "\"") + + var tss []string + if len(ts) > 0 { + tss = strings.Split(ts[0], "malformed") + } + + errMsg := "Error connecting to API. Do you have an http/https mismatching API request?" + if len(tss) > 0 { + errMsg = tss[0] + errMsg + } + return errMsg +} diff --git a/snaptel/plugin.go b/snaptel/plugin.go index 8f338a7..8c54957 100644 --- a/snaptel/plugin.go +++ b/snaptel/plugin.go @@ -47,13 +47,49 @@ func loadPlugin(ctx *cli.Context) error { } params := plugins.NewLoadPluginParamsWithTimeout(FlTimeout.Value) - f, err := os.Open(filepath.Join(paths...)) + + // Sets the plugin data. + f, err := os.Open(filepath.Join(paths[0])) if err != nil { return newUsageError("Cannot open the plugin", ctx) } defer f.Close() params.SetPluginData(f) + if !hasValidFlags(ctx.IsSet("plugin-cert"), ctx.IsSet("plugin-key"), scheme) { + return newUsageError("Both plugin certification and key are mandatory. The request has to use HTTPS", ctx) + } + + // Sets the plugin certificate. + if ctx.IsSet("plugin-cert") { + cert, err := os.Open(ctx.String("plugin-cert")) + if err != nil { + return newUsageError("Cannot open the plugin certificate", ctx) + } + defer cert.Close() + params.SetPluginCert(cert) + } + + // Sets the plugin key. + if ctx.IsSet("plugin-key") { + key, err := os.Open(ctx.String("plugin-key")) + if err != nil { + return newUsageError("Cannot open the plugin key", ctx) + } + defer key.Close() + params.SetPluginKey(key) + } + + // Sets the CA ceritificate. + if ctx.IsSet("plugin-ca-certs") { + caCert, err := os.Open(ctx.String("plugin-ca-certs")) + if err != nil { + return newUsageError("Cannot open the CA certificate", ctx) + } + defer caCert.Close() + params.SetCaCerts(caCert) + } + resp, err := client.Plugins.LoadPlugin(params, authInfoWriter) if err != nil { return getErrorDetail(err, ctx) @@ -154,3 +190,16 @@ func listPlugins(ctx *cli.Context) error { return nil } + +func hasValidFlags(key, cert bool, scheme string) bool { + // Validats TLS plugin load flags + if key && cert && scheme == "https" { + return true + } + + // Don't block normal flow + if !key && !cert { + return true + } + return false +} diff --git a/snaptel/watch.go b/snaptel/watch.go index 9c06dfc..e78f90a 100644 --- a/snaptel/watch.go +++ b/snaptel/watch.go @@ -22,6 +22,7 @@ package snaptel import ( "bufio" "bytes" + "crypto/tls" "encoding/json" "fmt" "io" @@ -54,8 +55,15 @@ func watchTask(ctx *cli.Context) error { // Therefore no timeout for this request. req, err := http.NewRequest("GET", url, nil) req.SetBasicAuth("snap", password) - cli := &http.Client{} - resp, err := cli.Do(req) + if err != nil { + return err + } + + tr := http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + wtClient := http.Client{Transport: &tr} + resp, err := wtClient.Do(req) if err != nil { return err }