mirror of https://github.com/status-im/consul.git
Adds the ability to blacklist specific HTTP endpoints. (#3252)
This commit is contained in:
parent
3d8ec60d2d
commit
66edec5dfd
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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"`
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
|
|
@ -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"}}},
|
||||
|
@ -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": "*",
|
||||
},
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue