Skip to content

Commit

Permalink
Fix ACL check on health endpoint (#17424)
Browse files Browse the repository at this point in the history
Fix ACL check on health endpoint

Prior to this change, the service health API would not explicitly return an
error whenever a token with invalid permissions was given, and it would instead
return empty results.  With this change, a "Permission denied" error is returned
whenever data is queried. This is done to better support the agent cache, which
performs a fetch backoff sleep whenever ACL errors are encountered.  Affected
endpoints are: `/v1/health/connect/` and `/v1/health/ingress/`.
  • Loading branch information
hashi-derek authored May 24, 2023
1 parent e2f15cf commit a90c9ce
Show file tree
Hide file tree
Showing 4 changed files with 24 additions and 15 deletions.
3 changes: 3 additions & 0 deletions .changelog/17424.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:breaking-change
api: The `/v1/health/connect/` and `/v1/health/ingress/` endpoints now immediately return 403 "Permission Denied" errors whenever a token with insufficient `service:read` permissions is provided. Prior to this change, the endpoints returned a success code with an empty result list when a token with insufficient permissions was provided.
```
19 changes: 8 additions & 11 deletions agent/consul/health_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,14 @@ func (h *Health) ServiceNodes(args *structs.ServiceSpecificRequest, reply *struc
return err
}

// If we're doing a connect or ingress query, we need read access to the service
// we're trying to find proxies for, so check that.
if args.Connect || args.Ingress {
if authz.ServiceRead(args.ServiceName, &authzContext) != acl.Allow {
return acl.ErrPermissionDenied
}
}

filter, err := bexpr.CreateFilter(args.Filter, nil, reply.Nodes)
if err != nil {
return err
Expand All @@ -247,17 +255,6 @@ func (h *Health) ServiceNodes(args *structs.ServiceSpecificRequest, reply *struc
return err
}

// If we're doing a connect or ingress query, we need read access to the service
// we're trying to find proxies for, so check that.
if args.Connect || args.Ingress {
// TODO(acl-error-enhancements) Look for ways to percolate this information up to give any feedback to the user.
if authz.ServiceRead(args.ServiceName, &authzContext) != acl.Allow {
// Return the index here so that the agent cache does not infinitely loop.
reply.Index = index
return nil
}
}

resolvedNodes := nodes
if args.MergeCentralConfig {
for _, node := range resolvedNodes {
Expand Down
7 changes: 3 additions & 4 deletions agent/consul/health_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1125,9 +1125,8 @@ node "foo" {
QueryOptions: structs.QueryOptions{Token: token},
}
var resp structs.IndexedCheckServiceNodes
assert.Nil(t, msgpackrpc.CallWithCodec(codec, "Health.ServiceNodes", &req, &resp))
assert.ErrorContains(t, msgpackrpc.CallWithCodec(codec, "Health.ServiceNodes", &req, &resp), "Permission denied")
assert.Len(t, resp.Nodes, 0)
assert.Greater(t, resp.Index, uint64(0))

// List w/ token. This should work since we're requesting "foo", but should
// also only contain the proxies with names that adhere to our ACL.
Expand Down Expand Up @@ -1465,7 +1464,7 @@ func TestHealth_ServiceNodes_Ingress_ACL(t *testing.T) {
ServiceName: "db",
Ingress: true,
}
require.Nil(t, msgpackrpc.CallWithCodec(codec, "Health.ServiceNodes", &req, &out2))
require.ErrorContains(t, msgpackrpc.CallWithCodec(codec, "Health.ServiceNodes", &req, &out2), "Permission denied")
require.Len(t, out2.Nodes, 0)

// Requesting a service that is not covered by the token's policy
Expand All @@ -1475,7 +1474,7 @@ func TestHealth_ServiceNodes_Ingress_ACL(t *testing.T) {
Ingress: true,
QueryOptions: structs.QueryOptions{Token: token.SecretID},
}
require.Nil(t, msgpackrpc.CallWithCodec(codec, "Health.ServiceNodes", &req, &out2))
require.ErrorContains(t, msgpackrpc.CallWithCodec(codec, "Health.ServiceNodes", &req, &out2), "Permission denied")
require.Len(t, out2.Nodes, 0)

// Requesting service covered by the token's policy
Expand Down
10 changes: 10 additions & 0 deletions website/content/docs/upgrading/upgrade-specific.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ upgrade flow.

## Consul 1.16.x

#### API health endpoints return different status code

Consul versions 1.16.0+ now return an error 403 "Permission denied" status
whenever the `/v1/health/connect/` and `/v1/health/ingress/` endpoints are
queried with insufficient ACL `service:read` privileges.

In Consul versions older than 1.16.0, the service health API did not return an explicit error when given a token with invalid permissions. Instead, it returned an empty list with a success status code.

Before upgrading, ensure that all of your applications can handle this new API behavior properly. An update is not required unless you have a custom application that is querying one of these API endpoints directly.

#### Remove deprecated service-defaults peer upstream override behavior

When configuring a service defaults configuration entry, the [`UpstreamConfig.Overrides` configuration](/consul/docs/connect/config-entries/service-defaults#upstreamconfig-overrides)
Expand Down

0 comments on commit a90c9ce

Please sign in to comment.