diff --git a/api/api.go b/api/api.go index 207f6510f61..15044a7b8e8 100644 --- a/api/api.go +++ b/api/api.go @@ -3,6 +3,7 @@ package api import ( "bytes" "compress/gzip" + "crypto/tls" "encoding/json" "fmt" "io" @@ -14,6 +15,7 @@ import ( "time" "github.com/hashicorp/go-cleanhttp" + rootcerts "github.com/hashicorp/go-rootcerts" ) // QueryOptions are used to parameterize a query @@ -102,6 +104,35 @@ type Config struct { // WaitTime limits how long a Watch will block. If not provided, // the agent default values will be used. WaitTime time.Duration + + // TLSConfig provides the various TLS related configurations for the http + // client + TLSConfig *TLSConfig +} + +// TLSConfig contains the parameters needed to configure TLS on the HTTP client +// used to communicate with Nomad. +type TLSConfig struct { + // CACert is the path to a PEM-encoded CA cert file to use to verify the + // Nomad server SSL certificate. + CACert string + + // CAPath is the path to a directory of PEM-encoded CA cert files to verify + // the Nomad server SSL certificate. + CAPath string + + // ClientCert is the path to the certificate for Nomad communication + ClientCert string + + // ClientKey is the path to the private key for Nomad communication + ClientKey string + + // TLSServerName, if set, is used to set the SNI host when connecting via + // TLS. + TLSServerName string + + // Insecure enables or disables SSL verification + Insecure bool } // DefaultConfig returns a default configuration for the client @@ -109,7 +140,15 @@ func DefaultConfig() *Config { config := &Config{ Address: "http://127.0.0.1:4646", HttpClient: cleanhttp.DefaultClient(), + TLSConfig: &TLSConfig{}, } + config.HttpClient.Timeout = time.Second * 60 + transport := config.HttpClient.Transport.(*http.Transport) + transport.TLSHandshakeTimeout = 10 * time.Second + transport.TLSClientConfig = &tls.Config{ + MinVersion: tls.VersionTLS12, + } + if addr := os.Getenv("NOMAD_ADDR"); addr != "" { config.Address = addr } @@ -128,9 +167,71 @@ func DefaultConfig() *Config { Password: password, } } + + // Read TLS specific env vars + if v := os.Getenv("NOMAD_CACERT"); v != "" { + config.TLSConfig.CACert = v + } + if v := os.Getenv("NOMAD_CAPATH"); v != "" { + config.TLSConfig.CAPath = v + } + if v := os.Getenv("NOMAD_CLIENT_CERT"); v != "" { + config.TLSConfig.ClientCert = v + } + if v := os.Getenv("NOMAD_CLIENT_KEY"); v != "" { + config.TLSConfig.ClientKey = v + } + if v := os.Getenv("NOMAD_SKIP_VERIFY"); v != "" { + if insecure, err := strconv.ParseBool(v); err == nil { + config.TLSConfig.Insecure = insecure + } + } + return config } +// ConfigureTLS applies a set of TLS configurations to the the HTTP client. +func (c *Config) ConfigureTLS() error { + if c.HttpClient == nil { + return fmt.Errorf("config HTTP Client must be set") + } + + var clientCert tls.Certificate + foundClientCert := false + if c.TLSConfig.ClientCert != "" || c.TLSConfig.ClientKey != "" { + if c.TLSConfig.ClientCert != "" && c.TLSConfig.ClientKey != "" { + var err error + clientCert, err = tls.LoadX509KeyPair(c.TLSConfig.ClientCert, c.TLSConfig.ClientKey) + if err != nil { + return err + } + foundClientCert = true + } else if c.TLSConfig.ClientCert != "" || c.TLSConfig.ClientKey != "" { + return fmt.Errorf("Both client cert and client key must be provided") + } + } + + clientTLSConfig := c.HttpClient.Transport.(*http.Transport).TLSClientConfig + rootConfig := &rootcerts.Config{ + CAFile: c.TLSConfig.CACert, + CAPath: c.TLSConfig.CAPath, + } + if err := rootcerts.ConfigureTLS(clientTLSConfig, rootConfig); err != nil { + return err + } + + clientTLSConfig.InsecureSkipVerify = c.TLSConfig.Insecure + + if foundClientCert { + clientTLSConfig.Certificates = []tls.Certificate{clientCert} + } + if c.TLSConfig.TLSServerName != "" { + clientTLSConfig.ServerName = c.TLSConfig.TLSServerName + } + + return nil +} + // Client provides a client to the Nomad API type Client struct { config Config @@ -151,6 +252,11 @@ func NewClient(config *Config) (*Client, error) { config.HttpClient = defConfig.HttpClient } + // Configure the TLS cofigurations + if err := config.ConfigureTLS(); err != nil { + return nil, err + } + client := &Client{ config: *config, } diff --git a/client/config/config.go b/client/config/config.go index c52aecd2498..14a689030f4 100644 --- a/client/config/config.go +++ b/client/config/config.go @@ -135,10 +135,10 @@ type Config struct { PublishAllocationMetrics bool // HttpTLS enables TLS for the HTTP endpoints on the clients. - HttpTLS bool `mapstructure:"http_tls"` + HttpTLS bool // RpcTLS enables TLS for the outgoing TLS connections to the Nomad servers. - RpcTLS bool `mapstructure:"rpc_tls"` + RpcTLS bool // VerifyServerHostname is used to enable hostname verification of servers. This // ensures that the certificate presented is valid for server... @@ -146,19 +146,19 @@ type Config struct { // intercepting request traffic as well as being added as a raft peer. This should be // enabled by default with VerifyOutgoing, but for legacy reasons we cannot break // existing clients. - VerifyServerHostname bool `mapstructure:"verify_server_hostname"` + VerifyServerHostname bool // CAFile is a path to a certificate authority file. This is used with VerifyIncoming // or VerifyOutgoing to verify the TLS connection. - CAFile string `mapstructure:"ca_file"` + CAFile string // CertFile is used to provide a TLS certificate that is used for serving TLS connections. // Must be provided to serve TLS connections. - CertFile string `mapstructure:"cert_file"` + CertFile string // KeyFile is used to provide a TLS key that is used for serving TLS connections. // Must be provided to serve TLS connections. - KeyFile string `mapstructure:"key_file"` + KeyFile string } func (c *Config) Copy() *Config { diff --git a/command/agent/config-test-fixtures/basic.hcl b/command/agent/config-test-fixtures/basic.hcl index bce114fd839..f4d5bdef893 100644 --- a/command/agent/config-test-fixtures/basic.hcl +++ b/command/agent/config-test-fixtures/basic.hcl @@ -122,8 +122,8 @@ vault { tls_skip_verify = true } tls { - enable_http = true - enable_rpc = true + http = true + rpc = true verify_server_hostname = true ca_file = "foo" cert_file = "bar" diff --git a/command/agent/config.go b/command/agent/config.go index 3791cc81910..19dd5995b6f 100644 --- a/command/agent/config.go +++ b/command/agent/config.go @@ -143,10 +143,10 @@ type AtlasConfig struct { type TLSConfig struct { // EnableHTTP enabled TLS for http traffic to the Nomad server and clients - EnableHTTP bool `mapstructure:"enable_http"` + EnableHTTP bool `mapstructure:"http"` // EnableRPC enables TLS for RPC and Raft traffic to the Nomad servers - EnableRPC bool `mapstructure:"enable_rpc"` + EnableRPC bool `mapstructure:"rpc"` // VerifyServerHostname is used to enable hostname verification of servers. This // ensures that the certificate presented is valid for server... diff --git a/command/agent/config_parse.go b/command/agent/config_parse.go index e1fd5049c42..2f12c216e21 100644 --- a/command/agent/config_parse.go +++ b/command/agent/config_parse.go @@ -662,8 +662,8 @@ func parseTLSConfig(result **TLSConfig, list *ast.ObjectList) error { listVal := list.Items[0].Val valid := []string{ - "enable_http", - "enable_rpc", + "http", + "rpc", "verify_server_hostname", "ca_file", "cert_file", diff --git a/command/agent/http.go b/command/agent/http.go index 02567da1625..050a5940642 100644 --- a/command/agent/http.go +++ b/command/agent/http.go @@ -54,6 +54,7 @@ func NewHTTPServer(agent *Agent, config *Config, logOutput io.Writer) (*HTTPServ return nil, fmt.Errorf("failed to start HTTP listener: %v", err) } + // If TLS is enabled, wrap the listener with a TLS listener if config.TLSConfig.EnableHTTP { tlsConf := &tlsutil.Config{ VerifyIncoming: false, diff --git a/command/meta.go b/command/meta.go index 2f1ecbb25fb..0b40de46181 100644 --- a/command/meta.go +++ b/command/meta.go @@ -46,6 +46,12 @@ type Meta struct { // The region to send API requests region string + + caCert string + caPath string + clientCert string + clientKey string + insecure bool } // FlagSet returns a FlagSet with the common flags that every @@ -61,6 +67,13 @@ func (m *Meta) FlagSet(n string, fs FlagSetFlags) *flag.FlagSet { f.StringVar(&m.flagAddress, "address", "", "") f.StringVar(&m.region, "region", "", "") f.BoolVar(&m.noColor, "no-color", false, "") + f.StringVar(&m.caCert, "ca-cert", "", "") + f.StringVar(&m.caPath, "ca-path", "", "") + f.StringVar(&m.clientCert, "client-cert", "", "") + f.StringVar(&m.clientKey, "client-key", "", "") + f.BoolVar(&m.insecure, "insecure", false, "") + f.BoolVar(&m.insecure, "tls-skip-verify", false, "") + } // Create an io.Writer that writes to our UI properly for errors. @@ -95,6 +108,18 @@ func (m *Meta) Client() (*api.Client, error) { if m.region != "" { config.Region = m.region } + // If we need custom TLS configuration, then set it + if m.caCert != "" || m.caPath != "" || m.clientCert != "" || m.clientKey != "" || m.insecure { + t := &api.TLSConfig{ + CACert: m.caCert, + CAPath: m.caPath, + ClientCert: m.clientCert, + ClientKey: m.clientKey, + Insecure: m.insecure, + } + config.TLSConfig = t + } + return api.NewClient(config) } @@ -121,6 +146,31 @@ func generalOptionsUsage() string { -no-color Disables colored command output. + + -ca-cert= + Path to a PEM encoded CA cert file to use to verify the + Nomad server SSL certificate. Overrides the NOMAD_CACERT + environment variable if set. + + -ca-path= + Path to a directory of PEM encoded CA cert files to verify + the Nomad server SSL certificate. If both -ca-cert and + -ca-path are specified, -ca-cert is used. Overrides the + NOMAD_CAPATH environment variable if set. + + -client-cert= + Path to a PEM encoded client certificate for TLS authentication + to the Nomad server. Must also specify -client-key. Overrides + the NOMAD_CLIENT_CERT environment variable if set. + + -client-key= + Path to an unencrypted PEM encoded private key matching the + client certificate from -client-cert. Overrides the + NOMAD_CLIENT_KEY environment variable if set. + + -tls-skip-verify + Do not verify TLS certificate. This is highly not recommended. Verification + will also be skipped if NOMAD_SKIP_VERIFY is set. ` return strings.TrimSpace(helpText) }