diff --git a/.gitignore b/.gitignore index 547ad2481a..405017d587 100644 --- a/.gitignore +++ b/.gitignore @@ -23,7 +23,6 @@ _testmain.go *.test bin/ .vagrant/ - - website/npm-debug.log - +*.old +*.attr diff --git a/command/agent/agent.go b/command/agent/agent.go index c456f1bd98..80bd701ca1 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -169,6 +169,13 @@ func (a *Agent) consulConfig() *consul.Config { base.ProtocolVersion = uint8(a.config.Protocol) } + // Copy the TLS configuration + base.VerifyIncoming = a.config.VerifyIncoming + base.VerifyOutgoing = a.config.VerifyOutgoing + base.CAFile = a.config.CAFile + base.CertFile = a.config.CertFile + base.KeyFile = a.config.KeyFile + // Setup the ServerUp callback base.ServerUp = a.state.ConsulServerUp diff --git a/command/agent/config.go b/command/agent/config.go index a2e83740b9..18edee0d53 100644 --- a/command/agent/config.go +++ b/command/agent/config.go @@ -103,6 +103,28 @@ type Config struct { // EnableDebug is used to enable various debugging features EnableDebug bool `mapstructure:"enable_debug"` + // 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. + VerifyIncoming bool `mapstructure:"verify_incoming"` + + // VerifyOutgoing is used to verify the authenticity of outgoing connections. + // This means that TLS requests are used. TLS connections must match a provided + // certificate authority. This is used to verify authenticity of server nodes. + VerifyOutgoing bool `mapstructure:"verify_outgoing"` + + // 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"` + + // 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"` + // Checks holds the provided check definitions Checks []*CheckDefinition `mapstructure:"-"` @@ -335,6 +357,21 @@ func MergeConfig(a, b *Config) *Config { if b.EnableDebug { result.EnableDebug = true } + if b.VerifyIncoming { + result.VerifyIncoming = true + } + if b.VerifyOutgoing { + result.VerifyOutgoing = true + } + if b.CAFile != "" { + result.CAFile = b.CAFile + } + if b.CertFile != "" { + result.CertFile = b.CertFile + } + if b.KeyFile != "" { + result.KeyFile = b.KeyFile + } if b.Checks != nil { result.Checks = append(result.Checks, b.Checks...) } diff --git a/command/agent/config_test.go b/command/agent/config_test.go index d6d3f6eaa2..6864e83c4d 100644 --- a/command/agent/config_test.go +++ b/command/agent/config_test.go @@ -193,6 +193,38 @@ func TestDecodeConfig(t *testing.T) { if config.EnableDebug != true { t.Fatalf("bad: %#v", config) } + + // TLS + input = `{"verify_incoming": true, "verify_outgoing": true}` + config, err = DecodeConfig(bytes.NewReader([]byte(input))) + if err != nil { + t.Fatalf("err: %s", err) + } + + if config.VerifyIncoming != true { + t.Fatalf("bad: %#v", config) + } + + if config.VerifyOutgoing != true { + t.Fatalf("bad: %#v", config) + } + + // TLS keys + input = `{"ca_file": "my/ca/file", "cert_file": "my.cert", "key_file": "key.pem"}` + config, err = DecodeConfig(bytes.NewReader([]byte(input))) + if err != nil { + t.Fatalf("err: %s", err) + } + + if config.CAFile != "my/ca/file" { + t.Fatalf("bad: %#v", config) + } + if config.CertFile != "my.cert" { + t.Fatalf("bad: %#v", config) + } + if config.KeyFile != "key.pem" { + t.Fatalf("bad: %#v", config) + } } func TestDecodeConfig_Service(t *testing.T) { @@ -318,6 +350,11 @@ func TestMergeConfig(t *testing.T) { LeaveOnTerm: true, SkipLeaveOnInt: true, EnableDebug: true, + VerifyIncoming: true, + VerifyOutgoing: true, + CAFile: "test/ca.pem", + CertFile: "test/cert.pem", + KeyFile: "test/key.pem", Checks: []*CheckDefinition{nil}, Services: []*ServiceDefinition{nil}, } diff --git a/consul/client.go b/consul/client.go index 74dbc3fba7..5e575ed92d 100644 --- a/consul/client.go +++ b/consul/client.go @@ -1,6 +1,7 @@ package consul import ( + "crypto/tls" "fmt" "github.com/hashicorp/consul/consul/structs" "github.com/hashicorp/serf/serf" @@ -78,13 +79,22 @@ func NewClient(config *Config) (*Client, error) { config.LogOutput = os.Stderr } + // Create the tlsConfig + var tlsConfig *tls.Config + var err error + if config.VerifyOutgoing { + if tlsConfig, err = config.OutgoingTLSConfig(); err != nil { + return nil, err + } + } + // Create a logger logger := log.New(config.LogOutput, "", log.LstdFlags) // Create server c := &Client{ config: config, - connPool: NewPool(clientRPCCache), + connPool: NewPool(clientRPCCache, tlsConfig), eventCh: make(chan serf.Event, 256), logger: logger, shutdownCh: make(chan struct{}), @@ -94,7 +104,6 @@ func NewClient(config *Config) (*Client, error) { go c.lanEventHandler() // Initialize the lan Serf - var err error c.serf, err = c.setupSerf(config.SerfLANConfig, c.eventCh, serfLANSnapshot) if err != nil { diff --git a/consul/client_test.go b/consul/client_test.go index 980080c78c..7aff15fcef 100644 --- a/consul/client_test.go +++ b/consul/client_test.go @@ -9,14 +9,10 @@ import ( "time" ) -func testClient(t *testing.T) (string, *Client) { - return testClientDC(t, "dc1") -} - -func testClientDC(t *testing.T, dc string) (string, *Client) { +func testClientConfig(t *testing.T) (string, *Config) { dir := tmpDir(t) config := DefaultConfig() - config.Datacenter = dc + config.Datacenter = "dc1" config.DataDir = dir // Adjust the ports @@ -32,6 +28,17 @@ func testClientDC(t *testing.T, dc string) (string, *Client) { config.SerfLANConfig.MemberlistConfig.ProbeInterval = time.Second config.SerfLANConfig.MemberlistConfig.GossipInterval = 100 * time.Millisecond + return dir, config +} + +func testClient(t *testing.T) (string, *Client) { + return testClientDC(t, "dc1") +} + +func testClientDC(t *testing.T, dc string) (string, *Client) { + dir, config := testClientConfig(t) + config.Datacenter = dc + client, err := NewClient(config) if err != nil { t.Fatalf("err: %v", err) @@ -119,3 +126,55 @@ func TestClient_RPC(t *testing.T) { t.Fatalf("err: %v", err) } } + +func TestClient_RPC_TLS(t *testing.T) { + dir1, conf1 := testServerConfig(t) + conf1.VerifyIncoming = true + conf1.VerifyOutgoing = true + configureTLS(conf1) + s1, err := NewServer(conf1) + if err != nil { + t.Fatalf("err: %v", err) + } + defer os.RemoveAll(dir1) + defer s1.Shutdown() + + dir2, conf2 := testClientConfig(t) + conf2.VerifyOutgoing = true + configureTLS(conf2) + c1, err := NewClient(conf2) + if err != nil { + t.Fatalf("err: %v", err) + } + defer os.RemoveAll(dir2) + defer c1.Shutdown() + + // Try an RPC + var out struct{} + if err := c1.RPC("Status.Ping", struct{}{}, &out); err != structs.ErrNoServers { + t.Fatalf("err: %v", err) + } + + // Try to join + addr := fmt.Sprintf("127.0.0.1:%d", + s1.config.SerfLANConfig.MemberlistConfig.BindPort) + if _, err := c1.JoinLAN([]string{addr}); err != nil { + t.Fatalf("err: %v", err) + } + + // Check the members + if len(s1.LANMembers()) != 2 { + t.Fatalf("bad len") + } + + if len(c1.LANMembers()) != 2 { + t.Fatalf("bad len") + } + + time.Sleep(10 * time.Millisecond) + + // RPC shoudl succeed + if err := c1.RPC("Status.Ping", struct{}{}, &out); err != nil { + t.Fatalf("err: %v", err) + } +} diff --git a/consul/config.go b/consul/config.go index 8a632ee1da..d74cbaa071 100644 --- a/consul/config.go +++ b/consul/config.go @@ -1,11 +1,15 @@ package consul import ( + "crypto/tls" + "crypto/x509" + "encoding/pem" "fmt" "github.com/hashicorp/memberlist" "github.com/hashicorp/raft" "github.com/hashicorp/serf/serf" "io" + "io/ioutil" "net" "os" "time" @@ -81,6 +85,29 @@ type Config struct { // ProtocolVersionMin and ProtocolVersionMax. ProtocolVersion uint8 + // 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. + VerifyIncoming bool + + // VerifyOutgoing is used to verify the authenticity of outgoing connections. + // This means that TLS requests are used, and TCP requests are not made. TLS connections + // must match a provided certificate authority. This is used to verify authenticity of + // server nodes. + VerifyOutgoing 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 + // ServerUp callback can be used to trigger a notification that // a Consul server is now up and known about. ServerUp func() @@ -98,6 +125,113 @@ func (c *Config) CheckVersion() error { return nil } +// CACertificate is used to open and parse a CA file +func (c *Config) CACertificate() (*x509.Certificate, error) { + if c.CAFile == "" { + return nil, nil + } + + // Read the file + data, err := ioutil.ReadFile(c.CAFile) + if err != nil { + return nil, fmt.Errorf("Failed to read CA file: %v", err) + } + + // Decode from the PEM format + block, _ := pem.Decode(data) + if block == nil { + return nil, fmt.Errorf("Failed to decode CA PEM!") + } + + // Parse the certificate + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, fmt.Errorf("Failed to parse CA file: %v", err) + } + return cert, nil +} + +// KeyPair is used to open and parse a certificate and key file +func (c *Config) KeyPair() (*tls.Certificate, error) { + 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 +} + +// OutgoingTLSConfig generates a TLS configuration for outgoing requests +func (c *Config) OutgoingTLSConfig() (*tls.Config, error) { + // Create the tlsConfig + tlsConfig := &tls.Config{ + RootCAs: x509.NewCertPool(), + InsecureSkipVerify: !c.VerifyOutgoing, + } + + // Parse the CA cert if any + ca, err := c.CACertificate() + if err != nil { + return nil, err + } else if ca != nil { + tlsConfig.RootCAs.AddCert(ca) + } + + // Ensure we have a CA if VerifyOutgoing is set + if c.VerifyOutgoing && ca == nil { + return nil, fmt.Errorf("VerifyOutgoing set, and no CA certificate provided!") + } + + // Add cert/key + cert, err := c.KeyPair() + if err != nil { + return nil, err + } else if cert != nil { + tlsConfig.Certificates = []tls.Certificate{*cert} + } + + return tlsConfig, nil +} + +// IncomingTLSConfig generates a TLS configuration for incoming requests +func (c *Config) IncomingTLSConfig() (*tls.Config, error) { + // Create the tlsConfig + tlsConfig := &tls.Config{ + ClientCAs: x509.NewCertPool(), + ClientAuth: tls.NoClientCert, + } + + // Parse the CA cert if any + ca, err := c.CACertificate() + if err != nil { + return nil, err + } else if ca != nil { + tlsConfig.ClientCAs.AddCert(ca) + } + + // Add cert/key + cert, err := c.KeyPair() + if err != nil { + return nil, err + } else if cert != nil { + tlsConfig.Certificates = []tls.Certificate{*cert} + } + + // Check if we require verification + if c.VerifyIncoming { + tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert + if ca == nil { + return nil, fmt.Errorf("VerifyIncoming set, and no CA certificate provided!") + } + if cert == nil { + return nil, fmt.Errorf("VerifyIncoming set, and no Cert/Key pair provided!") + } + } + return tlsConfig, nil +} + // DefaultConfig is used to return a sane default configuration func DefaultConfig() *Config { hostname, err := os.Hostname() diff --git a/consul/config_test.go b/consul/config_test.go new file mode 100644 index 0000000000..6d4c106dba --- /dev/null +++ b/consul/config_test.go @@ -0,0 +1,200 @@ +package consul + +import ( + "crypto/tls" + "testing" +) + +func TestConfig_CACertificate_None(t *testing.T) { + conf := &Config{} + cert, err := conf.CACertificate() + if err != nil { + t.Fatalf("err: %v", err) + } + if cert != nil { + t.Fatalf("bad: %v", cert) + } +} + +func TestConfig_CACertificate_Valid(t *testing.T) { + conf := &Config{ + CAFile: "../test/ca/root.cer", + } + cert, err := conf.CACertificate() + if err != nil { + t.Fatalf("err: %v", err) + } + if cert == nil { + t.Fatalf("expected cert") + } +} + +func TestConfig_KeyPair_None(t *testing.T) { + conf := &Config{} + cert, err := conf.KeyPair() + if err != nil { + t.Fatalf("err: %v", err) + } + if cert != nil { + t.Fatalf("bad: %v", cert) + } +} + +func TestConfig_KeyPair_Valid(t *testing.T) { + conf := &Config{ + CertFile: "../test/key/ourdomain.cer", + KeyFile: "../test/key/ourdomain.key", + } + cert, err := conf.KeyPair() + if err != nil { + t.Fatalf("err: %v", err) + } + if cert == nil { + t.Fatalf("expected cert") + } +} + +func TestConfig_OutgoingTLS_MissingCA(t *testing.T) { + conf := &Config{ + VerifyOutgoing: true, + } + tls, err := conf.OutgoingTLSConfig() + if err == nil { + t.Fatalf("expected err") + } + if tls != nil { + t.Fatalf("bad: %v", tls) + } +} + +func TestConfig_OutgoingTLS_OnlyCA(t *testing.T) { + conf := &Config{ + CAFile: "../test/ca/root.cer", + } + tls, err := conf.OutgoingTLSConfig() + if err != nil { + t.Fatalf("err: %v", err) + } + if tls == nil { + t.Fatalf("expected config") + } + if len(tls.RootCAs.Subjects()) != 1 { + t.Fatalf("expect root cert") + } + if !tls.InsecureSkipVerify { + t.Fatalf("expect to skip verification") + } +} + +func TestConfig_OutgoingTLS_VerifyOutgoing(t *testing.T) { + conf := &Config{ + VerifyOutgoing: true, + CAFile: "../test/ca/root.cer", + } + tls, err := conf.OutgoingTLSConfig() + if err != nil { + t.Fatalf("err: %v", err) + } + if tls == nil { + t.Fatalf("expected config") + } + if len(tls.RootCAs.Subjects()) != 1 { + t.Fatalf("expect root cert") + } + if tls.InsecureSkipVerify { + t.Fatalf("should not skip verification") + } +} + +func TestConfig_OutgoingTLS_WithKeyPair(t *testing.T) { + conf := &Config{ + VerifyOutgoing: true, + CAFile: "../test/ca/root.cer", + CertFile: "../test/key/ourdomain.cer", + KeyFile: "../test/key/ourdomain.key", + } + tls, err := conf.OutgoingTLSConfig() + if err != nil { + t.Fatalf("err: %v", err) + } + if tls == nil { + t.Fatalf("expected config") + } + if len(tls.RootCAs.Subjects()) != 1 { + t.Fatalf("expect root cert") + } + if tls.InsecureSkipVerify { + t.Fatalf("should not skip verification") + } + if len(tls.Certificates) != 1 { + t.Fatalf("expected client cert") + } +} + +func TestConfig_IncomingTLS(t *testing.T) { + conf := &Config{ + VerifyIncoming: true, + CAFile: "../test/ca/root.cer", + CertFile: "../test/key/ourdomain.cer", + KeyFile: "../test/key/ourdomain.key", + } + tlsC, err := conf.IncomingTLSConfig() + if err != nil { + t.Fatalf("err: %v", err) + } + if tlsC == nil { + t.Fatalf("expected config") + } + if len(tlsC.ClientCAs.Subjects()) != 1 { + t.Fatalf("expect client cert") + } + if tlsC.ClientAuth != tls.RequireAndVerifyClientCert { + t.Fatalf("should not skip verification") + } + if len(tlsC.Certificates) != 1 { + t.Fatalf("expected client cert") + } +} + +func TestConfig_IncomingTLS_MissingCA(t *testing.T) { + conf := &Config{ + VerifyIncoming: true, + CertFile: "../test/key/ourdomain.cer", + KeyFile: "../test/key/ourdomain.key", + } + _, err := conf.IncomingTLSConfig() + if err == nil { + t.Fatalf("expected err") + } +} + +func TestConfig_IncomingTLS_MissingKey(t *testing.T) { + conf := &Config{ + VerifyIncoming: true, + CAFile: "../test/ca/root.cer", + } + _, err := conf.IncomingTLSConfig() + if err == nil { + t.Fatalf("expected err") + } +} + +func TestConfig_IncomingTLS_NoVerify(t *testing.T) { + conf := &Config{} + tlsC, err := conf.IncomingTLSConfig() + if err != nil { + t.Fatalf("err: %v", err) + } + if tlsC == nil { + t.Fatalf("expected config") + } + if len(tlsC.ClientCAs.Subjects()) != 0 { + t.Fatalf("do not expect client cert") + } + if tlsC.ClientAuth != tls.NoClientCert { + t.Fatalf("should skip verification") + } + if len(tlsC.Certificates) != 0 { + t.Fatalf("unexpected client cert") + } +} diff --git a/consul/pool.go b/consul/pool.go index 6749f61ac6..e2ca9e2bbf 100644 --- a/consul/pool.go +++ b/consul/pool.go @@ -1,6 +1,7 @@ package consul import ( + "crypto/tls" "fmt" "github.com/inconshreveable/muxado" "github.com/ugorji/go/codec" @@ -37,6 +38,9 @@ type ConnPool struct { // Pool maps an address to a open connection pool map[string]*Conn + // TLS settings + tlsConfig *tls.Config + // Used to indicate the pool is shutdown shutdown bool shutdownCh chan struct{} @@ -44,11 +48,13 @@ type ConnPool struct { // NewPool is used to make a new connection pool // Maintain at most one connection per host, for up to maxTime. -// Set maxTime to 0 to disable reaping. -func NewPool(maxTime time.Duration) *ConnPool { +// Set maxTime to 0 to disable reaping. If TLS settings are provided +// outgoing connections use TLS. +func NewPool(maxTime time.Duration, tlsConfig *tls.Config) *ConnPool { pool := &ConnPool{ maxTime: maxTime, pool: make(map[string]*Conn), + tlsConfig: tlsConfig, shutdownCh: make(chan struct{}), } if maxTime > 0 { @@ -104,20 +110,34 @@ func (p *ConnPool) getPooled(addr net.Addr) *Conn { // getNewConn is used to return a new connection func (p *ConnPool) getNewConn(addr net.Addr) (*Conn, error) { // Try to dial the conn - rawConn, err := net.DialTimeout("tcp", addr.String(), 10*time.Second) + conn, err := net.DialTimeout("tcp", addr.String(), 10*time.Second) if err != nil { return nil, err } // Cast to TCPConn - conn := rawConn.(*net.TCPConn) + if tcp, ok := conn.(*net.TCPConn); ok { + tcp.SetKeepAlive(true) + tcp.SetNoDelay(true) + } - // Enable keep alives - conn.SetKeepAlive(true) - conn.SetNoDelay(true) + // Check if TLS is enabled + if p.tlsConfig != nil { + // Switch the connection into TLS mode + if _, err := conn.Write([]byte{byte(rpcTLS)}); err != nil { + conn.Close() + return nil, err + } + + // Wrap the connection in a TLS client + conn = tls.Client(conn, p.tlsConfig) + } // Write the Consul multiplex byte to set the mode - conn.Write([]byte{byte(rpcMultiplex)}) + if _, err := conn.Write([]byte{byte(rpcMultiplex)}); err != nil { + conn.Close() + return nil, err + } // Create a multiplexed session session := muxado.Client(conn) diff --git a/consul/raft_rpc.go b/consul/raft_rpc.go index 4ed7437e9e..1221ce06f2 100644 --- a/consul/raft_rpc.go +++ b/consul/raft_rpc.go @@ -1,6 +1,7 @@ package consul import ( + "crypto/tls" "fmt" "net" "sync" @@ -16,6 +17,9 @@ type RaftLayer struct { // connCh is used to accept connections connCh chan net.Conn + // TLS configuration + tlsConfig *tls.Config + // Tracks if we are closed closed bool closeCh chan struct{} @@ -23,12 +27,14 @@ type RaftLayer struct { } // NewRaftLayer is used to initialize a new RaftLayer which can -// be used as a StreamLayer for Raft -func NewRaftLayer(addr net.Addr) *RaftLayer { +// be used as a StreamLayer for Raft. If a tlsConfig is provided, +// then the connection will use TLS. +func NewRaftLayer(addr net.Addr, tlsConfig *tls.Config) *RaftLayer { layer := &RaftLayer{ - addr: addr, - connCh: make(chan net.Conn), - closeCh: make(chan struct{}), + addr: addr, + connCh: make(chan net.Conn), + tlsConfig: tlsConfig, + closeCh: make(chan struct{}), } return layer } @@ -79,6 +85,18 @@ func (l *RaftLayer) Dial(address string, timeout time.Duration) (net.Conn, error return nil, err } + // Check for tls mode + if l.tlsConfig != nil { + // Switch the connection into TLS mode + if _, err := conn.Write([]byte{byte(rpcTLS)}); err != nil { + conn.Close() + return nil, err + } + + // Wrap the connection in a TLS client + conn = tls.Client(conn, l.tlsConfig) + } + // Write the Raft byte to set the mode _, err = conn.Write([]byte{byte(rpcRaft)}) if err != nil { diff --git a/consul/rpc.go b/consul/rpc.go index 3f5d8913d1..cb4185a3dc 100644 --- a/consul/rpc.go +++ b/consul/rpc.go @@ -1,6 +1,7 @@ package consul import ( + "crypto/tls" "fmt" "github.com/armon/go-metrics" "github.com/hashicorp/consul/consul/structs" @@ -19,6 +20,7 @@ const ( rpcConsul RPCType = iota rpcRaft rpcMultiplex + rpcTLS ) const ( @@ -43,14 +45,14 @@ func (s *Server) listen() { s.rpcClients[conn] = struct{}{} s.rpcClientLock.Unlock() - go s.handleConn(conn) + go s.handleConn(conn, false) metrics.IncrCounter([]string{"consul", "rpc", "accept_conn"}, 1) } } // handleConn is used to determine if this is a Raft or // Consul type RPC connection and invoke the correct handler -func (s *Server) handleConn(conn net.Conn) { +func (s *Server) handleConn(conn net.Conn, isTLS bool) { // Read a single byte buf := make([]byte, 1) if _, err := conn.Read(buf); err != nil { @@ -59,6 +61,13 @@ func (s *Server) handleConn(conn net.Conn) { return } + // Enforce TLS if VerifyIncoming is set + if s.config.VerifyIncoming && !isTLS && RPCType(buf[0]) != rpcTLS { + s.logger.Printf("[WARN] consul.rpc: Non-TLS connection attempted with VerifyIncoming set") + conn.Close() + return + } + // Switch on the byte switch RPCType(buf[0]) { case rpcConsul: @@ -71,6 +80,15 @@ func (s *Server) handleConn(conn net.Conn) { case rpcMultiplex: s.handleMultiplex(conn) + case rpcTLS: + if s.rpcTLS == nil { + s.logger.Printf("[WARN] consul.rpc: TLS connection attempted, server not configured for TLS") + conn.Close() + return + } + conn = tls.Server(conn, s.rpcTLS) + s.handleConn(conn, true) + default: s.logger.Printf("[ERR] consul.rpc: unrecognized RPC byte: %v", buf[0]) conn.Close() diff --git a/consul/server.go b/consul/server.go index 9a4a6a59f0..640140ed1f 100644 --- a/consul/server.go +++ b/consul/server.go @@ -1,6 +1,7 @@ package consul import ( + "crypto/tls" "fmt" "github.com/hashicorp/raft" "github.com/hashicorp/serf/serf" @@ -82,6 +83,9 @@ type Server struct { rpcListener net.Listener rpcServer *rpc.Server + // rpcTLS is the TLS config for incoming TLS requests + rpcTLS *tls.Config + // serfLAN is the Serf cluster maintained inside the DC // which contains all the DC nodes serfLAN *serf.Serf @@ -122,13 +126,28 @@ func NewServer(config *Config) (*Server, error) { config.LogOutput = os.Stderr } + // Create the tlsConfig for outgoing connections + var tlsConfig *tls.Config + var err error + if config.VerifyOutgoing { + if tlsConfig, err = config.OutgoingTLSConfig(); err != nil { + return nil, err + } + } + + // Get the incoming tls config + incomingTLS, err := config.IncomingTLSConfig() + if err != nil { + return nil, err + } + // Create a logger logger := log.New(config.LogOutput, "", log.LstdFlags) // Create server s := &Server{ config: config, - connPool: NewPool(time.Minute), + connPool: NewPool(time.Minute, tlsConfig), eventChLAN: make(chan serf.Event, 256), eventChWAN: make(chan serf.Event, 256), logger: logger, @@ -136,11 +155,12 @@ func NewServer(config *Config) (*Server, error) { remoteConsuls: make(map[string][]net.Addr), rpcClients: make(map[net.Conn]struct{}), rpcServer: rpc.NewServer(), + rpcTLS: incomingTLS, shutdownCh: make(chan struct{}), } // Initialize the RPC layer - if err := s.setupRPC(); err != nil { + if err := s.setupRPC(tlsConfig); err != nil { s.Shutdown() return nil, fmt.Errorf("Failed to start RPC layer: %v", err) } @@ -156,7 +176,6 @@ func NewServer(config *Config) (*Server, error) { go s.wanEventHandler() // Initialize the lan Serf - var err error s.serfLAN, err = s.setupSerf(config.SerfLANConfig, s.eventChLAN, serfLANSnapshot) if err != nil { @@ -271,7 +290,7 @@ func (s *Server) setupRaft() error { } // setupRPC is used to setup the RPC listener -func (s *Server) setupRPC() error { +func (s *Server) setupRPC(tlsConfig *tls.Config) error { // Create endpoints s.endpoints.Status = &Status{s} s.endpoints.Raft = &Raft{s} @@ -310,7 +329,7 @@ func (s *Server) setupRPC() error { return fmt.Errorf("RPC advertise address is not advertisable: %v", addr) } - s.raftLayer = NewRaftLayer(advertise) + s.raftLayer = NewRaftLayer(advertise, tlsConfig) go s.listen() return nil } diff --git a/consul/server_test.go b/consul/server_test.go index f73ec6f2ee..45edac53d9 100644 --- a/consul/server_test.go +++ b/consul/server_test.go @@ -25,19 +25,17 @@ func tmpDir(t *testing.T) string { return dir } -func testServer(t *testing.T) (string, *Server) { - return testServerDC(t, "dc1") +func configureTLS(config *Config) { + config.CAFile = "../test/ca/root.cer" + config.CertFile = "../test/key/ourdomain.cer" + config.KeyFile = "../test/key/ourdomain.key" } -func testServerDC(t *testing.T, dc string) (string, *Server) { - return testServerDCBootstrap(t, dc, true) -} - -func testServerDCBootstrap(t *testing.T, dc string, bootstrap bool) (string, *Server) { +func testServerConfig(t *testing.T) (string, *Config) { dir := tmpDir(t) config := DefaultConfig() - config.Bootstrap = bootstrap - config.Datacenter = dc + config.Bootstrap = true + config.Datacenter = "dc1" config.DataDir = dir // Adjust the ports @@ -65,7 +63,21 @@ func testServerDCBootstrap(t *testing.T, dc string, bootstrap bool) (string, *Se config.RaftConfig.ElectionTimeout = 40 * time.Millisecond config.ReconcileInterval = 100 * time.Millisecond + return dir, config +} +func testServer(t *testing.T) (string, *Server) { + return testServerDC(t, "dc1") +} + +func testServerDC(t *testing.T, dc string) (string, *Server) { + return testServerDCBootstrap(t, dc, true) +} + +func testServerDCBootstrap(t *testing.T, dc string, bootstrap bool) (string, *Server) { + dir, config := testServerConfig(t) + config.Datacenter = dc + config.Bootstrap = bootstrap server, err := NewServer(config) if err != nil { t.Fatalf("err: %v", err) @@ -219,3 +231,55 @@ func TestServer_RPC(t *testing.T) { t.Fatalf("err: %v", err) } } + +func TestServer_JoinLAN_TLS(t *testing.T) { + dir1, conf1 := testServerConfig(t) + conf1.VerifyIncoming = true + conf1.VerifyOutgoing = true + configureTLS(conf1) + s1, err := NewServer(conf1) + if err != nil { + t.Fatalf("err: %v", err) + } + defer os.RemoveAll(dir1) + defer s1.Shutdown() + + dir2, conf2 := testServerConfig(t) + conf2.Bootstrap = false + conf2.VerifyIncoming = true + conf2.VerifyOutgoing = true + configureTLS(conf2) + s2, err := NewServer(conf2) + if err != nil { + t.Fatalf("err: %v", err) + } + defer os.RemoveAll(dir2) + defer s2.Shutdown() + + // Try to join + addr := fmt.Sprintf("127.0.0.1:%d", + s1.config.SerfLANConfig.MemberlistConfig.BindPort) + if _, err := s2.JoinLAN([]string{addr}); err != nil { + t.Fatalf("err: %v", err) + } + + // Check the members + if len(s1.LANMembers()) != 2 { + t.Fatalf("bad len") + } + + if len(s2.LANMembers()) != 2 { + t.Fatalf("bad len") + } + + // Wait a while + time.Sleep(100 * time.Millisecond) + + // Verify Raft has established a peer + if s1.Stats()["raft"]["num_peers"] != "1" { + t.Fatalf("bad: %v", s1.Stats()["raft"]) + } + if s2.Stats()["raft"]["num_peers"] != "1" { + t.Fatalf("bad: %v", s2.Stats()["raft"]) + } +} diff --git a/test/ca/certindex b/test/ca/certindex new file mode 100644 index 0000000000..976623958e --- /dev/null +++ b/test/ca/certindex @@ -0,0 +1,2 @@ +V 150407190456Z 0A unknown /CN=testco.internal/ST=California/C=US/emailAddress=test@testco.com/O=TestCo/OU=Beta +V 150407194146Z 0B unknown /CN=testco.internal/ST=California/C=US/emailAddress=test@testco.com/O=TestCo/OU=Beta diff --git a/test/ca/myca.conf b/test/ca/myca.conf new file mode 100644 index 0000000000..922660decb --- /dev/null +++ b/test/ca/myca.conf @@ -0,0 +1,34 @@ +[ ca ] +default_ca = myca + +[ crl_ext ] +# issuerAltName=issuer:copy #this would copy the issuer name to altname +authorityKeyIdentifier=keyid:always + +[ myca ] +new_certs_dir = /tmp +unique_subject = no +certificate = root.cer +database = certindex +private_key = privkey.pem +serial = serialfile +default_days = 365 +default_md = sha1 +policy = myca_policy +x509_extensions = myca_extensions + +[ myca_policy ] +commonName = supplied +stateOrProvinceName = supplied +countryName = supplied +emailAddress = optional +organizationName = supplied +organizationalUnitName = optional + +[ myca_extensions ] +basicConstraints = CA:false +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always +keyUsage = digitalSignature,keyEncipherment +extendedKeyUsage = serverAuth,clientAuth +crlDistributionPoints = URI:http://path.to.crl/myca.crl diff --git a/test/ca/privkey.pem b/test/ca/privkey.pem new file mode 100644 index 0000000000..3f22711a11 --- /dev/null +++ b/test/ca/privkey.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAxrs6JK4NpiOItxrpNR/1ppUUmH7p2BgLCBZ6eHdclle9J56i +68adt8J85zaqphCfz6VDP58DsFx+N50PZyjQaDsUd0HejRqfHRMtg2O+UQkv4Z66 ++Vo+gc6uGuANi2xMtSYDVTAqqzF48OOPQDgYkzcGxcFZzTRFFZt2vPnyHj8cHcaF +o/NMNVh7C3yTXevRGNm9u2mrbxCEeiHzFC2WUnvgU2jQuC7Fhnl33Zd3B6d3mQH6 +O23ncmwxTcPUJe6xZaIRrDuzwUcyhLj5Z3faag/fpFIIcHSiHRfoqHLGsGg+3swI +d/zVJSSDHr7pJUu7Cre+vZa63FqDaooqvnisrQIDAQABAoIBABreo6zj76p/8XM4 +a0GokZE1ZPR9bGawUYWFbIevM9CMCmI5+7M/RoHbBQJKDOapJsJviNkoSdpllxcz +4CpFhXAiVNEPEeUoLU1EE4pJSSkxwcySppsiTYNFi5rMomgwe2qeuiKhgZNl/AEt +82dubjwxW3QPgXHSWGjkfTht3wOhrczA8xyEjc9Bsad2ooA9IQk+VXYlPZXyXjs1 +WwLYHmcSfveauLliLXeVU2Ux5PPwyreKMhyAfSHVQCycxK008u8WPy8nkAlpxKMC +UwCN+JKl69WCCA3CxXgM83zz4pXvB4EyMr8aTiqmOID8RIIrPcjCmVJki6KbJ9WG +S2CQVG0CgYEA5kVACrnjLtov426ZNifF2zUXu9x//7D6GkbJxzZLwXP/BJFcEOdQ +Fnjcs3s7wYh/wdTnEcQVWSJSAqnRt98c9yAXVnG5z1M0DYpAsY8xrdhEitxOf2oB +2cbvi4+cvUuUxk1hgva18UCT23aLP+iY2+t/ydBXAZ9kq1zz5CcpEBMCgYEA3O/R +g1Y9O36XxBmSYnkoCF5yGrPunnKKNBJc/WA7pTkQFYHr64Y/h5EKubzHD/VEd1Li +nDuGYxVMewf+5pHUhqSdpZtTxv25hjOsqLf5o5wm18JThGifs2zEVCTJOPti5n2M +RHakxuq1I625/QHidLBTQYuEBS/vywhapfaSaD8CgYEAhd1OPK4R30PiQRIjqXL3 +t9ampISsOKXWz33FgbUT1zOq1in23rDKQzYh/4ktlPXYZ4NwjUhzrKyiBoBYtc7T +1OpoBs34Wgmhohl0QIThOZIXTq6CR9oFl2fqDDUBxp3wsFN905e+77A+BIBmtVFv +w7GlSVp/qibSbDiOZF1LptcCgYB8sJBi+jnmqOSIVRJLpysTxhHJxkDmhahACRkY +Gsau0cylBsUaEJMsNIyEFOmXtQml+k5QdDu9EdkvGm0evbDfKGqce1RF2w5okiNg +uSwXzVoSrOartMxk2/7VqkkycpX3lWWjgf4vEWmXsEVmaDjhOF5UgKPKtao0wQs/ +3S/1ywKBgAIGgOuvL/GBcGqLikHLC+cputMvBAuE/tJnFHPxFoobskocVsMKbDTy +NYF7uPlzSGGClZsjE6DQyyGf5E9/U+EdwDKZwHYGCkzVjplUBo0BT3EN0vcc9jB/ +ML9Ta4ETPyf66BhSVcD+eeNipPFAul0Q7uZhErH1zr1evTy8XXyI +-----END RSA PRIVATE KEY----- diff --git a/test/ca/root.cer b/test/ca/root.cer new file mode 100644 index 0000000000..ae9fb00b1c --- /dev/null +++ b/test/ca/root.cer @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIEtzCCA5+gAwIBAgIJAIewRMI8OnvTMA0GCSqGSIb3DQEBBQUAMIGYMQswCQYD +VQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xHDAa +BgNVBAoTE0hhc2hpQ29ycCBUZXN0IENlcnQxDDAKBgNVBAsTA0RldjEWMBQGA1UE +AxMNdGVzdC5pbnRlcm5hbDEgMB4GCSqGSIb3DQEJARYRdGVzdEBpbnRlcm5hbC5j +b20wHhcNMTQwNDA3MTkwMTA4WhcNMjQwNDA0MTkwMTA4WjCBmDELMAkGA1UEBhMC +VVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMRwwGgYDVQQK +ExNIYXNoaUNvcnAgVGVzdCBDZXJ0MQwwCgYDVQQLEwNEZXYxFjAUBgNVBAMTDXRl +c3QuaW50ZXJuYWwxIDAeBgkqhkiG9w0BCQEWEXRlc3RAaW50ZXJuYWwuY29tMIIB +IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxrs6JK4NpiOItxrpNR/1ppUU +mH7p2BgLCBZ6eHdclle9J56i68adt8J85zaqphCfz6VDP58DsFx+N50PZyjQaDsU +d0HejRqfHRMtg2O+UQkv4Z66+Vo+gc6uGuANi2xMtSYDVTAqqzF48OOPQDgYkzcG +xcFZzTRFFZt2vPnyHj8cHcaFo/NMNVh7C3yTXevRGNm9u2mrbxCEeiHzFC2WUnvg +U2jQuC7Fhnl33Zd3B6d3mQH6O23ncmwxTcPUJe6xZaIRrDuzwUcyhLj5Z3faag/f +pFIIcHSiHRfoqHLGsGg+3swId/zVJSSDHr7pJUu7Cre+vZa63FqDaooqvnisrQID +AQABo4IBADCB/TAdBgNVHQ4EFgQUo/nrOfqvbee2VklVKIFlyQEbuJUwgc0GA1Ud +IwSBxTCBwoAUo/nrOfqvbee2VklVKIFlyQEbuJWhgZ6kgZswgZgxCzAJBgNVBAYT +AlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEcMBoGA1UE +ChMTSGFzaGlDb3JwIFRlc3QgQ2VydDEMMAoGA1UECxMDRGV2MRYwFAYDVQQDEw10 +ZXN0LmludGVybmFsMSAwHgYJKoZIhvcNAQkBFhF0ZXN0QGludGVybmFsLmNvbYIJ +AIewRMI8OnvTMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBADa9fV9h +gjapBlkNmu64WX0Ufub5dsJrdHS8672P30S7ILB7Mk0W8sL65IezRsZnG898yHf9 +2uzmz5OvNTM9K380g7xFlyobSVq+6yqmmSAlA/ptAcIIZT727P5jig/DB7fzJM3g +jctDlEGOmEe50GQXc25VKpcpjAsNQi5ER5gowQ0v3IXNZs+yU+LvxLHc0rUJ/XSp +lFCAMOqd5uRoMOejnT51G6krvLNzPaQ3N9jQfNVY4Q0zfs0M+6dRWvqfqB9Vyq8/ +POLMld+HyAZEBk9zK3ZVIXx6XS4dkDnSNR91njLq7eouf6M7+7s/oMQZZRtAfQ6r +wlW975rYa1ZqEdA= +-----END CERTIFICATE----- diff --git a/test/ca/serialfile b/test/ca/serialfile new file mode 100644 index 0000000000..d73cdef37a --- /dev/null +++ b/test/ca/serialfile @@ -0,0 +1 @@ +0C diff --git a/test/key/ourdomain.cer b/test/key/ourdomain.cer new file mode 100644 index 0000000000..e795f20562 --- /dev/null +++ b/test/key/ourdomain.cer @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDtTCCAp2gAwIBAgIBCzANBgkqhkiG9w0BAQUFADCBmDELMAkGA1UEBhMCVVMx +CzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMRwwGgYDVQQKExNI +YXNoaUNvcnAgVGVzdCBDZXJ0MQwwCgYDVQQLEwNEZXYxFjAUBgNVBAMTDXRlc3Qu +aW50ZXJuYWwxIDAeBgkqhkiG9w0BCQEWEXRlc3RAaW50ZXJuYWwuY29tMB4XDTE0 +MDQwNzE5NDE0NloXDTE1MDQwNzE5NDE0NlowfDEYMBYGA1UEAxMPdGVzdGNvLmlu +dGVybmFsMRMwEQYDVQQIEwpDYWxpZm9ybmlhMQswCQYDVQQGEwJVUzEeMBwGCSqG +SIb3DQEJARYPdGVzdEB0ZXN0Y28uY29tMQ8wDQYDVQQKEwZUZXN0Q28xDTALBgNV +BAsTBEJldGEwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBALXNUbqVUjsspMHh +46PB37kI6rKzZlSsyaBYWsS19tdrwS83nSXZJlaMAVPLR/TR+b1P81zxZD8m8ZmQ +2bK70No5usFkdlbowVLAIMySIQmZF1tTLXbiCKldiwEjkOWa1xKwJauoM0XKnWkF +mLGDxIAl84DZaLgmNj2t/q8d+laDAgMBAAGjgagwgaUwCQYDVR0TBAIwADAdBgNV +HQ4EFgQUDNy9EoPn+YNrIlvkWMxh5QKS5JwwHwYDVR0jBBgwFoAUo/nrOfqvbee2 +VklVKIFlyQEbuJUwCwYDVR0PBAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggr +BgEFBQcDAjAsBgNVHR8EJTAjMCGgH6AdhhtodHRwOi8vcGF0aC50by5jcmwvbXlj +YS5jcmwwDQYJKoZIhvcNAQEFBQADggEBABCvJP2zMF60ooDYLaaNuPcmkokWIp/n +X00dZ6y1aI063y8OE1CSY8rGv3W/ONkS3cOQvVhdAVtAzqGnDK3VsFZzmWR+tuqR +KUhkzcC0X4nNq91iik1dTj2skl1Jkq6lJhrY1sR6JXOSn68Iv2KAuLVNn5tC5hzB +WOK7S2ffqfof3eV+g0cgFNCzaS75cn8YlXBqQGpn5odcVDX8c80Xj/Si18wWDPR9 +/kq5xKRsaFkKJYOoswRwoq9kwukruMndxf7/Az/YEHdimZKMpxfK/qzI0KUw4XO0 +lpEkMZaA3l+xYB2fNHwlwyz77RNCoySCnii61hmxLNDwUiokgdJcY9U= +-----END CERTIFICATE----- diff --git a/test/key/ourdomain.csr b/test/key/ourdomain.csr new file mode 100644 index 0000000000..40e7e8cd8d --- /dev/null +++ b/test/key/ourdomain.csr @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIB1TCCAT4CAQAwgZQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlh +MRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMQ8wDQYDVQQKEwZUZXN0Q28xDTALBgNV +BAsTBEJldGExGDAWBgNVBAMTD3Rlc3Rjby5pbnRlcm5hbDEeMBwGCSqGSIb3DQEJ +ARYPdGVzdEB0ZXN0Y28uY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC1 +zVG6lVI7LKTB4eOjwd+5COqys2ZUrMmgWFrEtfbXa8EvN50l2SZWjAFTy0f00fm9 +T/Nc8WQ/JvGZkNmyu9DaObrBZHZW6MFSwCDMkiEJmRdbUy124gipXYsBI5DlmtcS +sCWrqDNFyp1pBZixg8SAJfOA2Wi4JjY9rf6vHfpWgwIDAQABoAAwDQYJKoZIhvcN +AQEFBQADgYEAG7SdzgaTcJ1sMJ+pH+42J9Fyp2SY+WGEP3dA7f1/Lwc1dFHeKLPL +X0Gv6DgNkxUwWOe/yncq+dkuUkDGx3M1FRvpKCKFAywp+j0NxIll7821/2Jvf5/f +BVHPmgUIzZEYz0d6vcCQl2RKd83wLWcR77JTrD2S1JqAEkrMw/xUa/s= +-----END CERTIFICATE REQUEST----- diff --git a/test/key/ourdomain.key b/test/key/ourdomain.key new file mode 100644 index 0000000000..18d52ba1c2 --- /dev/null +++ b/test/key/ourdomain.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICWwIBAAKBgQC1zVG6lVI7LKTB4eOjwd+5COqys2ZUrMmgWFrEtfbXa8EvN50l +2SZWjAFTy0f00fm9T/Nc8WQ/JvGZkNmyu9DaObrBZHZW6MFSwCDMkiEJmRdbUy12 +4gipXYsBI5DlmtcSsCWrqDNFyp1pBZixg8SAJfOA2Wi4JjY9rf6vHfpWgwIDAQAB +AoGAQNOSxg6CwPj9UulCa90w8mD8l3YjEiR+zP9UdnpQJ6aTv0t8bMeOxSOtQXzm +DqVlAR1mMQkebuprEhA1oGcaZGx5hYIfhad8k5e18nkfogMG2J23fjyw/Zvcsbpi +AebEHGwrPnGplG2AZ2knRVpQ0EkKLUZ3sHGQjCuh/srw5QECQQDb5vKCz181/wnE +0CVIAg61ibwyWNR2s3i2Xhsp0UmhbTSknP2mQzdUqegMB5Y5mbruXy46EHuia5H7 +LnFXfx6BAkEA06VF154NirTBU6MCPtj0ynUkTFSYu7oJG7U+rMA7Bx4itLjAy76g +wniz6HWHMqgQP1G8mhkHYb6fmkBQY1V7AwJANjP07t1ioJKeu856ggdPzNuIcfiH +VkLirEEB/QrDVXDvmuu/ce37g3jl46EzHDuSYhM/97v8XYqaTwmhkmmZAQJAbFVq +3KVwdRF06+TCn3zaQE+Z1uBulZjyVJZ/kFmNXWVVioAPX7sh+qliHZkbLRjNyDuE +eLRbDPNQKtrEyzPUFQJAXQ4AKXWs1LMzNFueMH+qZ3NjX+GdVBrvdri7D+yzLXPL +IwNWljwTONXQHwwRlEHIQJVw85qfdLfNngHKEW5X7w== +-----END RSA PRIVATE KEY----- diff --git a/test/notes.txt b/test/notes.txt new file mode 100644 index 0000000000..ae4f709824 --- /dev/null +++ b/test/notes.txt @@ -0,0 +1 @@ +Instructions from https://langui.sh/2009/01/18/openssl-self-signed-ca/ diff --git a/website/source/docs/agent/encryption.html.markdown b/website/source/docs/agent/encryption.html.markdown index b4c396e52f..94710b6dd7 100644 --- a/website/source/docs/agent/encryption.html.markdown +++ b/website/source/docs/agent/encryption.html.markdown @@ -8,11 +8,12 @@ sidebar_current: "docs-agent-encryption" The Consul agent supports encrypting all of its network traffic. The exact method of this encryption is described on the -[encryption internals page](/docs/internals/security.html). +[encryption internals page](/docs/internals/security.html). There are two +seperate systems, one for gossip traffic and one for RPC. -## Enabling Encryption +## Gossip Encryption -Enabling encryption only requires that you set an encryption key when +Enabling gossip encryption only requires that you set an encryption key when starting the Consul agent. The key can be set using the `-encrypt` flag on `consul agent` or by setting the `encrypt_key` in a configuration file. It is advisable to put the key in a configuration file to avoid other users @@ -47,3 +48,30 @@ $ consul agent -data=/tmp/consul -encrypt=cg8StVXbQJ0gPvMd9o7yrg== All nodes within a Consul cluster must share the same encryption key in order to send and receive cluster information. +# RPC Encryption with TLS + +Consul supports using TLS to verify the authenticity of servers and clients. For this +to work, Consul requires that all clients and servers have key pairs that are generated +by a single Certificate Authority. This can be a private CA, used only internally. The +CA then signs keys for each of the agents. [Here](https://langui.sh/2009/01/18/openssl-self-signed-ca/) +is a tutorial on generating both a CA and signing keys using OpenSSL. + +There are a number of things to consider when setting up TLS for Consul. Either we can +use TLS just to verify the authenticity of the servers, or we can also verify the authenticity +of clients. The former can be used to prevent unauthorized access. This behavior is controlled +using either the `verify_incoming` and `verify_outgoing` [options](/docs/agent/options.html). + +If `verify_outgoing` is set, then agents verify the authenticity of Consuls for outgoing +connections. This means server nodes must present a certificate signed by the `ca_file` that +the agent has. This option must be set on all agents, and there must be a `ca_file` provided +to check the certificate against. If this is set, then all server nodes must have an appropriate +key pair set using `cert_file` and `key_file`. + +If `verify_incoming` is set, then the servers verify the authenticity of all incoming +connections. Servers will also disallow any non-TLS connections. If this is set, then all +clients must have a valid key pair set using `cert_file` and `key_file`. To force clients to +use TLs, `verify_outgoing` must also be set. + +TLS is used to secure the RPC calls between agents, but gossip between nodes is done over UDP +and is secured using a symmetric key. See above for enabling gossip encryption. + diff --git a/website/source/docs/agent/options.html.markdown b/website/source/docs/agent/options.html.markdown index 4cebb0cb08..3081a86a76 100644 --- a/website/source/docs/agent/options.html.markdown +++ b/website/source/docs/agent/options.html.markdown @@ -205,3 +205,27 @@ definitions support being updated during a reload. * `statsite_addr` - Equivalent to the `-statsite` command-line flag. +* `verify_incoming` - If set to True, Consul requires that all incoming + connections make use of TLS, and that the client provides a certificate signed + by the Certificate Authority from the `ca_file`. By default, this is false, and + Consul will not enforce the use of TLS or verify a client's authenticity. This + only applies to Consul servers, since a client never has an incoming connection. + +* `verify_outgoing` - If set to True, Consul requires that all outgoing connections + make use of TLS, and that the server provide a certificate that is signed by + the Certificate Authority from the `ca_file`. By default, this is false, and Consul + will not make use of TLS for outgoing connections. This applies to clients and servers, + as both will make outgoing connections. + +* `ca_file` - This provides a the file path to a PEM encoded certificate authority. + The certificate authority is used to check the authenticity of client and server + connections with the appropriate `verify_incoming` or `verify_outgoing` flags. + +* `cert_file` - This provides a the file path to a PEM encoded certificate. + The certificate is provided to clients or servers to verify the agents authenticity. + Must be provided along with the `key_file`. + +* `key_file` - This provides a the file path to a PEM encoded private key. + The key is used with the certificate to verify the agents authenticity. + Must be provided along with the `cert_file`. +