diff --git a/agent/agent.go b/agent/agent.go index 9c74b37605..ff840d1626 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -2115,6 +2115,39 @@ func (a *Agent) RemoveProxy(proxyID string, persist bool) error { return nil } +// verifyProxyToken takes a proxy service ID and a token and verifies +// that the token is allowed to access proxy-related information (leaf +// cert, config, etc.). +// +// The given token may be a local-only proxy token or it may be an ACL +// token. We will attempt to verify the local proxy token first. +func (a *Agent) verifyProxyToken(proxyId, token string) error { + proxy := a.State.Proxy(proxyId) + if proxy == nil { + return fmt.Errorf("unknown proxy service ID: %q", proxyId) + } + + // Easy case is if the token just matches our local proxy token. + // If this happens we can return without any requests. + if token == proxy.ProxyToken { + return nil + } + + // Doesn't match, we have to do a full token resolution. The required + // permission for any proxy-related endpont is service:write, since + // to register a proxy you require that permission and sensitive data + // is usually present in the configuration. + rule, err := a.resolveToken(token) + if err != nil { + return err + } + if rule != nil && !rule.ServiceWrite(proxy.Proxy.TargetServiceID, nil) { + return acl.ErrPermissionDenied + } + + return nil +} + func (a *Agent) cancelCheckMonitors(checkID types.CheckID) { // Stop any monitors delete(a.checkReapAfter, checkID) diff --git a/agent/agent_endpoint.go b/agent/agent_endpoint.go index 8f080ea7a1..de1c0d48ea 100644 --- a/agent/agent_endpoint.go +++ b/agent/agent_endpoint.go @@ -965,6 +965,10 @@ func (s *HTTPServer) AgentConnectProxyConfig(resp http.ResponseWriter, req *http return nil, nil } + // Parse the token + var token string + s.parseToken(req, &token) + // Parse hash specially since it's only this endpoint that uses it currently. // Eventually this should happen in parseWait and end up in QueryOptions but I // didn't want to make very general changes right away. @@ -980,6 +984,11 @@ func (s *HTTPServer) AgentConnectProxyConfig(resp http.ResponseWriter, req *http return "", nil, nil } + // Validate the ACL token + if err := s.agent.verifyProxyToken(id, token); err != nil { + return "", nil, err + } + // Lookup the target service as a convenience target := s.agent.State.Service(proxy.Proxy.TargetServiceID) if target == nil { diff --git a/agent/agent_endpoint_test.go b/agent/agent_endpoint_test.go index fa01eab89a..66ebc59ef0 100644 --- a/agent/agent_endpoint_test.go +++ b/agent/agent_endpoint_test.go @@ -2517,6 +2517,217 @@ func TestAgentConnectProxyConfig_Blocking(t *testing.T) { } } +func TestAgentConnectProxyConfig_aclDefaultDeny(t *testing.T) { + t.Parallel() + + require := require.New(t) + a := NewTestAgent(t.Name(), TestACLConfig()+` + connect { + enabled = true + } + `) + defer a.Shutdown() + + // Register a service with a managed proxy + { + reg := &structs.ServiceDefinition{ + ID: "test-id", + Name: "test", + Address: "127.0.0.1", + Port: 8000, + Check: structs.CheckType{ + TTL: 15 * time.Second, + }, + Connect: &structs.ServiceDefinitionConnect{ + Proxy: &structs.ServiceDefinitionConnectProxy{}, + }, + } + + req, _ := http.NewRequest("PUT", "/v1/agent/service/register?token=root", jsonReader(reg)) + resp := httptest.NewRecorder() + _, err := a.srv.AgentRegisterService(resp, req) + require.NoError(err) + require.Equal(200, resp.Code, "body: %s", resp.Body.String()) + } + + req, _ := http.NewRequest("GET", "/v1/agent/connect/proxy/test-id-proxy", nil) + resp := httptest.NewRecorder() + _, err := a.srv.AgentConnectProxyConfig(resp, req) + require.True(acl.IsErrPermissionDenied(err)) + +} + +func TestAgentConnectProxyConfig_aclProxyToken(t *testing.T) { + t.Parallel() + + require := require.New(t) + a := NewTestAgent(t.Name(), TestACLConfig()+` + connect { + enabled = true + } + `) + defer a.Shutdown() + + // Register a service with a managed proxy + { + reg := &structs.ServiceDefinition{ + ID: "test-id", + Name: "test", + Address: "127.0.0.1", + Port: 8000, + Check: structs.CheckType{ + TTL: 15 * time.Second, + }, + Connect: &structs.ServiceDefinitionConnect{ + Proxy: &structs.ServiceDefinitionConnectProxy{}, + }, + } + + req, _ := http.NewRequest("PUT", "/v1/agent/service/register?token=root", jsonReader(reg)) + resp := httptest.NewRecorder() + _, err := a.srv.AgentRegisterService(resp, req) + require.NoError(err) + require.Equal(200, resp.Code, "body: %s", resp.Body.String()) + } + + // Get the proxy token from the agent directly, since there is no API + // to expose this. + proxy := a.State.Proxy("test-id-proxy") + require.NotNil(proxy) + token := proxy.ProxyToken + require.NotEmpty(token) + + req, _ := http.NewRequest( + "GET", "/v1/agent/connect/proxy/test-id-proxy?token="+token, nil) + resp := httptest.NewRecorder() + obj, err := a.srv.AgentConnectProxyConfig(resp, req) + require.NoError(err) + proxyCfg := obj.(*api.ConnectProxyConfig) + require.Equal("test-id-proxy", proxyCfg.ProxyServiceID) + require.Equal("test-id", proxyCfg.TargetServiceID) + require.Equal("test", proxyCfg.TargetServiceName) +} + +func TestAgentConnectProxyConfig_aclServiceWrite(t *testing.T) { + t.Parallel() + + require := require.New(t) + a := NewTestAgent(t.Name(), TestACLConfig()+` + connect { + enabled = true + } + `) + defer a.Shutdown() + + // Register a service with a managed proxy + { + reg := &structs.ServiceDefinition{ + ID: "test-id", + Name: "test", + Address: "127.0.0.1", + Port: 8000, + Check: structs.CheckType{ + TTL: 15 * time.Second, + }, + Connect: &structs.ServiceDefinitionConnect{ + Proxy: &structs.ServiceDefinitionConnectProxy{}, + }, + } + + req, _ := http.NewRequest("PUT", "/v1/agent/service/register?token=root", jsonReader(reg)) + resp := httptest.NewRecorder() + _, err := a.srv.AgentRegisterService(resp, req) + require.NoError(err) + require.Equal(200, resp.Code, "body: %s", resp.Body.String()) + } + + // Create an ACL with service:write for our service + var token string + { + args := map[string]interface{}{ + "Name": "User Token", + "Type": "client", + "Rules": `service "test" { policy = "write" }`, + } + req, _ := http.NewRequest("PUT", "/v1/acl/create?token=root", jsonReader(args)) + resp := httptest.NewRecorder() + obj, err := a.srv.ACLCreate(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + aclResp := obj.(aclCreateResponse) + token = aclResp.ID + } + + req, _ := http.NewRequest( + "GET", "/v1/agent/connect/proxy/test-id-proxy?token="+token, nil) + resp := httptest.NewRecorder() + obj, err := a.srv.AgentConnectProxyConfig(resp, req) + require.NoError(err) + proxyCfg := obj.(*api.ConnectProxyConfig) + require.Equal("test-id-proxy", proxyCfg.ProxyServiceID) + require.Equal("test-id", proxyCfg.TargetServiceID) + require.Equal("test", proxyCfg.TargetServiceName) +} + +func TestAgentConnectProxyConfig_aclServiceReadDeny(t *testing.T) { + t.Parallel() + + require := require.New(t) + a := NewTestAgent(t.Name(), TestACLConfig()+` + connect { + enabled = true + } + `) + defer a.Shutdown() + + // Register a service with a managed proxy + { + reg := &structs.ServiceDefinition{ + ID: "test-id", + Name: "test", + Address: "127.0.0.1", + Port: 8000, + Check: structs.CheckType{ + TTL: 15 * time.Second, + }, + Connect: &structs.ServiceDefinitionConnect{ + Proxy: &structs.ServiceDefinitionConnectProxy{}, + }, + } + + req, _ := http.NewRequest("PUT", "/v1/agent/service/register?token=root", jsonReader(reg)) + resp := httptest.NewRecorder() + _, err := a.srv.AgentRegisterService(resp, req) + require.NoError(err) + require.Equal(200, resp.Code, "body: %s", resp.Body.String()) + } + + // Create an ACL with service:read for our service + var token string + { + args := map[string]interface{}{ + "Name": "User Token", + "Type": "client", + "Rules": `service "test" { policy = "read" }`, + } + req, _ := http.NewRequest("PUT", "/v1/acl/create?token=root", jsonReader(args)) + resp := httptest.NewRecorder() + obj, err := a.srv.ACLCreate(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + aclResp := obj.(aclCreateResponse) + token = aclResp.ID + } + + req, _ := http.NewRequest( + "GET", "/v1/agent/connect/proxy/test-id-proxy?token="+token, nil) + resp := httptest.NewRecorder() + _, err := a.srv.AgentConnectProxyConfig(resp, req) + require.True(acl.IsErrPermissionDenied(err)) +} + func TestAgentConnectProxyConfig_ConfigHandling(t *testing.T) { t.Parallel()