Skip to content

Commit

Permalink
Merge pull request #9490 from gyuho/cors
Browse files Browse the repository at this point in the history
*: support CORS for v3 HTTP requests
  • Loading branch information
gyuho authored Mar 27, 2018
2 parents 2b77830 + 57f036d commit 473793b
Show file tree
Hide file tree
Showing 17 changed files with 722 additions and 469 deletions.
8 changes: 6 additions & 2 deletions CHANGELOG-3.4.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ See [code changes](https://github.com/coreos/etcd/compare/v3.3.0...v3.4.0) and [
- e.g. exit with error on `ETCD_INITIAL_CLUSTER_TOKEN=abc etcd --initial-cluster-token=def`.
- e.g. exit with error on `ETCDCTL_ENDPOINTS=abc.com ETCDCTL_API=3 etcdctl endpoint health --endpoints=def.com`.
- Change [`etcdserverpb.AuthRoleRevokePermissionRequest/key,range_end` fields type from `string` to `bytes`](https://github.com/coreos/etcd/pull/9433).
- Change [`embed.Config.CorsInfo` in `*cors.CORSInfo` type to `embed.Config.CORS` in `map[string]struct{}` type](https://github.com/coreos/etcd/pull/9490).
- Remove [`pkg/cors` package](https://github.com/coreos/etcd/pull/9490).
- Move `"github.com/coreos/etcd/snap"` to [`"github.com/coreos/etcd/raftsnap"`](https://github.com/coreos/etcd/pull/9211).
- Move `"github.com/coreos/etcd/etcdserver/auth"` to [`"github.com/coreos/etcd/etcdserver/v2auth"`](https://github.com/coreos/etcd/pull/9275).
- Move `"github.com/coreos/etcd/error"` to [`"github.com/coreos/etcd/etcdserver/v2error"`](https://github.com/coreos/etcd/pull/9274).
Expand Down Expand Up @@ -78,11 +80,11 @@ See [security doc](https://github.com/coreos/etcd/blob/master/Documentation/op-g
- Client origin enforce policy works as follow:
- If client connection is secure via HTTPS, allow any hostnames..
- If client connection is not secure and `"HostWhitelist"` is not empty, only allow HTTP requests whose Host field is listed in whitelist.
- By default, `"HostWhitelist"` is empty, which means insecure server allows all client HTTP requests.
- By default, `"HostWhitelist"` is `"*"`, which means insecure server allows all client HTTP requests.
- Note that the client origin policy is enforced whether authentication is enabled or not, for tighter controls.
- 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"`).
- TODO: Support `CORS`.
- Support [`etcd --cors`](https://github.com/coreos/etcd/pull/9490) in v3 HTTP requests (gRPC gateway).
- TODO: 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=<pub key path>,priv-key=<priv key path>,sign-method=<sign method>,ttl=5m`.
Expand All @@ -108,6 +110,7 @@ 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 [`etcd --cors`](https://github.com/coreos/etcd/pull/9490) in v3 HTTP requests (gRPC gateway).

### Added: `embed`

Expand Down Expand Up @@ -140,6 +143,7 @@ See [security doc](https://github.com/coreos/etcd/blob/master/Documentation/op-g
- To deprecate [`/v3beta`](https://github.com/coreos/etcd/issues/9189) in `v3.5`.
- Add API endpoints [`/{v3beta,v3}/lease/leases, /{v3beta,v3}/lease/revoke, /{v3beta,v3}/lease/timetolive`](https://github.com/coreos/etcd/pull/9450).
- To deprecate [`/{v3beta,v3}/kv/lease/leases, /{v3beta,v3}/kv/lease/revoke, /{v3beta,v3}/kv/lease/timetolive`](https://github.com/coreos/etcd/issues/9430) in `v3.5`.
- Support [`etcd --cors`](https://github.com/coreos/etcd/pull/9490) in v3 HTTP requests (gRPC gateway).

### Package `raft`

Expand Down
51 changes: 29 additions & 22 deletions embed/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import (

"github.com/coreos/etcd/compactor"
"github.com/coreos/etcd/etcdserver"
"github.com/coreos/etcd/pkg/cors"
"github.com/coreos/etcd/pkg/flags"
"github.com/coreos/etcd/pkg/netutil"
"github.com/coreos/etcd/pkg/srv"
"github.com/coreos/etcd/pkg/transport"
Expand Down Expand Up @@ -79,9 +79,8 @@ var (
DefaultInitialAdvertisePeerURLs = "http://localhost:2380"
DefaultAdvertiseClientURLs = "http://localhost:2379"

defaultHostname string
defaultHostStatus error
defaultHostWhitelist = []string{} // if empty, allow all
defaultHostname string
defaultHostStatus error
)

var (
Expand All @@ -107,7 +106,6 @@ func init() {

// Config holds the arguments for configuring an etcd server.
type Config struct {
CorsInfo *cors.CORSInfo
LPUrls, LCUrls []url.URL
Dir string `json:"data-dir"`
WalDir string `json:"wal-dir"`
Expand Down Expand Up @@ -171,6 +169,8 @@ type Config struct {
PeerTLSInfo transport.TLSInfo
PeerAutoTLS bool

CORS map[string]struct{}

// HostWhitelist lists acceptable hostnames from HTTP client requests.
// Client origin policy protects against "DNS Rebinding" attacks
// to insecure etcd servers. That is, any website can simply create
Expand All @@ -186,16 +186,16 @@ type Config struct {
// Note that the client origin policy is enforced whether authentication
// is enabled or not, for tighter controls.
//
// By default, "HostWhitelist" is empty, which allows any hostnames.
// By default, "HostWhitelist" is "*", which allows any hostnames.
// Note that when specifying hostnames, loopback addresses are not added
// automatically. To allow loopback interfaces, leave it empty or add them
// to whitelist manually (e.g. "localhost", "127.0.0.1", etc.).
// automatically. To allow loopback interfaces, leave it empty or set it "*",
// or add them to whitelist manually (e.g. "localhost", "127.0.0.1", etc.).
//
// CVE-2018-5702 reference:
// - https://bugs.chromium.org/p/project-zero/issues/detail?id=1447#c2
// - https://github.com/transmission/transmission/pull/468
// - https://github.com/coreos/etcd/issues/9353
HostWhitelist []string `json:"host-whitelist"`
HostWhitelist map[string]struct{}

Debug bool `json:"debug"`
LogPkgLevels string `json:"log-package-levels"`
Expand Down Expand Up @@ -237,11 +237,14 @@ type configYAML struct {

// configJSON has file options that are translated into Config options
type configJSON struct {
LPUrlsJSON string `json:"listen-peer-urls"`
LCUrlsJSON string `json:"listen-client-urls"`
CorsJSON string `json:"cors"`
APUrlsJSON string `json:"initial-advertise-peer-urls"`
ACUrlsJSON string `json:"advertise-client-urls"`
LPUrlsJSON string `json:"listen-peer-urls"`
LCUrlsJSON string `json:"listen-client-urls"`
APUrlsJSON string `json:"initial-advertise-peer-urls"`
ACUrlsJSON string `json:"advertise-client-urls"`

CORSJSON string `json:"cors"`
HostWhitelistJSON string `json:"host-whitelist"`

ClientSecurityJSON securityConfig `json:"client-transport-security"`
PeerSecurityJSON securityConfig `json:"peer-transport-security"`
}
Expand All @@ -261,7 +264,6 @@ func NewConfig() *Config {
lcurl, _ := url.Parse(DefaultListenClientURLs)
acurl, _ := url.Parse(DefaultAdvertiseClientURLs)
cfg := &Config{
CorsInfo: &cors.CORSInfo{},
MaxSnapFiles: DefaultMaxSnapshots,
MaxWalFiles: DefaultMaxWALs,
Name: DefaultName,
Expand All @@ -283,7 +285,8 @@ func NewConfig() *Config {
LogOutput: DefaultLogOutput,
Metrics: "basic",
EnableV2: DefaultEnableV2,
HostWhitelist: defaultHostWhitelist,
CORS: map[string]struct{}{"*": {}},
HostWhitelist: map[string]struct{}{"*": {}},
AuthToken: "simple",
PreVote: false, // TODO: enable by default in v3.5
}
Expand Down Expand Up @@ -381,12 +384,6 @@ func (cfg *configYAML) configFromFile(path string) error {
cfg.LCUrls = []url.URL(u)
}

if cfg.CorsJSON != "" {
if err := cfg.CorsInfo.Set(cfg.CorsJSON); err != nil {
plog.Panicf("unexpected error setting up cors: %v", err)
}
}

if cfg.APUrlsJSON != "" {
u, err := types.NewURLs(strings.Split(cfg.APUrlsJSON, ","))
if err != nil {
Expand All @@ -411,6 +408,16 @@ func (cfg *configYAML) configFromFile(path string) error {
cfg.ListenMetricsUrls = []url.URL(u)
}

if cfg.CORSJSON != "" {
uv := flags.NewUniqueURLsWithExceptions(cfg.CORSJSON, "*")
cfg.CORS = uv.Values
}

if cfg.HostWhitelistJSON != "" {
uv := flags.NewUniqueStringsValue(cfg.HostWhitelistJSON)
cfg.HostWhitelist = uv.Values
}

// If a discovery flag is set, clear default initial cluster set by InitialClusterFromName
if (cfg.Durl != "" || cfg.DNSCluster != "") && cfg.InitialCluster == defaultInitialCluster {
cfg.InitialCluster = ""
Expand Down
34 changes: 20 additions & 14 deletions embed/etcd.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"net"
"net/http"
"net/url"
"sort"
"strconv"
"sync"
"time"
Expand All @@ -33,7 +34,6 @@ import (
"github.com/coreos/etcd/etcdserver/api/v2v3"
"github.com/coreos/etcd/etcdserver/api/v3client"
"github.com/coreos/etcd/etcdserver/api/v3rpc"
"github.com/coreos/etcd/pkg/cors"
"github.com/coreos/etcd/pkg/debugutil"
runtimeutil "github.com/coreos/etcd/pkg/runtime"
"github.com/coreos/etcd/pkg/transport"
Expand Down Expand Up @@ -168,24 +168,35 @@ func StartEtcd(inCfg *Config) (e *Etcd, err error) {
StrictReconfigCheck: cfg.StrictReconfigCheck,
ClientCertAuthEnabled: cfg.ClientTLSInfo.ClientCertAuth,
AuthToken: cfg.AuthToken,
CORS: cfg.CORS,
HostWhitelist: cfg.HostWhitelist,
InitialCorruptCheck: cfg.ExperimentalInitialCorruptCheck,
CorruptCheckTime: cfg.ExperimentalCorruptCheckTime,
PreVote: cfg.PreVote,
Debug: cfg.Debug,
ForceNewCluster: cfg.ForceNewCluster,
}

srvcfg.HostWhitelist = make(map[string]struct{}, len(cfg.HostWhitelist))
for _, h := range cfg.HostWhitelist {
if h != "" {
srvcfg.HostWhitelist[h] = struct{}{}
}
}

if e.Server, err = etcdserver.NewServer(srvcfg); err != nil {
return e, err
}
plog.Infof("%s starting with host whitelist %q", e.Server.ID(), cfg.HostWhitelist)

if len(e.cfg.CORS) > 0 {
ss := make([]string, 0, len(e.cfg.CORS))
for v := range e.cfg.CORS {
ss = append(ss, v)
}
sort.Strings(ss)
plog.Infof("%s starting with cors %q", e.Server.ID(), ss)
}
if len(e.cfg.HostWhitelist) > 0 {
ss := make([]string, 0, len(e.cfg.HostWhitelist))
for v := range e.cfg.HostWhitelist {
ss = append(ss, v)
}
sort.Strings(ss)
plog.Infof("%s starting with host whitelist %q", e.Server.ID(), ss)
}

// buffer channel so goroutines on closed connections won't wait forever
e.errc = make(chan error, len(e.Peers)+len(e.Clients)+2*len(e.sctxs))
Expand Down Expand Up @@ -479,10 +490,6 @@ func (e *Etcd) serveClients() (err error) {
plog.Infof("ClientTLS: %s", e.cfg.ClientTLSInfo)
}

if e.cfg.CorsInfo.String() != "" {
plog.Infof("cors = %s", e.cfg.CorsInfo)
}

// Start a client server goroutine for each listen address
var h http.Handler
if e.Config().EnableV2 {
Expand All @@ -497,7 +504,6 @@ func (e *Etcd) serveClients() (err error) {
etcdhttp.HandleBasic(mux, e.Server)
h = mux
}
h = http.Handler(&cors.CORSHandler{Handler: h, Info: e.cfg.CorsInfo})

gopts := []grpc.ServerOption{}
if e.cfg.GRPCKeepAliveMinTime > time.Duration(0) {
Expand Down
66 changes: 57 additions & 9 deletions embed/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ func (sctx *serveCtx) serve(
httpmux := sctx.createMux(gwmux, handler)

srvhttp := &http.Server{
Handler: wrapMux(s, httpmux),
Handler: createAccessController(s, httpmux),
ErrorLog: logger, // do not log user error
}
httpl := m.Match(cmux.HTTP1())
Expand Down Expand Up @@ -159,7 +159,7 @@ func (sctx *serveCtx) serve(
httpmux := sctx.createMux(gwmux, handler)

srv := &http.Server{
Handler: wrapMux(s, httpmux),
Handler: createAccessController(s, httpmux),
TLSConfig: tlscfg,
ErrorLog: logger, // do not log user error
}
Expand Down Expand Up @@ -250,28 +250,28 @@ func (sctx *serveCtx) createMux(gwmux *gw.ServeMux, handler http.Handler) *http.
return httpmux
}

// wrapMux wraps HTTP multiplexer:
// createAccessController wraps HTTP multiplexer:
// - mutate gRPC gateway request paths
// - check hostname whitelist
// client HTTP requests goes here first
func wrapMux(s *etcdserver.EtcdServer, mux *http.ServeMux) http.Handler {
return &httpWrapper{s: s, mux: mux}
func createAccessController(s *etcdserver.EtcdServer, mux *http.ServeMux) http.Handler {
return &accessController{s: s, mux: mux}
}

type httpWrapper struct {
type accessController struct {
s *etcdserver.EtcdServer
mux *http.ServeMux
}

func (m *httpWrapper) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
func (ac *accessController) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// redirect for backward compatibilities
if req != nil && req.URL != nil && strings.HasPrefix(req.URL.Path, "/v3beta/") {
req.URL.Path = strings.Replace(req.URL.Path, "/v3beta/", "/v3/", 1)
}

if req.TLS == nil { // check origin if client connection is not secure
host := httputil.GetHostname(req)
if !m.s.IsHostWhitelisted(host) {
if !ac.s.AccessController.IsHostWhitelisted(host) {
plog.Warningf("rejecting HTTP request from %q to prevent DNS rebinding attacks", host)
// TODO: use Go's "http.StatusMisdirectedRequest" (421)
// https://github.com/golang/go/commit/4b8a7eafef039af1834ef9bfa879257c4a72b7b5
Expand All @@ -280,7 +280,26 @@ func (m *httpWrapper) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
}
}

m.mux.ServeHTTP(rw, req)
// Write CORS header.
if ac.s.AccessController.OriginAllowed("*") {
addCORSHeader(rw, "*")
} else if origin := req.Header.Get("Origin"); ac.s.OriginAllowed(origin) {
addCORSHeader(rw, origin)
}

if req.Method == "OPTIONS" {
rw.WriteHeader(http.StatusOK)
return
}

ac.mux.ServeHTTP(rw, req)
}

// addCORSHeader adds the correct cors headers given an origin
func addCORSHeader(w http.ResponseWriter, origin string) {
w.Header().Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
w.Header().Add("Access-Control-Allow-Origin", origin)
w.Header().Add("Access-Control-Allow-Headers", "accept, content-type, authorization")
}

// https://github.com/transmission/transmission/pull/468
Expand All @@ -297,6 +316,35 @@ This requirement has been added to help prevent "DNS Rebinding" attacks (CVE-201
`, host)
}

// WrapCORS wraps existing handler with CORS.
// TODO: deprecate this after v2 proxy deprecate
func WrapCORS(cors map[string]struct{}, h http.Handler) http.Handler {
return &corsHandler{
ac: &etcdserver.AccessController{CORS: cors},
h: h,
}
}

type corsHandler struct {
ac *etcdserver.AccessController
h http.Handler
}

func (ch *corsHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if ch.ac.OriginAllowed("*") {
addCORSHeader(rw, "*")
} else if origin := req.Header.Get("Origin"); ch.ac.OriginAllowed(origin) {
addCORSHeader(rw, origin)
}

if req.Method == "OPTIONS" {
rw.WriteHeader(http.StatusOK)
return
}

ch.h.ServeHTTP(rw, req)
}

func (sctx *serveCtx) registerUserHandler(s string, h http.Handler) {
if sctx.userHandlers[s] != nil {
plog.Warningf("path %s already registered by user handler", s)
Expand Down
Loading

0 comments on commit 473793b

Please sign in to comment.