Adds the ability to blacklist specific HTTP endpoints. (#3252)

pull/3153/merge
James Phillips 2017-07-10 13:51:25 -07:00 committed by GitHub
parent 3d8ec60d2d
commit 66edec5dfd
7 changed files with 147 additions and 3 deletions

27
agent/blacklist.go Normal file
View File

@ -0,0 +1,27 @@
package agent
import (
"github.com/armon/go-radix"
)
// Blacklist implements an HTTP endpoint blacklist based on a list of endpoint
// prefixes which should be blocked.
type Blacklist struct {
tree *radix.Tree
}
// NewBlacklist returns a blacklist for the given list of prefixes.
func NewBlacklist(prefixes []string) *Blacklist {
tree := radix.New()
for _, prefix := range prefixes {
tree.Insert(prefix, nil)
}
return &Blacklist{tree}
}
// Block will return true if the given path is included among any of the
// blocked prefixes.
func (b *Blacklist) Block(path string) bool {
_, _, blocked := b.tree.LongestPrefix(path)
return blocked
}

39
agent/blacklist_test.go Normal file
View File

@ -0,0 +1,39 @@
package agent
import (
"testing"
)
func TestBlacklist(t *testing.T) {
t.Parallel()
complex := []string{
"/a",
"/b/c",
}
tests := []struct {
desc string
prefixes []string
path string
block bool
}{
{"nothing blocked root", nil, "/", false},
{"nothing blocked path", nil, "/a", false},
{"exact match 1", complex, "/a", true},
{"exact match 2", complex, "/b/c", true},
{"subpath", complex, "/a/b", true},
{"longer prefix", complex, "/apple", true},
{"longer subpath", complex, "/b/c/d", true},
{"partial prefix", complex, "/b/d", false},
{"no match", complex, "/c", false},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
blacklist := NewBlacklist(tt.prefixes)
if got, want := blacklist.Block(tt.path), tt.block; got != want {
t.Fatalf("got %v want %v", got, want)
}
})
}
}

View File

@ -131,6 +131,10 @@ type DNSConfig struct {
// HTTPConfig is used to fine tune the Http sub-system. // HTTPConfig is used to fine tune the Http sub-system.
type HTTPConfig struct { type HTTPConfig struct {
// BlockEndpoints is a list of endpoint prefixes to block in the
// HTTP API. Any requests to these will get a 403 response.
BlockEndpoints []string `mapstructure:"block_endpoints"`
// ResponseHeaders are used to add HTTP header response fields to the HTTP API responses. // ResponseHeaders are used to add HTTP header response fields to the HTTP API responses.
ResponseHeaders map[string]string `mapstructure:"response_headers"` ResponseHeaders map[string]string `mapstructure:"response_headers"`
} }
@ -1996,6 +2000,9 @@ func MergeConfig(a, b *Config) *Config {
result.SessionTTLMin = b.SessionTTLMin result.SessionTTLMin = b.SessionTTLMin
result.SessionTTLMinRaw = b.SessionTTLMinRaw result.SessionTTLMinRaw = b.SessionTTLMinRaw
} }
result.HTTPConfig.BlockEndpoints = append(a.HTTPConfig.BlockEndpoints,
b.HTTPConfig.BlockEndpoints...)
if len(b.HTTPConfig.ResponseHeaders) > 0 { if len(b.HTTPConfig.ResponseHeaders) > 0 {
if result.HTTPConfig.ResponseHeaders == nil { if result.HTTPConfig.ResponseHeaders == nil {
result.HTTPConfig.ResponseHeaders = make(map[string]string) result.HTTPConfig.ResponseHeaders = make(map[string]string)
@ -2004,6 +2011,7 @@ func MergeConfig(a, b *Config) *Config {
result.HTTPConfig.ResponseHeaders[field] = value result.HTTPConfig.ResponseHeaders[field] = value
} }
} }
if len(b.Meta) != 0 { if len(b.Meta) != 0 {
if result.Meta == nil { if result.Meta == nil {
result.Meta = make(map[string]string) result.Meta = make(map[string]string)

View File

@ -330,6 +330,10 @@ func TestDecodeConfig(t *testing.T) {
in: `{"encrypt_verify_outgoing":true}`, in: `{"encrypt_verify_outgoing":true}`,
c: &Config{EncryptVerifyOutgoing: Bool(true)}, c: &Config{EncryptVerifyOutgoing: Bool(true)},
}, },
{
in: `{"http_config":{"block_endpoints":["a","b","c","d"]}}`,
c: &Config{HTTPConfig: HTTPConfig{BlockEndpoints: []string{"a", "b", "c", "d"}}},
},
{ {
in: `{"http_api_response_headers":{"a":"b","c":"d"}}`, in: `{"http_api_response_headers":{"a":"b","c":"d"}}`,
c: &Config{HTTPConfig: HTTPConfig{ResponseHeaders: map[string]string{"a": "b", "c": "d"}}}, c: &Config{HTTPConfig: HTTPConfig{ResponseHeaders: map[string]string{"a": "b", "c": "d"}}},
@ -1394,6 +1398,10 @@ func TestMergeConfig(t *testing.T) {
DisableUpdateCheck: true, DisableUpdateCheck: true,
DisableAnonymousSignature: true, DisableAnonymousSignature: true,
HTTPConfig: HTTPConfig{ HTTPConfig: HTTPConfig{
BlockEndpoints: []string{
"/v1/agent/self",
"/v1/acl",
},
ResponseHeaders: map[string]string{ ResponseHeaders: map[string]string{
"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Origin": "*",
}, },

View File

@ -19,12 +19,19 @@ import (
type HTTPServer struct { type HTTPServer struct {
*http.Server *http.Server
agent *Agent agent *Agent
blacklist *Blacklist
// proto is filled by the agent to "http" or "https".
proto string proto string
} }
func NewHTTPServer(addr string, a *Agent) *HTTPServer { func NewHTTPServer(addr string, a *Agent) *HTTPServer {
s := &HTTPServer{Server: &http.Server{Addr: addr}, agent: a} s := &HTTPServer{
s.Server.Handler = s.handler(s.agent.config.EnableDebug) Server: &http.Server{Addr: addr},
agent: a,
blacklist: NewBlacklist(a.config.HTTPConfig.BlockEndpoints),
}
s.Server.Handler = s.handler(a.config.EnableDebug)
return s return s
} }
@ -183,6 +190,14 @@ func (s *HTTPServer) wrap(handler func(resp http.ResponseWriter, req *http.Reque
} }
} }
if s.blacklist.Block(req.URL.Path) {
errMsg := "Endpoint is blocked by agent configuration"
s.agent.logger.Printf("[ERR] http: Request %s %v, error: %v from=%s", req.Method, logURL, err, req.RemoteAddr)
resp.WriteHeader(http.StatusForbidden)
fmt.Fprint(resp, errMsg)
return
}
handleErr := func(err error) { handleErr := func(err error) {
s.agent.logger.Printf("[ERR] http: Request %s %v, error: %v from=%s", req.Method, logURL, err, req.RemoteAddr) s.agent.logger.Printf("[ERR] http: Request %s %v, error: %v from=%s", req.Method, logURL, err, req.RemoteAddr)
code := http.StatusInternalServerError // 500 code := http.StatusInternalServerError // 500

View File

@ -195,6 +195,42 @@ func TestSetMeta(t *testing.T) {
} }
} }
func TestHTTPAPI_BlockEndpoints(t *testing.T) {
t.Parallel()
cfg := TestConfig()
cfg.HTTPConfig.BlockEndpoints = []string{
"/v1/agent/self",
}
a := NewTestAgent(t.Name(), cfg)
defer a.Shutdown()
handler := func(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
return nil, nil
}
// Try a blocked endpoint, which should get a 403.
{
req, _ := http.NewRequest("GET", "/v1/agent/self", nil)
resp := httptest.NewRecorder()
a.srv.wrap(handler)(resp, req)
if got, want := resp.Code, http.StatusForbidden; got != want {
t.Fatalf("bad response code got %d want %d", got, want)
}
}
// Make sure some other endpoint still works.
{
req, _ := http.NewRequest("GET", "/v1/agent/checks", nil)
resp := httptest.NewRecorder()
a.srv.wrap(handler)(resp, req)
if got, want := resp.Code, http.StatusOK; got != want {
t.Fatalf("bad response code got %d want %d", got, want)
}
}
}
func TestHTTPAPI_TranslateAddrHeader(t *testing.T) { func TestHTTPAPI_TranslateAddrHeader(t *testing.T) {
t.Parallel() t.Parallel()
// Header should not be present if address translation is off. // Header should not be present if address translation is off.

View File

@ -757,6 +757,17 @@ Consul will not enable TLS for the HTTP API unless the `https` port has been ass
<br><br> <br><br>
The following sub-keys are available: The following sub-keys are available:
* <a name="block_endpoints"></a><a href="#block_endpoints">`block_endpoints`</a>
This object is a list of HTTP endpoint prefixes to block on the agent, and defaults to
an empty list, meaning all endpoints are enabled. Any endpoint that has a common prefix
with one of the entries on this list will be blocked and will return a 403 response code
when accessed. For example, to block all of the V1 ACL endpoints, set this to
`["/v1/acl"]`, which will block `/v1/acl/create`, `/v1/acl/update`, and the other ACL
endpoints that begin with `/v1/acl`. Any CLI commands that use disabled endpoints will
no longer function as well. For more general access control, Consul's
[ACL system](/docs/guides/acl.html) should be used, but this option is useful for removing
access to HTTP endpoints completely, or on specific agents.
* <a name="response_headers"></a><a href="#response_headers">`response_headers`</a> * <a name="response_headers"></a><a href="#response_headers">`response_headers`</a>
This object allows adding headers to the HTTP API responses. This object allows adding headers to the HTTP API responses.
For example, the following config can be used to enable For example, the following config can be used to enable