mirror of https://github.com/hashicorp/consul
Adds the ability to blacklist specific HTTP endpoints. (#3252)
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.
|
// 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)
|
||||||
|
|
|
@ -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": "*",
|
||||||
},
|
},
|
||||||
|
|
|
@ -18,13 +18,20 @@ import (
|
||||||
// HTTPServer provides an HTTP api for an agent.
|
// HTTPServer provides an HTTP api for an agent.
|
||||||
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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue