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

Adds the ability to blacklist specific HTTP endpoints. #3252

Merged
merged 7 commits into from
Jul 10, 2017
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
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