Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TLS support for http and RPC #1853

Merged
merged 7 commits into from
Oct 25, 2016
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -29,6 +29,7 @@ import (
"github.com/hashicorp/nomad/command/agent/consul"
"github.com/hashicorp/nomad/nomad"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/hashicorp/nomad/tlsutil"
vaultapi "github.com/hashicorp/vault/api"
"github.com/mitchellh/hashstructure"
)
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space after //

var tlsWrap tlsutil.Wrapper
if cfg.RpcTLS {
tw, err := cfg.TLSConfig().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
41 changes: 41 additions & 0 deletions client/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/hashicorp/nomad/nomad/structs"
"github.com/hashicorp/nomad/nomad/structs/config"
"github.com/hashicorp/nomad/tlsutil"
)

var (
Expand Down Expand Up @@ -132,6 +133,32 @@ type Config struct {
// PublishAllocationMetrics determines whether nomad is going to publish
// allocation metrics to remote Telemetry sinks
PublishAllocationMetrics bool

// HttpTLS enables TLS for the HTTP endpoints on the clients.
HttpTLS bool
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you just embed the TLSConfig struct


// RpcTLS enables TLS for the outgoing TLS connections to the Nomad servers.
RpcTLS bool

// VerifyServerHostname is used to enable hostname verification of servers. This
// ensures that the certificate presented is valid for server.<datacenter>.<domain>.
// This prevents a compromised client from being restarted as a server, and then
// 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

// CAFile is a path to a certificate authority file. This is used with VerifyIncoming
// or VerifyOutgoing to verify the TLS connection.
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

// KeyFile is used to provide a TLS key that is used for serving TLS connections.
// Must be provided to serve TLS connections.
KeyFile string
}

func (c *Config) Copy() *Config {
Expand Down Expand Up @@ -226,3 +253,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) TLSConfig() *tlsutil.Config {
tlsConf := &tlsutil.Config{
VerifyIncoming: true,
VerifyOutgoing: true,
VerifyServerHostname: c.VerifyServerHostname,
CAFile: c.CAFile,
CertFile: c.CertFile,
KeyFile: c.KeyFile,
ServerName: c.Node.Name,
}
return tlsConf
}
17 changes: 17 additions & 0 deletions command/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,14 @@ func (a *Agent) serverConfig() (*nomad.Config, error) {
conf.ConsulConfig = a.config.Consul
conf.VaultConfig = a.config.Vault

// Set the TLS related configs
conf.RpcTLS = a.config.TLSConfig.EnableRPC
conf.RequireTLS = conf.RpcTLS
conf.VerifyServerHostname = a.config.TLSConfig.VerifyServerHostname
conf.CAFile = a.config.TLSConfig.CAFile
conf.CertFile = a.config.TLSConfig.CertFile
conf.KeyFile = a.config.TLSConfig.KeyFile

return conf, nil
}

Expand Down Expand Up @@ -357,6 +365,15 @@ 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.HttpTLS = a.config.TLSConfig.EnableHTTP
conf.RpcTLS = a.config.TLSConfig.EnableRPC
conf.VerifyServerHostname = a.config.TLSConfig.VerifyServerHostname
conf.CAFile = a.config.TLSConfig.CAFile
conf.CertFile = a.config.TLSConfig.CertFile
conf.KeyFile = a.config.TLSConfig.KeyFile

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"
}
69 changes: 69 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 *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 All @@ -135,6 +139,36 @@ type AtlasConfig struct {
Endpoint string `mapstructure:"endpoint"`
}

// TLSConfig provides TLS related configuration
type TLSConfig struct {

// EnableHTTP enabled TLS for http traffic to the Nomad server and clients
EnableHTTP bool `mapstructure:"http"`

// EnableRPC enables TLS for RPC and Raft traffic to the Nomad servers
EnableRPC bool `mapstructure:"rpc"`

// VerifyServerHostname is used to enable hostname verification of servers. This
// ensures that the certificate presented is valid for server.<datacenter>.<domain>.
// This prevents a compromised client from being restarted as a server, and then
// 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"`

// 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"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing CAPath


// 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"`

// 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"`
}

// ClientConfig is configuration specific to the client mode
type ClientConfig struct {
// Enabled controls if we are a client
Expand Down Expand Up @@ -486,6 +520,7 @@ func DefaultConfig() *Config {
CollectionInterval: "1s",
collectionInterval: 1 * time.Second,
},
TLSConfig: &TLSConfig{},
}
}

Expand Down Expand Up @@ -566,6 +601,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 Expand Up @@ -764,6 +807,32 @@ func (a *ClientConfig) Merge(b *ClientConfig) *ClientConfig {
return &result
}

// Merge is used to merge two TLS configs together
func (t *TLSConfig) Merge(b *TLSConfig) *TLSConfig {
result := *t

if b.EnableHTTP {
result.EnableHTTP = true
}
if b.EnableRPC {
result.EnableRPC = true
}
if b.VerifyServerHostname {
result.VerifyServerHostname = true
}
if b.CAFile != "" {
result.CAFile = b.CAFile
}
if b.CertFile != "" {
result.CertFile = b.CertFile
}
if b.KeyFile != "" {
result.KeyFile = b.KeyFile
}

return &result
}

// Merge is used to merge two telemetry configs together
func (a *Telemetry) Merge(b *Telemetry) *Telemetry {
result := *a
Expand Down
Loading