From 69b9e47d69bd237770dde66e53b397fa887f6302 Mon Sep 17 00:00:00 2001 From: Ronald Ekambi Date: Fri, 7 Jul 2023 16:15:17 -0400 Subject: [PATCH] Use JWT-auth filter in metadata mode & Delegate validation to RBAC filter --- agent/xds/jwt_authn.go | 118 +++++---- agent/xds/jwt_authn_test.go | 99 -------- agent/xds/listeners.go | 3 + agent/xds/rbac.go | 227 ++++++++++++++---- agent/xds/rbac_test.go | 56 ++--- .../jwt_authn/intention-with-path.golden | 9 +- .../testdata/jwt_authn/local-provider.golden | 7 +- ...ltiple-providers-and-one-permission.golden | 46 ++-- .../testdata/jwt_authn/remote-provider.golden | 7 +- .../top-level-provider-with-permission.golden | 30 ++- ...jwt-with-one-permission--httpfilter.golden | 56 +++-- ...evel-jwt-no-permissions--httpfilter.golden | 42 +++- ...th-multiple-permissions--httpfilter.golden | 180 +++++++++----- ...jwt-with-one-permission--httpfilter.golden | 106 +++++--- 14 files changed, 619 insertions(+), 367 deletions(-) diff --git a/agent/xds/jwt_authn.go b/agent/xds/jwt_authn.go index ba1c17bbc2096..783ed0179a3b7 100644 --- a/agent/xds/jwt_authn.go +++ b/agent/xds/jwt_authn.go @@ -13,6 +13,7 @@ import ( envoy_http_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" "github.com/hashicorp/consul/agent/structs" "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/wrapperspb" ) @@ -31,7 +32,7 @@ type jwtAuthnProvider struct { func makeJWTAuthFilter(pCE map[string]*structs.JWTProviderConfigEntry, intentions structs.SimplifiedIntentions) (*envoy_http_v3.HttpFilter, error) { providers := map[string]*envoy_http_jwt_authn_v3.JwtProvider{} - var rules []*envoy_http_jwt_authn_v3.RequirementRule + var jwtReqs []*envoy_http_jwt_authn_v3.JwtRequirement for _, intention := range intentions { if intention.JWT == nil && !hasJWTconfig(intention.Permissions) { @@ -63,16 +64,15 @@ func makeJWTAuthFilter(pCE map[string]*structs.JWTProviderConfigEntry, intention continue } for _, prov := range perm.JWT.Providers { - rule := buildRouteRule(prov, perm, "/", k) - rules = append(rules, rule) + reqs := intentionJWTProviderToJWTRequirement(prov, perm, k) + jwtReqs = append(jwtReqs, reqs) } } if intention.JWT != nil { for _, provider := range intention.JWT.Providers { - // The top-level provider applies to all requests. - rule := buildRouteRule(provider, nil, "/", 0) - rules = append(rules, rule) + reqs := intentionJWTProviderToJWTRequirement(provider, nil, 0) + jwtReqs = append(jwtReqs, reqs) } } } @@ -84,11 +84,78 @@ func makeJWTAuthFilter(pCE map[string]*structs.JWTProviderConfigEntry, intention cfg := &envoy_http_jwt_authn_v3.JwtAuthentication{ Providers: providers, - Rules: rules, + } + // only add rules if any of the existing providers are referenced by intentions + if len(jwtReqs) > 0 { + cfg.Rules = []*envoy_http_jwt_authn_v3.RequirementRule{ + { + Match: &envoy_route_v3.RouteMatch{ + PathSpecifier: &envoy_route_v3.RouteMatch_Prefix{Prefix: "/"}, + }, + RequirementType: makeJWTRequirementRule(andJWTRequirements(jwtReqs)), + }, + } } return makeEnvoyHTTPFilter(jwtEnvoyFilter, cfg) } +// andJWTRequirements combines list of jwt requirements into a single jwt requirement. +func andJWTRequirements(reqs []*envoy_http_jwt_authn_v3.JwtRequirement) *envoy_http_jwt_authn_v3.JwtRequirement { + switch len(reqs) { + case 0: + return anyJWTRequirement() + case 1: + return reqs[0] + default: + return &envoy_http_jwt_authn_v3.JwtRequirement{ + RequiresType: &envoy_http_jwt_authn_v3.JwtRequirement_RequiresAll{ + RequiresAll: &envoy_http_jwt_authn_v3.JwtRequirementAndList{ + Requirements: reqs, + }, + }, + } + } +} + +func makeJWTRequirementRule(r *envoy_http_jwt_authn_v3.JwtRequirement) *envoy_http_jwt_authn_v3.RequirementRule_Requires { + return &envoy_http_jwt_authn_v3.RequirementRule_Requires{ + Requires: r, + } +} + +func anyJWTRequirement() *envoy_http_jwt_authn_v3.JwtRequirement { + return &envoy_http_jwt_authn_v3.JwtRequirement{ + RequiresType: &envoy_http_jwt_authn_v3.JwtRequirement_RequiresAny{}, + } +} + +// intentionJWTProviderToJWTRequirement builds the envoy jwtRequirement. +// If a permission is provided, it computes the provider name based on permission index. +// +// Note: since the rbac filter is in charge of making decisions of allow/denied, this +// requirement uses `allow_missing_or_failed` to ensure it is always satisfied. +func intentionJWTProviderToJWTRequirement(ip *structs.IntentionJWTProvider, perm *structs.IntentionPermission, permIdx int) *envoy_http_jwt_authn_v3.JwtRequirement { + return &envoy_http_jwt_authn_v3.JwtRequirement{ + RequiresType: &envoy_http_jwt_authn_v3.JwtRequirement_RequiresAny{ + RequiresAny: &envoy_http_jwt_authn_v3.JwtRequirementOrList{ + Requirements: []*envoy_http_jwt_authn_v3.JwtRequirement{ + { + RequiresType: &envoy_http_jwt_authn_v3.JwtRequirement_ProviderName{ + ProviderName: makeComputedProviderName(ip.Name, perm, permIdx), + }, + }, + // We use allowMissingOrFailed to allow rbac filter to do the validation + { + RequiresType: &envoy_http_jwt_authn_v3.JwtRequirement_AllowMissingOrFailed{ + AllowMissingOrFailed: &emptypb.Empty{}, + }, + }, + }, + }, + }, + } +} + func collectJWTAuthnProviders(i *structs.Intention) []*jwtAuthnProvider { var reqs []*jwtAuthnProvider @@ -262,43 +329,6 @@ func buildJWTRetryPolicy(r *structs.JWKSRetryPolicy) *envoy_core_v3.RetryPolicy return &pol } -func buildRouteRule(provider *structs.IntentionJWTProvider, perm *structs.IntentionPermission, defaultPrefix string, permIdx int) *envoy_http_jwt_authn_v3.RequirementRule { - rule := &envoy_http_jwt_authn_v3.RequirementRule{ - Match: &envoy_route_v3.RouteMatch{ - PathSpecifier: &envoy_route_v3.RouteMatch_Prefix{Prefix: defaultPrefix}, - }, - RequirementType: &envoy_http_jwt_authn_v3.RequirementRule_Requires{ - Requires: &envoy_http_jwt_authn_v3.JwtRequirement{ - RequiresType: &envoy_http_jwt_authn_v3.JwtRequirement_ProviderName{ - ProviderName: makeComputedProviderName(provider.Name, perm, permIdx), - }, - }, - }, - } - - if perm != nil && perm.HTTP != nil { - if perm.HTTP.PathPrefix != "" { - rule.Match.PathSpecifier = &envoy_route_v3.RouteMatch_Prefix{ - Prefix: perm.HTTP.PathPrefix, - } - } - - if perm.HTTP.PathExact != "" { - rule.Match.PathSpecifier = &envoy_route_v3.RouteMatch_Path{ - Path: perm.HTTP.PathExact, - } - } - - if perm.HTTP.PathRegex != "" { - rule.Match.PathSpecifier = &envoy_route_v3.RouteMatch_SafeRegex{ - SafeRegex: makeEnvoyRegexMatch(perm.HTTP.PathRegex), - } - } - } - - return rule -} - func hasJWTconfig(p []*structs.IntentionPermission) bool { for _, perm := range p { if perm.JWT != nil { diff --git a/agent/xds/jwt_authn_test.go b/agent/xds/jwt_authn_test.go index b2a7d7ce54df9..5da3f40235dc6 100644 --- a/agent/xds/jwt_authn_test.go +++ b/agent/xds/jwt_authn_test.go @@ -9,7 +9,6 @@ import ( "testing" envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" - envoy_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" envoy_http_jwt_authn_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/jwt_authn/v3" "github.com/hashicorp/consul/agent/structs" "github.com/stretchr/testify/require" @@ -620,104 +619,6 @@ func TestBuildJWTRetryPolicy(t *testing.T) { } } -func TestBuildRouteRule(t *testing.T) { - var ( - pWithExactPath = &structs.IntentionPermission{ - Action: structs.IntentionActionAllow, - HTTP: &structs.IntentionHTTPPermission{ - PathExact: "/exact-match", - }, - } - pWithRegex = &structs.IntentionPermission{ - Action: structs.IntentionActionAllow, - HTTP: &structs.IntentionHTTPPermission{ - PathRegex: "p([a-z]+)ch", - }, - } - ) - tests := map[string]struct { - provider *structs.IntentionJWTProvider - perm *structs.IntentionPermission - route string - expected *envoy_http_jwt_authn_v3.RequirementRule - }{ - "permission-nil": { - provider: &oktaProvider, - perm: nil, - route: "/my-route", - expected: &envoy_http_jwt_authn_v3.RequirementRule{ - Match: &envoy_route_v3.RouteMatch{PathSpecifier: &envoy_route_v3.RouteMatch_Prefix{Prefix: "/my-route"}}, - RequirementType: &envoy_http_jwt_authn_v3.RequirementRule_Requires{ - Requires: &envoy_http_jwt_authn_v3.JwtRequirement{ - RequiresType: &envoy_http_jwt_authn_v3.JwtRequirement_ProviderName{ - ProviderName: oktaProvider.Name, - }, - }, - }, - }, - }, - "permission-with-path-prefix": { - provider: &oktaProvider, - perm: pWithOktaProvider, - route: "/my-route", - expected: &envoy_http_jwt_authn_v3.RequirementRule{ - Match: &envoy_route_v3.RouteMatch{PathSpecifier: &envoy_route_v3.RouteMatch_Prefix{ - Prefix: pWithMultiProviders.HTTP.PathPrefix, - }}, - RequirementType: &envoy_http_jwt_authn_v3.RequirementRule_Requires{ - Requires: &envoy_http_jwt_authn_v3.JwtRequirement{ - RequiresType: &envoy_http_jwt_authn_v3.JwtRequirement_ProviderName{ - ProviderName: makeComputedProviderName(oktaProvider.Name, pWithMultiProviders, 0), - }, - }, - }, - }, - }, - "permission-with-exact-path": { - provider: &oktaProvider, - perm: pWithExactPath, - route: "/", - expected: &envoy_http_jwt_authn_v3.RequirementRule{ - Match: &envoy_route_v3.RouteMatch{PathSpecifier: &envoy_route_v3.RouteMatch_Path{ - Path: pWithExactPath.HTTP.PathExact, - }}, - RequirementType: &envoy_http_jwt_authn_v3.RequirementRule_Requires{ - Requires: &envoy_http_jwt_authn_v3.JwtRequirement{ - RequiresType: &envoy_http_jwt_authn_v3.JwtRequirement_ProviderName{ - ProviderName: makeComputedProviderName(oktaProvider.Name, pWithExactPath, 0), - }, - }, - }, - }, - }, - "permission-with-regex": { - provider: &oktaProvider, - perm: pWithRegex, - route: "/", - expected: &envoy_http_jwt_authn_v3.RequirementRule{ - Match: &envoy_route_v3.RouteMatch{PathSpecifier: &envoy_route_v3.RouteMatch_SafeRegex{ - SafeRegex: makeEnvoyRegexMatch(pWithRegex.HTTP.PathRegex), - }}, - RequirementType: &envoy_http_jwt_authn_v3.RequirementRule_Requires{ - Requires: &envoy_http_jwt_authn_v3.JwtRequirement{ - RequiresType: &envoy_http_jwt_authn_v3.JwtRequirement_ProviderName{ - ProviderName: makeComputedProviderName(oktaProvider.Name, pWithRegex, 0), - }, - }, - }, - }, - }, - } - - for name, tt := range tests { - tt := tt - t.Run(name, func(t *testing.T) { - res := buildRouteRule(tt.provider, tt.perm, tt.route, 0) - require.Equal(t, res, tt.expected) - }) - } -} - func TestHasJWTconfig(t *testing.T) { tests := map[string]struct { perms []*structs.IntentionPermission diff --git a/agent/xds/listeners.go b/agent/xds/listeners.go index 6e67cd1c564e2..3316298b46ed2 100644 --- a/agent/xds/listeners.go +++ b/agent/xds/listeners.go @@ -1291,6 +1291,7 @@ func (s *ResourceGenerator) makeInboundListener(cfgSnap *proxycfg.ConfigSnapshot partition: cfgSnap.ProxyID.PartitionOrDefault(), }, cfgSnap.ConnectProxy.InboundPeerTrustBundles, + cfgSnap.JWTProviders, ) if err != nil { return nil, err @@ -1377,6 +1378,7 @@ func (s *ResourceGenerator) makeInboundListener(cfgSnap *proxycfg.ConfigSnapshot partition: cfgSnap.ProxyID.PartitionOrDefault(), }, cfgSnap.ConnectProxy.InboundPeerTrustBundles, + cfgSnap.JWTProviders, ) if err != nil { return nil, err @@ -1844,6 +1846,7 @@ func (s *ResourceGenerator) makeFilterChainTerminatingGateway(cfgSnap *proxycfg. partition: cfgSnap.ProxyID.PartitionOrDefault(), }, nil, // TODO(peering): verify intentions w peers don't apply to terminatingGateway + cfgSnap.JWTProviders, ) if err != nil { return nil, err diff --git a/agent/xds/rbac.go b/agent/xds/rbac.go index 4cb77ad7f028e..78633643c2cac 100644 --- a/agent/xds/rbac.go +++ b/agent/xds/rbac.go @@ -28,7 +28,10 @@ func makeRBACNetworkFilter( localInfo rbacLocalInfo, peerTrustBundles []*pbpeering.PeeringTrustBundle, ) (*envoy_listener_v3.Filter, error) { - rules := makeRBACRules(intentions, intentionDefaultAllow, localInfo, false, peerTrustBundles) + rules, err := makeRBACRules(intentions, intentionDefaultAllow, localInfo, false, peerTrustBundles, map[string]*structs.JWTProviderConfigEntry{}) + if err != nil { + return nil, err + } cfg := &envoy_network_rbac_v3.RBAC{ StatPrefix: "connect_authz", @@ -42,8 +45,12 @@ func makeRBACHTTPFilter( intentionDefaultAllow bool, localInfo rbacLocalInfo, peerTrustBundles []*pbpeering.PeeringTrustBundle, + pCE map[string]*structs.JWTProviderConfigEntry, ) (*envoy_http_v3.HttpFilter, error) { - rules := makeRBACRules(intentions, intentionDefaultAllow, localInfo, true, peerTrustBundles) + rules, err := makeRBACRules(intentions, intentionDefaultAllow, localInfo, true, peerTrustBundles, pCE) + if err != nil { + return nil, err + } cfg := &envoy_http_rbac_v3.RBAC{ Rules: rules, @@ -56,7 +63,8 @@ func intentionListToIntermediateRBACForm( localInfo rbacLocalInfo, isHTTP bool, trustBundlesByPeer map[string]*pbpeering.PeeringTrustBundle, -) []*rbacIntention { + pCE map[string]*structs.JWTProviderConfigEntry, +) ([]*rbacIntention, error) { sort.Sort(structs.IntentionPrecedenceSorter(intentions)) // Omit any lower-precedence intentions that share the same source. @@ -73,10 +81,13 @@ func intentionListToIntermediateRBACForm( continue } - rixn := intentionToIntermediateRBACForm(ixn, localInfo, isHTTP, trustBundle) + rixn, err := intentionToIntermediateRBACForm(ixn, localInfo, isHTTP, trustBundle, pCE) + if err != nil { + return nil, err + } rbacIxns = append(rbacIxns, rixn) } - return rbacIxns + return rbacIxns, nil } func removeSourcePrecedence(rbacIxns []*rbacIntention, intentionDefaultAction intentionAction, localInfo rbacLocalInfo) []*rbacIntention { @@ -205,18 +216,60 @@ func removePermissionPrecedence(perms []*rbacPermission, intentionDefaultAction } perm.ComputedPermission = perm.Flatten() + + //attach jwt permissions + perm.ComputedPermission = perm.GenerateJWTPermissions() out = append(out, perm) } return out } +func (p *rbacPermission) GenerateJWTPermissions() *envoy_rbac_v3.Permission { + if p.jwtInfos == nil { + return p.ComputedPermission + } + + var jwtPerms []*envoy_rbac_v3.Permission + + for _, info := range p.jwtInfos { + claimsPermission := jwtInfosToPermission(info.ProviderRequirement.VerifyClaims, info.MetadataPayloadKey) + segments := pathToSegments([]string{"iss"}, info.MetadataPayloadKey) + perm := &envoy_rbac_v3.Permission{ + Rule: &envoy_rbac_v3.Permission_Metadata{ + Metadata: &envoy_matcher_v3.MetadataMatcher{ + Filter: jwtEnvoyFilter, + Path: segments, + Value: &envoy_matcher_v3.ValueMatcher{ + MatchPattern: &envoy_matcher_v3.ValueMatcher_StringMatch{ + StringMatch: &envoy_matcher_v3.StringMatcher{ + MatchPattern: &envoy_matcher_v3.StringMatcher_Exact{ + Exact: info.Provider.Issuer, + }, + }, + }, + }, + }, + }, + } + perm = andPermissions([]*envoy_rbac_v3.Permission{perm, claimsPermission}) + jwtPerms = append(jwtPerms, perm) + } + if len(jwtPerms) > 0 { + jwtPerm := orPermissions(jwtPerms) + return andPermissions([]*envoy_rbac_v3.Permission{p.ComputedPermission, jwtPerm}) + } + + return p.ComputedPermission +} + func intentionToIntermediateRBACForm( ixn *structs.Intention, localInfo rbacLocalInfo, isHTTP bool, bundle *pbpeering.PeeringTrustBundle, -) *rbacIntention { + pCE map[string]*structs.JWTProviderConfigEntry, +) (*rbacIntention, error) { rixn := &rbacIntention{ Source: rbacService{ ServiceName: ixn.SourceServiceName(), @@ -233,14 +286,18 @@ func intentionToIntermediateRBACForm( } if isHTTP && ixn.JWT != nil { - var c []*JWTInfo + var jwts []*JWTInfo for _, prov := range ixn.JWT.Providers { - if len(prov.VerifyClaims) > 0 { - c = append(c, makeJWTInfos(prov, nil, 0)) + jwtProvider, ok := pCE[prov.Name] + + if !ok { + return nil, fmt.Errorf("provider specified in intention does not exist. Provider name: %s", prov.Name) } + jwts = append(jwts, makeJWTInfos(prov, nil, 0, jwtProvider)) } - if len(c) > 0 { - rixn.jwtInfos = c + + if len(jwts) > 0 { + rixn.jwtInfos = jwts } } @@ -254,15 +311,18 @@ func intentionToIntermediateRBACForm( Action: intentionActionFromString(perm.Action), Perm: convertPermission(perm), } + if perm.JWT != nil { - var c []*JWTInfo + var jwts []*JWTInfo for _, prov := range perm.JWT.Providers { - if len(prov.VerifyClaims) > 0 { - c = append(c, makeJWTInfos(prov, perm, k)) + jwtProvider, ok := pCE[prov.Name] + if !ok { + return nil, fmt.Errorf("provider specified in intention does not exist. Provider name: %s", prov.Name) } + jwts = append(jwts, makeJWTInfos(prov, perm, k, jwtProvider)) } - if len(c) > 0 { - rbacPerm.jwtInfos = c + if len(jwts) > 0 { + rbacPerm.jwtInfos = jwts } } rixn.Permissions = append(rixn.Permissions, &rbacPerm) @@ -275,18 +335,23 @@ func intentionToIntermediateRBACForm( rixn.Action = intentionActionFromString(ixn.Action) } - return rixn + return rixn, nil } -func makeJWTInfos(p *structs.IntentionJWTProvider, perm *structs.IntentionPermission, permKey int) *JWTInfo { - return &JWTInfo{Claims: p.VerifyClaims, MetadataPayloadKey: buildPayloadInMetadataKey(p.Name, perm, permKey)} +func makeJWTInfos(p *structs.IntentionJWTProvider, perm *structs.IntentionPermission, permKey int, ce *structs.JWTProviderConfigEntry) *JWTInfo { + return &JWTInfo{ + ProviderRequirement: p, + MetadataPayloadKey: buildPayloadInMetadataKey(p.Name, perm, permKey), + Provider: ce, + } } type intentionAction int type JWTInfo struct { - Claims []*structs.IntentionJWTClaimVerification - MetadataPayloadKey string + Provider *structs.JWTProviderConfigEntry + MetadataPayloadKey string + ProviderRequirement *structs.IntentionJWTProvider } const ( @@ -526,7 +591,8 @@ func makeRBACRules( localInfo rbacLocalInfo, isHTTP bool, peerTrustBundles []*pbpeering.PeeringTrustBundle, -) *envoy_rbac_v3.RBAC { + pCE map[string]*structs.JWTProviderConfigEntry, +) (*envoy_rbac_v3.RBAC, error) { // TODO(banks,rb): Implement revocation list checking? // TODO(peering): mkeeler asked that these maps come from proxycfg instead of @@ -546,7 +612,10 @@ func makeRBACRules( } // First build up just the basic principal matches. - rbacIxns := intentionListToIntermediateRBACForm(intentions, localInfo, isHTTP, trustBundlesByPeer) + rbacIxns, err := intentionListToIntermediateRBACForm(intentions, localInfo, isHTTP, trustBundlesByPeer, pCE) + if err != nil { + return nil, err + } // Normalize: if we are in default-deny then all intentions must be allows and vice versa intentionDefaultAction := intentionActionFromBool(intentionDefaultAllow) @@ -576,7 +645,7 @@ func makeRBACRules( for i, rbacIxn := range rbacIxns { var infos []*JWTInfo if isHTTP { - infos = collectJWTInfos(rbacIxn) + infos = collectTopLevelJWTInfos(rbacIxn) } if rbacIxn.Action == intentionActionLayer7 { if len(rbacIxn.Permissions) == 0 { @@ -588,8 +657,7 @@ func makeRBACRules( rbacPrincipals := optimizePrincipals([]*envoy_rbac_v3.Principal{rbacIxn.ComputedPrincipal}) if len(infos) > 0 { - claimsPrincipal := jwtInfosToPrincipals(infos) - rbacPrincipals = combineBasePrincipalWithJWTPrincipals(rbacPrincipals, claimsPrincipal) + rbacPrincipals = combineBasePrincipalWithJWTPrincipals(rbacPrincipals, infos) } // For L7: we should generate one Policy per Principal and list all of the Permissions policy := &envoy_rbac_v3.Policy{ @@ -605,8 +673,7 @@ func makeRBACRules( principalsL4 = append(principalsL4, rbacIxn.ComputedPrincipal) // Append JWT principals to list of principals if len(infos) > 0 { - claimsPrincipal := jwtInfosToPrincipals(infos) - principalsL4 = combineBasePrincipalWithJWTPrincipals(principalsL4, claimsPrincipal) + principalsL4 = combineBasePrincipalWithJWTPrincipals(principalsL4, infos) } } } @@ -620,14 +687,41 @@ func makeRBACRules( if len(rbac.Policies) == 0 { rbac.Policies = nil } - return rbac + return rbac, nil } // combineBasePrincipalWithJWTPrincipals ensure each RBAC/Network principal is associated with // the JWT principal -func combineBasePrincipalWithJWTPrincipals(p []*envoy_rbac_v3.Principal, cp *envoy_rbac_v3.Principal) []*envoy_rbac_v3.Principal { +func combineBasePrincipalWithJWTPrincipals(p []*envoy_rbac_v3.Principal, infos []*JWTInfo) []*envoy_rbac_v3.Principal { res := make([]*envoy_rbac_v3.Principal, 0) + jwtPrincipals := make([]*envoy_rbac_v3.Principal, 0) + for _, info := range infos { + claimsPrincipal := jwtClaimsToPrincipals(info.ProviderRequirement.VerifyClaims, info.MetadataPayloadKey) + segments := pathToSegments([]string{"iss"}, info.MetadataPayloadKey) + p := &envoy_rbac_v3.Principal{ + Identifier: &envoy_rbac_v3.Principal_Metadata{ + Metadata: &envoy_matcher_v3.MetadataMatcher{ + Filter: jwtEnvoyFilter, + Path: segments, + Value: &envoy_matcher_v3.ValueMatcher{ + MatchPattern: &envoy_matcher_v3.ValueMatcher_StringMatch{ + StringMatch: &envoy_matcher_v3.StringMatcher{ + MatchPattern: &envoy_matcher_v3.StringMatcher_Exact{ + Exact: info.Provider.Issuer, + }, + }, + }, + }, + }, + }, + } + p = andPrincipals([]*envoy_rbac_v3.Principal{p, claimsPrincipal}) + jwtPrincipals = append(jwtPrincipals, p) + } + + cp := orPrincipals(jwtPrincipals) + for _, principal := range p { if principal != nil && cp != nil { p := andPrincipals([]*envoy_rbac_v3.Principal{principal, cp}) @@ -637,32 +731,29 @@ func combineBasePrincipalWithJWTPrincipals(p []*envoy_rbac_v3.Principal, cp *env return res } -// collectJWTInfos extracts all the collected JWTInfos top level infos -// and permission level infos and returns them as a single array -func collectJWTInfos(rbacIxn *rbacIntention) []*JWTInfo { +// collectTopLevelJWTInfos extracts all the top level jwt infos. +func collectTopLevelJWTInfos(rbacIxn *rbacIntention) []*JWTInfo { infos := make([]*JWTInfo, 0, len(rbacIxn.jwtInfos)) if len(rbacIxn.jwtInfos) > 0 { infos = append(infos, rbacIxn.jwtInfos...) } - for _, perm := range rbacIxn.Permissions { - infos = append(infos, perm.jwtInfos...) - } return infos } -func jwtInfosToPrincipals(c []*JWTInfo) *envoy_rbac_v3.Principal { +func jwtClaimsToPrincipals(claims []*structs.IntentionJWTClaimVerification, payloadkey string) *envoy_rbac_v3.Principal { ps := make([]*envoy_rbac_v3.Principal, 0) - for _, jwtInfo := range c { - if jwtInfo != nil { - for _, claim := range jwtInfo.Claims { - ps = append(ps, jwtClaimToPrincipal(claim, jwtInfo.MetadataPayloadKey)) - } - } + for _, claim := range claims { + ps = append(ps, jwtClaimToPrincipal(claim, payloadkey)) + } + switch len(ps) { + case 1: + return ps[0] + default: + return andPrincipals(ps) } - return orPrincipals(ps) } // jwtClaimToPrincipal takes in a payloadkey which is the metadata key. This key is generated by using provider name, @@ -692,6 +783,37 @@ func jwtClaimToPrincipal(c *structs.IntentionJWTClaimVerification, payloadKey st } } +func jwtInfosToPermission(claims []*structs.IntentionJWTClaimVerification, payloadkey string) *envoy_rbac_v3.Permission { + ps := make([]*envoy_rbac_v3.Permission, 0) + + for _, claim := range claims { + ps = append(ps, jwtClaimToPermission(claim, payloadkey)) + } + return andPermissions(ps) +} + +func jwtClaimToPermission(c *structs.IntentionJWTClaimVerification, payloadKey string) *envoy_rbac_v3.Permission { + + segments := pathToSegments(c.Path, payloadKey) + return &envoy_rbac_v3.Permission{ + Rule: &envoy_rbac_v3.Permission_Metadata{ + Metadata: &envoy_matcher_v3.MetadataMatcher{ + Filter: jwtEnvoyFilter, + Path: segments, + Value: &envoy_matcher_v3.ValueMatcher{ + MatchPattern: &envoy_matcher_v3.ValueMatcher_StringMatch{ + StringMatch: &envoy_matcher_v3.StringMatcher{ + MatchPattern: &envoy_matcher_v3.StringMatcher_Exact{ + Exact: c.Value, + }, + }, + }, + }, + }, + }, + } +} + // pathToSegments generates an array of MetadataMatcher_PathSegment that starts with the payloadkey // and is followed by all existing strings in the path. // @@ -1206,3 +1328,20 @@ func andPermissions(perms []*envoy_rbac_v3.Permission) *envoy_rbac_v3.Permission } } } + +func orPermissions(perms []*envoy_rbac_v3.Permission) *envoy_rbac_v3.Permission { + switch len(perms) { + case 0: + return anyPermission() + case 1: + return perms[0] + default: + return &envoy_rbac_v3.Permission{ + Rule: &envoy_rbac_v3.Permission_OrRules{ + OrRules: &envoy_rbac_v3.Permission_Set{ + Rules: perms, + }, + }, + } + } +} diff --git a/agent/xds/rbac_test.go b/agent/xds/rbac_test.go index 76f4467bffa66..6bc36028707a5 100644 --- a/agent/xds/rbac_test.go +++ b/agent/xds/rbac_test.go @@ -451,10 +451,11 @@ func TestRemoveIntentionPrecedence(t *testing.T) { for name, tt := range tests { t.Run(name, func(t *testing.T) { - rbacIxns := intentionListToIntermediateRBACForm(tt.intentions, testLocalInfo, tt.http, testPeerTrustBundle) + rbacIxns, err := intentionListToIntermediateRBACForm(tt.intentions, testLocalInfo, tt.http, testPeerTrustBundle, nil) intentionDefaultAction := intentionActionFromBool(tt.intentionDefaultAllow) rbacIxns = removeIntentionPrecedence(rbacIxns, intentionDefaultAction, testLocalInfo) + require.NoError(t, err) require.Equal(t, tt.expect, rbacIxns) }) } @@ -529,6 +530,10 @@ func TestMakeRBACNetworkAndHTTPFilters(t *testing.T) { {Path: []string{"perms", "role"}, Value: "admin"}, }, } + testJWTProviderConfigEntry = map[string]*structs.JWTProviderConfigEntry{ + "okta": {Name: "okta", Issuer: "mytest.okta-issuer"}, + "auth0": {Name: "auth0", Issuer: "mytest.auth0-issuer"}, + } jwtRequirement = &structs.IntentionJWTRequirement{ Providers: []*structs.IntentionJWTProvider{ &oktaWithClaims, @@ -922,7 +927,7 @@ func TestMakeRBACNetworkAndHTTPFilters(t *testing.T) { }) }) t.Run("http filter", func(t *testing.T) { - filter, err := makeRBACHTTPFilter(tt.intentions, tt.intentionDefaultAllow, testLocalInfo, testPeerTrustBundle) + filter, err := makeRBACHTTPFilter(tt.intentions, tt.intentionDefaultAllow, testLocalInfo, testPeerTrustBundle, testJWTProviderConfigEntry) require.NoError(t, err) t.Run("current", func(t *testing.T) { @@ -1202,7 +1207,7 @@ func TestPathToSegments(t *testing.T) { } } -func TestJwtClaimToPrincipal(t *testing.T) { +func TestJWTClaimsToPrincipals(t *testing.T) { var ( firstClaim = structs.IntentionJWTClaimVerification{ Path: []string{"perms"}, @@ -1234,7 +1239,7 @@ func TestJwtClaimToPrincipal(t *testing.T) { Identifier: &envoy_rbac_v3.Principal_Metadata{ Metadata: &envoy_matcher_v3.MetadataMatcher{ Filter: jwtEnvoyFilter, - Path: pathToSegments(secondClaim.Path, "second-key"), + Path: pathToSegments(secondClaim.Path, payloadKey), Value: &envoy_matcher_v3.ValueMatcher{ MatchPattern: &envoy_matcher_v3.ValueMatcher_StringMatch{ StringMatch: &envoy_matcher_v3.StringMatcher{ @@ -1249,38 +1254,21 @@ func TestJwtClaimToPrincipal(t *testing.T) { } ) tests := map[string]struct { - jwtInfos []*JWTInfo - expected *envoy_rbac_v3.Principal + claims []*structs.IntentionJWTClaimVerification + metadataPayloadKey string + expected *envoy_rbac_v3.Principal }{ - "single-jwt-info": { - jwtInfos: []*JWTInfo{ - { - Claims: []*structs.IntentionJWTClaimVerification{&firstClaim}, - MetadataPayloadKey: payloadKey, - }, - }, - expected: &envoy_rbac_v3.Principal{ - Identifier: &envoy_rbac_v3.Principal_OrIds{ - OrIds: &envoy_rbac_v3.Principal_Set{ - Ids: []*envoy_rbac_v3.Principal{&firstPrincipal}, - }, - }, - }, + "single-claim": { + claims: []*structs.IntentionJWTClaimVerification{&firstClaim}, + metadataPayloadKey: payloadKey, + expected: &firstPrincipal, }, - "multiple-jwt-info": { - jwtInfos: []*JWTInfo{ - { - Claims: []*structs.IntentionJWTClaimVerification{&firstClaim}, - MetadataPayloadKey: payloadKey, - }, - { - Claims: []*structs.IntentionJWTClaimVerification{&secondClaim}, - MetadataPayloadKey: "second-key", - }, - }, + "multiple-claims": { + claims: []*structs.IntentionJWTClaimVerification{&firstClaim, &secondClaim}, + metadataPayloadKey: payloadKey, expected: &envoy_rbac_v3.Principal{ - Identifier: &envoy_rbac_v3.Principal_OrIds{ - OrIds: &envoy_rbac_v3.Principal_Set{ + Identifier: &envoy_rbac_v3.Principal_AndIds{ + AndIds: &envoy_rbac_v3.Principal_Set{ Ids: []*envoy_rbac_v3.Principal{&firstPrincipal, &secondPrincipal}, }, }, @@ -1291,7 +1279,7 @@ func TestJwtClaimToPrincipal(t *testing.T) { for name, tt := range tests { tt := tt t.Run(name, func(t *testing.T) { - principal := jwtInfosToPrincipals(tt.jwtInfos) + principal := jwtClaimsToPrincipals(tt.claims, tt.metadataPayloadKey) require.Equal(t, principal, tt.expected) }) } diff --git a/agent/xds/testdata/jwt_authn/intention-with-path.golden b/agent/xds/testdata/jwt_authn/intention-with-path.golden index 6e925758ca36c..e3cb0b7e3270e 100644 --- a/agent/xds/testdata/jwt_authn/intention-with-path.golden +++ b/agent/xds/testdata/jwt_authn/intention-with-path.golden @@ -21,10 +21,15 @@ "rules": [ { "match": { - "prefix": "some-special-path" + "prefix": "/" }, "requires": { - "providerName": "okta_0" + "requiresAny": { + "requirements": [ + {"providerName": "okta_0"}, + {"allowMissingOrFailed": {}} + ] + } } } ] diff --git a/agent/xds/testdata/jwt_authn/local-provider.golden b/agent/xds/testdata/jwt_authn/local-provider.golden index 9efda0042bfde..528c0556a94b8 100644 --- a/agent/xds/testdata/jwt_authn/local-provider.golden +++ b/agent/xds/testdata/jwt_authn/local-provider.golden @@ -17,7 +17,12 @@ "prefix": "/" }, "requires": { - "providerName": "okta" + "requiresAny": { + "requirements": [ + {"providerName": "okta"}, + {"allowMissingOrFailed": {}} + ] + } } } ] diff --git a/agent/xds/testdata/jwt_authn/multiple-providers-and-one-permission.golden b/agent/xds/testdata/jwt_authn/multiple-providers-and-one-permission.golden index ca9a99265ee85..47ffffbaa8a98 100644 --- a/agent/xds/testdata/jwt_authn/multiple-providers-and-one-permission.golden +++ b/agent/xds/testdata/jwt_authn/multiple-providers-and-one-permission.golden @@ -47,28 +47,40 @@ } }, "rules": [ - { - "match": { - "prefix": "some-special-path" - }, - "requires": { - "providerName": "okta_0" - } - }, - { - "match": { - "prefix": "/" - }, - "requires": { - "providerName": "okta" - } - }, { "match": { "prefix": "/" }, "requires": { - "providerName": "auth0" + "requiresAll": { + "requirements": [ + { + "requiresAny": { + "requirements": [ + {"providerName": "okta_0"}, + {"allowMissingOrFailed": {}} + ] + } + }, + { + "requiresAny": { + "requirements": [ + {"providerName": "okta"}, + {"allowMissingOrFailed": {}} + ] + } + }, + { + "requiresAny": { + "requirements": [ + {"providerName": "auth0"}, + {"allowMissingOrFailed": {}} + ] + } + } + ] + } + } } ] diff --git a/agent/xds/testdata/jwt_authn/remote-provider.golden b/agent/xds/testdata/jwt_authn/remote-provider.golden index 6116a58cec006..3a66e2dcf362b 100644 --- a/agent/xds/testdata/jwt_authn/remote-provider.golden +++ b/agent/xds/testdata/jwt_authn/remote-provider.golden @@ -24,7 +24,12 @@ "prefix": "/" }, "requires": { - "providerName": "okta" + "requiresAny": { + "requirements": [ + {"providerName": "okta"}, + {"allowMissingOrFailed": {}} + ] + } } } ] diff --git a/agent/xds/testdata/jwt_authn/top-level-provider-with-permission.golden b/agent/xds/testdata/jwt_authn/top-level-provider-with-permission.golden index 6eed6793df526..518abe4734b00 100644 --- a/agent/xds/testdata/jwt_authn/top-level-provider-with-permission.golden +++ b/agent/xds/testdata/jwt_authn/top-level-provider-with-permission.golden @@ -33,20 +33,32 @@ } }, "rules": [ - { - "match": { - "prefix": "some-special-path" - }, - "requires": { - "providerName": "okta_0" - } - }, { "match": { "prefix": "/" }, "requires": { - "providerName": "okta" + "requiresAll": { + "requirements": [ + { + "requiresAny": { + "requirements": [ + {"providerName": "okta_0"}, + {"allowMissingOrFailed": {}} + ] + } + }, + { + "requiresAny": { + "requirements": [ + {"providerName": "okta"}, + {"allowMissingOrFailed": {}} + ] + } + } + ] + } + } } ] diff --git a/agent/xds/testdata/rbac/empty-top-level-jwt-with-one-permission--httpfilter.golden b/agent/xds/testdata/rbac/empty-top-level-jwt-with-one-permission--httpfilter.golden index cd5c35bab27e2..fd1f6c22ff9ac 100644 --- a/agent/xds/testdata/rbac/empty-top-level-jwt-with-one-permission--httpfilter.golden +++ b/agent/xds/testdata/rbac/empty-top-level-jwt-with-one-permission--httpfilter.golden @@ -7,33 +7,35 @@ "consul-intentions-layer7-0": { "permissions": [ { - "urlPath": { - "path": { - "prefix": "some-path" - } - } - } - ], - "principals": [ - { - "andIds": { - "ids": [ + "andRules": { + "rules": [ { - "authenticated": { - "principalName": { - "safeRegex": { - "googleRe2": {}, - "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" - } + "urlPath": { + "path": { + "prefix": "some-path" } } }, { - "orIds": { - "ids": [ - { + "andRules": { + "rules": [ + { "metadata": { - "filter":"envoy.filters.http.jwt_authn", + "filter": "envoy.filters.http.jwt_authn", + "path": [ + {"key": "jwt_payload_okta_0"}, + {"key": "iss"} + ], + "value": { + "stringMatch": { + "exact": "mytest.okta-issuer" + } + } + } + }, + { + "metadata": { + "filter": "envoy.filters.http.jwt_authn", "path": [ {"key": "jwt_payload_okta_0"}, {"key": "roles"} @@ -51,6 +53,18 @@ ] } } + ], + "principals": [ + { + "authenticated": { + "principalName": { + "safeRegex": { + "googleRe2": {}, + "regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$" + } + } + } + } ] } } diff --git a/agent/xds/testdata/rbac/top-level-jwt-no-permissions--httpfilter.golden b/agent/xds/testdata/rbac/top-level-jwt-no-permissions--httpfilter.golden index 35b3792e66583..bb834217aeb83 100644 --- a/agent/xds/testdata/rbac/top-level-jwt-no-permissions--httpfilter.golden +++ b/agent/xds/testdata/rbac/top-level-jwt-no-permissions--httpfilter.golden @@ -29,18 +29,38 @@ { "orIds": { "ids": [ - { - "metadata": { - "filter":"envoy.filters.http.jwt_authn", - "path": [ - {"key": "jwt_payload_okta"}, - {"key": "roles"} - ], - "value": { - "stringMatch": { - "exact": "testing" + { + "andIds": { + "ids": [ + { + "metadata": { + "filter":"envoy.filters.http.jwt_authn", + "path": [ + {"key": "jwt_payload_okta"}, + {"key": "iss"} + ], + "value": { + "stringMatch": { + "exact": "mytest.okta-issuer" + } + } + } + }, + { + "metadata": { + "filter":"envoy.filters.http.jwt_authn", + "path": [ + {"key": "jwt_payload_okta"}, + {"key": "roles"} + ], + "value": { + "stringMatch": { + "exact": "testing" + } + } + } } - } + ] } } ] diff --git a/agent/xds/testdata/rbac/top-level-jwt-with-multiple-permissions--httpfilter.golden b/agent/xds/testdata/rbac/top-level-jwt-with-multiple-permissions--httpfilter.golden index 409a3a4bd6b4b..c5eedad1e2a16 100644 --- a/agent/xds/testdata/rbac/top-level-jwt-with-multiple-permissions--httpfilter.golden +++ b/agent/xds/testdata/rbac/top-level-jwt-with-multiple-permissions--httpfilter.golden @@ -6,34 +6,116 @@ "policies": { "consul-intentions-layer7-0": { "permissions": [ - { - "urlPath": { - "path": { - "exact": "/v1/secret" - } - } - }, { "andRules": { "rules": [ { "urlPath": { "path": { - "exact": "/v1/admin" + "exact": "/v1/secret" } } }, { - "notRule": { - "urlPath": { - "path": { - "exact": "/v1/secret" + "andRules": { + "rules": [ + { + "metadata": { + "filter": "envoy.filters.http.jwt_authn", + "path": [ + {"key": "jwt_payload_auth0_0"}, + {"key": "iss"} + ], + "value": { + "stringMatch": { + "exact": "mytest.auth0-issuer" + } + } + } + }, + { + "metadata": { + "filter": "envoy.filters.http.jwt_authn", + "path": [ + {"key": "jwt_payload_auth0_0"}, + {"key": "perms"}, + {"key": "role"} + ], + "value": { + "stringMatch": { + "exact": "admin" + } + } + } } - } + ] } } ] } + }, + { + "andRules": { + "rules": [ + { + "andRules": { + "rules": [ + { + "urlPath": { + "path": { + "exact": "/v1/admin" + } + } + }, + { + "notRule": { + "urlPath": { + "path": { + "exact": "/v1/secret" + } + } + } + } + ] + } + }, + { + "andRules": { + "rules": [ + { + "metadata": { + "filter": "envoy.filters.http.jwt_authn", + "path": [ + {"key": "jwt_payload_auth0_1"}, + {"key": "iss"} + ], + "value": { + "stringMatch": { + "exact": "mytest.auth0-issuer" + } + } + } + }, + { + "metadata": { + "filter": "envoy.filters.http.jwt_authn", + "path": [ + {"key": "jwt_payload_auth0_1"}, + {"key": "perms"}, + {"key": "role"} + ], + "value": { + "stringMatch": { + "exact": "admin" + } + } + } + } + ] + } + } + ] + } } ], "principals": [ @@ -55,48 +137,38 @@ { "orIds": { "ids": [ - { - "metadata": { - "filter":"envoy.filters.http.jwt_authn", - "path": [ - {"key": "jwt_payload_okta"}, - {"key": "roles"} - ], - "value": { - "stringMatch": { - "exact": "testing" + { + "andIds": { + "ids": [ + { + "metadata": { + "filter":"envoy.filters.http.jwt_authn", + "path": [ + {"key": "jwt_payload_okta"}, + {"key": "iss"} + ], + "value": { + "stringMatch": { + "exact": "mytest.okta-issuer" + } + } + } + }, + { + "metadata": { + "filter":"envoy.filters.http.jwt_authn", + "path": [ + {"key": "jwt_payload_okta"}, + {"key": "roles"} + ], + "value": { + "stringMatch": { + "exact": "testing" + } + } + } } - } - } - }, - { - "metadata": { - "filter":"envoy.filters.http.jwt_authn", - "path": [ - {"key": "jwt_payload_auth0_0"}, - {"key": "perms"}, - {"key": "role"} - ], - "value": { - "stringMatch": { - "exact": "admin" - } - } - } - }, - { - "metadata": { - "filter":"envoy.filters.http.jwt_authn", - "path": [ - {"key": "jwt_payload_auth0_1"}, - {"key": "perms"}, - {"key": "role"} - ], - "value": { - "stringMatch": { - "exact": "admin" - } - } + ] } } ] diff --git a/agent/xds/testdata/rbac/top-level-jwt-with-one-permission--httpfilter.golden b/agent/xds/testdata/rbac/top-level-jwt-with-one-permission--httpfilter.golden index edf027f3e0cbb..cf70c02b3e4b9 100644 --- a/agent/xds/testdata/rbac/top-level-jwt-with-one-permission--httpfilter.golden +++ b/agent/xds/testdata/rbac/top-level-jwt-with-one-permission--httpfilter.golden @@ -7,10 +7,51 @@ "consul-intentions-layer7-0": { "permissions": [ { - "urlPath": { - "path": { - "exact": "/v1/secret" - } + "andRules": { + "rules": [ + { + "urlPath": { + "path": { + "exact": "/v1/secret" + } + } + }, + { + "andRules": { + "rules": [ + { + "metadata": { + "filter": "envoy.filters.http.jwt_authn", + "path": [ + {"key": "jwt_payload_auth0_0"}, + {"key": "iss"} + ], + "value": { + "stringMatch": { + "exact": "mytest.auth0-issuer" + } + } + } + }, + { + "metadata": { + "filter": "envoy.filters.http.jwt_authn", + "path": [ + {"key": "jwt_payload_auth0_0"}, + {"key": "perms"}, + {"key": "role"} + ], + "value": { + "stringMatch": { + "exact": "admin" + } + } + } + } + ] + } + } + ] } }, { @@ -55,33 +96,38 @@ { "orIds": { "ids": [ - { - "metadata": { - "filter":"envoy.filters.http.jwt_authn", - "path": [ - {"key": "jwt_payload_okta"}, - {"key": "roles"} - ], - "value": { - "stringMatch": { - "exact": "testing" + { + "andIds": { + "ids": [ + { + "metadata": { + "filter":"envoy.filters.http.jwt_authn", + "path": [ + {"key": "jwt_payload_okta"}, + {"key": "iss"} + ], + "value": { + "stringMatch": { + "exact": "mytest.okta-issuer" + } + } + } + }, + { + "metadata": { + "filter":"envoy.filters.http.jwt_authn", + "path": [ + {"key": "jwt_payload_okta"}, + {"key": "roles"} + ], + "value": { + "stringMatch": { + "exact": "testing" + } + } + } } - } - } - }, - { - "metadata": { - "filter":"envoy.filters.http.jwt_authn", - "path": [ - {"key": "jwt_payload_auth0_0"}, - {"key": "perms"}, - {"key": "role"} - ], - "value": { - "stringMatch": { - "exact": "admin" - } - } + ] } } ]