Skip to content

Commit

Permalink
Adds the ability to blacklist specific HTTP endpoints. (#3252)
Browse files Browse the repository at this point in the history
  • Loading branch information
slackpad authored Jul 10, 2017
1 parent 3d8ec60 commit 66edec5
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 3 deletions.
27 changes: 27 additions & 0 deletions agent/blacklist.go
Original file line number Diff line number Diff line change
@@ -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 changes: 39 additions & 0 deletions agent/blacklist_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
8 changes: 8 additions & 0 deletions agent/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@ type DNSConfig struct {

// HTTPConfig is used to fine tune the Http sub-system.
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 map[string]string `mapstructure:"response_headers"`
}
Expand Down Expand Up @@ -1996,6 +2000,9 @@ func MergeConfig(a, b *Config) *Config {
result.SessionTTLMin = b.SessionTTLMin
result.SessionTTLMinRaw = b.SessionTTLMinRaw
}

result.HTTPConfig.BlockEndpoints = append(a.HTTPConfig.BlockEndpoints,
b.HTTPConfig.BlockEndpoints...)
if len(b.HTTPConfig.ResponseHeaders) > 0 {
if result.HTTPConfig.ResponseHeaders == nil {
result.HTTPConfig.ResponseHeaders = make(map[string]string)
Expand All @@ -2004,6 +2011,7 @@ func MergeConfig(a, b *Config) *Config {
result.HTTPConfig.ResponseHeaders[field] = value
}
}

if len(b.Meta) != 0 {
if result.Meta == nil {
result.Meta = make(map[string]string)
Expand Down
8 changes: 8 additions & 0 deletions agent/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,10 @@ func TestDecodeConfig(t *testing.T) {
in: `{"encrypt_verify_outgoing":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"}}`,
c: &Config{HTTPConfig: HTTPConfig{ResponseHeaders: map[string]string{"a": "b", "c": "d"}}},
Expand Down Expand Up @@ -1394,6 +1398,10 @@ func TestMergeConfig(t *testing.T) {
DisableUpdateCheck: true,
DisableAnonymousSignature: true,
HTTPConfig: HTTPConfig{
BlockEndpoints: []string{
"/v1/agent/self",
"/v1/acl",
},
ResponseHeaders: map[string]string{
"Access-Control-Allow-Origin": "*",
},
Expand Down
21 changes: 18 additions & 3 deletions agent/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,20 @@ import (
// HTTPServer provides an HTTP api for an agent.
type HTTPServer struct {
*http.Server
agent *Agent
agent *Agent
blacklist *Blacklist

// proto is filled by the agent to "http" or "https".
proto string
}

func NewHTTPServer(addr string, a *Agent) *HTTPServer {
s := &HTTPServer{Server: &http.Server{Addr: addr}, agent: a}
s.Server.Handler = s.handler(s.agent.config.EnableDebug)
s := &HTTPServer{
Server: &http.Server{Addr: addr},
agent: a,
blacklist: NewBlacklist(a.config.HTTPConfig.BlockEndpoints),
}
s.Server.Handler = s.handler(a.config.EnableDebug)
return s
}

Expand Down Expand Up @@ -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) {
s.agent.logger.Printf("[ERR] http: Request %s %v, error: %v from=%s", req.Method, logURL, err, req.RemoteAddr)
code := http.StatusInternalServerError // 500
Expand Down
36 changes: 36 additions & 0 deletions agent/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
t.Parallel()
// Header should not be present if address translation is off.
Expand Down
11 changes: 11 additions & 0 deletions website/source/docs/agent/options.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,17 @@ Consul will not enable TLS for the HTTP API unless the `https` port has been ass
<br><br>
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>
This object allows adding headers to the HTTP API responses.
For example, the following config can be used to enable
Expand Down

0 comments on commit 66edec5

Please sign in to comment.