diff --git a/audit/format.go b/audit/format.go index 585de1d69618..8bbf1b1ccf70 100644 --- a/audit/format.go +++ b/audit/format.go @@ -135,6 +135,20 @@ func (f *AuditFormatter) FormatRequest(ctx context.Context, w io.Writer, config reqEntry.Auth.TokenIssueTime = auth.IssueTime.Format(time.RFC3339) } + if auth.PolicyResults != nil { + reqEntry.Auth.PolicyResults = &AuditPolicyResults{ + Allowed: auth.PolicyResults.Allowed, + } + + for _, p := range auth.PolicyResults.GrantingPolicies { + reqEntry.Auth.PolicyResults.GrantingPolicies = append(reqEntry.Auth.PolicyResults.GrantingPolicies, PolicyInfo{ + Name: p.Name, + NamespaceId: p.NamespaceId, + Type: p.Type, + }) + } + } + if req.WrapInfo != nil { reqEntry.Request.WrapTTL = int(req.WrapInfo.TTL / time.Second) } @@ -277,6 +291,7 @@ func (f *AuditFormatter) FormatResponse(ctx context.Context, w io.Writer, config ID: req.ID, ClientToken: req.ClientToken, ClientTokenAccessor: req.ClientTokenAccessor, + ClientID: req.ClientID, Operation: req.Operation, MountType: req.MountType, MountAccessor: req.MountAccessor, @@ -307,6 +322,20 @@ func (f *AuditFormatter) FormatResponse(ctx context.Context, w io.Writer, config }, } + if auth.PolicyResults != nil { + respEntry.Auth.PolicyResults = &AuditPolicyResults{ + Allowed: auth.PolicyResults.Allowed, + } + + for _, p := range auth.PolicyResults.GrantingPolicies { + respEntry.Auth.PolicyResults.GrantingPolicies = append(respEntry.Auth.PolicyResults.GrantingPolicies, PolicyInfo{ + Name: p.Name, + NamespaceId: p.NamespaceId, + Type: p.Type, + }) + } + } + if !auth.IssueTime.IsZero() { respEntry.Auth.TokenIssueTime = auth.IssueTime.Format(time.RFC3339) } @@ -381,6 +410,7 @@ type AuditAuth struct { IdentityPolicies []string `json:"identity_policies,omitempty"` ExternalNamespacePolicies map[string][]string `json:"external_namespace_policies,omitempty"` NoDefaultPolicy bool `json:"no_default_policy,omitempty"` + PolicyResults *AuditPolicyResults `json:"policy_results,omitempty"` Metadata map[string]string `json:"metadata,omitempty"` NumUses int `json:"num_uses,omitempty"` RemainingUses int `json:"remaining_uses,omitempty"` @@ -390,6 +420,17 @@ type AuditAuth struct { TokenIssueTime string `json:"token_issue_time,omitempty"` } +type AuditPolicyResults struct { + Allowed bool `json:"allowed"` + GrantingPolicies []PolicyInfo `json:"granting_policies,omitempty"` +} + +type PolicyInfo struct { + Name string `json:"name,omitempty"` + NamespaceId string `json:"namespace_id,omitempty"` + Type string `json:"type"` +} + type AuditSecret struct { LeaseID string `json:"lease_id,omitempty"` } diff --git a/changelog/15457.txt b/changelog/15457.txt new file mode 100644 index 000000000000..d3a2f18a9493 --- /dev/null +++ b/changelog/15457.txt @@ -0,0 +1,4 @@ +```release-note:improvement +audit: Add a policy_results block into the audit log that contains the set of +policies that granted this request access. +``` diff --git a/sdk/logical/auth.go b/sdk/logical/auth.go index 7f68bc936e8b..9e9524a224bc 100644 --- a/sdk/logical/auth.go +++ b/sdk/logical/auth.go @@ -8,7 +8,8 @@ import ( ) // Auth is the resulting authentication information that is part of -// Response for credential backends. +// Response for credential backends. It's also attached to Request objects and +// defines the authentication used for the request. This value is audit logged. type Auth struct { LeaseOptions @@ -101,6 +102,10 @@ type Auth struct { // Orphan is set if the token does not have a parent Orphan bool `json:"orphan"` + // PolicyResults is the set of policies that grant the token access to the + // requesting path. + PolicyResults *PolicyResults `json:"policy_results"` + // MFARequirement MFARequirement *MFARequirement `json:"mfa_requirement"` } @@ -108,3 +113,14 @@ type Auth struct { func (a *Auth) GoString() string { return fmt.Sprintf("*%#v", *a) } + +type PolicyResults struct { + Allowed bool `json:"allowed"` + GrantingPolicies []PolicyInfo `json:"granting_policies"` +} + +type PolicyInfo struct { + Name string `json:"name"` + NamespaceId string `json:"namespace_id"` + Type string `json:"type"` +} diff --git a/vault/acl.go b/vault/acl.go index fc9f353aa8af..5ea548941486 100644 --- a/vault/acl.go +++ b/vault/acl.go @@ -40,11 +40,12 @@ type PolicyCheckOpts struct { } type AuthResults struct { - ACLResults *ACLResults - Allowed bool - RootPrivs bool - DeniedError bool - Error *multierror.Error + ACLResults *ACLResults + SentinelResults *SentinelResults + Allowed bool + RootPrivs bool + DeniedError bool + Error *multierror.Error } type ACLResults struct { @@ -54,6 +55,11 @@ type ACLResults struct { MFAMethods []string ControlGroup *ControlGroup CapabilitiesBitmap uint32 + GrantingPolicies []logical.PolicyInfo +} + +type SentinelResults struct { + GrantingPolicies []logical.PolicyInfo } // NewACL is used to construct a policy based ACL from a set of policies. @@ -126,6 +132,10 @@ func NewACL(ctx context.Context, policies []*Policy) (*ACL, error) { if err != nil { return nil, fmt.Errorf("error cloning ACL permissions: %w", err) } + + // Store this policy name as the policy that permits these + // capabilities + clonedPerms.GrantingPoliciesMap = addGrantingPoliciesToMap(nil, policy, clonedPerms.CapabilitiesBitmap) switch { case pc.HasSegmentWildcards: a.segmentWildcardPaths[pc.Path] = clonedPerms @@ -155,6 +165,7 @@ func NewACL(ctx context.Context, policies []*Policy) (*ACL, error) { // Insert the capabilities in this new policy into the existing // value existingPerms.CapabilitiesBitmap = existingPerms.CapabilitiesBitmap | pc.Permissions.CapabilitiesBitmap + existingPerms.GrantingPoliciesMap = addGrantingPoliciesToMap(existingPerms.GrantingPoliciesMap, policy, pc.Permissions.CapabilitiesBitmap) } // Note: In these stanzas, we're preferring minimum lifetimes. So @@ -326,6 +337,11 @@ func (a *ACL) AllowOperation(ctx context.Context, req *logical.Request, capCheck ret.Allowed = true ret.RootPrivs = true ret.IsRoot = true + ret.GrantingPolicies = []logical.PolicyInfo{{ + Name: "root", + NamespaceId: "root", + Type: "acl", + }} return } op := req.Operation @@ -397,25 +413,33 @@ CHECK: ret.MFAMethods = permissions.MFAMethods ret.ControlGroup = permissions.ControlGroup + var grantingPolicies []logical.PolicyInfo operationAllowed := false switch op { case logical.ReadOperation: operationAllowed = capabilities&ReadCapabilityInt > 0 + grantingPolicies = permissions.GrantingPoliciesMap[ReadCapabilityInt] case logical.ListOperation: operationAllowed = capabilities&ListCapabilityInt > 0 + grantingPolicies = permissions.GrantingPoliciesMap[ListCapabilityInt] case logical.UpdateOperation: operationAllowed = capabilities&UpdateCapabilityInt > 0 + grantingPolicies = permissions.GrantingPoliciesMap[UpdateCapabilityInt] case logical.DeleteOperation: operationAllowed = capabilities&DeleteCapabilityInt > 0 + grantingPolicies = permissions.GrantingPoliciesMap[DeleteCapabilityInt] case logical.CreateOperation: operationAllowed = capabilities&CreateCapabilityInt > 0 + grantingPolicies = permissions.GrantingPoliciesMap[CreateCapabilityInt] case logical.PatchOperation: operationAllowed = capabilities&PatchCapabilityInt > 0 + grantingPolicies = permissions.GrantingPoliciesMap[PatchCapabilityInt] // These three re-use UpdateCapabilityInt since that's the most appropriate // capability/operation mapping case logical.RevokeOperation, logical.RenewOperation, logical.RollbackOperation: operationAllowed = capabilities&UpdateCapabilityInt > 0 + grantingPolicies = permissions.GrantingPoliciesMap[UpdateCapabilityInt] default: return @@ -425,6 +449,8 @@ CHECK: return } + ret.GrantingPolicies = grantingPolicies + if permissions.MaxWrappingTTL > 0 { if req.WrapInfo == nil || req.WrapInfo.TTL > permissions.MaxWrappingTTL { return diff --git a/vault/acl_test.go b/vault/acl_test.go index 6854e85afee0..c7fd0f64a464 100644 --- a/vault/acl_test.go +++ b/vault/acl_test.go @@ -841,6 +841,127 @@ func TestACL_CreationRace(t *testing.T) { wg.Wait() } +func TestACLGrantingPolicies(t *testing.T) { + ns := namespace.RootNamespace + policy, err := ParseACLPolicy(ns, grantingTestPolicy) + if err != nil { + t.Fatalf("err: %v", err) + } + merged, err := ParseACLPolicy(ns, grantingTestPolicyMerged) + if err != nil { + t.Fatalf("err: %v", err) + } + ctx := namespace.ContextWithNamespace(context.Background(), ns) + + type tcase struct { + path string + op logical.Operation + policies []*Policy + expected []logical.PolicyInfo + allowed bool + } + + policyInfo := logical.PolicyInfo{ + Name: "granting_policy", + NamespaceId: "root", + Type: "acl", + } + mergedInfo := logical.PolicyInfo{ + Name: "granting_policy_merged", + NamespaceId: "root", + Type: "acl", + } + + tcases := []tcase{ + {"kv/foo", logical.ReadOperation, []*Policy{policy}, []logical.PolicyInfo{policyInfo}, true}, + {"kv/foo", logical.UpdateOperation, []*Policy{policy}, []logical.PolicyInfo{policyInfo}, true}, + {"kv/bad", logical.ReadOperation, []*Policy{policy}, nil, false}, + {"kv/deny", logical.ReadOperation, []*Policy{policy}, nil, false}, + {"kv/path/foo", logical.ReadOperation, []*Policy{policy}, []logical.PolicyInfo{policyInfo}, true}, + {"kv/path/longer", logical.ReadOperation, []*Policy{policy}, []logical.PolicyInfo{policyInfo}, true}, + {"kv/foo", logical.ReadOperation, []*Policy{policy, merged}, []logical.PolicyInfo{policyInfo, mergedInfo}, true}, + {"kv/path/longer3", logical.ReadOperation, []*Policy{policy, merged}, []logical.PolicyInfo{mergedInfo}, true}, + {"kv/bar", logical.ReadOperation, []*Policy{policy, merged}, []logical.PolicyInfo{mergedInfo}, true}, + {"kv/deny", logical.ReadOperation, []*Policy{policy, merged}, nil, false}, + {"kv/path/longer", logical.UpdateOperation, []*Policy{policy, merged}, []logical.PolicyInfo{policyInfo}, true}, + {"kv/path/foo", logical.ReadOperation, []*Policy{policy, merged}, []logical.PolicyInfo{policyInfo, mergedInfo}, true}, + } + + for _, tc := range tcases { + request := &logical.Request{ + Path: tc.path, + Operation: tc.op, + } + + acl, err := NewACL(ctx, tc.policies) + if err != nil { + t.Fatalf("err: %v", err) + } + + authResults := acl.AllowOperation(ctx, request, false) + if authResults.Allowed != tc.allowed { + t.Fatalf("bad: case %#v: %v", tc, authResults.Allowed) + } + if !reflect.DeepEqual(authResults.GrantingPolicies, tc.expected) { + t.Fatalf("bad: case %#v: got\n%#v\nexpected\n%#v\n", tc, authResults.GrantingPolicies, tc.expected) + } + } +} + +var grantingTestPolicy = ` +name = "granting_policy" +path "kv/foo" { + capabilities = ["update", "read"] +} + +path "kv/path/*" { + capabilities = ["read"] +} + +path "kv/path/longer" { + capabilities = ["update", "read"] +} + +path "kv/path/longer2" { + capabilities = ["update"] +} + +path "kv/deny" { + capabilities = ["deny"] +} + +path "ns1/kv/foo" { + capabilities = ["update", "read"] +} +` + +var grantingTestPolicyMerged = ` +name = "granting_policy_merged" +path "kv/foo" { + capabilities = ["update", "read"] +} + +path "kv/bar" { + capabilities = ["update", "read"] +} + +path "kv/path/*" { + capabilities = ["read"] +} + +path "kv/path/longer" { + capabilities = ["read"] +} + +path "kv/path/longer3" { + capabilities = ["read"] +} + +path "kv/deny" { + capabilities = ["update"] +} +` + var tokenCreationPolicy = ` name = "tokenCreation" path "auth/token/create*" { diff --git a/vault/policy.go b/vault/policy.go index e80d1657e98d..da11f822f17b 100644 --- a/vault/policy.go +++ b/vault/policy.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/vault/helper/namespace" "github.com/hashicorp/vault/sdk/helper/hclutil" "github.com/hashicorp/vault/sdk/helper/identitytpl" + "github.com/hashicorp/vault/sdk/logical" "github.com/mitchellh/copystructure" ) @@ -161,14 +162,15 @@ type IdentityFactor struct { } type ACLPermissions struct { - CapabilitiesBitmap uint32 - MinWrappingTTL time.Duration - MaxWrappingTTL time.Duration - AllowedParameters map[string][]interface{} - DeniedParameters map[string][]interface{} - RequiredParameters []string - MFAMethods []string - ControlGroup *ControlGroup + CapabilitiesBitmap uint32 + MinWrappingTTL time.Duration + MaxWrappingTTL time.Duration + AllowedParameters map[string][]interface{} + DeniedParameters map[string][]interface{} + RequiredParameters []string + MFAMethods []string + ControlGroup *ControlGroup + GrantingPoliciesMap map[uint32][]logical.PolicyInfo } func (p *ACLPermissions) Clone() (*ACLPermissions, error) { @@ -225,9 +227,43 @@ func (p *ACLPermissions) Clone() (*ACLPermissions, error) { ret.ControlGroup = clonedControlGroup.(*ControlGroup) } + switch { + case p.GrantingPoliciesMap == nil: + case len(p.GrantingPoliciesMap) == 0: + ret.GrantingPoliciesMap = make(map[uint32][]logical.PolicyInfo) + default: + clonedGrantingPoliciesMap, err := copystructure.Copy(p.GrantingPoliciesMap) + if err != nil { + return nil, err + } + ret.GrantingPoliciesMap = clonedGrantingPoliciesMap.(map[uint32][]logical.PolicyInfo) + } + return ret, nil } +func addGrantingPoliciesToMap(m map[uint32][]logical.PolicyInfo, policy *Policy, capabilitiesBitmap uint32) map[uint32][]logical.PolicyInfo { + if m == nil { + m = make(map[uint32][]logical.PolicyInfo) + } + + // For all possible policies, check if the provided capabilities include + // them + for _, capability := range cap2Int { + if capabilitiesBitmap&capability == 0 { + continue + } + + m[capability] = append(m[capability], logical.PolicyInfo{ + Name: policy.Name, + NamespaceId: policy.namespace.ID, + Type: "acl", + }) + } + + return m +} + // ParseACLPolicy is used to parse the specified ACL rules into an // intermediary set of policies, before being compiled into // the ACL diff --git a/vault/request_handling.go b/vault/request_handling.go index e9ebef5fef03..c3f03c887b6c 100644 --- a/vault/request_handling.go +++ b/vault/request_handling.go @@ -385,6 +385,10 @@ func (c *Core) checkToken(ctx context.Context, req *logical.Request, unauth bool RootPrivsRequired: rootPath, }) + auth.PolicyResults = &logical.PolicyResults{ + Allowed: authResults.Allowed, + } + if !authResults.Allowed { retErr := authResults.Error @@ -410,6 +414,13 @@ func (c *Core) checkToken(ctx context.Context, req *logical.Request, unauth bool return auth, te, retErr } + if authResults.ACLResults != nil && len(authResults.ACLResults.GrantingPolicies) > 0 { + auth.PolicyResults.GrantingPolicies = authResults.ACLResults.GrantingPolicies + } + if authResults.SentinelResults != nil && len(authResults.SentinelResults.GrantingPolicies) > 0 { + auth.PolicyResults.GrantingPolicies = append(auth.PolicyResults.GrantingPolicies, authResults.SentinelResults.GrantingPolicies...) + } + // If it is an authenticated ( i.e with vault token ) request, increment client count if !unauth && c.activityLog != nil { c.activityLog.HandleTokenUsage(ctx, te, clientID, isTWE)