consul: Enforce service registration ACLs

This commit is contained in:
Armon Dadgar 2014-11-30 21:05:15 -07:00
parent 7b8faf4cb3
commit d74f79b3fa
4 changed files with 83 additions and 3 deletions

View file

@ -2,10 +2,11 @@ package consul
import ( import (
"fmt" "fmt"
"github.com/armon/go-metrics"
"github.com/hashicorp/consul/consul/structs"
"sort" "sort"
"time" "time"
"github.com/armon/go-metrics"
"github.com/hashicorp/consul/consul/structs"
) )
// Catalog endpoint is used to manipulate the service catalog // Catalog endpoint is used to manipulate the service catalog
@ -35,6 +36,20 @@ func (c *Catalog) Register(args *structs.RegisterRequest, reply *struct{}) error
if args.Service.ID != "" && args.Service.Service == "" { if args.Service.ID != "" && args.Service.Service == "" {
return fmt.Errorf("Must provide service name with ID") return fmt.Errorf("Must provide service name with ID")
} }
// Apply the ACL policy if any
// The 'consul' service is excluded since it is managed
// automatically internally.
if args.Service.Service != ConsulServiceName {
acl, err := c.srv.resolveToken(args.Token)
if err != nil {
return err
} else if acl != nil && !acl.ServiceWrite(args.Service.Service) {
c.srv.logger.Printf("[WARN] consul.catalog: Register of service '%s' on '%s' denied due to ACLs",
args.Service.Service, args.Node)
return permissionDeniedErr
}
}
} }
if args.Check != nil { if args.Check != nil {

View file

@ -45,6 +45,56 @@ func TestCatalogRegister(t *testing.T) {
}) })
} }
func TestCatalogRegister_ACLDeny(t *testing.T) {
dir1, s1 := testServerWithConfig(t, func(c *Config) {
c.ACLDatacenter = "dc1"
c.ACLMasterToken = "root"
c.ACLDefaultPolicy = "deny"
c.ACLToken = "root"
})
defer os.RemoveAll(dir1)
defer s1.Shutdown()
client := rpcClient(t, s1)
defer client.Close()
testutil.WaitForLeader(t, client.Call, "dc1")
// Create the ACL
arg := structs.ACLRequest{
Datacenter: "dc1",
Op: structs.ACLSet,
ACL: structs.ACL{
Name: "User token",
Type: structs.ACLTypeClient,
Rules: testRegisterRules,
},
WriteRequest: structs.WriteRequest{Token: "root"},
}
var out string
if err := client.Call("ACL.Apply", &arg, &out); err != nil {
t.Fatalf("err: %v", err)
}
id := out
argR := structs.RegisterRequest{
Datacenter: "dc1",
Node: "foo",
Address: "127.0.0.1",
Service: &structs.NodeService{
Service: "db",
Tags: []string{"master"},
Port: 8000,
},
WriteRequest: structs.WriteRequest{Token: id},
}
var outR struct{}
err := client.Call("Catalog.Register", &argR, &outR)
if err == nil || !strings.Contains(err.Error(), permissionDenied) {
t.Fatalf("err: %v", err)
}
}
func TestCatalogRegister_ForwardLeader(t *testing.T) { func TestCatalogRegister_ForwardLeader(t *testing.T) {
dir1, s1 := testServer(t) dir1, s1 := testServer(t)
defer os.RemoveAll(dir1) defer os.RemoveAll(dir1)
@ -722,3 +772,9 @@ func TestCatalogRegister_FailedCase1(t *testing.T) {
t.Fatalf("Bad: %v", out2) t.Fatalf("Bad: %v", out2)
} }
} }
var testRegisterRules = `
service "foo" {
policy = "read"
}
`

View file

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"net" "net"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/armon/go-metrics" "github.com/armon/go-metrics"
@ -265,6 +266,11 @@ func (s *Server) reconcileMember(member serf.Member) error {
if err != nil { if err != nil {
s.logger.Printf("[ERR] consul: failed to reconcile member: %v: %v", s.logger.Printf("[ERR] consul: failed to reconcile member: %v: %v",
member, err) member, err)
// Permission denied should not bubble up
if strings.Contains(err.Error(), permissionDenied) {
return nil
}
return err return err
} }
return nil return nil
@ -344,6 +350,7 @@ AFTER_CHECK:
Status: structs.HealthPassing, Status: structs.HealthPassing,
Output: SerfCheckAliveOutput, Output: SerfCheckAliveOutput,
}, },
WriteRequest: structs.WriteRequest{Token: s.config.ACLToken},
} }
var out struct{} var out struct{}
return s.endpoints.Catalog.Register(&req, &out) return s.endpoints.Catalog.Register(&req, &out)
@ -379,6 +386,7 @@ func (s *Server) handleFailedMember(member serf.Member) error {
Status: structs.HealthCritical, Status: structs.HealthCritical,
Output: SerfCheckFailedOutput, Output: SerfCheckFailedOutput,
}, },
WriteRequest: structs.WriteRequest{Token: s.config.ACLToken},
} }
var out struct{} var out struct{}
return s.endpoints.Catalog.Register(&req, &out) return s.endpoints.Catalog.Register(&req, &out)

View file

@ -163,5 +163,6 @@ enforced using an exact match policy. The default rule is provided using
the empty string. The policy is either "read", "write", or "deny". A "write" the empty string. The policy is either "read", "write", or "deny". A "write"
policy implies "read", and there is no way to specify write-only. If there policy implies "read", and there is no way to specify write-only. If there
is no applicable rule, the `acl_default_policy` is applied. Currently, only is no applicable rule, the `acl_default_policy` is applied. Currently, only
the "write" level is enforced for registration of services. the "write" level is enforced for registration of services. The policy for
the "consul" service is always "write" as it is managed internally.