diff --git a/.changelog/9101.txt b/.changelog/9101.txt new file mode 100644 index 000000000..3b49975b2 --- /dev/null +++ b/.changelog/9101.txt @@ -0,0 +1,3 @@ +```release-note:feature +agent: return the default ACL policy to callers as a header +``` diff --git a/agent/config/runtime.go b/agent/config/runtime.go index 4183524ed..4acc823fa 100644 --- a/agent/config/runtime.go +++ b/agent/config/runtime.go @@ -77,8 +77,8 @@ type RuntimeConfig struct { // ACLDefaultPolicy is used to control the ACL interaction when // there is no defined policy. This can be "allow" which means - // ACLs are used to black-list, or "deny" which means ACLs are - // white-lists. + // ACLs are used to deny-list, or "deny" which means ACLs are + // allow-lists. // // hcl: acl.default_policy = ("allow"|"deny") ACLDefaultPolicy string diff --git a/agent/consul/config.go b/agent/consul/config.go index d333058fa..73db2d296 100644 --- a/agent/consul/config.go +++ b/agent/consul/config.go @@ -268,8 +268,8 @@ type Config struct { // ACLDefaultPolicy is used to control the ACL interaction when // there is no defined policy. This can be "allow" which means - // ACLs are used to black-list, or "deny" which means ACLs are - // white-lists. + // ACLs are used to deny-list, or "deny" which means ACLs are + // allow-lists. ACLDefaultPolicy string // ACLDownPolicy controls the behavior of ACLs if the ACLDatacenter diff --git a/agent/http.go b/agent/http.go index 10233fa6b..c1c2b5b1a 100644 --- a/agent/http.go +++ b/agent/http.go @@ -357,6 +357,7 @@ func (s *HTTPHandlers) wrap(handler endpoint, methods []string) http.HandlerFunc return func(resp http.ResponseWriter, req *http.Request) { setHeaders(resp, s.agent.config.HTTPResponseHeaders) setTranslateAddr(resp, s.agent.config.TranslateWANAddrs) + setACLDefaultPolicy(resp, s.agent.config.ACLDefaultPolicy) // Obfuscate any tokens from appearing in the logs formVals, err := url.ParseQuery(req.URL.RawQuery) @@ -697,6 +698,12 @@ func setConsistency(resp http.ResponseWriter, consistency string) { } } +func setACLDefaultPolicy(resp http.ResponseWriter, aclDefaultPolicy string) { + if aclDefaultPolicy != "" { + resp.Header().Set("X-Consul-Default-ACL-Policy", aclDefaultPolicy) + } +} + // setLastContact is used to set the last contact header func setLastContact(resp http.ResponseWriter, last time.Duration) { if last < 0 { diff --git a/agent/http_test.go b/agent/http_test.go index 883bf70c2..48f0b13f5 100644 --- a/agent/http_test.go +++ b/agent/http_test.go @@ -415,6 +415,54 @@ func TestHTTPAPI_TranslateAddrHeader(t *testing.T) { } } +func TestHTTPAPI_DefaultACLPolicy(t *testing.T) { + t.Parallel() + + type testcase struct { + name string + hcl string + expect string + } + + cases := []testcase{ + { + name: "default is allow", + hcl: ``, + expect: "allow", + }, + { + name: "explicit allow", + hcl: `acl { default_policy = "allow" }`, + expect: "allow", + }, + { + name: "explicit deny", + hcl: `acl { default_policy = "deny" }`, + expect: "deny", + }, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + a := NewTestAgent(t, tc.hcl) + defer a.Shutdown() + + resp := httptest.NewRecorder() + handler := func(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + return nil, nil + } + + req, _ := http.NewRequest("GET", "/v1/agent/self", nil) + a.srv.wrap(handler, []string{"GET"})(resp, req) + + require.Equal(t, tc.expect, resp.Header().Get("X-Consul-Default-ACL-Policy")) + }) + } +} + func TestHTTPAPIResponseHeaders(t *testing.T) { t.Parallel() a := NewTestAgent(t, ` diff --git a/api/api.go b/api/api.go index 38a4e98fb..4de3e77c8 100644 --- a/api/api.go +++ b/api/api.go @@ -254,6 +254,11 @@ type QueryMeta struct { // CacheAge is set if request was ?cached and indicates how stale the cached // response is. CacheAge time.Duration + + // DefaultACLPolicy is used to control the ACL interaction when there is no + // defined policy. This can be "allow" which means ACLs are used to + // deny-list, or "deny" which means ACLs are allow-lists. + DefaultACLPolicy string } // WriteMeta is used to return meta data about a write @@ -962,6 +967,12 @@ func parseQueryMeta(resp *http.Response, q *QueryMeta) error { q.AddressTranslationEnabled = false } + // Parse X-Consul-Default-ACL-Policy + switch v := header.Get("X-Consul-Default-ACL-Policy"); v { + case "allow", "deny": + q.DefaultACLPolicy = v + } + // Parse Cache info if cacheStr := header.Get("X-Cache"); cacheStr != "" { q.CacheHit = strings.EqualFold(cacheStr, "HIT") diff --git a/api/api_test.go b/api/api_test.go index b52ab97a2..d4e6dab46 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -840,6 +840,7 @@ func TestAPI_ParseQueryMeta(t *testing.T) { resp.Header.Set("X-Consul-LastContact", "80") resp.Header.Set("X-Consul-KnownLeader", "true") resp.Header.Set("X-Consul-Translate-Addresses", "true") + resp.Header.Set("X-Consul-Default-ACL-Policy", "deny") qm := &QueryMeta{} if err := parseQueryMeta(resp, qm); err != nil { @@ -858,6 +859,9 @@ func TestAPI_ParseQueryMeta(t *testing.T) { if !qm.AddressTranslationEnabled { t.Fatalf("Bad: %v", qm) } + if qm.DefaultACLPolicy != "deny" { + t.Fatalf("Bad: %v", qm) + } } func TestAPI_UnixSocket(t *testing.T) { diff --git a/website/pages/api-docs/index.mdx b/website/pages/api-docs/index.mdx index c9c2b7fbd..0f1d09919 100644 --- a/website/pages/api-docs/index.mdx +++ b/website/pages/api-docs/index.mdx @@ -86,6 +86,18 @@ to allow clients to know if address translation is in effect, the and will have a value of `true`. If translation is not enabled then this header will not be present. +## Default ACL Policy + +All API responses for Consul versions after 1.9 will include an HTTP response +header `X-Consul-Default-ACL-Policy` set to either "allow" or "deny" which +mirrors the current value of the agent's +[`acl.default_policy`](/docs/agent/options#acl_default_policy) option. + +This is also the default [intention](/docs/connect/intentions) enforcement +action if no intention matches. + +This is returned even if ACLs are disabled. + ## UUID Format UUID-format identifiers generated by the Consul API use the