From 27d01270c8419d435b940c8e73fa9e73e1a3586e Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sun, 5 Jul 2015 14:58:38 -0700 Subject: [PATCH 1/9] vault: look for glob character in policy --- vault/policy.go | 9 +++++++++ vault/policy_test.go | 12 ++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/vault/policy.go b/vault/policy.go index c66f19a23..4c41085d8 100644 --- a/vault/policy.go +++ b/vault/policy.go @@ -2,6 +2,7 @@ package vault import ( "fmt" + "strings" "github.com/hashicorp/hcl" ) @@ -34,6 +35,7 @@ type Policy struct { type PathPolicy struct { Prefix string `hcl:",key"` Policy string + Glob bool } // Parse is used to parse the specified ACL rules into an @@ -48,6 +50,13 @@ func Parse(rules string) (*Policy, error) { // Validate the path policy for _, pp := range p.Paths { + // Strip the glob character if found + if strings.HasSuffix(pp.Prefix, "*") { + pp.Prefix = strings.TrimSuffix(pp.Prefix, "*") + pp.Glob = true + } + + // Check the policy is valid switch pp.Policy { case PathPolicyDeny: case PathPolicyRead: diff --git a/vault/policy_test.go b/vault/policy_test.go index 0c75f2905..0133f73d0 100644 --- a/vault/policy_test.go +++ b/vault/policy_test.go @@ -16,9 +16,9 @@ func TestPolicy_Parse(t *testing.T) { } expect := []*PathPolicy{ - &PathPolicy{"", "deny"}, - &PathPolicy{"stage/", "sudo"}, - &PathPolicy{"prod/", "read"}, + &PathPolicy{"", "deny", true}, + &PathPolicy{"stage/", "sudo", true}, + &PathPolicy{"prod/version", "read", false}, } if !reflect.DeepEqual(p.Paths, expect) { t.Fatalf("bad: %#v", p) @@ -30,17 +30,17 @@ var rawPolicy = ` name = "dev" # Deny all paths by default -path "" { +path "*" { policy = "deny" } # Allow full access to staging -path "stage/" { +path "stage/*" { policy = "sudo" } # Limited read privilege to production -path "prod/" { +path "prod/version" { policy = "read" } ` From eda88c18ffa2f01beb6bf0d774c6464adf00ecd5 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sun, 5 Jul 2015 17:30:19 -0600 Subject: [PATCH 2/9] vault: Adding precedence logic for conflicting policy --- vault/policy.go | 49 ++++++++++++++++++++++++++++++++++++-------- vault/policy_test.go | 36 ++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 9 deletions(-) diff --git a/vault/policy.go b/vault/policy.go index 4c41085d8..3368c45ba 100644 --- a/vault/policy.go +++ b/vault/policy.go @@ -14,15 +14,6 @@ const ( PathPolicySudo = "sudo" ) -var ( - pathPolicyLevel = map[string]int{ - PathPolicyDeny: 0, - PathPolicyRead: 1, - PathPolicyWrite: 2, - PathPolicySudo: 3, - } -) - // Policy is used to represent the policy specified by // an ACL configuration. type Policy struct { @@ -38,6 +29,46 @@ type PathPolicy struct { Glob bool } +// TakesPrecedence is used when multiple policies +// collide on a path to determine which policy takes +// precendence. +func (p *PathPolicy) TakesPrecedence(other *PathPolicy) bool { + // Handle the full merge matrix + switch p.Policy { + case PathPolicyDeny: + // Deny always takes precendence + return true + + case PathPolicyRead: + // Read never takes precedence + return false + + case PathPolicyWrite: + switch other.Policy { + case PathPolicyRead: + return true + case PathPolicyDeny, PathPolicyWrite, PathPolicySudo: + return false + default: + panic("missing case") + } + + case PathPolicySudo: + switch other.Policy { + case PathPolicyRead, PathPolicyWrite: + return true + case PathPolicyDeny, PathPolicySudo: + return false + default: + panic("missing case") + } + + default: + panic("missing case") + } + return false +} + // Parse is used to parse the specified ACL rules into an // intermediary set of policies, before being compiled into // the ACL diff --git a/vault/policy_test.go b/vault/policy_test.go index 0133f73d0..10e794324 100644 --- a/vault/policy_test.go +++ b/vault/policy_test.go @@ -5,6 +5,42 @@ import ( "testing" ) +func TestPolicy_TakesPrecedence(t *testing.T) { + type tcase struct { + a, b string + precedence bool + } + tests := []tcase{ + tcase{PathPolicyDeny, PathPolicyDeny, true}, + tcase{PathPolicyDeny, PathPolicyRead, true}, + tcase{PathPolicyDeny, PathPolicyWrite, true}, + tcase{PathPolicyDeny, PathPolicySudo, true}, + + tcase{PathPolicyRead, PathPolicyDeny, false}, + tcase{PathPolicyRead, PathPolicyRead, false}, + tcase{PathPolicyRead, PathPolicyWrite, false}, + tcase{PathPolicyRead, PathPolicySudo, false}, + + tcase{PathPolicyWrite, PathPolicyDeny, false}, + tcase{PathPolicyWrite, PathPolicyRead, true}, + tcase{PathPolicyWrite, PathPolicyWrite, false}, + tcase{PathPolicyWrite, PathPolicySudo, false}, + + tcase{PathPolicySudo, PathPolicyDeny, false}, + tcase{PathPolicySudo, PathPolicyRead, true}, + tcase{PathPolicySudo, PathPolicyWrite, true}, + tcase{PathPolicySudo, PathPolicySudo, false}, + } + for idx, test := range tests { + a := &PathPolicy{Policy: test.a} + b := &PathPolicy{Policy: test.b} + if out := a.TakesPrecedence(b); out != test.precedence { + t.Fatalf("bad: idx %d expect: %v out: %v", + idx, test.precedence, out) + } + } +} + func TestPolicy_Parse(t *testing.T) { p, err := Parse(rawPolicy) if err != nil { From 05b3fa836e1b71fc2baa714c9f136baf0e7ab5b7 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sun, 5 Jul 2015 17:31:30 -0600 Subject: [PATCH 3/9] vault: Handle exact vs glob match, deny has highest precedence --- vault/acl.go | 118 +++++++++++++++++++++++++++++++--------------- vault/acl_test.go | 20 ++++---- 2 files changed, 90 insertions(+), 48 deletions(-) diff --git a/vault/acl.go b/vault/acl.go index d12bc1c78..8154ba9f7 100644 --- a/vault/acl.go +++ b/vault/acl.go @@ -5,23 +5,34 @@ import ( "github.com/hashicorp/vault/logical" ) -// operationPolicyLevel is used to map each logical operation -// into the minimum required permissions to allow the operation. -var operationPolicyLevel = map[logical.Operation]int{ - logical.ReadOperation: pathPolicyLevel[PathPolicyRead], - logical.WriteOperation: pathPolicyLevel[PathPolicyWrite], - logical.DeleteOperation: pathPolicyLevel[PathPolicyWrite], - logical.ListOperation: pathPolicyLevel[PathPolicyRead], - logical.RevokeOperation: pathPolicyLevel[PathPolicyWrite], - logical.RenewOperation: pathPolicyLevel[PathPolicyRead], - logical.HelpOperation: pathPolicyLevel[PathPolicyDeny], -} +var ( + // Policy lists are used to restrict what is eligible for an operation + anyPolicy = []string{PathPolicyDeny} + readWriteSudo = []string{PathPolicyRead, PathPolicyWrite, PathPolicySudo} + writeSudo = []string{PathPolicyWrite, PathPolicySudo} + + // permittedPolicyLevel is used to map each logical operation + // into the set of policies that allow the operation. + permittedPolicyLevels = map[logical.Operation][]string{ + logical.ReadOperation: readWriteSudo, + logical.WriteOperation: writeSudo, + logical.DeleteOperation: writeSudo, + logical.ListOperation: readWriteSudo, + logical.HelpOperation: anyPolicy, + logical.RevokeOperation: writeSudo, + logical.RenewOperation: writeSudo, + logical.RollbackOperation: writeSudo, + } +) // ACL is used to wrap a set of policies to provide // an efficient interface for access control. type ACL struct { - // pathRules contains the path policies - pathRules *radix.Tree + // exactRules contains the path policies that are exact + exactRules *radix.Tree + + // globRules contains the path policies that glob + globRules *radix.Tree // root is enabled if the "root" named policy is present. root bool @@ -31,8 +42,9 @@ type ACL struct { func NewACL(policies []*Policy) (*ACL, error) { // Initialize a := &ACL{ - pathRules: radix.New(), - root: false, + exactRules: radix.New(), + globRules: radix.New(), + root: false, } // Inject each policy @@ -46,21 +58,23 @@ func NewACL(policies []*Policy) (*ACL, error) { a.root = true } for _, pp := range policy.Paths { - // Convert to a policy level - policyLevel := pathPolicyLevel[pp.Policy] + // Check which tree to use + tree := a.exactRules + if pp.Glob { + tree = a.globRules + } // Check for an existing policy - raw, ok := a.pathRules.Get(pp.Prefix) + raw, ok := tree.Get(pp.Prefix) if !ok { - a.pathRules.Insert(pp.Prefix, policyLevel) + tree.Insert(pp.Prefix, pp) continue } - existing := raw.(int) + existing := raw.(*PathPolicy) - // Check if this policy is a higher access level, - // we want to store the highest permission permitted. - if policyLevel > existing { - a.pathRules.Insert(pp.Prefix, policyLevel) + // Check if this policy is takes precedence + if pp.TakesPrecedence(existing) { + tree.Insert(pp.Prefix, pp) } } } @@ -74,18 +88,36 @@ func (a *ACL) AllowOperation(op logical.Operation, path string) bool { return true } - // Find a matching rule, default deny if no match - policyLevel := 0 - _, rule, ok := a.pathRules.LongestPrefix(path) - if ok { - policyLevel = rule.(int) + // Check if any policy level allows this operation + permitted := permittedPolicyLevels[op] + if permitted[0] == PathPolicyDeny { + return true } - // Convert the operation to a minimum required level - requiredLevel := operationPolicyLevel[op] + // Find an exact matching rule, look for glob if no match + var policy *PathPolicy + raw, ok := a.exactRules.Get(path) + if ok { + policy = raw.(*PathPolicy) + goto CHECK + } + // Find a glob rule, default deny if no match + _, raw, ok = a.globRules.LongestPrefix(path) + if !ok { + return false + } else { + policy = raw.(*PathPolicy) + } + +CHECK: // Check if the minimum permissions are met - return policyLevel >= requiredLevel + for _, allowed := range permitted { + if allowed == policy.Policy { + return true + } + } + return false } // RootPrivilege checks if the user has root level permission @@ -97,13 +129,23 @@ func (a *ACL) RootPrivilege(path string) bool { return true } - // Check the rules for a match - _, rule, ok := a.pathRules.LongestPrefix(path) - if !ok { - return false + // Find an exact matching rule, look for glob if no match + var policy *PathPolicy + raw, ok := a.exactRules.Get(path) + if ok { + policy = raw.(*PathPolicy) + goto CHECK } + // Check the rules for a match, default deny if no match + _, raw, ok = a.globRules.LongestPrefix(path) + if !ok { + return false + } else { + policy = raw.(*PathPolicy) + } + +CHECK: // Check the policy level - policyLevel := rule.(int) - return policyLevel == pathPolicyLevel[PathPolicySudo] + return policy.Policy == PathPolicySudo } diff --git a/vault/acl_test.go b/vault/acl_test.go index 46e831046..ddbdf0552 100644 --- a/vault/acl_test.go +++ b/vault/acl_test.go @@ -105,7 +105,7 @@ func testLayeredACL(t *testing.T, acl *ACL) { {logical.DeleteOperation, "stage/foo", true}, {logical.WriteOperation, "stage/aws/foo", false}, - {logical.WriteOperation, "stage/aws/policy/foo", true}, + {logical.WriteOperation, "stage/aws/policy/foo", false}, {logical.DeleteOperation, "prod/foo", true}, {logical.WriteOperation, "prod/foo", true}, @@ -124,35 +124,35 @@ func testLayeredACL(t *testing.T, acl *ACL) { var aclPolicy = ` name = "dev" -path "dev/" { +path "dev/*" { policy = "sudo" } -path "stage/" { +path "stage/*" { policy = "write" } -path "stage/aws/" { +path "stage/aws/*" { policy = "read" } -path "stage/aws/policy/" { +path "stage/aws/policy/*" { policy = "sudo" } -path "prod/" { +path "prod/*" { policy = "read" } -path "prod/aws/" { +path "prod/aws/*" { policy = "deny" } ` var aclPolicy2 = ` name = "ops" -path "dev/hide/" { +path "dev/hide/*" { policy = "deny" } -path "stage/aws/policy/" { +path "stage/aws/policy/*" { policy = "deny" } -path "prod/" { +path "prod/*" { policy = "write" } ` From dc8cc308af3d3a33d8e3de5a9fe6104329c52f99 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sun, 5 Jul 2015 17:31:41 -0600 Subject: [PATCH 4/9] vault: fixing test with glob change --- vault/core_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vault/core_test.go b/vault/core_test.go index 2bb743a74..207d3d4f2 100644 --- a/vault/core_test.go +++ b/vault/core_test.go @@ -636,7 +636,7 @@ func TestCore_HandleRequest_PermissionAllowed(t *testing.T) { Operation: logical.WriteOperation, Path: "sys/policy/test", Data: map[string]interface{}{ - "rules": `path "secret/" { policy = "write" }`, + "rules": `path "secret/*" { policy = "write" }`, }, ClientToken: root, } From 3d2fa8818ea699f9666fb23fffbaa9193f3d25ee Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sun, 5 Jul 2015 17:34:34 -0600 Subject: [PATCH 5/9] vault: adding another ACL test --- vault/acl_test.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/vault/acl_test.go b/vault/acl_test.go index ddbdf0552..c1ddedfe0 100644 --- a/vault/acl_test.go +++ b/vault/acl_test.go @@ -112,6 +112,9 @@ func testLayeredACL(t *testing.T, acl *ACL) { {logical.ReadOperation, "prod/foo", true}, {logical.ListOperation, "prod/foo", true}, {logical.ReadOperation, "prod/aws/foo", false}, + + {logical.ReadOperation, "sys/status", false}, + {logical.WriteOperation, "sys/seal", true}, } for _, tc := range tcases { @@ -142,6 +145,9 @@ path "prod/*" { path "prod/aws/*" { policy = "deny" } +path "sys/*" { + policy = "deny" +} ` var aclPolicy2 = ` @@ -155,4 +161,7 @@ path "stage/aws/policy/*" { path "prod/*" { policy = "write" } +path "sys/seal" { + policy = "write" +} ` From 01b0257c5fd4e5f4c02e908a904a02f727b11d25 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sun, 5 Jul 2015 17:43:13 -0600 Subject: [PATCH 6/9] website: update for glob matching --- website/source/docs/concepts/policies.html.md | 27 +++++++++++-------- .../source/docs/internals/security.html.md | 4 +-- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/website/source/docs/concepts/policies.html.md b/website/source/docs/concepts/policies.html.md index d7bba0b85..6b3e19083 100644 --- a/website/source/docs/concepts/policies.html.md +++ b/website/source/docs/concepts/policies.html.md @@ -18,41 +18,46 @@ that describe what parts of Vault a user is allowed to access. An example of a policy is shown below: ```javascript -path "sys" { +path "sys/*" { policy = "deny" } -path "secret" { +path "secret/*" { policy = "write" } path "secret/foo" { policy = "read" } + +path "secret/super-secret" { + policy = "deny" +} ``` -Policies use prefix-based routing to apply rules. They are deny by default, -so if a path isn't explicitly given, Vault will reject any access to it. +Policies use path based matching to apply rules. A policy may be an exact +match, or might be a glob pattern which uses a prefix. The default policy +is always deny so if a path isn't explicitly allowed, Vault will reject access to it. This works well due to Vault's architecture of being like a filesystem: everything has a path associated with it, including the core configuration mechanism under "sys". -~> Policy paths are matched using a longest-prefix match, which is the most -specific defined policy. This means if you define a policy for `"secret/foo"`, -the policy would also match `"secret/foobar"`. +~> Policy paths are matched using the most specific defined policy. This may +be an exact match or the longest-prefix match of a glob. This means if you +define a policy for `"secret/foo*"`, the policy would also match `"secret/foobar"`. ## Policies Allowed policies for a path are: + * `deny` - No access allowed. Highest precedence. + + * `sudo` - Read, write, and root access to a path. + * `write` - Read, write access to a path. * `read` - Read-only access to a path. - * `deny` - No access allowed. - - * `sudo` - Read, write, and root access to a path. - The only non-obvious policy is "sudo". Some routes within Vault and mounted backends are marked as _root_ paths. Clients aren't allowed to access root paths unless they are a root user (have the special policy "root") or diff --git a/website/source/docs/internals/security.html.md b/website/source/docs/internals/security.html.md index 5e146b41f..6ebbf628e 100644 --- a/website/source/docs/internals/security.html.md +++ b/website/source/docs/internals/security.html.md @@ -110,8 +110,8 @@ a level of access granted to a path in Vault. When the policies are merged (if m policies are associated with a client), the highest access level permitted is used. For example, if the "engineering" policy permits read/write access to the "eng/" path, and the "ops" policy permits read access to the "ops/" path, then the user gets the -union of those. Policy is matched using a longest-prefix match, which is the most -specific defined policy. +union of those. Policy is matched using the most specific defined policy, which may be +an exact match or the longest-prefix match glob pattern. Certain operations are only permitted by "root" users, which is a distinguished policy built into Vault. This is similar to the concept of a root user on a Unix system From 37b68d6dce2d51d6d76acfe0bd3fb545ba3c5083 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sun, 5 Jul 2015 18:40:05 -0600 Subject: [PATCH 7/9] website: clarify getting started ACL docs --- website/source/intro/getting-started/acl.html.md | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/website/source/intro/getting-started/acl.html.md b/website/source/intro/getting-started/acl.html.md index 11325a9ad..6bff3d5c6 100644 --- a/website/source/intro/getting-started/acl.html.md +++ b/website/source/intro/getting-started/acl.html.md @@ -30,11 +30,7 @@ format that is also JSON-compatible, so you can use JSON as well. An example policy is shown below: ```javascript -path "sys" { - policy = "deny" -} - -path "secret" { +path "secret/*" { policy = "write" } @@ -43,10 +39,11 @@ path "secret/foo" { } ``` -The policy format uses a longest matching prefix system on the API path -to determine access control. Since everything in Vault must be accessed -via the API, this gives strict control over every aspect of Vault, including -mounting backends, authenticating, as well as secret access. +The policy format uses a prefix matching system on the API path +to determine access control. The most specific defined policy is used, +either an exact match or the longest-prefix glob match. Since everything +in Vault must be accessed via the API, this gives strict control over every +aspect of Vault, including mounting backends, authenticating, as well as secret access. In the policy above, a user could write any secret to `secret/`, except to `secret/foo`, where only read access is allowed. Policies default to From 03be7a59994b32523b05802a9d3ceebad1bf72b9 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sun, 5 Jul 2015 19:14:15 -0600 Subject: [PATCH 8/9] vault: upgrade old policies with implicit glob --- vault/policy_store.go | 51 ++++++++++++++++++++++++++++++-------- vault/policy_store_test.go | 25 +++++++++++++++++++ 2 files changed, 66 insertions(+), 10 deletions(-) diff --git a/vault/policy_store.go b/vault/policy_store.go index efcee07f0..99f9256b3 100644 --- a/vault/policy_store.go +++ b/vault/policy_store.go @@ -25,6 +25,12 @@ type PolicyStore struct { lru *lru.Cache } +// PolicyEntry is used to store a policy by name +type PolicyEntry struct { + Version int + Raw string +} + // NewPolicyStore creates a new PolicyStore that is backed // using a given view. It used used to durable store and manage named policy. func NewPolicyStore(view *BarrierView) *PolicyStore { @@ -64,9 +70,13 @@ func (ps *PolicyStore) SetPolicy(p *Policy) error { return fmt.Errorf("policy name missing") } - entry := &logical.StorageEntry{ - Key: p.Name, - Value: []byte(p.Raw), + // Create the entry + entry, err := logical.StorageEntryJSON(p.Name, &PolicyEntry{ + Version: 2, + Raw: p.Raw, + }) + if err != nil { + return fmt.Errorf("failed to create entry: %v", err) } if err := ps.view.Put(entry); err != nil { return fmt.Errorf("failed to persist policy: %v", err) @@ -101,16 +111,37 @@ func (ps *PolicyStore) GetPolicy(name string) (*Policy, error) { return nil, nil } - // Parse into a policy object - p, err := Parse(string(out.Value)) - if err != nil { - return nil, fmt.Errorf("failed to parse policy: %v", err) + // In Vault 0.1.X we stored the raw policy, but in + // Vault 0.2 we switch to the PolicyEntry + policyEntry := new(PolicyEntry) + var policy *Policy + if err := out.DecodeJSON(policyEntry); err == nil { + // Parse normally + p, err := Parse(policyEntry.Raw) + if err != nil { + return nil, fmt.Errorf("failed to parse policy: %v", err) + } + p.Name = name + policy = p + + } else { + // On error, attempt to use V1 parsing + p, err := Parse(string(out.Value)) + if err != nil { + return nil, fmt.Errorf("failed to parse policy: %v", err) + } + p.Name = name + + // V1 used implicit glob, we need to do a fix-up + for _, pp := range p.Paths { + pp.Glob = true + } + policy = p } - p.Name = name // Update the LRU cache - ps.lru.Add(p.Name, p) - return p, nil + ps.lru.Add(name, policy) + return policy, nil } // ListPolicies is used to list the available policies diff --git a/vault/policy_store_test.go b/vault/policy_store_test.go index 61ed4af75..b9ec3268d 100644 --- a/vault/policy_store_test.go +++ b/vault/policy_store_test.go @@ -3,6 +3,8 @@ package vault import ( "reflect" "testing" + + "github.com/hashicorp/vault/logical" ) func mockPolicyStore(t *testing.T) *PolicyStore { @@ -128,3 +130,26 @@ func TestPolicyStore_ACL(t *testing.T) { } testLayeredACL(t, acl) } + +func TestPolicyStore_v1Upgrade(t *testing.T) { + ps := mockPolicyStore(t) + + // Put a V1 record + raw := `path "foo" { policy = "read" }` + ps.view.Put(&logical.StorageEntry{"old", []byte(raw)}) + + // Do a read + p, err := ps.GetPolicy("old") + if err != nil { + t.Fatalf("err: %v", err) + } + + if p == nil || len(p.Paths) != 1 { + t.Fatalf("bad policy: %#v", p) + } + + // Check that glob is enabled + if !p.Paths[0].Glob { + t.Fatalf("should enable glob") + } +} From 768a6e33b0630c262f865e3ae677b000ac3c9b20 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Mon, 6 Jul 2015 11:10:09 -0600 Subject: [PATCH 9/9] website: clarify changes in addition to feedback --- website/source/docs/concepts/policies.html.md | 31 +++++++++++++++++++ .../source/intro/getting-started/acl.html.md | 3 +- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/website/source/docs/concepts/policies.html.md b/website/source/docs/concepts/policies.html.md index 6b3e19083..ebe476b68 100644 --- a/website/source/docs/concepts/policies.html.md +++ b/website/source/docs/concepts/policies.html.md @@ -45,6 +45,7 @@ mechanism under "sys". ~> Policy paths are matched using the most specific defined policy. This may be an exact match or the longest-prefix match of a glob. This means if you define a policy for `"secret/foo*"`, the policy would also match `"secret/foobar"`. +The glob character is only supported at the end of the path specification. ## Policies @@ -100,3 +101,33 @@ If an _existing_ policy is modified, the modifications propagate to all associated users instantly. The above paragraph is more specifically stating that you can't add new or remove policies associated with an active identity. + +## Changes from 0.1 + +In Vault versions prior to 0.2, the ACL policy language had a slightly +different specification and semantics. The current specification requires +that glob behavior explicitly be specified by adding the `*` character to +the end of a path. Previously, all paths were glob based matches and no +exact match could be specified. + +The other change is that deny had the lowest precedence. This meant if there +were two policies being merged (e.g. "ops" and "prod") and they had a conflicting +policy like: + +``` +path "sys/seal" { + policy = "deny" +} + +path "sys/seal" { + policy = "read" +} +``` + +The merge would previously give the "read" higher precedence. The current +version of Vault prioritizes the explicit deny, so that the "deny" would +take precedence. + +To make all Vault 0.1 policies compatible with Vault 0.2, the explicit +glob character must be added to all the path prefixes. + diff --git a/website/source/intro/getting-started/acl.html.md b/website/source/intro/getting-started/acl.html.md index 6bff3d5c6..8f89bec06 100644 --- a/website/source/intro/getting-started/acl.html.md +++ b/website/source/intro/getting-started/acl.html.md @@ -47,7 +47,8 @@ aspect of Vault, including mounting backends, authenticating, as well as secret In the policy above, a user could write any secret to `secret/`, except to `secret/foo`, where only read access is allowed. Policies default to -deny, so any access to an unspecified path is not allowed. +deny, so any access to an unspecified path is not allowed. The policy +langauge changed slightly in Vault 0.2, [see this page for details](/docs/concepts/policies.html). Save the above policy as `acl.hcl`.