Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Security fixes #7182

Merged
merged 2 commits into from
Jan 31, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 53 additions & 6 deletions agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"sync"
"time"

"github.com/hashicorp/go-connlimit"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-memdb"

Expand Down Expand Up @@ -305,6 +306,10 @@ type Agent struct {
// store within the data directory. This will prevent loading while writing as
// well as multiple concurrent writes.
persistedTokensLock sync.RWMutex

// httpConnLimiter is used to limit connections to the HTTP server by client
// IP.
httpConnLimiter connlimit.Limiter
}

// New verifies the configuration given has a Datacenter and DataDir
Expand Down Expand Up @@ -524,6 +529,11 @@ func (a *Agent) Start() error {
return err
}

// Configure the http connection limiter.
a.httpConnLimiter.SetConfig(connlimit.Config{
MaxConnsPerClientIP: a.config.HTTPMaxConnsPerClient,
})

// Create listeners and unstarted servers; see comment on listenHTTP why
// we are doing this.
servers, err := a.listenHTTP()
Expand Down Expand Up @@ -866,6 +876,7 @@ func (a *Agent) listenHTTP() ([]*HTTPServer, error) {
tlscfg = a.tlsConfigurator.IncomingHTTPSConfig()
l = tls.NewListener(l, tlscfg)
}

srv := &HTTPServer{
Server: &http.Server{
Addr: l.Addr().String(),
Expand All @@ -878,13 +889,37 @@ func (a *Agent) listenHTTP() ([]*HTTPServer, error) {
}
srv.Server.Handler = srv.handler(a.config.EnableDebug)

// This will enable upgrading connections to HTTP/2 as
// part of TLS negotiation.
// Load the connlimit helper into the server
connLimitFn := a.httpConnLimiter.HTTPConnStateFunc()

if proto == "https" {
// Enforce TLS handshake timeout
srv.Server.ConnState = func(conn net.Conn, state http.ConnState) {
switch state {
case http.StateNew:
// Set deadline to prevent slow send before TLS handshake or first
// byte of request.
conn.SetReadDeadline(time.Now().Add(a.config.HTTPSHandshakeTimeout))
case http.StateActive:
// Clear read deadline. We should maybe set read timeouts more
// generally but that's a bigger task as some HTTP endpoints may
// stream large requests and responses (e.g. snapshot) so we can't
// set sensible blanket timeouts here.
conn.SetReadDeadline(time.Time{})
}
// Pass through to conn limit. This is OK because we didn't change
// state (i.e. Close conn).
connLimitFn(conn, state)
}

// This will enable upgrading connections to HTTP/2 as
// part of TLS negotiation.
err = http2.ConfigureServer(srv.Server, nil)
if err != nil {
return err
}
} else {
srv.Server.ConnState = connLimitFn
}

ln = append(ln, l)
Expand Down Expand Up @@ -1252,10 +1287,18 @@ func (a *Agent) consulConfig() (*consul.Config, error) {
base.RPCMaxBurst = a.config.RPCMaxBurst
}

// RPC-related performance configs.
if a.config.RPCHoldTimeout > 0 {
base.RPCHoldTimeout = a.config.RPCHoldTimeout
// RPC timeouts/limits.
if a.config.RPCHandshakeTimeout > 0 {
base.RPCHandshakeTimeout = a.config.RPCHandshakeTimeout
}
if a.config.RPCMaxConnsPerClient > 0 {
base.RPCMaxConnsPerClient = a.config.RPCMaxConnsPerClient
}

// RPC-related performance configs. We allow explicit zero value to disable so
// copy it whatever the value.
base.RPCHoldTimeout = a.config.RPCHoldTimeout

if a.config.LeaveDrainTime > 0 {
base.LeaveDrainTime = a.config.LeaveDrainTime
}
Expand Down Expand Up @@ -3970,6 +4013,10 @@ func (a *Agent) ReloadConfig(newCfg *config.RuntimeConfig) error {

a.loadLimits(newCfg)

a.httpConnLimiter.SetConfig(connlimit.Config{
MaxConnsPerClientIP: newCfg.HTTPMaxConnsPerClient,
})

for _, s := range a.dnsServers {
if err := s.ReloadConfig(newCfg); err != nil {
return fmt.Errorf("Failed reloading dns config : %v", err)
Expand All @@ -3979,7 +4026,7 @@ func (a *Agent) ReloadConfig(newCfg *config.RuntimeConfig) error {
// this only gets used by the consulConfig function and since
// that is only ever done during init and reload here then
// an in place modification is safe as reloads cannot be
// concurrent due to both gaing a full lock on the stateLock
// concurrent due to both gaining a full lock on the stateLock
a.config.ConfigEntryBootstrap = newCfg.ConfigEntryBootstrap

// create the config for the rpc server/client
Expand Down
27 changes: 27 additions & 0 deletions agent/agent_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -737,10 +737,23 @@ func (s *HTTPServer) AgentHealthServiceByID(resp http.ResponseWriter, req *http.
return nil, err
}

var token string
s.parseToken(req, &token)

// need to resolve to default the meta
var authzContext acl.AuthorizerContext
authz, err := s.agent.resolveTokenAndDefaultMeta(token, &entMeta, &authzContext)
if err != nil {
return nil, err
}

var sid structs.ServiceID
sid.Init(serviceID, &entMeta)

if service := s.agent.State.Service(sid); service != nil {
if authz != nil && authz.ServiceRead(service.Service, &authzContext) != acl.Allow {
return nil, acl.ErrPermissionDenied
}
code, status, healthChecks := agentHealthService(sid, s)
if returnTextPlain(req) {
return status, CodeWithPayloadError{StatusCode: code, Reason: status, ContentType: "text/plain"}
Expand Down Expand Up @@ -777,6 +790,20 @@ func (s *HTTPServer) AgentHealthServiceByName(resp http.ResponseWriter, req *htt
return nil, err
}

var token string
s.parseToken(req, &token)

// need to resolve to default the meta
var authzContext acl.AuthorizerContext
authz, err := s.agent.resolveTokenAndDefaultMeta(token, &entMeta, &authzContext)
if err != nil {
return nil, err
}

if authz != nil && authz.ServiceRead(serviceName, &authzContext) != acl.Allow {
return nil, acl.ErrPermissionDenied
}

code := http.StatusNotFound
status := fmt.Sprintf("ServiceName %s Not Found", serviceName)
services := s.agent.State.Services(&entMeta)
Expand Down
53 changes: 53 additions & 0 deletions agent/agent_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1081,6 +1081,59 @@ func TestAgent_HealthServiceByName(t *testing.T) {
})
}

func TestAgent_HealthServicesACLEnforcement(t *testing.T) {
t.Parallel()
a := NewTestAgent(t, t.Name(), TestACLConfigWithParams(nil))
defer a.Shutdown()

service := &structs.NodeService{
ID: "mysql1",
Service: "mysql",
}
require.NoError(t, a.AddService(service, nil, false, "", ConfigSourceLocal))

service = &structs.NodeService{
ID: "foo1",
Service: "foo",
}
require.NoError(t, a.AddService(service, nil, false, "", ConfigSourceLocal))

// no token
t.Run("no-token-health-by-id", func(t *testing.T) {
req, err := http.NewRequest("GET", "/v1/agent/health/service/id/mysql1", nil)
require.NoError(t, err)
resp := httptest.NewRecorder()
_, err = a.srv.AgentHealthServiceByID(resp, req)
require.Equal(t, acl.ErrPermissionDenied, err)
})

t.Run("no-token-health-by-name", func(t *testing.T) {
req, err := http.NewRequest("GET", "/v1/agent/health/service/name/mysql", nil)
require.NoError(t, err)
resp := httptest.NewRecorder()
_, err = a.srv.AgentHealthServiceByName(resp, req)
require.Equal(t, acl.ErrPermissionDenied, err)
})

t.Run("root-token-health-by-id", func(t *testing.T) {
req, err := http.NewRequest("GET", "/v1/agent/health/service/id/foo1", nil)
require.NoError(t, err)
req.Header.Add("X-Consul-Token", TestDefaultMasterToken)
resp := httptest.NewRecorder()
_, err = a.srv.AgentHealthServiceByID(resp, req)
require.NotEqual(t, acl.ErrPermissionDenied, err)
})

t.Run("root-token-health-by-name", func(t *testing.T) {
req, err := http.NewRequest("GET", "/v1/agent/health/service/name/foo", nil)
require.NoError(t, err)
req.Header.Add("X-Consul-Token", TestDefaultMasterToken)
resp := httptest.NewRecorder()
_, err = a.srv.AgentHealthServiceByName(resp, req)
require.NotEqual(t, acl.ErrPermissionDenied, err)
})
}

func TestAgent_Checks_ACLFilter(t *testing.T) {
t.Parallel()
a := NewTestAgent(t, t.Name(), TestACLConfig())
Expand Down
4 changes: 4 additions & 0 deletions agent/config/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -906,6 +906,8 @@ func (b *Builder) Build() (rt RuntimeConfig, err error) {
EncryptVerifyOutgoing: b.boolVal(c.EncryptVerifyOutgoing),
GRPCPort: grpcPort,
GRPCAddrs: grpcAddrs,
HTTPMaxConnsPerClient: b.intVal(c.Limits.HTTPMaxConnsPerClient),
HTTPSHandshakeTimeout: b.durationVal("limits.https_handshake_timeout", c.Limits.HTTPSHandshakeTimeout),
KeyFile: b.stringVal(c.KeyFile),
KVMaxValueSize: b.uint64Val(c.Limits.KVMaxValueSize),
LeaveDrainTime: b.durationVal("performance.leave_drain_time", c.Performance.LeaveDrainTime),
Expand All @@ -925,8 +927,10 @@ func (b *Builder) Build() (rt RuntimeConfig, err error) {
PrimaryDatacenter: primaryDatacenter,
RPCAdvertiseAddr: rpcAdvertiseAddr,
RPCBindAddr: rpcBindAddr,
RPCHandshakeTimeout: b.durationVal("limits.rpc_handshake_timeout", c.Limits.RPCHandshakeTimeout),
RPCHoldTimeout: b.durationVal("performance.rpc_hold_timeout", c.Performance.RPCHoldTimeout),
RPCMaxBurst: b.intVal(c.Limits.RPCMaxBurst),
RPCMaxConnsPerClient: b.intVal(c.Limits.RPCMaxConnsPerClient),
RPCProtocol: b.intVal(c.RPCProtocol),
RPCRateLimit: rate.Limit(b.float64Val(c.Limits.RPCRate)),
RaftProtocol: b.intVal(c.RaftProtocol),
Expand Down
10 changes: 7 additions & 3 deletions agent/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -675,9 +675,13 @@ type UnixSocket struct {
}

type Limits struct {
RPCMaxBurst *int `json:"rpc_max_burst,omitempty" hcl:"rpc_max_burst" mapstructure:"rpc_max_burst"`
RPCRate *float64 `json:"rpc_rate,omitempty" hcl:"rpc_rate" mapstructure:"rpc_rate"`
KVMaxValueSize *uint64 `json:"kv_max_value_size,omitempty" hcl:"kv_max_value_size" mapstructure:"kv_max_value_size"`
HTTPMaxConnsPerClient *int `json:"http_max_conns_per_client,omitempty" hcl:"http_max_conns_per_client" mapstructure:"http_max_conns_per_client"`
HTTPSHandshakeTimeout *string `json:"https_handshake_timeout,omitempty" hcl:"https_handshake_timeout" mapstructure:"https_handshake_timeout"`
RPCHandshakeTimeout *string `json:"rpc_handshake_timeout,omitempty" hcl:"rpc_handshake_timeout" mapstructure:"rpc_handshake_timeout"`
RPCMaxBurst *int `json:"rpc_max_burst,omitempty" hcl:"rpc_max_burst" mapstructure:"rpc_max_burst"`
RPCMaxConnsPerClient *int `json:"rpc_max_conns_per_client,omitempty" hcl:"rpc_max_conns_per_client" mapstructure:"rpc_max_conns_per_client"`
RPCRate *float64 `json:"rpc_rate,omitempty" hcl:"rpc_rate" mapstructure:"rpc_rate"`
KVMaxValueSize *uint64 `json:"kv_max_value_size,omitempty" hcl:"kv_max_value_size" mapstructure:"kv_max_value_size"`
}

type Segment struct {
Expand Down
4 changes: 4 additions & 0 deletions agent/config/default.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,12 @@ func DefaultSource() Source {
recursor_timeout = "2s"
}
limits = {
http_max_conns_per_client = 100
https_handshake_timeout = "5s"
rpc_handshake_timeout = "5s"
rpc_rate = -1
rpc_max_burst = 1000
rpc_max_conns_per_client = 100
kv_max_value_size = ` + strconv.FormatInt(raft.SuggestedMaxDataSize, 10) + `
}
performance = {
Expand Down
27 changes: 27 additions & 0 deletions agent/config/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,18 @@ type RuntimeConfig struct {
// hcl: client_addr = string addresses { https = string } ports { https = int }
HTTPSAddrs []net.Addr

// HTTPMaxConnsPerClient limits the number of concurrent TCP connections the
// HTTP(S) server will accept from any single source IP address.
//
// hcl: limits{ http_max_conns_per_client = 100 }
HTTPMaxConnsPerClient int

// HTTPSHandshakeTimeout is the time allowed for HTTPS client to complete the
// TLS handshake and send first bytes of the request.
//
// hcl: limits{ https_handshake_timeout = "5s" }
HTTPSHandshakeTimeout time.Duration

// HTTPSPort is the port the HTTP server listens on. The default is -1.
// Setting this to a value <= 0 disables the endpoint.
//
Expand Down Expand Up @@ -927,6 +939,15 @@ type RuntimeConfig struct {
// hcl: bind_addr = string ports { server = int }
RPCBindAddr *net.TCPAddr

// RPCHandshakeTimeout is the timeout for reading the initial magic byte on a
// new RPC connection. If this is set high it may allow unauthenticated users
// to hold connections open arbitrarily long, even when mutual TLS is being
// enforced. It may be set to 0 explicitly to disable the timeout but this
// should never be used in production. Default is 5 seconds.
//
// hcl: limits { rpc_handshake_timeout = "duration" }
RPCHandshakeTimeout time.Duration

// RPCHoldTimeout is how long an RPC can be "held" before it is errored.
// This is used to paper over a loss of leadership by instead holding RPCs,
// so that the caller experiences a slow response rather than an error.
Expand All @@ -949,6 +970,12 @@ type RuntimeConfig struct {
RPCRateLimit rate.Limit
RPCMaxBurst int

// RPCMaxConnsPerClient limits the number of concurrent TCP connections the
// RPC server will accept from any single source IP address.
//
// hcl: limits{ rpc_max_conns_per_client = 100 }
RPCMaxConnsPerClient int

// RPCProtocol is the Consul protocol version to use.
//
// hcl: protocol = int
Expand Down
Loading