diff --git a/cmd/gatekeeper.go b/cmd/gatekeeper.go index d2378a55..2957fdb7 100644 --- a/cmd/gatekeeper.go +++ b/cmd/gatekeeper.go @@ -14,6 +14,11 @@ import ( "github.com/manifoldco/torus-cli/gatekeeper" ) +var ( + certFlag = newPlaceholder("cert, c", "CERT", "Certificate for SSL", "", "TORUS_GATEKEEPER_CERT", false) + keyFlag = newPlaceholder("key, k", "KEY", "Certificate key for SSL", "", "TORUS_GATEKEEPER_CERT_KEY", false) +) + func init() { gatekeeper := cli.Command{ Name: "gatekeeper", @@ -26,6 +31,12 @@ func init() { Action: chain(ensureDaemon, ensureSession, loadDirPrefs, loadPrefDefaults, startGatekeeperCmd, ), + Flags: []cli.Flag{ + orgFlag("Use this organization by default in Gatekeeper", false), + roleFlag("Use this role.", false), + certFlag, + keyFlag, + }, }, }, } @@ -42,9 +53,10 @@ func startGatekeeperCmd(ctx *cli.Context) error { return errs.NewErrorExitError("Failed to load config.", err) } - gatekeeper, err := gatekeeper.New(ctx.String("org"), ctx.String("team"), cfg) + gatekeeper, err := gatekeeper.New(ctx.String("org"), ctx.String("role"), ctx.String("cert"), ctx.String("key"), cfg) if err != nil { log.Printf("Error starting a new Gatekeeper instance: %s", err) + return err } log.Printf("v%s of the Gatekeeper is now listeneing on %s", cfg.Version, gatekeeper.Addr()) diff --git a/cmd/machines.go b/cmd/machines.go index cf22c429..dbe02e0f 100644 --- a/cmd/machines.go +++ b/cmd/machines.go @@ -47,6 +47,10 @@ func authProviderFlag(usage string, required bool) cli.Flag { return newPlaceholder("auth, a", "AUTHPROVIDER", usage, "", "TORUS_AUTH_PROVIDER", required) } +func caFlag(usage string, required bool) cli.Flag { + return newPlaceholder("ca", "CA_BUNDLE", usage, "", "TORUS_BOOTSTRAP_CA", required) +} + func init() { machines := cli.Command{ Name: "machines", @@ -143,6 +147,7 @@ func init() { roleFlag("Role the machine will belong to", true), machineFlag("Machine name to bootstrap", false), orgFlag("Org the machine will belong to", false), + caFlag("CA Bundle to use for certificate verification. Uses system if none is provided", false), }, Action: chain(checkRequiredFlags, bootstrapCmd), }, @@ -437,6 +442,7 @@ func bootstrapCmd(ctx *cli.Context) error { ctx.String("machine"), ctx.String("org"), ctx.String("role"), + ctx.String("ca"), ) if err != nil { return fmt.Errorf("bootstrap provision failed: %s", err) diff --git a/gatekeeper/bootstrap/aws/bootstrap.go b/gatekeeper/bootstrap/aws/bootstrap.go index 1abb0f25..9e601b25 100644 --- a/gatekeeper/bootstrap/aws/bootstrap.go +++ b/gatekeeper/bootstrap/aws/bootstrap.go @@ -17,9 +17,12 @@ const ( ) // Bootstrap bootstraps a the AWS instance into a role to a given Gatekeeper instance -func Bootstrap(url, name, org, role string) (*apitypes.BootstrapResponse, error) { +func Bootstrap(url, name, org, role, caFile string) (*apitypes.BootstrapResponse, error) { var err error - client := client.NewClient(url) + client, err := client.NewClient(url, caFile) + if err != nil { + return nil, fmt.Errorf("cannot initialize bootstrap client: %s", err) + } identity, err := metadata() if err != nil { diff --git a/gatekeeper/bootstrap/bootstrap.go b/gatekeeper/bootstrap/bootstrap.go index 0598f7b1..7af302f0 100644 --- a/gatekeeper/bootstrap/bootstrap.go +++ b/gatekeeper/bootstrap/bootstrap.go @@ -17,10 +17,10 @@ const ( ) // Do will execute the bootstrap request for the given provider -func Do(provider Provider, url, name, org, role string) (*apitypes.BootstrapResponse, error) { +func Do(provider Provider, url, name, org, role, caFile string) (*apitypes.BootstrapResponse, error) { switch provider { case AWSPublic: - return aws.Bootstrap(url, name, org, role) + return aws.Bootstrap(url, name, org, role, caFile) default: return nil, fmt.Errorf("invalid provider: %s", provider) diff --git a/gatekeeper/client/client.go b/gatekeeper/client/client.go index e9da9661..18de76f8 100644 --- a/gatekeeper/client/client.go +++ b/gatekeeper/client/client.go @@ -3,7 +3,11 @@ package client import ( "context" + "crypto/tls" + "crypto/x509" "fmt" + "io/ioutil" + "log" "net/http" "strings" "time" @@ -29,20 +33,47 @@ type Client struct { } // NewClient returns a new client to a Gatekeeper host that can bootstrap this machine -func NewClient(host string) *Client { +func NewClient(host, caFile string) (*Client, error) { if !strings.HasSuffix(host, "/") { host += "/" } + + caPool, err := x509.SystemCertPool() + if err != nil { + log.Printf("Could not load system certificate pool: %s. Creating custom pool", err) + caPool = x509.NewCertPool() + } + + if caFile != "" { + caBytes, err := ioutil.ReadFile(caFile) + if err != nil { + return nil, fmt.Errorf("unable to read ca file: %s", err) + } + ok := caPool.AppendCertsFromPEM(caBytes) + if !ok { + return nil, fmt.Errorf("unable to parse and append ca file: %s", err) + } + } + + tlsConfig := &tls.Config{ + RootCAs: caPool, + } + + transport := &http.Transport{ + TLSClientConfig: tlsConfig, + } + return &Client{ rt: &clientRoundTripper{ DefaultRequestDoer: registry.DefaultRequestDoer{ Client: &http.Client{ - Timeout: clientTimeout, + Timeout: clientTimeout, + Transport: transport, }, Host: host, }, }, - } + }, nil } // Bootstrap bootstraps the machine with Gatekeeper diff --git a/gatekeeper/gatekeeper.go b/gatekeeper/gatekeeper.go index fe1cc4f4..e8187de5 100644 --- a/gatekeeper/gatekeeper.go +++ b/gatekeeper/gatekeeper.go @@ -9,9 +9,12 @@ import ( ) // New returns a new Gatekeeper -func New(org, team string, cfg *config.Config) (g *http.Gatekeeper, err error) { +func New(org, team, certpath, keypath string, cfg *config.Config) (g *http.Gatekeeper, err error) { api := api.NewClient(cfg) - http := http.NewGatekeeper(org, team, cfg, api) + http, err := http.NewGatekeeper(org, team, certpath, keypath, cfg, api) + if err != nil { + return nil, err + } return http, nil } diff --git a/gatekeeper/http/http.go b/gatekeeper/http/http.go index 23763df4..c696fe88 100644 --- a/gatekeeper/http/http.go +++ b/gatekeeper/http/http.go @@ -1,6 +1,9 @@ package http import ( + "crypto/tls" + "fmt" + "io/ioutil" "log" "net/http" @@ -28,11 +31,27 @@ type Gatekeeper struct { } // NewGatekeeper returns a new Gatekeeper. -func NewGatekeeper(org, team string, cfg *config.Config, api *api.Client) *Gatekeeper { +func NewGatekeeper(org, team, certpath, keypath string, cfg *config.Config, api *api.Client) (*Gatekeeper, error) { server := &http.Server{ Addr: cfg.GatekeeperAddress, } + keypair, err := tlsKeypair(certpath, keypath) + if err != nil { + log.Printf("Starting Gatekeeper without SSL: %s", err) + } else { + if err != nil { + return nil, err + } + + tlsConfig := &tls.Config{ + MinVersion: tls.VersionTLS12, + Certificates: []tls.Certificate{*keypair}, + } + + server.TLSConfig = tlsConfig + } + g := &Gatekeeper{ defaults: gatekeeperDefaults{ Org: org, @@ -43,7 +62,7 @@ func NewGatekeeper(org, team string, cfg *config.Config, api *api.Client) *Gatek api: api, } - return g + return g, nil } // Listen listens on a TCP port for HTTP machine requests @@ -82,3 +101,26 @@ func loggingHandler(next http.Handler) http.Handler { log.Printf("%s %s", r.Method, p) }) } + +func tlsKeypair(certpath, keypath string) (*tls.Certificate, error) { + if certpath == "" { + return nil, fmt.Errorf("no certificate provided") + } + + certBytes, err := ioutil.ReadFile(certpath) + if err != nil { + return nil, fmt.Errorf("unable to read certificate: %s", err) + } + + if keypath == "" { + return nil, fmt.Errorf("no certificate key provided") + } + + keyBytes, err := ioutil.ReadFile(keypath) + if err != nil { + return nil, fmt.Errorf("unable to read keyfile: %s", err) + } + + cert, err := tls.X509KeyPair(certBytes, keyBytes) + return &cert, err +}