Skip to content

Commit

Permalink
Merge pull request #1853 from hashicorp/f-rpc-http-tls
Browse files Browse the repository at this point in the history
TLS support for http and RPC
  • Loading branch information
diptanu committed Oct 25, 2016
2 parents 855fe5f + f416ac7 commit 70ec22f
Show file tree
Hide file tree
Showing 31 changed files with 1,342 additions and 16 deletions.
106 changes: 106 additions & 0 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package api
import (
"bytes"
"compress/gzip"
"crypto/tls"
"encoding/json"
"fmt"
"io"
Expand All @@ -14,6 +15,7 @@ import (
"time"

"github.com/hashicorp/go-cleanhttp"
rootcerts "github.com/hashicorp/go-rootcerts"
)

// QueryOptions are used to parameterize a query
Expand Down Expand Up @@ -102,14 +104,51 @@ 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
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
}
Expand All @@ -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
Expand All @@ -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,
}
Expand Down
13 changes: 12 additions & 1 deletion client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/hashicorp/nomad/client/stats"
"github.com/hashicorp/nomad/client/vaultclient"
"github.com/hashicorp/nomad/command/agent/consul"
"github.com/hashicorp/nomad/helper/tlsutil"
"github.com/hashicorp/nomad/nomad"
"github.com/hashicorp/nomad/nomad/structs"
vaultapi "github.com/hashicorp/vault/api"
Expand Down Expand Up @@ -164,12 +165,22 @@ var (

// NewClient is used to create a new client from the given configuration
func NewClient(cfg *config.Config, consulSyncer *consul.Syncer, logger *log.Logger) (*Client, error) {
// Create the tls wrapper
var tlsWrap tlsutil.Wrapper
if cfg.TLSConfig.EnableRPC {
tw, err := cfg.TLSConfiguration().OutgoingTLSWrapper()
if err != nil {
return nil, err
}
tlsWrap = tw
}

// Create the client
c := &Client{
config: cfg,
consulSyncer: consulSyncer,
start: time.Now(),
connPool: nomad.NewPool(cfg.LogOutput, clientRPCCache, clientMaxStreams, nil),
connPool: nomad.NewPool(cfg.LogOutput, clientRPCCache, clientMaxStreams, tlsWrap),
logger: logger,
hostStatsCollector: stats.NewHostStatsCollector(),
allocs: make(map[string]*AllocRunner),
Expand Down
18 changes: 18 additions & 0 deletions client/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"strings"
"time"

"github.com/hashicorp/nomad/helper/tlsutil"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/hashicorp/nomad/nomad/structs/config"
)
Expand Down Expand Up @@ -132,6 +133,9 @@ type Config struct {
// PublishAllocationMetrics determines whether nomad is going to publish
// allocation metrics to remote Telemetry sinks
PublishAllocationMetrics bool

// TLSConfig holds various TLS related configurations
TLSConfig *config.TLSConfig
}

func (c *Config) Copy() *Config {
Expand Down Expand Up @@ -226,3 +230,17 @@ func (c *Config) ReadStringListToMapDefault(key, defaultValue string) map[string
}
return list
}

// TLSConfig returns a TLSUtil Config based on the client configuration
func (c *Config) TLSConfiguration() *tlsutil.Config {
tlsConf := &tlsutil.Config{
VerifyIncoming: true,
VerifyOutgoing: true,
VerifyServerHostname: c.TLSConfig.VerifyServerHostname,
CAFile: c.TLSConfig.CAFile,
CertFile: c.TLSConfig.CertFile,
KeyFile: c.TLSConfig.KeyFile,
ServerName: c.Node.Name,
}
return tlsConf
}
7 changes: 7 additions & 0 deletions command/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,9 @@ func (a *Agent) serverConfig() (*nomad.Config, error) {
conf.ConsulConfig = a.config.Consul
conf.VaultConfig = a.config.Vault

// Set the TLS config
conf.TLSConfig = a.config.TLSConfig

return conf, nil
}

Expand Down Expand Up @@ -357,6 +360,10 @@ func (a *Agent) clientConfig() (*clientconfig.Config, error) {
conf.StatsCollectionInterval = a.config.Telemetry.collectionInterval
conf.PublishNodeMetrics = a.config.Telemetry.PublishNodeMetrics
conf.PublishAllocationMetrics = a.config.Telemetry.PublishAllocationMetrics

// Set the TLS related configs
conf.TLSConfig = a.config.TLSConfig

return conf, nil
}

Expand Down
8 changes: 8 additions & 0 deletions command/agent/config-test-fixtures/basic.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,11 @@ vault {
tls_server_name = "foobar"
tls_skip_verify = true
}
tls {
http = true
rpc = true
verify_server_hostname = true
ca_file = "foo"
cert_file = "bar"
key_file = "pipe"
}
13 changes: 13 additions & 0 deletions command/agent/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ type Config struct {
// List of config files that have been loaded (in order)
Files []string `mapstructure:"-"`

// TLSConfig provides TLS related configuration for the Nomad server and
// client
TLSConfig *config.TLSConfig `mapstructure:"tls"`

// HTTPAPIResponseHeaders allows users to configure the Nomad http agent to
// set arbritrary headers on API responses
HTTPAPIResponseHeaders map[string]string `mapstructure:"http_api_response_headers"`
Expand Down Expand Up @@ -486,6 +490,7 @@ func DefaultConfig() *Config {
CollectionInterval: "1s",
collectionInterval: 1 * time.Second,
},
TLSConfig: &config.TLSConfig{},
}
}

Expand Down Expand Up @@ -566,6 +571,14 @@ func (c *Config) Merge(b *Config) *Config {
result.Telemetry = result.Telemetry.Merge(b.Telemetry)
}

// Apply the TLS Config
if result.TLSConfig == nil && b.TLSConfig != nil {
tlsConfig := *b.TLSConfig
result.TLSConfig = &tlsConfig
} else if b.TLSConfig != nil {
result.TLSConfig = result.TLSConfig.Merge(b.TLSConfig)
}

// Apply the client config
if result.Client == nil && b.Client != nil {
client := *b.Client
Expand Down
44 changes: 44 additions & 0 deletions command/agent/config_parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ func parseConfig(result *Config, list *ast.ObjectList) error {
"atlas",
"consul",
"vault",
"tls",
"http_api_response_headers",
}
if err := checkHCLKeys(list, valid); err != nil {
Expand All @@ -115,6 +116,7 @@ func parseConfig(result *Config, list *ast.ObjectList) error {
delete(m, "atlas")
delete(m, "consul")
delete(m, "vault")
delete(m, "tls")
delete(m, "http_api_response_headers")

// Decode the rest
Expand Down Expand Up @@ -185,6 +187,13 @@ func parseConfig(result *Config, list *ast.ObjectList) error {
}
}

// Parse the TLS config
if o := list.Filter("tls"); len(o.Items) > 0 {
if err := parseTLSConfig(&result.TLSConfig, o); err != nil {
return multierror.Prefix(err, "tls ->")
}
}

// Parse out http_api_response_headers fields. These are in HCL as a list so
// we need to iterate over them and merge them.
if headersO := list.Filter("http_api_response_headers"); len(headersO.Items) > 0 {
Expand Down Expand Up @@ -643,6 +652,41 @@ func parseConsulConfig(result **config.ConsulConfig, list *ast.ObjectList) error
return nil
}

func parseTLSConfig(result **config.TLSConfig, list *ast.ObjectList) error {
list = list.Elem()
if len(list.Items) > 1 {
return fmt.Errorf("only one 'tls' block allowed")
}

// Get the TLS object
listVal := list.Items[0].Val

valid := []string{
"http",
"rpc",
"verify_server_hostname",
"ca_file",
"cert_file",
"key_file",
}

if err := checkHCLKeys(listVal, valid); err != nil {
return err
}

var m map[string]interface{}
if err := hcl.DecodeObject(&m, listVal); err != nil {
return err
}

var tlsConfig config.TLSConfig
if err := mapstructure.WeakDecode(m, &tlsConfig); err != nil {
return err
}
*result = &tlsConfig
return nil
}

func parseVaultConfig(result **config.VaultConfig, list *ast.ObjectList) error {
list = list.Elem()
if len(list.Items) > 1 {
Expand Down
8 changes: 8 additions & 0 deletions command/agent/config_parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,14 @@ func TestConfig_Parse(t *testing.T) {
TaskTokenTTL: "1s",
Token: "12345",
},
TLSConfig: &TLSConfig{
EnableHTTP: true,
EnableRPC: true,
VerifyServerHostname: true,
CAFile: "foo",
CertFile: "bar",
KeyFile: "pipe",
},
HTTPAPIResponseHeaders: map[string]string{
"Access-Control-Allow-Origin": "*",
},
Expand Down
Loading

0 comments on commit 70ec22f

Please sign in to comment.