Skip to content

Commit a7e5790

Browse files
authored
Merge pull request #9372 from gyuho/origin
*: mitigate DNS rebinding attacks in insecure etcd server
2 parents f0eb772 + 9f0027d commit a7e5790

File tree

14 files changed

+211
-17
lines changed

14 files changed

+211
-17
lines changed

CHANGELOG-3.4.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,15 @@ See [code changes](https://github.com/coreos/etcd/compare/v3.3.0...v3.4.0) and [
5252
- If not given, etcd queries `_etcd-server-ssl._tcp.[YOUR_HOST]` and `_etcd-server._tcp.[YOUR_HOST]`.
5353
- If `--discovery-srv-name="foo"`, then query `_etcd-server-ssl-foo._tcp.[YOUR_HOST]` and `_etcd-server-foo._tcp.[YOUR_HOST]`.
5454
- Useful for operating multiple etcd clusters under the same domain.
55+
- Add [`--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.
56+
- 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).
57+
- Client origin enforce policy works as follow:
58+
- If client connection is secure via HTTPS, allow any hostnames..
59+
- If client connection is not secure and `"HostWhitelist"` is not empty, only allow HTTP requests whose Host field is listed in whitelist.
60+
- By default, `"HostWhitelist"` is empty, which means insecure server allows all client HTTP requests.
61+
- Note that the client origin policy is enforced whether authentication is enabled or not, for tighter controls.
62+
- 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.).
63+
- 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"`).
5564
- Define `embed.CompactorModePeriodic` for `compactor.ModePeriodic`.
5665
- Define `embed.CompactorModeRevision` for `compactor.ModeRevision`.
5766

Documentation/op-guide/security.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,19 @@ I | embed: serving client requests on 127.0.0.1:22379
321321
I | embed: serving client requests on 127.0.0.1:2379
322322
```
323323

324+
## Notes for Host Whitelist
325+
326+
`etcd --host-whitelist` flag specifies acceptable hostnames from HTTP client requests. Client origin policy protects against ["DNS Rebinding"](https://en.wikipedia.org/wiki/DNS_rebinding) attacks to insecure etcd servers. That is, 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. See [CVE-2018-5702](https://bugs.chromium.org/p/project-zero/issues/detail?id=1447#c2) for more detail.
327+
328+
Client origin policy works as follows:
329+
330+
1. If client connection is secure via HTTPS, allow any hostnames.
331+
2. If client connection is not secure and `"HostWhitelist"` is not empty, only allow HTTP requests whose Host field is listed in whitelist.
332+
333+
Note that the client origin policy is enforced whether authentication is enabled or not, for tighter controls.
334+
335+
By default, `etcd --host-whitelist` and `embed.Config.HostWhitelist` are set *empty* to allow all hostnames. Note that 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.).
336+
324337
## Frequently asked questions
325338

326339
### I'm seeing a SSLv3 alert handshake failure when using TLS client authentication?

embed/config.go

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,9 @@ var (
7979
DefaultInitialAdvertisePeerURLs = "http://localhost:2380"
8080
DefaultAdvertiseClientURLs = "http://localhost:2379"
8181

82-
defaultHostname string
83-
defaultHostStatus error
82+
defaultHostname string
83+
defaultHostStatus error
84+
defaultHostWhitelist = []string{} // if empty, allow all
8485
)
8586

8687
var (
@@ -171,6 +172,32 @@ type Config struct {
171172
PeerTLSInfo transport.TLSInfo
172173
PeerAutoTLS bool
173174

175+
// HostWhitelist lists acceptable hostnames from HTTP client requests.
176+
// Client origin policy protects against "DNS Rebinding" attacks
177+
// to insecure etcd servers. That is, any website can simply create
178+
// an authorized DNS name, and direct DNS to "localhost" (or any
179+
// other address). Then, all HTTP endpoints of etcd server listening
180+
// on "localhost" becomes accessible, thus vulnerable to DNS rebinding
181+
// attacks. See "CVE-2018-5702" for more detail.
182+
//
183+
// 1. If client connection is secure via HTTPS, allow any hostnames.
184+
// 2. If client connection is not secure and "HostWhitelist" is not empty,
185+
// only allow HTTP requests whose Host field is listed in whitelist.
186+
//
187+
// Note that the client origin policy is enforced whether authentication
188+
// is enabled or not, for tighter controls.
189+
//
190+
// By default, "HostWhitelist" is empty, which allows any hostnames.
191+
// Note that when specifying hostnames, loopback addresses are not added
192+
// automatically. To allow loopback interfaces, leave it empty or add them
193+
// to whitelist manually (e.g. "localhost", "127.0.0.1", etc.).
194+
//
195+
// CVE-2018-5702 reference:
196+
// - https://bugs.chromium.org/p/project-zero/issues/detail?id=1447#c2
197+
// - https://github.com/transmission/transmission/pull/468
198+
// - https://github.com/coreos/etcd/issues/9353
199+
HostWhitelist []string `json:"host-whitelist"`
200+
174201
// debug
175202

176203
Debug bool `json:"debug"`
@@ -264,6 +291,7 @@ func NewConfig() *Config {
264291
LogOutput: DefaultLogOutput,
265292
Metrics: "basic",
266293
EnableV2: DefaultEnableV2,
294+
HostWhitelist: defaultHostWhitelist,
267295
AuthToken: "simple",
268296
}
269297
cfg.InitialCluster = cfg.InitialClusterFromName(cfg.Name)

embed/etcd.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,17 @@ func StartEtcd(inCfg *Config) (e *Etcd, err error) {
174174
Debug: cfg.Debug,
175175
}
176176

177+
srvcfg.HostWhitelist = make(map[string]struct{}, len(cfg.HostWhitelist))
178+
for _, h := range cfg.HostWhitelist {
179+
if h != "" {
180+
srvcfg.HostWhitelist[h] = struct{}{}
181+
}
182+
}
183+
177184
if e.Server, err = etcdserver.NewServer(srvcfg); err != nil {
178185
return e, err
179186
}
187+
plog.Infof("%s starting with host whitelist %q", e.Server.ID(), cfg.HostWhitelist)
180188

181189
// buffer channel so goroutines on closed connections won't wait forever
182190
e.errc = make(chan error, len(e.Peers)+len(e.Clients)+2*len(e.sctxs))

embed/serve.go

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package embed
1616

1717
import (
1818
"context"
19+
"fmt"
1920
"io/ioutil"
2021
defaultLog "log"
2122
"net"
@@ -33,6 +34,7 @@ import (
3334
"github.com/coreos/etcd/etcdserver/api/v3rpc"
3435
etcdservergw "github.com/coreos/etcd/etcdserver/etcdserverpb/gw"
3536
"github.com/coreos/etcd/pkg/debugutil"
37+
"github.com/coreos/etcd/pkg/httputil"
3638
"github.com/coreos/etcd/pkg/transport"
3739

3840
gw "github.com/grpc-ecosystem/grpc-gateway/runtime"
@@ -114,7 +116,7 @@ func (sctx *serveCtx) serve(
114116
httpmux := sctx.createMux(gwmux, handler)
115117

116118
srvhttp := &http.Server{
117-
Handler: wrapMux(httpmux),
119+
Handler: wrapMux(s, httpmux),
118120
ErrorLog: logger, // do not log user error
119121
}
120122
httpl := m.Match(cmux.HTTP1())
@@ -157,7 +159,7 @@ func (sctx *serveCtx) serve(
157159
httpmux := sctx.createMux(gwmux, handler)
158160

159161
srv := &http.Server{
160-
Handler: wrapMux(httpmux),
162+
Handler: wrapMux(s, httpmux),
161163
TLSConfig: tlscfg,
162164
ErrorLog: logger, // do not log user error
163165
}
@@ -252,11 +254,12 @@ func (sctx *serveCtx) createMux(gwmux *gw.ServeMux, handler http.Handler) *http.
252254
// - mutate gRPC gateway request paths
253255
// - check hostname whitelist
254256
// client HTTP requests goes here first
255-
func wrapMux(mux *http.ServeMux) http.Handler {
256-
return &httpWrapper{mux: mux}
257+
func wrapMux(s *etcdserver.EtcdServer, mux *http.ServeMux) http.Handler {
258+
return &httpWrapper{s: s, mux: mux}
257259
}
258260

259261
type httpWrapper struct {
262+
s *etcdserver.EtcdServer
260263
mux *http.ServeMux
261264
}
262265

@@ -265,9 +268,35 @@ func (m *httpWrapper) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
265268
if req != nil && req.URL != nil && strings.HasPrefix(req.URL.Path, "/v3beta/") {
266269
req.URL.Path = strings.Replace(req.URL.Path, "/v3beta/", "/v3/", 1)
267270
}
271+
272+
if req.TLS == nil { // check origin if client connection is not secure
273+
host := httputil.GetHostname(req)
274+
if !m.s.IsHostWhitelisted(host) {
275+
plog.Warningf("rejecting HTTP request from %q to prevent DNS rebinding attacks", host)
276+
// TODO: use Go's "http.StatusMisdirectedRequest" (421)
277+
// https://github.com/golang/go/commit/4b8a7eafef039af1834ef9bfa879257c4a72b7b5
278+
http.Error(rw, errCVE20185702(host), 421)
279+
return
280+
}
281+
}
282+
268283
m.mux.ServeHTTP(rw, req)
269284
}
270285

286+
// https://github.com/transmission/transmission/pull/468
287+
func errCVE20185702(host string) string {
288+
return fmt.Sprintf(`
289+
etcd received your request, but the Host header was unrecognized.
290+
291+
To fix this, choose one of the following options:
292+
- Enable TLS, then any HTTPS request will be allowed.
293+
- Add the hostname you want to use to the whitelist in settings.
294+
- e.g. etcd --host-whitelist %q
295+
296+
This requirement has been added to help prevent "DNS Rebinding" attacks (CVE-2018-5702).
297+
`, host)
298+
}
299+
271300
func (sctx *serveCtx) registerUserHandler(s string, h http.Handler) {
272301
if sctx.userHandlers[s] != nil {
273302
plog.Warningf("path %s already registered by user handler", s)

etcdmain/config.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,11 @@ type config struct {
8585

8686
// configFlags has the set of flags used for command line parsing a Config
8787
type configFlags struct {
88-
flagSet *flag.FlagSet
89-
clusterState *flags.StringsFlag
90-
fallback *flags.StringsFlag
91-
proxy *flags.StringsFlag
88+
flagSet *flag.FlagSet
89+
hostWhitelist string
90+
clusterState *flags.StringsFlag
91+
fallback *flags.StringsFlag
92+
proxy *flags.StringsFlag
9293
}
9394

9495
func newConfig() *config {
@@ -189,6 +190,7 @@ func newConfig() *config {
189190
fs.BoolVar(&cfg.ec.PeerAutoTLS, "peer-auto-tls", false, "Peer TLS using generated certificates")
190191
fs.StringVar(&cfg.ec.PeerTLSInfo.CRLFile, "peer-crl-file", "", "Path to the peer certificate revocation list file.")
191192
fs.StringVar(&cfg.ec.PeerTLSInfo.AllowedCN, "peer-cert-allowed-cn", "", "Allowed CN for inter peer authentication.")
193+
fs.StringVar(&cfg.cf.hostWhitelist, "host-whitelist", "", "Comma-separated acceptable hostnames from HTTP client requests, if server is not secure (empty means allow all).")
192194

193195
// logging
194196
fs.BoolVar(&cfg.ec.Debug, "debug", false, "Enable debug-level logging for etcd.")
@@ -275,6 +277,15 @@ func (cfg *config) configFromCmdLine() error {
275277
cfg.ec.ListenMetricsUrls = []url.URL(u)
276278
}
277279

280+
hosts := []string{}
281+
for _, h := range strings.Split(cfg.cf.hostWhitelist, ",") {
282+
h = strings.TrimSpace(h)
283+
if h != "" {
284+
hosts = append(hosts, h)
285+
}
286+
}
287+
cfg.ec.HostWhitelist = hosts
288+
278289
cfg.ec.ClusterState = cfg.cf.clusterState.String()
279290
cfg.cp.Fallback = cfg.cf.fallback.String()
280291
cfg.cp.Proxy = cfg.cf.proxy.String()

etcdmain/help.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ security flags:
158158
peer TLS using self-generated certificates if --peer-key-file and --peer-cert-file are not provided.
159159
--peer-crl-file ''
160160
path to the peer certificate revocation list file.
161+
--host-whitelist ''
162+
acceptable hostnames from HTTP client requests, if server is not secure (empty means allow all).
161163
162164
logging flags
163165

etcdserver/config.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ type ServerConfig struct {
4747
ForceNewCluster bool
4848
PeerTLSInfo transport.TLSInfo
4949

50+
// HostWhitelist lists acceptable hostnames from client requests.
51+
// If server is insecure (no TLS), server only accepts requests
52+
// whose Host header value exists in this white list.
53+
HostWhitelist map[string]struct{}
54+
5055
TickMs uint
5156
ElectionTicks int
5257
BootstrapTimeout time.Duration

etcdserver/server.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,8 @@ type EtcdServer struct {
251251

252252
leadTimeMu sync.RWMutex
253253
leadElectedTime time.Time
254+
255+
hostWhitelist map[string]struct{}
254256
}
255257

256258
// NewServer creates a new EtcdServer from the supplied configuration. The
@@ -434,6 +436,7 @@ func NewServer(cfg ServerConfig) (srv *EtcdServer, err error) {
434436
peerRt: prt,
435437
reqIDGen: idutil.NewGenerator(uint16(id), time.Now()),
436438
forceVersionC: make(chan struct{}),
439+
hostWhitelist: cfg.HostWhitelist,
437440
}
438441

439442
srv.applyV2 = &applierV2store{store: srv.v2store, cluster: srv.cluster}
@@ -626,6 +629,16 @@ func (s *EtcdServer) ReportSnapshot(id uint64, status raft.SnapshotStatus) {
626629
s.r.ReportSnapshot(id, status)
627630
}
628631

632+
// IsHostWhitelisted returns true if the host is whitelisted.
633+
// If whitelist is empty, allow all.
634+
func (s *EtcdServer) IsHostWhitelisted(host string) bool {
635+
if len(s.hostWhitelist) == 0 { // allow all
636+
return true
637+
}
638+
_, ok := s.hostWhitelist[host]
639+
return ok
640+
}
641+
629642
type etcdProgress struct {
630643
confState raftpb.ConfState
631644
snapi uint64

hack/scripts-dev/docker-dns/certs/run.sh

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ ETCDCTL_API=3 ./etcdctl \
3232
--endpoints=https://m1.etcd.local:2379,https://m2.etcd.local:22379,https://m3.etcd.local:32379 \
3333
get abc
3434

35-
# TODO: add host header check to enforce same-origin-policy
3635
printf "\nWriting v2 key...\n"
3736
curl -L https://127.0.0.1:2379/v2/keys/queue \
3837
--cacert /certs/ca.crt \

0 commit comments

Comments
 (0)