diff --git a/CHANGELOG-3.2.md b/CHANGELOG-3.2.md index 42196323a1a..367b112404c 100644 --- a/CHANGELOG-3.2.md +++ b/CHANGELOG-3.2.md @@ -3,6 +3,23 @@ Previous change logs can be found at [CHANGELOG-3.1](https://github.com/coreos/etcd/blob/master/CHANGELOG-3.1.md). +## [v3.2.22](https://github.com/coreos/etcd/releases/tag/v3.2.22) (TBD 2018-06) + +See [code changes](https://github.com/coreos/etcd/compare/v3.2.21...v3.2.22) and [v3.2 upgrade guide](https://github.com/coreos/etcd/blob/master/Documentation/upgrades/upgrade_3_2.md) for any breaking changes. **Again, before running upgrades from any previous release, please make sure to read change logs below and [v3.2 upgrade guide](https://github.com/coreos/etcd/blob/master/Documentation/upgrades/upgrade_3_2.md).** + +### etcd server + +- Support TLS cipher suite whitelisting. + - To block [weak cipher suites](https://github.com/coreos/etcd/issues/8320). + - TLS handshake fails when client hello is requested with invalid cipher suites. + - Add [`etcd --cipher-suites`](https://github.com/coreos/etcd/pull/9801) flag. + - If empty, Go auto-populates the list. + +### Go + +- Compile with [*Go 1.8.7*](https://golang.org/doc/devel/release.html#go1.8). + + ## [v3.2.21](https://github.com/coreos/etcd/releases/tag/v3.2.21) (2018-05-31) See [code changes](https://github.com/coreos/etcd/compare/v3.2.20...v3.2.21) and [v3.2 upgrade guide](https://github.com/coreos/etcd/blob/master/Documentation/upgrades/upgrade_3_2.md) for any breaking changes. **Again, before running upgrades from any previous release, please make sure to read change logs below and [v3.2 upgrade guide](https://github.com/coreos/etcd/blob/master/Documentation/upgrades/upgrade_3_2.md).** diff --git a/CHANGELOG-3.3.md b/CHANGELOG-3.3.md index 9f300d03d7b..4618863bdad 100644 --- a/CHANGELOG-3.3.md +++ b/CHANGELOG-3.3.md @@ -3,6 +3,23 @@ Previous change logs can be found at [CHANGELOG-3.2](https://github.com/coreos/etcd/blob/master/CHANGELOG-3.2.md). +## [v3.3.7](https://github.com/coreos/etcd/releases/tag/v3.3.7) (TBD 2018-06) + +See [code changes](https://github.com/coreos/etcd/compare/v3.3.6...v3.3.7) and [v3.3 upgrade guide](https://github.com/coreos/etcd/blob/master/Documentation/upgrades/upgrade_3_3.md) for any breaking changes. **Again, before running upgrades from any previous release, please make sure to read change logs below and [v3.3 upgrade guide](https://github.com/coreos/etcd/blob/master/Documentation/upgrades/upgrade_3_3.md).** + +### etcd server + +- Support TLS cipher suite whitelisting. + - To block [weak cipher suites](https://github.com/coreos/etcd/issues/8320). + - TLS handshake fails when client hello is requested with invalid cipher suites. + - Add [`etcd --cipher-suites`](https://github.com/coreos/etcd/pull/9801) flag. + - If empty, Go auto-populates the list. + +### Go + +- Compile with [*Go 1.9.6*](https://golang.org/doc/devel/release.html#go1.9). + + ## [v3.3.6](https://github.com/coreos/etcd/releases/tag/v3.3.6) (2018-05-31) See [code changes](https://github.com/coreos/etcd/compare/v3.3.5...v3.3.6) and [v3.3 upgrade guide](https://github.com/coreos/etcd/blob/master/Documentation/upgrades/upgrade_3_3.md) for any breaking changes. **Again, before running upgrades from any previous release, please make sure to read change logs below and [v3.3 upgrade guide](https://github.com/coreos/etcd/blob/master/Documentation/upgrades/upgrade_3_3.md).** diff --git a/CHANGELOG-3.4.md b/CHANGELOG-3.4.md index d3c826bb191..45a9b7e8142 100644 --- a/CHANGELOG-3.4.md +++ b/CHANGELOG-3.4.md @@ -156,6 +156,12 @@ See [code changes](https://github.com/coreos/etcd/compare/v3.3.0...v3.4.0) and [ See [security doc](https://github.com/coreos/etcd/blob/master/Documentation/op-guide/security.md) for more details. +- Support TLS cipher suite whitelisting. + - To block [weak cipher suites](https://github.com/coreos/etcd/issues/8320). + - TLS handshake fails when client hello is requested with invalid cipher suites. + - Add [`etcd --client-cipher-suites`](https://github.com/coreos/etcd/pull/9801) flag. + - Add [`etcd --peer-cipher-suites`](https://github.com/coreos/etcd/pull/9801) flag. + - If empty, Go auto-populates the list. - Add [`etcd --host-whitelist`](https://github.com/coreos/etcd/pull/9372) flag, [`etcdserver.Config.HostWhitelist`](https://github.com/coreos/etcd/pull/9372), and [`embed.Config.HostWhitelist`](https://github.com/coreos/etcd/pull/9372), to prevent ["DNS Rebinding"](https://en.wikipedia.org/wiki/DNS_rebinding) attack. - Any website can simply create an authorized DNS name, and direct DNS to `"localhost"` (or any other address). Then, all HTTP endpoints of etcd server listening on `"localhost"` becomes accessible, thus vulnerable to [DNS rebinding attacks (CVE-2018-5702)](https://bugs.chromium.org/p/project-zero/issues/detail?id=1447#c2). - Client origin enforce policy works as follow: @@ -166,7 +172,6 @@ See [security doc](https://github.com/coreos/etcd/blob/master/Documentation/op-g - When specifying hostnames, loopback addresses are not added automatically. To allow loopback interfaces, add them to whitelist manually (e.g. `"localhost"`, `"127.0.0.1"`, etc.). - e.g. `etcd --host-whitelist example.com`, then the server will reject all HTTP requests whose Host field is not `example.com` (also rejects requests to `"localhost"`). - Support [`etcd --cors`](https://github.com/coreos/etcd/pull/9490) in v3 HTTP requests (gRPC gateway). -- Support [TLS cipher suite lists](TODO). - Support [`ttl` field for `etcd` Authentication JWT token](https://github.com/coreos/etcd/pull/8302). - e.g. `etcd --auth-token jwt,pub-key=,priv-key=,sign-method=,ttl=5m`. - Allow empty token provider in [`etcdserver.ServerConfig.AuthToken`](https://github.com/coreos/etcd/pull/9369). @@ -207,6 +212,11 @@ See [security doc](https://github.com/coreos/etcd/blob/master/Documentation/op-g - If not given, etcd queries `_etcd-server-ssl._tcp.[YOUR_HOST]` and `_etcd-server._tcp.[YOUR_HOST]`. - If `--discovery-srv-name="foo"`, then query `_etcd-server-ssl-foo._tcp.[YOUR_HOST]` and `_etcd-server-foo._tcp.[YOUR_HOST]`. - Useful for operating multiple etcd clusters under the same domain. +- Support TLS cipher suite whitelisting. + - To block [weak cipher suites](https://github.com/coreos/etcd/issues/8320). + - TLS handshake fails when client hello is requested with invalid cipher suites. + - Add [`etcd --cipher-suites`](https://github.com/coreos/etcd/pull/9801) flag. + - If empty, Go auto-populates the list. - Support [`etcd --cors`](https://github.com/coreos/etcd/pull/9490) in v3 HTTP requests (gRPC gateway). - Rename [`etcd --log-output` to `--log-outputs`](https://github.com/coreos/etcd/pull/9624) to support multiple log outputs. - **`etcd --log-output` will be deprecated in v3.5**. @@ -271,6 +281,10 @@ Note: **v3.5 will deprecate `etcd --log-package-levels` flag for `capnslog`**; ` ### Package `embed` +- Add [`embed.Config.CipherSuites`](https://github.com/coreos/etcd/pull/9801) to specify a list of supported cipher suites for TLS handshake between client/server and peers. + - If empty, Go auto-populates the list. + - Both `embed.Config.ClientTLSInfo.CipherSuites` and `embed.Config.CipherSuites` cannot be non-empty at the same time. + - If not empty, specify either `embed.Config.ClientTLSInfo.CipherSuites` or `embed.Config.CipherSuites`. - Add [`embed.Config.InitialElectionTickAdvance`](https://github.com/coreos/etcd/pull/9591) to enable/disable initial election tick fast-forward. - `embed.NewConfig()` would return `*embed.Config` with `InitialElectionTickAdvance` as true by default. - Define [`embed.CompactorModePeriodic`](https://godoc.org/github.com/coreos/etcd/embed#pkg-variables) for `compactor.ModePeriodic`. diff --git a/Documentation/op-guide/security.md b/Documentation/op-guide/security.md index 582fd1a6b48..f8210ab6c95 100644 --- a/Documentation/op-guide/security.md +++ b/Documentation/op-guide/security.md @@ -38,6 +38,8 @@ The peer options work the same way as the client-to-server options: If either a client-to-server or peer certificate is supplied the key must also be set. All of these configuration options are also available through the environment variables, `ETCD_CA_FILE`, `ETCD_PEER_CA_FILE` and so on. +`--cipher-suites`: Comma-separated list of supported TLS cipher suites between server/client and peers (empty will be auto-populated by Go). Available from v3.2.22+, v3.3.7+, and v3.4+. + ## Example 1: Client-to-server transport security with HTTPS For this, have a CA certificate (`ca.crt`) and signed key pair (`server.crt`, `server.key`) ready. @@ -122,6 +124,49 @@ And also the response from the server: } ``` +Specify cipher suites to block [weak TLS cipher suites](https://github.com/coreos/etcd/issues/8320). + +TLS handshake would fail when client hello is requested with invalid cipher suites. + +For instance: + +```bash +$ etcd \ + --cert-file ./server.crt \ + --key-file ./server.key \ + --trusted-ca-file ./ca.crt \ + --cipher-suites TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 +``` + +Then, client requests must specify one of the cipher suites specified in the server: + +```bash +# valid cipher suite +$ curl \ + --cacert ./ca.crt \ + --cert ./server.crt \ + --key ./server.key \ + -L [CLIENT-URL]/metrics \ + --ciphers ECDHE-RSA-AES128-GCM-SHA256 + +# request succeeds +etcd_server_version{server_version="3.2.22"} 1 +... +``` + +```bash +# invalid cipher suite +$ curl \ + --cacert ./ca.crt \ + --cert ./server.crt \ + --key ./server.key \ + -L [CLIENT-URL]/metrics \ + --ciphers ECDHE-RSA-DES-CBC3-SHA + +# request fails with +(35) error:14094410:SSL routines:ssl3_read_bytes:sslv3 alert handshake failure +``` + ## Example 3: Transport security & client certificates in a cluster etcd supports the same model as above for **peer communication**, that means the communication between etcd members in a cluster. diff --git a/embed/config.go b/embed/config.go index 850b0c9b063..0fccc74bf61 100644 --- a/embed/config.go +++ b/embed/config.go @@ -32,6 +32,7 @@ import ( "github.com/coreos/etcd/pkg/flags" "github.com/coreos/etcd/pkg/netutil" "github.com/coreos/etcd/pkg/srv" + "github.com/coreos/etcd/pkg/tlsutil" "github.com/coreos/etcd/pkg/transport" "github.com/coreos/etcd/pkg/types" @@ -175,6 +176,11 @@ type Config struct { PeerTLSInfo transport.TLSInfo PeerAutoTLS bool + // CipherSuites is a list of supported TLS cipher suites between + // client/server and peers. If empty, Go auto-populates the list. + // Note that cipher suites are prioritized in the given order. + CipherSuites []string `json:"cipher-suites"` + ClusterState string `json:"initial-cluster-state"` DNSCluster string `json:"discovery-srv"` DNSClusterServiceName string `json:"discovery-srv-name"` @@ -510,6 +516,24 @@ func (cfg *configYAML) configFromFile(path string) error { return cfg.Validate() } +func updateCipherSuites(tls *transport.TLSInfo, ss []string) error { + if len(tls.CipherSuites) > 0 && len(ss) > 0 { + return fmt.Errorf("TLSInfo.CipherSuites is already specified (given %v)", ss) + } + if len(ss) > 0 { + cs := make([]uint16, len(ss)) + for i, s := range ss { + var ok bool + cs[i], ok = tlsutil.GetCipherSuite(s) + if !ok { + return fmt.Errorf("unexpected TLS cipher suite %q", s) + } + } + tls.CipherSuites = cs + } + return nil +} + // Validate ensures that '*embed.Config' fields are properly configured. func (cfg *Config) Validate() error { if err := cfg.setupLogging(); err != nil { @@ -703,39 +727,49 @@ func (cfg Config) defaultClientHost() bool { } func (cfg *Config) ClientSelfCert() (err error) { - if cfg.ClientAutoTLS && cfg.ClientTLSInfo.Empty() { - chosts := make([]string, len(cfg.LCUrls)) - for i, u := range cfg.LCUrls { - chosts[i] = u.Host - } - cfg.ClientTLSInfo, err = transport.SelfCert(cfg.logger, filepath.Join(cfg.Dir, "fixtures", "client"), chosts) - return err - } else if cfg.ClientAutoTLS { + if !cfg.ClientAutoTLS { + return nil + } + if !cfg.ClientTLSInfo.Empty() { if cfg.logger != nil { cfg.logger.Warn("ignoring client auto TLS since certs given") } else { plog.Warningf("ignoring client auto TLS since certs given") } + return nil } - return nil + chosts := make([]string, len(cfg.LCUrls)) + for i, u := range cfg.LCUrls { + chosts[i] = u.Host + } + cfg.ClientTLSInfo, err = transport.SelfCert(cfg.logger, filepath.Join(cfg.Dir, "fixtures", "client"), chosts) + if err != nil { + return err + } + return updateCipherSuites(&cfg.ClientTLSInfo, cfg.CipherSuites) } func (cfg *Config) PeerSelfCert() (err error) { - if cfg.PeerAutoTLS && cfg.PeerTLSInfo.Empty() { - phosts := make([]string, len(cfg.LPUrls)) - for i, u := range cfg.LPUrls { - phosts[i] = u.Host - } - cfg.PeerTLSInfo, err = transport.SelfCert(cfg.logger, filepath.Join(cfg.Dir, "fixtures", "peer"), phosts) - return err - } else if cfg.PeerAutoTLS { + if !cfg.PeerAutoTLS { + return nil + } + if !cfg.PeerTLSInfo.Empty() { if cfg.logger != nil { cfg.logger.Warn("ignoring peer auto TLS since certs given") } else { plog.Warningf("ignoring peer auto TLS since certs given") } + return nil } - return nil + phosts := make([]string, len(cfg.LPUrls)) + for i, u := range cfg.LPUrls { + phosts[i] = u.Host + } + cfg.PeerTLSInfo, err = transport.SelfCert(cfg.logger, filepath.Join(cfg.Dir, "fixtures", "peer"), phosts) + if err != nil { + return err + } + return updateCipherSuites(&cfg.PeerTLSInfo, cfg.CipherSuites) } // UpdateDefaultClusterFromName updates cluster advertise URLs with, if available, default host, diff --git a/embed/etcd.go b/embed/etcd.go index d102243316b..f83eb8e2338 100644 --- a/embed/etcd.go +++ b/embed/etcd.go @@ -375,6 +375,9 @@ func stopServers(ctx context.Context, ss *servers) { func (e *Etcd) Err() <-chan error { return e.errc } func configurePeerListeners(cfg *Config) (peers []*peerListener, err error) { + if err = updateCipherSuites(&cfg.PeerTLSInfo, cfg.CipherSuites); err != nil { + return nil, err + } if err = cfg.PeerSelfCert(); err != nil { if cfg.logger != nil { cfg.logger.Fatal("failed to get peer self-signed certs", zap.Error(err)) @@ -384,7 +387,11 @@ func configurePeerListeners(cfg *Config) (peers []*peerListener, err error) { } if !cfg.PeerTLSInfo.Empty() { if cfg.logger != nil { - cfg.logger.Info("starting with peer TLS", zap.String("tls-info", fmt.Sprintf("%+v", cfg.PeerTLSInfo))) + cfg.logger.Info( + "starting with peer TLS", + zap.String("tls-info", fmt.Sprintf("%+v", cfg.PeerTLSInfo)), + zap.Strings("cipher-suites", cfg.CipherSuites), + ) } else { plog.Infof("peerTLS: %s", cfg.PeerTLSInfo) } @@ -505,6 +512,9 @@ func (e *Etcd) servePeers() (err error) { } func configureClientListeners(cfg *Config) (sctxs map[string]*serveCtx, err error) { + if err = updateCipherSuites(&cfg.ClientTLSInfo, cfg.CipherSuites); err != nil { + return nil, err + } if err = cfg.ClientSelfCert(); err != nil { if cfg.logger != nil { cfg.logger.Fatal("failed to get client self-signed certs", zap.Error(err)) @@ -623,6 +633,7 @@ func (e *Etcd) serveClients() (err error) { e.cfg.logger.Info( "starting with client TLS", zap.String("tls-info", fmt.Sprintf("%+v", e.cfg.ClientTLSInfo)), + zap.Strings("cipher-suites", e.cfg.CipherSuites), ) } else { plog.Infof("ClientTLS: %s", e.cfg.ClientTLSInfo) diff --git a/etcdmain/config.go b/etcdmain/config.go index 43b1d094dd1..dcbbbc99468 100644 --- a/etcdmain/config.go +++ b/etcdmain/config.go @@ -208,6 +208,7 @@ func newConfig() *config { fs.BoolVar(&cfg.ec.PeerAutoTLS, "peer-auto-tls", false, "Peer TLS using generated certificates") fs.StringVar(&cfg.ec.PeerTLSInfo.CRLFile, "peer-crl-file", "", "Path to the peer certificate revocation list file.") fs.StringVar(&cfg.ec.PeerTLSInfo.AllowedCN, "peer-cert-allowed-cn", "", "Allowed CN for inter peer authentication.") + fs.Var(flags.NewStringsValue(""), "cipher-suites", "Comma-separated list of supported TLS cipher suites between client/server and peers (empty will be auto-populated by Go).") fs.Var( flags.NewUniqueURLsWithExceptions("*", "*"), @@ -309,6 +310,8 @@ func (cfg *config) configFromCmdLine() error { cfg.ec.CORS = flags.UniqueURLsMapFromFlag(cfg.cf.flagSet, "cors") cfg.ec.HostWhitelist = flags.UniqueStringsMapFromFlag(cfg.cf.flagSet, "host-whitelist") + cfg.ec.CipherSuites = flags.StringsFromFlag(cfg.cf.flagSet, "cipher-suites") + // TODO: remove this in v3.5 output := flags.UniqueStringsMapFromFlag(cfg.cf.flagSet, "log-output") oss1 := make([]string, 0, len(output)) diff --git a/etcdmain/help.go b/etcdmain/help.go index 5a93874cec9..3c4cb3fdc15 100644 --- a/etcdmain/help.go +++ b/etcdmain/help.go @@ -142,6 +142,8 @@ Security: Peer TLS using self-generated certificates if --peer-key-file and --peer-cert-file are not provided. --peer-crl-file '' Path to the peer certificate revocation list file. + --cipher-suites '' + Comma-separated list of supported TLS cipher suites between client/server and peers (empty will be auto-populated by Go). --cors '*' Comma-separated whitelist of origins for CORS, or cross-origin resource sharing, (empty or * means allow all). --host-whitelist '*' diff --git a/integration/v3_tls_test.go b/integration/v3_tls_test.go new file mode 100644 index 00000000000..5ebe7ad3596 --- /dev/null +++ b/integration/v3_tls_test.go @@ -0,0 +1,71 @@ +// Copyright 2018 The etcd Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package integration + +import ( + "context" + "crypto/tls" + "testing" + "time" + + "github.com/coreos/etcd/clientv3" + "github.com/coreos/etcd/pkg/testutil" +) + +func TestTLSClientCipherSuitesValid(t *testing.T) { testTLSCipherSuites(t, true) } +func TestTLSClientCipherSuitesMismatch(t *testing.T) { testTLSCipherSuites(t, false) } + +// testTLSCipherSuites ensures mismatching client-side cipher suite +// fail TLS handshake with the server. +func testTLSCipherSuites(t *testing.T, valid bool) { + defer testutil.AfterTest(t) + + cipherSuites := []uint16{ + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + } + srvTLS, cliTLS := testTLSInfo, testTLSInfo + if valid { + srvTLS.CipherSuites, cliTLS.CipherSuites = cipherSuites, cipherSuites + } else { + srvTLS.CipherSuites, cliTLS.CipherSuites = cipherSuites[:2], cipherSuites[2:] + } + + clus := NewClusterV3(t, &ClusterConfig{Size: 1, ClientTLS: &srvTLS}) + defer clus.Terminate(t) + + cc, err := cliTLS.ClientConfig() + if err != nil { + t.Fatal(err) + } + cli, cerr := clientv3.New(clientv3.Config{ + Endpoints: []string{clus.Members[0].GRPCAddr()}, + DialTimeout: time.Second, + TLS: cc, + }) + if cli != nil { + cli.Close() + } + if !valid && cerr != context.DeadlineExceeded { + t.Fatalf("expected %v with TLS handshake failure, got %v", context.DeadlineExceeded, cerr) + } + if valid && cerr != nil { + t.Fatalf("expected TLS handshake success, got %v", cerr) + } +} diff --git a/pkg/tlsutil/cipher_suites.go b/pkg/tlsutil/cipher_suites.go new file mode 100644 index 00000000000..b5916bb54dc --- /dev/null +++ b/pkg/tlsutil/cipher_suites.go @@ -0,0 +1,51 @@ +// Copyright 2018 The etcd Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tlsutil + +import "crypto/tls" + +// cipher suites implemented by Go +// https://github.com/golang/go/blob/dev.boringcrypto.go1.10/src/crypto/tls/cipher_suites.go +var cipherSuites = map[string]uint16{ + "TLS_RSA_WITH_RC4_128_SHA": tls.TLS_RSA_WITH_RC4_128_SHA, + "TLS_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, + "TLS_RSA_WITH_AES_128_CBC_SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA, + "TLS_RSA_WITH_AES_256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA, + "TLS_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_RSA_WITH_AES_128_CBC_SHA256, + "TLS_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_RSA_WITH_AES_128_GCM_SHA256, + "TLS_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384, + "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + "TLS_ECDHE_RSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, + "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, +} + +// GetCipherSuite returns the corresponding cipher suite, +// and boolean value if it is supported. +func GetCipherSuite(s string) (uint16, bool) { + v, ok := cipherSuites[s] + return v, ok +} diff --git a/pkg/tlsutil/cipher_suites_test.go b/pkg/tlsutil/cipher_suites_test.go new file mode 100644 index 00000000000..ff6d97ffefb --- /dev/null +++ b/pkg/tlsutil/cipher_suites_test.go @@ -0,0 +1,42 @@ +// Copyright 2018 The etcd Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tlsutil + +import ( + "go/importer" + "reflect" + "strings" + "testing" +) + +func TestGetCipherSuites(t *testing.T) { + pkg, err := importer.For("source", nil).Import("crypto/tls") + if err != nil { + t.Fatal(err) + } + cm := make(map[string]uint16) + for _, s := range pkg.Scope().Names() { + if strings.HasPrefix(s, "TLS_RSA_") || strings.HasPrefix(s, "TLS_ECDHE_") { + v, ok := GetCipherSuite(s) + if !ok { + t.Fatalf("Go implements missing cipher suite %q (%v)", s, v) + } + cm[s] = v + } + } + if !reflect.DeepEqual(cm, cipherSuites) { + t.Fatalf("found unmatched cipher suites %v (Go) != %v", cm, cipherSuites) + } +} diff --git a/pkg/transport/listener.go b/pkg/transport/listener.go index 466ec4571e5..662a0e1780f 100644 --- a/pkg/transport/listener.go +++ b/pkg/transport/listener.go @@ -74,6 +74,11 @@ type TLSInfo struct { // connection will be closed immediately afterwards. HandshakeFailure func(*tls.Conn, error) + // CipherSuites is a list of supported cipher suites. + // If empty, Go auto-populates it by default. + // Note that cipher suites are prioritized in the given order. + CipherSuites []uint16 + selfCert bool // parseFunc exists to simplify testing. Typically, parseFunc @@ -243,6 +248,10 @@ func (info TLSInfo) baseConfig() (*tls.Config, error) { ServerName: info.ServerName, } + if len(info.CipherSuites) > 0 { + cfg.CipherSuites = info.CipherSuites + } + if info.AllowedCN != "" { cfg.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { for _, chains := range verifiedChains { diff --git a/pkg/transport/transport_test.go b/pkg/transport/transport_test.go new file mode 100644 index 00000000000..f0860f8e706 --- /dev/null +++ b/pkg/transport/transport_test.go @@ -0,0 +1,73 @@ +// Copyright 2018 The etcd Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package transport + +import ( + "crypto/tls" + "net/http" + "strings" + "testing" + "time" +) + +// TestNewTransportTLSInvalidCipherSuites expects a client with invalid +// cipher suites fail to handshake with the server. +func TestNewTransportTLSInvalidCipherSuites(t *testing.T) { + tlsInfo, del, err := createSelfCert() + if err != nil { + t.Fatalf("unable to create cert: %v", err) + } + defer del() + + cipherSuites := []uint16{ + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + } + + // make server and client have unmatched cipher suites + srvTLS, cliTLS := *tlsInfo, *tlsInfo + srvTLS.CipherSuites, cliTLS.CipherSuites = cipherSuites[:2], cipherSuites[2:] + + ln, err := NewListener("127.0.0.1:0", "https", &srvTLS) + if err != nil { + t.Fatalf("unexpected NewListener error: %v", err) + } + defer ln.Close() + + donec := make(chan struct{}) + go func() { + ln.Accept() + donec <- struct{}{} + }() + go func() { + tr, err := NewTransport(cliTLS, 3*time.Second) + if err != nil { + t.Fatalf("unexpected NewTransport error: %v", err) + } + cli := &http.Client{Transport: tr} + _, gerr := cli.Get("https://" + ln.Addr().String()) + if gerr == nil || !strings.Contains(gerr.Error(), "tls: handshake failure") { + t.Fatal("expected client TLS handshake error") + } + ln.Close() + donec <- struct{}{} + }() + <-donec + <-donec +} diff --git a/tests/e2e/cluster_test.go b/tests/e2e/cluster_test.go index 133b3ee359b..4b32db189df 100644 --- a/tests/e2e/cluster_test.go +++ b/tests/e2e/cluster_test.go @@ -117,6 +117,8 @@ type etcdProcessClusterConfig struct { isClientAutoTLS bool isClientCRL bool + cipherSuites []string + forceNewCluster bool initialToken string quotaBackendBytes int64 @@ -307,6 +309,10 @@ func (cfg *etcdProcessClusterConfig) tlsArgs() (args []string) { args = append(args, "--client-crl-file", crlPath, "--client-cert-auth") } + if len(cfg.cipherSuites) > 0 { + args = append(args, "--cipher-suites", strings.Join(cfg.cipherSuites, ",")) + } + return args } diff --git a/tests/e2e/v2_curl_test.go b/tests/e2e/v2_curl_test.go index f50520f0d75..22ef3bdcaaf 100644 --- a/tests/e2e/v2_curl_test.go +++ b/tests/e2e/v2_curl_test.go @@ -130,6 +130,8 @@ type cURLReq struct { header string metricsURLScheme string + + ciphers string } // cURLPrefixArgs builds the beginning of a curl command for a given key @@ -168,6 +170,10 @@ func cURLPrefixArgs(clus *etcdProcessCluster, method string, req cURLReq) []stri cmdArgs = append(cmdArgs, "-H", req.header) } + if req.ciphers != "" { + cmdArgs = append(cmdArgs, "--ciphers", req.ciphers) + } + switch method { case "POST", "PUT": dt := req.value diff --git a/tests/e2e/v3_curl_test.go b/tests/e2e/v3_curl_test.go index 250ea61aab4..1515cfb3112 100644 --- a/tests/e2e/v3_curl_test.go +++ b/tests/e2e/v3_curl_test.go @@ -26,6 +26,7 @@ import ( "github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes" pb "github.com/coreos/etcd/etcdserver/etcdserverpb" "github.com/coreos/etcd/pkg/testutil" + "github.com/coreos/etcd/version" "github.com/grpc-ecosystem/grpc-gateway/runtime" ) @@ -349,6 +350,48 @@ func testV3CurlResignMissiongLeaderKey(cx ctlCtx) { } } +func TestV3CurlCipherSuitesValid(t *testing.T) { testV3CurlCipherSuites(t, true) } +func TestV3CurlCipherSuitesMismatch(t *testing.T) { testV3CurlCipherSuites(t, false) } +func testV3CurlCipherSuites(t *testing.T, valid bool) { + cc := configClientTLS + cc.clusterSize = 1 + cc.cipherSuites = []string{ + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", + } + testFunc := cipherSuiteTestValid + if !valid { + testFunc = cipherSuiteTestMismatch + } + testCtl(t, testFunc, withCfg(cc)) +} + +func cipherSuiteTestValid(cx ctlCtx) { + if err := cURLGet(cx.epc, cURLReq{ + endpoint: "/metrics", + expected: fmt.Sprintf(`etcd_server_version{server_version="%s"} 1`, version.Version), + metricsURLScheme: cx.cfg.metricsURLScheme, + ciphers: "ECDHE-RSA-AES128-GCM-SHA256", // TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 + }); err != nil { + cx.t.Fatalf("failed get with curl (%v)", err) + } +} + +func cipherSuiteTestMismatch(cx ctlCtx) { + if err := cURLGet(cx.epc, cURLReq{ + endpoint: "/metrics", + expected: "alert handshake failure", + metricsURLScheme: cx.cfg.metricsURLScheme, + ciphers: "ECDHE-RSA-DES-CBC3-SHA", // TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA + }); err != nil { + cx.t.Fatalf("failed get with curl (%v)", err) + } +} + // to manually decode; JSON marshals integer fields with // string types, so can't unmarshal with epb.CampaignResponse type campaignResponse struct {