Merge pull request #1090 from hashicorp/f-keyring-acl

Keyring ACLs
This commit is contained in:
Ryan Uber 2015-07-24 10:23:18 -07:00
commit 6d38027689
12 changed files with 306 additions and 35 deletions

View File

@ -58,6 +58,13 @@ type ACL interface {
// EventWrite determines if a specific event may be fired.
EventWrite(string) bool
// KeyringRead determines if the encryption keyring used in
// the gossip layer can be read.
KeyringRead() bool
// KeyringWrite determines if the keyring can be manipulated
KeyringWrite() bool
// ACLList checks for permission to list all the ACLs
ACLList() bool
@ -101,6 +108,14 @@ func (s *StaticACL) EventWrite(string) bool {
return s.defaultAllow
}
func (s *StaticACL) KeyringRead() bool {
return s.defaultAllow
}
func (s *StaticACL) KeyringWrite() bool {
return s.defaultAllow
}
func (s *StaticACL) ACLList() bool {
return s.allowManage
}
@ -153,6 +168,11 @@ type PolicyACL struct {
// eventRules contains the user event policies
eventRules *radix.Tree
// keyringRules contains the keyring policies. The keyring has
// a very simple yes/no without prefix mathing, so here we
// don't need to use a radix tree.
keyringRule string
}
// New is used to construct a policy based ACL from a set of policies
@ -180,6 +200,9 @@ func New(parent ACL, policy *Policy) (*PolicyACL, error) {
p.eventRules.Insert(ep.Event, ep.Policy)
}
// Load the keyring policy
p.keyringRule = policy.Keyring
return p, nil
}
@ -321,6 +344,27 @@ func (p *PolicyACL) EventWrite(name string) bool {
return p.parent.EventWrite(name)
}
// KeyringRead is used to determine if the keyring can be
// read by the current ACL token.
func (p *PolicyACL) KeyringRead() bool {
switch p.keyringRule {
case KeyringPolicyRead, KeyringPolicyWrite:
return true
case KeyringPolicyDeny:
return false
default:
return p.parent.KeyringRead()
}
}
// KeyringWrite determines if the keyring can be manipulated.
func (p *PolicyACL) KeyringWrite() bool {
if p.keyringRule == KeyringPolicyWrite {
return true
}
return p.parent.KeyringWrite()
}
// ACLList checks if listing of ACLs is allowed
func (p *PolicyACL) ACLList() bool {
return p.parent.ACLList()

View File

@ -47,6 +47,18 @@ func TestStaticACL(t *testing.T) {
if !all.ServiceWrite("foobar") {
t.Fatalf("should allow")
}
if !all.EventRead("foobar") {
t.Fatalf("should allow")
}
if !all.EventWrite("foobar") {
t.Fatalf("should allow")
}
if !all.KeyringRead() {
t.Fatalf("should allow")
}
if !all.KeyringWrite() {
t.Fatalf("should allow")
}
if all.ACLList() {
t.Fatalf("should not allow")
}
@ -78,6 +90,12 @@ func TestStaticACL(t *testing.T) {
if none.EventWrite("") {
t.Fatalf("should not allow")
}
if none.KeyringRead() {
t.Fatalf("should now allow")
}
if none.KeyringWrite() {
t.Fatalf("should not allow")
}
if none.ACLList() {
t.Fatalf("should not allow")
}
@ -97,6 +115,18 @@ func TestStaticACL(t *testing.T) {
if !manage.ServiceWrite("foobar") {
t.Fatalf("should allow")
}
if !manage.EventRead("foobar") {
t.Fatalf("should allow")
}
if !manage.EventWrite("foobar") {
t.Fatalf("should allow")
}
if !manage.KeyringRead() {
t.Fatalf("should allow")
}
if !manage.KeyringWrite() {
t.Fatalf("should allow")
}
if !manage.ACLList() {
t.Fatalf("should allow")
}
@ -215,6 +245,7 @@ func TestPolicyACL(t *testing.T) {
}
}
// Test the events
type eventcase struct {
inp string
read bool
@ -339,3 +370,30 @@ func TestPolicyACL_Parent(t *testing.T) {
}
}
}
func TestPolicyACL_Keyring(t *testing.T) {
// Test keyring ACLs
type keyringcase struct {
inp string
read bool
write bool
}
keyringcases := []keyringcase{
{"", false, false},
{KeyringPolicyRead, true, false},
{KeyringPolicyWrite, true, true},
{KeyringPolicyDeny, false, false},
}
for _, c := range keyringcases {
acl, err := New(DenyAll(), &Policy{Keyring: c.inp})
if err != nil {
t.Fatalf("bad: %s", err)
}
if acl.KeyringRead() != c.read {
t.Fatalf("bad: %#v", c)
}
if acl.KeyringWrite() != c.write {
t.Fatalf("bad: %#v", c)
}
}
}

View File

@ -16,6 +16,9 @@ const (
EventPolicyRead = "read"
EventPolicyWrite = "write"
EventPolicyDeny = "deny"
KeyringPolicyWrite = "write"
KeyringPolicyRead = "read"
KeyringPolicyDeny = "deny"
)
// Policy is used to represent the policy specified by
@ -25,6 +28,7 @@ type Policy struct {
Keys []*KeyPolicy `hcl:"key,expand"`
Services []*ServicePolicy `hcl:"service,expand"`
Events []*EventPolicy `hcl:"event,expand"`
Keyring string `hcl:"keyring"`
}
// KeyPolicy represents a policy for a key
@ -105,5 +109,15 @@ func Parse(rules string) (*Policy, error) {
}
}
// Validate the keyring policy
switch p.Keyring {
case KeyringPolicyRead:
case KeyringPolicyWrite:
case KeyringPolicyDeny:
case "": // Special case to allow omitting the keyring policy
default:
return nil, fmt.Errorf("Invalid keyring policy: %#v", p.Keyring)
}
return p, nil
}

View File

@ -2,6 +2,7 @@ package acl
import (
"reflect"
"strings"
"testing"
)
@ -34,6 +35,7 @@ event "foo" {
event "bar" {
policy = "deny"
}
keyring = "deny"
`
exp := &Policy{
Keys: []*KeyPolicy{
@ -78,6 +80,7 @@ event "bar" {
Policy: EventPolicyDeny,
},
},
Keyring: KeyringPolicyDeny,
}
out, err := Parse(inp)
@ -124,7 +127,8 @@ func TestParse_JSON(t *testing.T) {
"bar": {
"policy": "deny"
}
}
},
"keyring": "deny"
}`
exp := &Policy{
Keys: []*KeyPolicy{
@ -169,6 +173,7 @@ func TestParse_JSON(t *testing.T) {
Policy: EventPolicyDeny,
},
},
Keyring: KeyringPolicyDeny,
}
out, err := Parse(inp)
@ -180,3 +185,18 @@ func TestParse_JSON(t *testing.T) {
t.Fatalf("bad: %#v %#v", out, exp)
}
}
func TestACLPolicy_badPolicy(t *testing.T) {
cases := []string{
`key "" { policy = "nope" }`,
`service "" { policy = "nope" }`,
`event "" { policy = "nope" }`,
`keyring = "nope"`,
}
for _, c := range cases {
_, err := Parse(c)
if err == nil || !strings.Contains(err.Error(), "Invalid") {
t.Fatalf("expected policy error, got: %#v", err)
}
}
}

View File

@ -121,25 +121,29 @@ func (a *Agent) keyringProcess(args *structs.KeyringRequest) (*structs.KeyringRe
// ListKeys lists out all keys installed on the collective Consul cluster. This
// includes both servers and clients in all DC's.
func (a *Agent) ListKeys() (*structs.KeyringResponses, error) {
func (a *Agent) ListKeys(token string) (*structs.KeyringResponses, error) {
args := structs.KeyringRequest{Operation: structs.KeyringList}
args.Token = token
return a.keyringProcess(&args)
}
// InstallKey installs a new gossip encryption key
func (a *Agent) InstallKey(key string) (*structs.KeyringResponses, error) {
func (a *Agent) InstallKey(key, token string) (*structs.KeyringResponses, error) {
args := structs.KeyringRequest{Key: key, Operation: structs.KeyringInstall}
args.Token = token
return a.keyringProcess(&args)
}
// UseKey changes the primary encryption key used to encrypt messages
func (a *Agent) UseKey(key string) (*structs.KeyringResponses, error) {
func (a *Agent) UseKey(key, token string) (*structs.KeyringResponses, error) {
args := structs.KeyringRequest{Key: key, Operation: structs.KeyringUse}
args.Token = token
return a.keyringProcess(&args)
}
// RemoveKey will remove a gossip encryption key from the keyring
func (a *Agent) RemoveKey(key string) (*structs.KeyringResponses, error) {
func (a *Agent) RemoveKey(key, token string) (*structs.KeyringResponses, error) {
args := structs.KeyringRequest{Key: key, Operation: structs.KeyringRemove}
args.Token = token
return a.keyringProcess(&args)
}

View File

@ -5,7 +5,10 @@ import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
"github.com/hashicorp/consul/testutil"
)
func TestAgent_LoadKeyrings(t *testing.T) {
@ -113,3 +116,66 @@ func TestAgent_InitKeyring(t *testing.T) {
t.Fatalf("bad: %s", content)
}
}
func TestAgentKeyring_ACL(t *testing.T) {
key1 := "tbLJg26ZJyJ9pK3qhc9jig=="
key2 := "4leC33rgtXKIVUr9Nr0snQ=="
conf := nextConfig()
conf.ACLDatacenter = "dc1"
conf.ACLMasterToken = "root"
conf.ACLDefaultPolicy = "deny"
dir, agent := makeAgentKeyring(t, conf, key1)
defer os.RemoveAll(dir)
defer agent.Shutdown()
testutil.WaitForLeader(t, agent.RPC, "dc1")
// List keys without access fails
_, err := agent.ListKeys("")
if err == nil || !strings.Contains(err.Error(), "denied") {
t.Fatalf("expected denied error, got: %#v", err)
}
// List keys with access works
_, err = agent.ListKeys("root")
if err != nil {
t.Fatalf("err: %s", err)
}
// Install without access fails
_, err = agent.InstallKey(key2, "")
if err == nil || !strings.Contains(err.Error(), "denied") {
t.Fatalf("expected denied error, got: %#v", err)
}
// Install with access works
_, err = agent.InstallKey(key2, "root")
if err != nil {
t.Fatalf("err: %s", err)
}
// Use without access fails
_, err = agent.UseKey(key2, "")
if err == nil || !strings.Contains(err.Error(), "denied") {
t.Fatalf("expected denied error, got: %#v", err)
}
// Use with access works
_, err = agent.UseKey(key2, "root")
if err != nil {
t.Fatalf("err: %s", err)
}
// Remove without access fails
_, err = agent.RemoveKey(key1, "")
if err == nil || !strings.Contains(err.Error(), "denied") {
t.Fatalf("expected denied error, got: %#v", err)
}
// Remove with access works
_, err = agent.RemoveKey(key1, "root")
if err != nil {
t.Fatalf("err: %s", err)
}
}

View File

@ -78,6 +78,7 @@ var msgpackHandle = &codec.MsgpackHandle{
type requestHeader struct {
Command string
Seq uint64
Token string
}
// Response header is sent before each response
@ -365,6 +366,7 @@ func (i *AgentRPC) handleRequest(client *rpcClient, reqHeader *requestHeader) er
// Look for a command field
command := reqHeader.Command
seq := reqHeader.Seq
token := reqHeader.Token
// Ensure the handshake is performed before other commands
if command != handshakeCommand && client.version == 0 {
@ -406,7 +408,7 @@ func (i *AgentRPC) handleRequest(client *rpcClient, reqHeader *requestHeader) er
return i.handleReload(client, seq)
case installKeyCommand, useKeyCommand, removeKeyCommand, listKeysCommand:
return i.handleKeyring(client, seq, command)
return i.handleKeyring(client, seq, command, token)
default:
respHeader := responseHeader{Seq: seq, Error: unsupportedCommand}
@ -618,7 +620,7 @@ func (i *AgentRPC) handleReload(client *rpcClient, seq uint64) error {
return client.Send(&resp, nil)
}
func (i *AgentRPC) handleKeyring(client *rpcClient, seq uint64, cmd string) error {
func (i *AgentRPC) handleKeyring(client *rpcClient, seq uint64, cmd, token string) error {
var req keyringRequest
var queryResp *structs.KeyringResponses
var r keyringResponse
@ -632,13 +634,13 @@ func (i *AgentRPC) handleKeyring(client *rpcClient, seq uint64, cmd string) erro
switch cmd {
case listKeysCommand:
queryResp, err = i.agent.ListKeys()
queryResp, err = i.agent.ListKeys(token)
case installKeyCommand:
queryResp, err = i.agent.InstallKey(req.Key)
queryResp, err = i.agent.InstallKey(req.Key, token)
case useKeyCommand:
queryResp, err = i.agent.UseKey(req.Key)
queryResp, err = i.agent.UseKey(req.Key, token)
case removeKeyCommand:
queryResp, err = i.agent.RemoveKey(req.Key)
queryResp, err = i.agent.RemoveKey(req.Key, token)
default:
respHeader := responseHeader{Seq: seq, Error: unsupportedCommand}
client.Send(&respHeader, nil)

View File

@ -188,20 +188,22 @@ func (c *RPCClient) WANMembers() ([]Member, error) {
return resp.Members, err
}
func (c *RPCClient) ListKeys() (keyringResponse, error) {
func (c *RPCClient) ListKeys(token string) (keyringResponse, error) {
header := requestHeader{
Command: listKeysCommand,
Seq: c.getSeq(),
Token: token,
}
var resp keyringResponse
err := c.genericRPC(&header, nil, &resp)
return resp, err
}
func (c *RPCClient) InstallKey(key string) (keyringResponse, error) {
func (c *RPCClient) InstallKey(key, token string) (keyringResponse, error) {
header := requestHeader{
Command: installKeyCommand,
Seq: c.getSeq(),
Token: token,
}
req := keyringRequest{key}
var resp keyringResponse
@ -209,10 +211,11 @@ func (c *RPCClient) InstallKey(key string) (keyringResponse, error) {
return resp, err
}
func (c *RPCClient) UseKey(key string) (keyringResponse, error) {
func (c *RPCClient) UseKey(key, token string) (keyringResponse, error) {
header := requestHeader{
Command: useKeyCommand,
Seq: c.getSeq(),
Token: token,
}
req := keyringRequest{key}
var resp keyringResponse
@ -220,10 +223,11 @@ func (c *RPCClient) UseKey(key string) (keyringResponse, error) {
return resp, err
}
func (c *RPCClient) RemoveKey(key string) (keyringResponse, error) {
func (c *RPCClient) RemoveKey(key, token string) (keyringResponse, error) {
header := requestHeader{
Command: removeKeyCommand,
Seq: c.getSeq(),
Token: token,
}
req := keyringRequest{key}
var resp keyringResponse

View File

@ -325,6 +325,7 @@ func TestRPCClientListKeys(t *testing.T) {
p1 := testRPCClientWithConfig(t, func(c *Config) {
c.EncryptKey = key1
c.Datacenter = "dc1"
c.ACLDatacenter = ""
})
defer p1.Close()
@ -343,6 +344,7 @@ func TestRPCClientInstallKey(t *testing.T) {
key2 := "xAEZ3uVHRMZD9GcYMZaRQw=="
p1 := testRPCClientWithConfig(t, func(c *Config) {
c.EncryptKey = key1
c.ACLDatacenter = ""
})
defer p1.Close()
@ -361,7 +363,7 @@ func TestRPCClientInstallKey(t *testing.T) {
})
// install key2
r, err := p1.client.InstallKey(key2)
r, err := p1.client.InstallKey(key2, "")
if err != nil {
t.Fatalf("err: %s", err)
}
@ -387,11 +389,12 @@ func TestRPCClientUseKey(t *testing.T) {
key2 := "xAEZ3uVHRMZD9GcYMZaRQw=="
p1 := testRPCClientWithConfig(t, func(c *Config) {
c.EncryptKey = key1
c.ACLDatacenter = ""
})
defer p1.Close()
// add a second key to the ring
r, err := p1.client.InstallKey(key2)
r, err := p1.client.InstallKey(key2, "")
if err != nil {
t.Fatalf("err: %s", err)
}
@ -412,21 +415,21 @@ func TestRPCClientUseKey(t *testing.T) {
})
// can't remove key1 yet
r, err = p1.client.RemoveKey(key1)
r, err = p1.client.RemoveKey(key1, "")
if err != nil {
t.Fatalf("err: %s", err)
}
keyringError(t, r)
// change primary key
r, err = p1.client.UseKey(key2)
r, err = p1.client.UseKey(key2, "")
if err != nil {
t.Fatalf("err: %s", err)
}
keyringSuccess(t, r)
// can remove key1 now
r, err = p1.client.RemoveKey(key1)
r, err = p1.client.RemoveKey(key1, "")
if err != nil {
t.Fatalf("err: %s", err)
}
@ -434,10 +437,12 @@ func TestRPCClientUseKey(t *testing.T) {
}
func TestRPCClientKeyOperation_encryptionDisabled(t *testing.T) {
p1 := testRPCClient(t)
p1 := testRPCClientWithConfig(t, func(c *Config) {
c.ACLDatacenter = ""
})
defer p1.Close()
r, err := p1.client.ListKeys()
r, err := p1.client.ListKeys("")
if err != nil {
t.Fatalf("err: %s", err)
}
@ -445,7 +450,7 @@ func TestRPCClientKeyOperation_encryptionDisabled(t *testing.T) {
}
func listKeys(t *testing.T, c *RPCClient) map[string]map[string]int {
resp, err := c.ListKeys()
resp, err := c.ListKeys("")
if err != nil {
t.Fatalf("err: %s", err)
}

View File

@ -16,7 +16,7 @@ type KeyringCommand struct {
}
func (c *KeyringCommand) Run(args []string) int {
var installKey, useKey, removeKey string
var installKey, useKey, removeKey, token string
var listKeys bool
cmdFlags := flag.NewFlagSet("keys", flag.ContinueOnError)
@ -26,6 +26,7 @@ func (c *KeyringCommand) Run(args []string) int {
cmdFlags.StringVar(&useKey, "use", "", "use key")
cmdFlags.StringVar(&removeKey, "remove", "", "remove key")
cmdFlags.BoolVar(&listKeys, "list", false, "list keys")
cmdFlags.StringVar(&token, "token", "", "acl token")
rpcAddr := RPCAddrFlag(cmdFlags)
if err := cmdFlags.Parse(args); err != nil {
@ -65,7 +66,7 @@ func (c *KeyringCommand) Run(args []string) int {
if listKeys {
c.Ui.Info("Gathering installed encryption keys...")
r, err := client.ListKeys()
r, err := client.ListKeys(token)
if err != nil {
c.Ui.Error(fmt.Sprintf("error: %s", err))
return 1
@ -79,7 +80,7 @@ func (c *KeyringCommand) Run(args []string) int {
if installKey != "" {
c.Ui.Info("Installing new gossip encryption key...")
r, err := client.InstallKey(installKey)
r, err := client.InstallKey(installKey, token)
if err != nil {
c.Ui.Error(fmt.Sprintf("error: %s", err))
return 1
@ -89,7 +90,7 @@ func (c *KeyringCommand) Run(args []string) int {
if useKey != "" {
c.Ui.Info("Changing primary gossip encryption key...")
r, err := client.UseKey(useKey)
r, err := client.UseKey(useKey, token)
if err != nil {
c.Ui.Error(fmt.Sprintf("error: %s", err))
return 1
@ -99,7 +100,7 @@ func (c *KeyringCommand) Run(args []string) int {
if removeKey != "" {
c.Ui.Info("Removing gossip encryption key...")
r, err := client.RemoveKey(removeKey)
r, err := client.RemoveKey(removeKey, token)
if err != nil {
c.Ui.Error(fmt.Sprintf("error: %s", err))
return 1
@ -199,13 +200,15 @@ Options:
-install=<key> Install a new encryption key. This will broadcast
the new key to all members in the cluster.
-use=<key> Change the primary encryption key, which is used to
encrypt messages. The key must already be installed
before this operation can succeed.
-list List all keys currently in use within the cluster.
-remove=<key> Remove the given key from the cluster. This
operation may only be performed on keys which are
not currently the primary key.
-list List all keys currently in use within the cluster.
-token="" ACL token to use during requests. Defaults to that
of the agent.
-use=<key> Change the primary encryption key, which is used to
encrypt messages. The key must already be installed
before this operation can succeed.
-rpc-addr=127.0.0.1:8400 RPC address of the Consul agent.
`
return strings.TrimSpace(helpText)

View File

@ -1,6 +1,8 @@
package consul
import (
"fmt"
"github.com/hashicorp/consul/consul/structs"
"github.com/hashicorp/serf/serf"
)
@ -83,6 +85,30 @@ func (m *Internal) KeyringOperation(
args *structs.KeyringRequest,
reply *structs.KeyringResponses) error {
// Check ACLs
acl, err := m.srv.resolveToken(args.Token)
if err != nil {
return err
}
if acl != nil {
switch args.Operation {
case structs.KeyringList:
if !acl.KeyringRead() {
return fmt.Errorf("Reading keyring denied by ACLs")
}
case structs.KeyringInstall:
fallthrough
case structs.KeyringUse:
fallthrough
case structs.KeyringRemove:
if !acl.KeyringWrite() {
return fmt.Errorf("Modifying keyring denied due to ACLs")
}
default:
panic("Invalid keyring operation")
}
}
// Only perform WAN keyring querying and RPC forwarding once
if !args.Forwarded {
args.Forwarded = true

View File

@ -18,8 +18,8 @@ on tokens to which fine grained rules can be applied. It is very similar to
When the ACL system was launched in Consul 0.4, it was only possible to specify
policies for the KV store. In Consul 0.5, ACL policies were extended to service
registrations. In Consul 0.6, ACL's were further extended to restrict the
service discovery mechanisms and user events..
registrations. In Consul 0.6, ACL's were further extended to restrict service
discovery mechanisms, user events, and encryption keyring operations.
## ACL Design
@ -147,6 +147,27 @@ event "" {
As always, the more secure way to handle user events is to explicitly grant
access to each API token based on the events they should be able to fire.
### Blacklist mode and Keyring Operations
Consul 0.6 and later supports securing the encryption keyring operations using
ACL's. Encryption is an optional component of the gossip layer. More information
about Consul's keyring operations can be found on the [keyring
command](/docs/commands/keyring.html) documentation page.
If your [`acl_default_policy`](/docs/agent/options.html#acl_default_policy) is
set to `deny`, then the `anonymous` token will not have access to read or write
to the encryption keyring. The keyring policy is yet another first-class citizen
in the ACL syntax. You can configure the anonymous token to have free reign over
the keyring using a policy like the following:
```
keyring = "write"
```
Encryption keyring operations are sensitive and should be properly secured. It
is recommended that instead of configuring a wide-open policy like above, a
per-token policy is applied to maximize security.
### Bootstrapping ACLs
Bootstrapping the ACL system is done by providing an initial [`acl_master_token`
@ -229,6 +250,9 @@ event "" {
event "destroy-" {
policy = "deny"
}
# Read-only mode for the encryption keyring by default (list only)
keyring = "read"
```
This is equivalent to the following JSON input:
@ -261,7 +285,8 @@ This is equivalent to the following JSON input:
"destroy-": {
"policy": "deny"
}
}
},
"keyring": "read"
}
```