Skip to content

Commit

Permalink
Allow server TLS configuration to be reloaded via SIGHUP
Browse files Browse the repository at this point in the history
  • Loading branch information
chelseakomlo committed Oct 31, 2017
1 parent 2d77197 commit d8a692c
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 23 deletions.
46 changes: 38 additions & 8 deletions helper/tlsutil/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import (
"fmt"
"io/ioutil"
"net"
"sync"
"time"

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

// RegionSpecificWrapper is used to invoke a static Region and turns a
Expand All @@ -30,6 +33,8 @@ type Wrapper func(conn net.Conn) (net.Conn, error)

// Config used to create tls.Config
type Config struct {
configLock sync.RWMutex

// VerifyIncoming is used to verify the authenticity of incoming connections.
// This means that TCP requests are forbidden, only allowing for TLS. TLS connections
// must match a provided certificate authority. This can be used to force client auth.
Expand Down Expand Up @@ -57,6 +62,10 @@ type Config struct {
// Must be provided to serve TLS connections.
CertFile string

// Stores a TLS certificate that has been loaded given the information in the
// configuration. This can be updated via config.Reload()
Certificate *tls.Certificate

// KeyFile is used to provide a TLS key that is used for serving TLS connections.
// Must be provided to serve TLS connections.
KeyFile string
Expand All @@ -82,21 +91,39 @@ func (c *Config) AppendCA(pool *x509.CertPool) error {
return nil
}

// KeyPair is used to open and parse a certificate and key file
func (c *Config) KeyPair() (*tls.Certificate, error) {
// Update syncs a new TLS config to a previously-created TLS config helper
func (c *Config) Update(newConfig *config.TLSConfig) {
c.configLock.Lock()

c.CAFile = newConfig.CAFile
c.CertFile = newConfig.CertFile
c.KeyFile = newConfig.KeyFile
c.configLock.Unlock()
}

// LoadKeyPair is used to open and parse a certificate and key file
func (c *Config) LoadKeyPair() (*tls.Certificate, error) {
c.configLock.Lock()

if c.CertFile == "" || c.KeyFile == "" {
return nil, nil
}

cert, err := tls.LoadX509KeyPair(c.CertFile, c.KeyFile)
if err != nil {
return nil, fmt.Errorf("Failed to load cert/key pair: %v", err)
}
return &cert, err

c.configLock.Unlock()

c.Certificate = &cert
return c.Certificate, nil
}

// OutgoingTLSConfig generates a TLS configuration for outgoing
// requests. It will return a nil config if this configuration should
// not use TLS for outgoing connections.
// not use TLS for outgoing connections. Provides a callback to
// fetch certificates, allowing for reloading on the fly.
func (c *Config) OutgoingTLSConfig() (*tls.Config, error) {
// If VerifyServerHostname is true, that implies VerifyOutgoing
if c.VerifyServerHostname {
Expand Down Expand Up @@ -125,17 +152,20 @@ func (c *Config) OutgoingTLSConfig() (*tls.Config, error) {
return nil, err
}

// Add cert/key
cert, err := c.KeyPair()
cert, err := c.LoadKeyPair()
if err != nil {
return nil, err
} else if cert != nil {
tlsConfig.Certificates = []tls.Certificate{*cert}
tlsConfig.GetCertificate = c.getOutgoingCertificate
}

return tlsConfig, nil
}

func (c *Config) getOutgoingCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error) {
return c.Certificate, nil
}

// OutgoingTLSWrapper returns a a Wrapper based on the OutgoingTLS
// configuration. If hostname verification is on, the wrapper
// will properly generate the dynamic server name for verification.
Expand Down Expand Up @@ -236,7 +266,7 @@ func (c *Config) IncomingTLSConfig() (*tls.Config, error) {
}

// Add cert/key
cert, err := c.KeyPair()
cert, err := c.LoadKeyPair()
if err != nil {
return nil, err
} else if cert != nil {
Expand Down
25 changes: 16 additions & 9 deletions helper/tlsutil/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ func TestConfig_CACertificate_Valid(t *testing.T) {
}
}

func TestConfig_KeyPair_None(t *testing.T) {
func TestConfig_LoadKeyPair_None(t *testing.T) {
conf := &Config{}
cert, err := conf.KeyPair()
cert, err := conf.LoadKeyPair()
if err != nil {
t.Fatalf("err: %v", err)
}
Expand All @@ -57,12 +57,12 @@ func TestConfig_KeyPair_None(t *testing.T) {
}
}

func TestConfig_KeyPair_Valid(t *testing.T) {
func TestConfig_LoadKeyPair_Valid(t *testing.T) {
conf := &Config{
CertFile: foocert,
KeyFile: fookey,
}
cert, err := conf.KeyPair()
cert, err := conf.LoadKeyPair()
if err != nil {
t.Fatalf("err: %v", err)
}
Expand Down Expand Up @@ -144,20 +144,27 @@ func TestConfig_OutgoingTLS_WithKeyPair(t *testing.T) {
CertFile: foocert,
KeyFile: fookey,
}
tls, err := conf.OutgoingTLSConfig()
tlsConf, err := conf.OutgoingTLSConfig()
if err != nil {
t.Fatalf("err: %v", err)
}
if tls == nil {
if tlsConf == nil {
t.Fatalf("expected config")
}
if len(tls.RootCAs.Subjects()) != 1 {
if len(tlsConf.RootCAs.Subjects()) != 1 {
t.Fatalf("expect root cert")
}
if !tls.InsecureSkipVerify {
if !tlsConf.InsecureSkipVerify {
t.Fatalf("should skip verification")
}
if len(tls.Certificates) != 1 {

clientHelloInfo := &tls.ClientHelloInfo{}
cert, err := tlsConf.GetCertificate(clientHelloInfo)
// TODO add asert package
if err != nil {
t.Fatalf("expected no error")
}
if cert == nil {
t.Fatalf("expected client cert")
}
}
Expand Down
31 changes: 29 additions & 2 deletions nomad/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"net"
"os"
"runtime"
"sync"
"time"

"github.com/hashicorp/memberlist"
Expand Down Expand Up @@ -57,6 +58,8 @@ type Config struct {
// must be handled via `atomic.*Int32()` calls.
BootstrapExpect int32

configLock sync.RWMutex

// DataDir is the directory to store our state in
DataDir string

Expand Down Expand Up @@ -229,6 +232,9 @@ type Config struct {
// TLSConfig holds various TLS related configurations
TLSConfig *config.TLSConfig

// tlsConfigHelper provides utility functions and a pointer to the TLS config
tlsConfigHelper *tlsutil.Config

// ACLEnabled controls if ACL enforcement and management is enabled.
ACLEnabled bool

Expand Down Expand Up @@ -259,6 +265,27 @@ func (c *Config) CheckVersion() error {
return nil
}

func (c *Config) SetTLSConfig(newTLSConfig *config.TLSConfig) error {
if newTLSConfig == nil {
return fmt.Errorf("no new tls configuration to reload")
}

c.configLock.Lock()
c.TLSConfig.Merge(newTLSConfig)
c.configLock.Unlock()

if c.tlsConfigHelper == nil {
return nil
}

// TODO can the TLSConfigHelper just have a TLSConfigCopy rather than copying
// fields?
c.tlsConfigHelper.Update(c.TLSConfig)
_, err := c.tlsConfigHelper.LoadKeyPair()

return err
}

// DefaultConfig returns the default configuration
func DefaultConfig() *Config {
hostname, err := os.Hostname()
Expand Down Expand Up @@ -335,13 +362,13 @@ func DefaultConfig() *Config {

// tlsConfig returns a TLSUtil Config based on the server configuration
func (c *Config) tlsConfig() *tlsutil.Config {
tlsConf := &tlsutil.Config{
c.tlsConfigHelper = &tlsutil.Config{
VerifyIncoming: true,
VerifyOutgoing: true,
VerifyServerHostname: c.TLSConfig.VerifyServerHostname,
CAFile: c.TLSConfig.CAFile,
CertFile: c.TLSConfig.CertFile,
KeyFile: c.TLSConfig.KeyFile,
}
return tlsConf
return c.tlsConfigHelper
}
4 changes: 2 additions & 2 deletions nomad/rpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import (
"strings"
"time"

"github.com/armon/go-metrics"
metrics "github.com/armon/go-metrics"
"github.com/hashicorp/consul/lib"
memdb "github.com/hashicorp/go-memdb"
"github.com/hashicorp/net-rpc-msgpackrpc"
msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc"
"github.com/hashicorp/nomad/nomad/state"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/hashicorp/raft"
Expand Down
10 changes: 8 additions & 2 deletions nomad/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ import (

consulapi "github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/lib"
"github.com/hashicorp/go-multierror"
multierror "github.com/hashicorp/go-multierror"
lru "github.com/hashicorp/golang-lru"
"github.com/hashicorp/nomad/command/agent/consul"
"github.com/hashicorp/nomad/helper/tlsutil"
"github.com/hashicorp/nomad/nomad/deploymentwatcher"
"github.com/hashicorp/nomad/nomad/state"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/hashicorp/raft"
"github.com/hashicorp/raft-boltdb"
raftboltdb "github.com/hashicorp/raft-boltdb"
"github.com/hashicorp/serf/serf"
)

Expand Down Expand Up @@ -512,6 +512,12 @@ func (s *Server) Reload(config *Config) error {
}
}

if s.config != nil && config.TLSConfig != nil {
if err := s.config.SetTLSConfig(config.TLSConfig); err != nil {
multierror.Append(&mErr, err)
}
}

return mErr.ErrorOrNil()
}

Expand Down
50 changes: 50 additions & 0 deletions nomad/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ import (
"time"

"github.com/hashicorp/consul/lib/freeport"
msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc"
"github.com/hashicorp/nomad/command/agent/consul"
"github.com/hashicorp/nomad/helper/tlsutil"
"github.com/hashicorp/nomad/helper/uuid"
"github.com/hashicorp/nomad/nomad/mock"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/hashicorp/nomad/nomad/structs/config"
"github.com/hashicorp/nomad/testutil"
"github.com/stretchr/testify/assert"
)

var (
Expand Down Expand Up @@ -276,3 +279,50 @@ func TestServer_Reload_Vault(t *testing.T) {
t.Fatalf("Vault client should be running")
}
}

func TestServer_Reload_TLS(t *testing.T) {
t.Parallel()
assert := assert.New(t)

const (
cafile = "../helper/tlsutil/testdata/ca.pem"
foocert = "../helper/tlsutil/testdata/nomad-foo.pem"
fookey = "../helper/tlsutil/testdata/nomad-foo-key.pem"
)
dir := tmpDir(t)
defer os.RemoveAll(dir)
s1 := testServer(t, func(c *Config) {
c.DataDir = path.Join(dir, "node1")
})
defer s1.Shutdown()

codec := rpcClient(t, s1)

// assert that the server started in plaintext mode
assert.Equal(s1.config.TLSConfig.CertFile, "")

newTLSConfig := &config.TLSConfig{
EnableHTTP: true,
EnableRPC: true,
VerifyServerHostname: true,
CAFile: cafile,
CertFile: foocert,
KeyFile: fookey,
}

config := s1.config
config.tlsConfigHelper = &tlsutil.Config{}

config.TLSConfig = newTLSConfig

err := s1.Reload(config)
assert.Nil(err)

// assert our server is now configured with the correct TLS configuration
assert.Equal(s1.config.TLSConfig.CertFile, foocert)

arg := struct{}{}
var out struct{}
err = msgpackrpc.CallWithCodec(codec, "Status.Ping", arg, &out)
assert.NotNil(err)
}

0 comments on commit d8a692c

Please sign in to comment.