Backport of Use JWT-auth filter in metadata mode & Delegate validation to RBAC filter into release/1.16.x (#18153)

## Backport

This PR is auto-generated from #18062 to be assessed for backporting due
to the inclusion of the label backport/1.16.



The below text is copied from the body of the original PR.

---

### Description

<!-- Please describe why you're making this change, in plain English.
-->

- Currently the jwt-auth filter doesn't take into account the service
identity when validating jwt-auth, it only takes into account the path
and jwt provider during validation. This causes issues when multiple
source intentions restrict access to an endpoint with different JWT
providers.
- To fix these issues, rather than use the JWT auth filter for
validation, we use it in metadata mode and allow it to forward the
successful validated JWT token payload to the RBAC filter which will
make the decisions.

This PR ensures requests with and without JWT tokens successfully go
through the jwt-authn filter. The filter however only forwards the data
for successful/valid tokens. On the RBAC filter level, we check the
payload for claims and token issuer + existing rbac rules.

### Testing & Reproduction steps

<!--

* In the case of bugs, describe how to replicate
* If any manual tests were done, document the steps and the conditions
to replicate
* Call out any important/ relevant unit tests, e2e tests or integration
tests you have added or are adding

-->

- This test covers a multi level jwt requirements (requirements at top
level and permissions level). It also assumes you have envoy running,
you have a redis and a sidecar proxy service registered, and have a way
to generate jwks with jwt. I mostly use:
https://www.scottbrady91.com/tools/jwt for this.

- first write your proxy defaults
```
Kind = "proxy-defaults"
name = "global"
config {
  protocol = "http"
}
```
- Create two providers 
```
Kind = "jwt-provider"
Name = "auth0"
Issuer = "https://ronald.local"

JSONWebKeySet = {
    Local = {
     JWKS = "eyJrZXlzIjog....."
    }
}
```

```
Kind = "jwt-provider"
Name = "okta"
Issuer = "https://ronald.local"

JSONWebKeySet = {
   Local = {
     JWKS = "eyJrZXlzIjogW3...."
    }
}
```

- add a service intention
```
Kind = "service-intentions"
Name = "redis"

JWT = {
  Providers = [
    {
      Name = "okta"
    },
  ]
}

Sources = [
  {
    Name = "*"
    Permissions = [{
      Action = "allow"
      HTTP = {
        PathPrefix = "/workspace"
      }
      JWT = {
        Providers = [
          {
            Name = "okta"
            VerifyClaims = [
              {
                  Path = ["aud"]
                  Value = "my_client_app"
              },
              {
                Path = ["sub"]
                Value = "5be86359073c434bad2da3932222dabe"
              }
            ]
          },
        ]
      }

    },
    {
      Action = "allow"
      HTTP = {
        PathPrefix = "/"
      }
      JWT = {
        Providers = [
          {
            Name = "auth0"
          },
        ]
      }

    }]
  }
]
```
- generate 3 jwt tokens: 1 from auth0 jwks, 1 from okta jwks with
different claims than `/workspace` expects and 1 with correct claims
- connect to your envoy (change service and address as needed) to view
logs and potential errors. You can add: `-- --log-level debug` to see
what data is being forwarded
```
consul connect envoy -sidecar-for redis1 -grpc-addr 127.0.0.1:8502
```
- Make the following requests: 
```
curl -s -H "Authorization: Bearer $Auth0_TOKEN" --insecure --cert leaf.cert --key leaf.key --cacert connect-ca.pem https://localhost:20000/workspace -v

RBAC filter denied

curl -s -H "Authorization: Bearer $Okta_TOKEN_with_wrong_claims" --insecure --cert leaf.cert --key leaf.key --cacert connect-ca.pem https://localhost:20000/workspace -v

RBAC filter denied

curl -s -H "Authorization: Bearer $Okta_TOKEN_with_correct_claims" --insecure --cert leaf.cert --key leaf.key --cacert connect-ca.pem https://localhost:20000/workspace -v

Successful request
```


### TODO

* [x] Update test coverage
* [ ] update integration tests (follow-up PR)
* [x] appropriate backport labels added


---

<details>
<summary> Overview of commits </summary>

  - 70536f5a38507d7468f62d00dd93a6968a3d9cf3 

</details>

Co-authored-by: Ronald Ekambi <ronekambi@gmail.com>
This commit is contained in:
hc-github-team-consul-core 2023-07-17 10:50:21 -05:00 committed by GitHub
parent 2a31583727
commit db4b2cb577
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 658 additions and 614 deletions

View File

@ -13,6 +13,7 @@ import (
envoy_http_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" envoy_http_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
"google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/wrapperspb" "google.golang.org/protobuf/types/known/wrapperspb"
) )
@ -22,129 +23,149 @@ const (
jwksClusterPrefix = "jwks_cluster" jwksClusterPrefix = "jwks_cluster"
) )
// This is an intermediate JWTProvider form used to associate // makeJWTAuthFilter builds jwt filter for envoy. It limits its use to referenced provider rather than every provider.
// unique payload keys to providers //
type jwtAuthnProvider struct { // Eg. If you have three providers: okta, auth0 and fusionAuth and only okta is referenced in your intentions, then this
ComputedName string // will create a jwt-auth filter containing just okta in the list of providers.
Provider *structs.IntentionJWTProvider func makeJWTAuthFilter(providerMap map[string]*structs.JWTProviderConfigEntry, intentions structs.SimplifiedIntentions) (*envoy_http_v3.HttpFilter, error) {
}
func makeJWTAuthFilter(pCE map[string]*structs.JWTProviderConfigEntry, intentions structs.SimplifiedIntentions) (*envoy_http_v3.HttpFilter, error) {
providers := map[string]*envoy_http_jwt_authn_v3.JwtProvider{} providers := map[string]*envoy_http_jwt_authn_v3.JwtProvider{}
var rules []*envoy_http_jwt_authn_v3.RequirementRule var jwtRequirements []*envoy_http_jwt_authn_v3.JwtRequirement
for _, intention := range intentions { for _, intention := range intentions {
if intention.JWT == nil && !hasJWTconfig(intention.Permissions) { if intention.JWT == nil && !hasJWTconfig(intention.Permissions) {
continue continue
} }
for _, jwtReq := range collectJWTAuthnProviders(intention) { for _, p := range collectJWTProviders(intention) {
if _, ok := providers[jwtReq.ComputedName]; ok { providerName := p.Name
if _, ok := providers[providerName]; ok {
continue continue
} }
jwtProvider, ok := pCE[jwtReq.Provider.Name] providerCE, ok := providerMap[providerName]
if !ok { if !ok {
return nil, fmt.Errorf("provider specified in intention does not exist. Provider name: %s", jwtReq.Provider.Name) return nil, fmt.Errorf("provider specified in intention does not exist. Provider name: %s", providerName)
} }
// If intention permissions use HTTP-match criteria with
// VerifyClaims, then generate a clone of the jwt provider with a envoyCfg, err := buildJWTProviderConfig(providerCE)
// unique key for payload_in_metadata. The RBAC filter relies on
// the key to check the correct claims for the matched request.
envoyCfg, err := buildJWTProviderConfig(jwtProvider, jwtReq.ComputedName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
providers[jwtReq.ComputedName] = envoyCfg providers[providerName] = envoyCfg
} reqs := providerToJWTRequirement(providerCE)
jwtRequirements = append(jwtRequirements, reqs)
for k, perm := range intention.Permissions {
if perm.JWT == nil {
continue
}
for _, prov := range perm.JWT.Providers {
rule := buildRouteRule(prov, perm, "/", k)
rules = append(rules, rule)
}
}
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)
}
} }
} }
if len(intentions) == 0 && len(providers) == 0 { if len(jwtRequirements) == 0 {
//do not add jwt_authn filter when intentions don't have JWT //do not add jwt_authn filter when intentions don't have JWTs
return nil, nil return nil, nil
} }
cfg := &envoy_http_jwt_authn_v3.JwtAuthentication{ cfg := &envoy_http_jwt_authn_v3.JwtAuthentication{
Providers: providers, Providers: providers,
Rules: rules, Rules: []*envoy_http_jwt_authn_v3.RequirementRule{
{
Match: &envoy_route_v3.RouteMatch{
PathSpecifier: &envoy_route_v3.RouteMatch_Prefix{Prefix: "/"},
},
RequirementType: makeJWTRequirementRule(andJWTRequirements(jwtRequirements)),
},
},
} }
return makeEnvoyHTTPFilter(jwtEnvoyFilter, cfg) return makeEnvoyHTTPFilter(jwtEnvoyFilter, cfg)
} }
func collectJWTAuthnProviders(i *structs.Intention) []*jwtAuthnProvider { func makeJWTRequirementRule(r *envoy_http_jwt_authn_v3.JwtRequirement) *envoy_http_jwt_authn_v3.RequirementRule_Requires {
var reqs []*jwtAuthnProvider return &envoy_http_jwt_authn_v3.RequirementRule_Requires{
Requires: r,
}
}
// 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 nil
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,
},
},
}
}
}
// providerToJWTRequirement builds the envoy jwtRequirement.
//
// 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 providerToJWTRequirement(provider *structs.JWTProviderConfigEntry) *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: provider.Name,
},
},
// We use allowMissingOrFailed to allow rbac filter to do the validation
{
RequiresType: &envoy_http_jwt_authn_v3.JwtRequirement_AllowMissingOrFailed{
AllowMissingOrFailed: &emptypb.Empty{},
},
},
},
},
},
}
}
// collectJWTProviders returns a list of all top level and permission level referenced providers.
func collectJWTProviders(i *structs.Intention) []*structs.IntentionJWTProvider {
// get permission level providers
reqs := getPermissionsProviders(i.Permissions)
if i.JWT != nil { if i.JWT != nil {
for _, prov := range i.JWT.Providers { // get top level providers
reqs = append(reqs, &jwtAuthnProvider{Provider: prov, ComputedName: makeComputedProviderName(prov.Name, nil, 0)}) reqs = append(reqs, i.JWT.Providers...)
}
} }
reqs = append(reqs, getPermissionsProviders(i.Permissions)...)
return reqs return reqs
} }
func getPermissionsProviders(p []*structs.IntentionPermission) []*jwtAuthnProvider { func getPermissionsProviders(perms []*structs.IntentionPermission) []*structs.IntentionJWTProvider {
var reqs []*jwtAuthnProvider var reqs []*structs.IntentionJWTProvider
for k, perm := range p { for _, p := range perms {
if perm.JWT == nil { if p.JWT == nil {
continue continue
} }
for _, prov := range perm.JWT.Providers {
reqs = append(reqs, &jwtAuthnProvider{Provider: prov, ComputedName: makeComputedProviderName(prov.Name, perm, k)}) reqs = append(reqs, p.JWT.Providers...)
}
} }
return reqs return reqs
} }
// makeComputedProviderName is used to create names for unique provider per permission // buildPayloadInMetadataKey is used to create a unique payload key per provider.
// This is to stop jwt claims cross validation across permissions/providers. // This is to ensure claims are validated/forwarded specifically under the right provider.
// The forwarded payload is used with other data (eg. service identity) by the RBAC filter
// to validate access to resource.
// //
// eg. If Permission x is the 3rd permission and has a provider of original name okta // eg. With a provider named okta will have a payload key of: jwt_payload_okta
// this function will return okta_3 as the computed provider name func buildPayloadInMetadataKey(providerName string) string {
func makeComputedProviderName(name string, perm *structs.IntentionPermission, idx int) string { return jwtMetadataKeyPrefix + "_" + providerName
if perm == nil {
return name
}
return fmt.Sprintf("%s_%d", name, idx)
} }
// buildPayloadInMetadataKey is used to create a unique payload key per provider/permissions. func buildJWTProviderConfig(p *structs.JWTProviderConfigEntry) (*envoy_http_jwt_authn_v3.JwtProvider, error) {
// This is to ensure claims are validated/forwarded specifically under the right permission/path
// and ensure we don't accidentally validate claims from different permissions/providers.
//
// eg. With a provider named okta, the second permission in permission list will have a provider of:
// okta_2 and a payload key of: jwt_payload_okta_2. Whereas an okta provider with no specific permission
// will have a payload key of: jwt_payload_okta
func buildPayloadInMetadataKey(providerName string, perm *structs.IntentionPermission, idx int) string {
return fmt.Sprintf("%s_%s", jwtMetadataKeyPrefix, makeComputedProviderName(providerName, perm, idx))
}
func buildJWTProviderConfig(p *structs.JWTProviderConfigEntry, metadataKeySuffix string) (*envoy_http_jwt_authn_v3.JwtProvider, error) {
envoyCfg := envoy_http_jwt_authn_v3.JwtProvider{ envoyCfg := envoy_http_jwt_authn_v3.JwtProvider{
Issuer: p.Issuer, Issuer: p.Issuer,
Audiences: p.Audiences, Audiences: p.Audiences,
PayloadInMetadata: buildPayloadInMetadataKey(metadataKeySuffix, nil, 0), PayloadInMetadata: buildPayloadInMetadataKey(p.Name),
} }
if p.Forwarding != nil { if p.Forwarding != nil {
@ -262,43 +283,6 @@ func buildJWTRetryPolicy(r *structs.JWKSRetryPolicy) *envoy_core_v3.RetryPolicy
return &pol 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 { func hasJWTconfig(p []*structs.IntentionPermission) bool {
for _, perm := range p { for _, perm := range p {
if perm.JWT != nil { if perm.JWT != nil {

View File

@ -9,7 +9,6 @@ import (
"testing" "testing"
envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" 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" 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/hashicorp/consul/agent/structs"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -173,6 +172,10 @@ func TestMakeJWTAUTHFilters(t *testing.T) {
intentions structs.SimplifiedIntentions intentions structs.SimplifiedIntentions
provider map[string]*structs.JWTProviderConfigEntry provider map[string]*structs.JWTProviderConfigEntry
}{ }{
"no-provider": {
intentions: simplified(makeTestIntention(t, ixnOpts{src: "web", action: structs.IntentionActionAllow})),
provider: nil,
},
"remote-provider": { "remote-provider": {
intentions: simplified(makeTestIntention(t, ixnOpts{src: "web", action: structs.IntentionActionAllow, jwt: oktaIntention})), intentions: simplified(makeTestIntention(t, ixnOpts{src: "web", action: structs.IntentionActionAllow, jwt: oktaIntention})),
provider: remoteCE, provider: remoteCE,
@ -206,123 +209,45 @@ func TestMakeJWTAUTHFilters(t *testing.T) {
} }
} }
func TestMakeComputedProviderName(t *testing.T) { func TestCollectJWTProviders(t *testing.T) {
tests := map[string]struct {
name string
perm *structs.IntentionPermission
idx int
expected string
}{
"no-permissions": {
name: "okta",
idx: 0,
expected: "okta",
},
"exact-path-permission": {
name: "auth0",
perm: &structs.IntentionPermission{
HTTP: &structs.IntentionHTTPPermission{
PathExact: "admin",
},
},
idx: 5,
expected: "auth0_5",
},
}
for name, tt := range tests {
tt := tt
t.Run(name, func(t *testing.T) {
reqs := makeComputedProviderName(tt.name, tt.perm, tt.idx)
require.Equal(t, reqs, tt.expected)
})
}
}
func TestBuildPayloadInMetadataKey(t *testing.T) {
tests := map[string]struct {
name string
perm *structs.IntentionPermission
permIdx int
expected string
}{
"no-permissions": {
name: "okta",
expected: "jwt_payload_okta",
},
"path-prefix-permission": {
name: "auth0",
perm: &structs.IntentionPermission{
HTTP: &structs.IntentionHTTPPermission{
PathPrefix: "admin",
},
},
permIdx: 4,
expected: "jwt_payload_auth0_4",
},
}
for name, tt := range tests {
tt := tt
t.Run(name, func(t *testing.T) {
reqs := buildPayloadInMetadataKey(tt.name, tt.perm, tt.permIdx)
require.Equal(t, reqs, tt.expected)
})
}
}
func TestCollectJWTAuthnProviders(t *testing.T) {
tests := map[string]struct { tests := map[string]struct {
intention *structs.Intention intention *structs.Intention
expected []*jwtAuthnProvider expected []*structs.IntentionJWTProvider
}{ }{
"empty-top-level-jwt-and-empty-permissions": { "empty-top-level-jwt-and-empty-permissions": {
intention: makeTestIntention(t, ixnOpts{src: "web"}), intention: makeTestIntention(t, ixnOpts{src: "web"}),
expected: []*jwtAuthnProvider{}, expected: []*structs.IntentionJWTProvider{},
}, },
"top-level-jwt-and-empty-permissions": { "top-level-jwt-and-empty-permissions": {
intention: makeTestIntention(t, ixnOpts{src: "web", jwt: oktaIntention}), intention: makeTestIntention(t, ixnOpts{src: "web", jwt: oktaIntention}),
expected: []*jwtAuthnProvider{{Provider: &oktaProvider, ComputedName: oktaProvider.Name}}, expected: []*structs.IntentionJWTProvider{&oktaProvider},
}, },
"multi-top-level-jwt-and-empty-permissions": { "multi-top-level-jwt-and-empty-permissions": {
intention: makeTestIntention(t, ixnOpts{src: "web", jwt: multiProviderIntentions}), intention: makeTestIntention(t, ixnOpts{src: "web", jwt: multiProviderIntentions}),
expected: []*jwtAuthnProvider{ expected: []*structs.IntentionJWTProvider{&oktaProvider, &auth0Provider},
{Provider: &oktaProvider, ComputedName: oktaProvider.Name},
{Provider: &auth0Provider, ComputedName: auth0Provider.Name},
},
}, },
"top-level-jwt-and-one-jwt-permission": { "top-level-jwt-and-one-jwt-permission": {
intention: makeTestIntention(t, ixnOpts{src: "web", jwt: auth0Intention, perms: pWithOktaProvider}), intention: makeTestIntention(t, ixnOpts{src: "web", jwt: auth0Intention, perms: pWithOktaProvider}),
expected: []*jwtAuthnProvider{ expected: []*structs.IntentionJWTProvider{&auth0Provider, &oktaProvider},
{Provider: &auth0Provider, ComputedName: auth0Provider.Name},
{Provider: &oktaProvider, ComputedName: "okta_0"},
},
}, },
"top-level-jwt-and-multi-jwt-permissions": { "top-level-jwt-and-multi-jwt-permissions": {
intention: makeTestIntention(t, ixnOpts{src: "web", jwt: fakeIntention, perms: pWithMultiProviders}), intention: makeTestIntention(t, ixnOpts{src: "web", jwt: fakeIntention, perms: pWithMultiProviders}),
expected: []*jwtAuthnProvider{ expected: []*structs.IntentionJWTProvider{&fakeProvider, &oktaProvider, &auth0Provider},
{Provider: &fakeProvider, ComputedName: fakeProvider.Name},
{Provider: &oktaProvider, ComputedName: "okta_0"},
{Provider: &auth0Provider, ComputedName: "auth0_0"},
},
}, },
"empty-top-level-jwt-and-one-jwt-permission": { "empty-top-level-jwt-and-one-jwt-permission": {
intention: makeTestIntention(t, ixnOpts{src: "web", perms: pWithOktaProvider}), intention: makeTestIntention(t, ixnOpts{src: "web", perms: pWithOktaProvider}),
expected: []*jwtAuthnProvider{{Provider: &oktaProvider, ComputedName: "okta_0"}}, expected: []*structs.IntentionJWTProvider{&oktaProvider},
}, },
"empty-top-level-jwt-and-multi-jwt-permission": { "empty-top-level-jwt-and-multi-jwt-permission": {
intention: makeTestIntention(t, ixnOpts{src: "web", perms: pWithMultiProviders}), intention: makeTestIntention(t, ixnOpts{src: "web", perms: pWithMultiProviders}),
expected: []*jwtAuthnProvider{ expected: []*structs.IntentionJWTProvider{&oktaProvider, &auth0Provider},
{Provider: &oktaProvider, ComputedName: "okta_0"},
{Provider: &auth0Provider, ComputedName: "auth0_0"},
},
}, },
} }
for name, tt := range tests { for name, tt := range tests {
tt := tt tt := tt
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
reqs := collectJWTAuthnProviders(tt.intention) reqs := collectJWTProviders(tt.intention)
require.ElementsMatch(t, reqs, tt.expected) require.ElementsMatch(t, reqs, tt.expected)
}) })
} }
@ -331,43 +256,35 @@ func TestCollectJWTAuthnProviders(t *testing.T) {
func TestGetPermissionsProviders(t *testing.T) { func TestGetPermissionsProviders(t *testing.T) {
tests := map[string]struct { tests := map[string]struct {
perms []*structs.IntentionPermission perms []*structs.IntentionPermission
expected []*jwtAuthnProvider expected []*structs.IntentionJWTProvider
}{ }{
"empty-permissions": { "empty-permissions": {
perms: []*structs.IntentionPermission{}, perms: []*structs.IntentionPermission{},
expected: []*jwtAuthnProvider{}, expected: []*structs.IntentionJWTProvider{},
}, },
"nil-permissions": { "nil-permissions": {
perms: nil, perms: nil,
expected: []*jwtAuthnProvider{}, expected: []*structs.IntentionJWTProvider{},
}, },
"permissions-with-no-jwt": { "permissions-with-no-jwt": {
perms: []*structs.IntentionPermission{pWithNoJWT}, perms: []*structs.IntentionPermission{pWithNoJWT},
expected: []*jwtAuthnProvider{}, expected: []*structs.IntentionJWTProvider{},
}, },
"permissions-with-one-jwt": { "permissions-with-one-jwt": {
perms: []*structs.IntentionPermission{pWithOktaProvider, pWithNoJWT}, perms: []*structs.IntentionPermission{pWithOktaProvider, pWithNoJWT},
expected: []*jwtAuthnProvider{ expected: []*structs.IntentionJWTProvider{&oktaProvider},
{Provider: &oktaProvider, ComputedName: "okta_0"},
},
}, },
"permissions-with-multiple-jwt": { "permissions-with-multiple-jwt": {
perms: []*structs.IntentionPermission{pWithMultiProviders, pWithNoJWT}, perms: []*structs.IntentionPermission{pWithMultiProviders, pWithNoJWT},
expected: []*jwtAuthnProvider{ expected: []*structs.IntentionJWTProvider{&auth0Provider, &oktaProvider},
{Provider: &auth0Provider, ComputedName: "auth0_0"},
{Provider: &oktaProvider, ComputedName: "okta_0"},
},
}, },
} }
for name, tt := range tests { for name, tt := range tests {
tt := tt tt := tt
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
t.Run("getPermissionsProviders", func(t *testing.T) { p := getPermissionsProviders(tt.perms)
p := getPermissionsProviders(tt.perms) require.ElementsMatch(t, p, tt.expected)
require.ElementsMatch(t, p, tt.expected)
})
}) })
} }
} }
@ -415,7 +332,7 @@ func TestBuildJWTProviderConfig(t *testing.T) {
Issuer: fullCE.Issuer, Issuer: fullCE.Issuer,
Audiences: fullCE.Audiences, Audiences: fullCE.Audiences,
ForwardPayloadHeader: "user-token", ForwardPayloadHeader: "user-token",
PayloadInMetadata: buildPayloadInMetadataKey(ceRemoteJWKS.Name, nil, 0), PayloadInMetadata: buildPayloadInMetadataKey(ceRemoteJWKS.Name),
PadForwardPayloadHeader: false, PadForwardPayloadHeader: false,
Forward: true, Forward: true,
JwksSourceSpecifier: &envoy_http_jwt_authn_v3.JwtProvider_LocalJwks{ JwksSourceSpecifier: &envoy_http_jwt_authn_v3.JwtProvider_LocalJwks{
@ -433,7 +350,7 @@ func TestBuildJWTProviderConfig(t *testing.T) {
expected: &envoy_http_jwt_authn_v3.JwtProvider{ expected: &envoy_http_jwt_authn_v3.JwtProvider{
Issuer: fullCE.Issuer, Issuer: fullCE.Issuer,
Audiences: fullCE.Audiences, Audiences: fullCE.Audiences,
PayloadInMetadata: buildPayloadInMetadataKey(ceRemoteJWKS.Name, nil, 0), PayloadInMetadata: buildPayloadInMetadataKey(ceRemoteJWKS.Name),
JwksSourceSpecifier: &envoy_http_jwt_authn_v3.JwtProvider_RemoteJwks{ JwksSourceSpecifier: &envoy_http_jwt_authn_v3.JwtProvider_RemoteJwks{
RemoteJwks: &envoy_http_jwt_authn_v3.RemoteJwks{ RemoteJwks: &envoy_http_jwt_authn_v3.RemoteJwks{
HttpUri: &envoy_core_v3.HttpUri{ HttpUri: &envoy_core_v3.HttpUri{
@ -453,7 +370,7 @@ func TestBuildJWTProviderConfig(t *testing.T) {
for name, tt := range tests { for name, tt := range tests {
tt := tt tt := tt
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
res, err := buildJWTProviderConfig(tt.ce, tt.ce.GetName()) res, err := buildJWTProviderConfig(tt.ce)
if tt.expectedError != "" { if tt.expectedError != "" {
require.Error(t, err) require.Error(t, err)
@ -620,104 +537,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) { func TestHasJWTconfig(t *testing.T) {
tests := map[string]struct { tests := map[string]struct {
perms []*structs.IntentionPermission perms []*structs.IntentionPermission

View File

@ -1291,6 +1291,7 @@ func (s *ResourceGenerator) makeInboundListener(cfgSnap *proxycfg.ConfigSnapshot
partition: cfgSnap.ProxyID.PartitionOrDefault(), partition: cfgSnap.ProxyID.PartitionOrDefault(),
}, },
cfgSnap.ConnectProxy.InboundPeerTrustBundles, cfgSnap.ConnectProxy.InboundPeerTrustBundles,
cfgSnap.JWTProviders,
) )
if err != nil { if err != nil {
return nil, err return nil, err
@ -1364,9 +1365,9 @@ func (s *ResourceGenerator) makeInboundListener(cfgSnap *proxycfg.ConfigSnapshot
logger: s.Logger, logger: s.Logger,
} }
if useHTTPFilter { if useHTTPFilter {
jwtFilter, jwtFilterErr := makeJWTAuthFilter(cfgSnap.JWTProviders, cfgSnap.ConnectProxy.Intentions) jwtFilter, err := makeJWTAuthFilter(cfgSnap.JWTProviders, cfgSnap.ConnectProxy.Intentions)
if jwtFilterErr != nil { if err != nil {
return nil, jwtFilterErr return nil, err
} }
rbacFilter, err := makeRBACHTTPFilter( rbacFilter, err := makeRBACHTTPFilter(
cfgSnap.ConnectProxy.Intentions, cfgSnap.ConnectProxy.Intentions,
@ -1377,6 +1378,7 @@ func (s *ResourceGenerator) makeInboundListener(cfgSnap *proxycfg.ConfigSnapshot
partition: cfgSnap.ProxyID.PartitionOrDefault(), partition: cfgSnap.ProxyID.PartitionOrDefault(),
}, },
cfgSnap.ConnectProxy.InboundPeerTrustBundles, cfgSnap.ConnectProxy.InboundPeerTrustBundles,
cfgSnap.JWTProviders,
) )
if err != nil { if err != nil {
return nil, err return nil, err
@ -1844,6 +1846,7 @@ func (s *ResourceGenerator) makeFilterChainTerminatingGateway(cfgSnap *proxycfg.
partition: cfgSnap.ProxyID.PartitionOrDefault(), partition: cfgSnap.ProxyID.PartitionOrDefault(),
}, },
nil, // TODO(peering): verify intentions w peers don't apply to terminatingGateway nil, // TODO(peering): verify intentions w peers don't apply to terminatingGateway
cfgSnap.JWTProviders,
) )
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -28,7 +28,10 @@ func makeRBACNetworkFilter(
localInfo rbacLocalInfo, localInfo rbacLocalInfo,
peerTrustBundles []*pbpeering.PeeringTrustBundle, peerTrustBundles []*pbpeering.PeeringTrustBundle,
) (*envoy_listener_v3.Filter, error) { ) (*envoy_listener_v3.Filter, error) {
rules := makeRBACRules(intentions, intentionDefaultAllow, localInfo, false, peerTrustBundles) rules, err := makeRBACRules(intentions, intentionDefaultAllow, localInfo, false, peerTrustBundles, nil)
if err != nil {
return nil, err
}
cfg := &envoy_network_rbac_v3.RBAC{ cfg := &envoy_network_rbac_v3.RBAC{
StatPrefix: "connect_authz", StatPrefix: "connect_authz",
@ -42,8 +45,12 @@ func makeRBACHTTPFilter(
intentionDefaultAllow bool, intentionDefaultAllow bool,
localInfo rbacLocalInfo, localInfo rbacLocalInfo,
peerTrustBundles []*pbpeering.PeeringTrustBundle, peerTrustBundles []*pbpeering.PeeringTrustBundle,
providerMap map[string]*structs.JWTProviderConfigEntry,
) (*envoy_http_v3.HttpFilter, error) { ) (*envoy_http_v3.HttpFilter, error) {
rules := makeRBACRules(intentions, intentionDefaultAllow, localInfo, true, peerTrustBundles) rules, err := makeRBACRules(intentions, intentionDefaultAllow, localInfo, true, peerTrustBundles, providerMap)
if err != nil {
return nil, err
}
cfg := &envoy_http_rbac_v3.RBAC{ cfg := &envoy_http_rbac_v3.RBAC{
Rules: rules, Rules: rules,
@ -56,7 +63,8 @@ func intentionListToIntermediateRBACForm(
localInfo rbacLocalInfo, localInfo rbacLocalInfo,
isHTTP bool, isHTTP bool,
trustBundlesByPeer map[string]*pbpeering.PeeringTrustBundle, trustBundlesByPeer map[string]*pbpeering.PeeringTrustBundle,
) []*rbacIntention { providerMap map[string]*structs.JWTProviderConfigEntry,
) ([]*rbacIntention, error) {
sort.Sort(structs.IntentionPrecedenceSorter(intentions)) sort.Sort(structs.IntentionPrecedenceSorter(intentions))
// Omit any lower-precedence intentions that share the same source. // Omit any lower-precedence intentions that share the same source.
@ -73,10 +81,13 @@ func intentionListToIntermediateRBACForm(
continue continue
} }
rixn := intentionToIntermediateRBACForm(ixn, localInfo, isHTTP, trustBundle) rixn, err := intentionToIntermediateRBACForm(ixn, localInfo, isHTTP, trustBundle, providerMap)
if err != nil {
return nil, err
}
rbacIxns = append(rbacIxns, rixn) rbacIxns = append(rbacIxns, rixn)
} }
return rbacIxns return rbacIxns, nil
} }
func removeSourcePrecedence(rbacIxns []*rbacIntention, intentionDefaultAction intentionAction, localInfo rbacLocalInfo) []*rbacIntention { func removeSourcePrecedence(rbacIxns []*rbacIntention, intentionDefaultAction intentionAction, localInfo rbacLocalInfo) []*rbacIntention {
@ -216,7 +227,8 @@ func intentionToIntermediateRBACForm(
localInfo rbacLocalInfo, localInfo rbacLocalInfo,
isHTTP bool, isHTTP bool,
bundle *pbpeering.PeeringTrustBundle, bundle *pbpeering.PeeringTrustBundle,
) *rbacIntention { providerMap map[string]*structs.JWTProviderConfigEntry,
) (*rbacIntention, error) {
rixn := &rbacIntention{ rixn := &rbacIntention{
Source: rbacService{ Source: rbacService{
ServiceName: ixn.SourceServiceName(), ServiceName: ixn.SourceServiceName(),
@ -233,36 +245,41 @@ func intentionToIntermediateRBACForm(
} }
if isHTTP && ixn.JWT != nil { if isHTTP && ixn.JWT != nil {
var c []*JWTInfo var jwts []*JWTInfo
for _, prov := range ixn.JWT.Providers { for _, prov := range ixn.JWT.Providers {
if len(prov.VerifyClaims) > 0 { jwtProvider, ok := providerMap[prov.Name]
c = append(c, makeJWTInfos(prov, nil, 0))
if !ok {
return nil, fmt.Errorf("provider specified in intention does not exist. Provider name: %s", prov.Name)
} }
jwts = append(jwts, newJWTInfo(prov, jwtProvider))
} }
if len(c) > 0 {
rixn.jwtInfos = c rixn.jwtInfos = jwts
}
} }
if len(ixn.Permissions) > 0 { if len(ixn.Permissions) > 0 {
if isHTTP { if isHTTP {
rixn.Action = intentionActionLayer7 rixn.Action = intentionActionLayer7
rixn.Permissions = make([]*rbacPermission, 0, len(ixn.Permissions)) rixn.Permissions = make([]*rbacPermission, 0, len(ixn.Permissions))
for k, perm := range ixn.Permissions { for _, perm := range ixn.Permissions {
rbacPerm := rbacPermission{ rbacPerm := rbacPermission{
Definition: perm, Definition: perm,
Action: intentionActionFromString(perm.Action), Action: intentionActionFromString(perm.Action),
Perm: convertPermission(perm), Perm: convertPermission(perm),
} }
if perm.JWT != nil { if perm.JWT != nil {
var c []*JWTInfo var jwts []*JWTInfo
for _, prov := range perm.JWT.Providers { for _, prov := range perm.JWT.Providers {
if len(prov.VerifyClaims) > 0 { jwtProvider, ok := providerMap[prov.Name]
c = append(c, makeJWTInfos(prov, perm, k)) if !ok {
return nil, fmt.Errorf("provider specified in intention does not exist. Provider name: %s", prov.Name)
} }
jwts = append(jwts, newJWTInfo(prov, jwtProvider))
} }
if len(c) > 0 { if len(jwts) > 0 {
rbacPerm.jwtInfos = c rbacPerm.jwtInfos = jwts
} }
} }
rixn.Permissions = append(rixn.Permissions, &rbacPerm) rixn.Permissions = append(rixn.Permissions, &rbacPerm)
@ -275,18 +292,24 @@ func intentionToIntermediateRBACForm(
rixn.Action = intentionActionFromString(ixn.Action) rixn.Action = intentionActionFromString(ixn.Action)
} }
return rixn return rixn, nil
} }
func makeJWTInfos(p *structs.IntentionJWTProvider, perm *structs.IntentionPermission, permKey int) *JWTInfo { func newJWTInfo(p *structs.IntentionJWTProvider, ce *structs.JWTProviderConfigEntry) *JWTInfo {
return &JWTInfo{Claims: p.VerifyClaims, MetadataPayloadKey: buildPayloadInMetadataKey(p.Name, perm, permKey)} return &JWTInfo{
Provider: p,
Issuer: ce.Issuer,
}
} }
type intentionAction int type intentionAction int
type JWTInfo struct { type JWTInfo struct {
Claims []*structs.IntentionJWTClaimVerification // Provider issuer
MetadataPayloadKey string // this information is coming from the config entry
Issuer string
// Provider is the intention provider
Provider *structs.IntentionJWTProvider
} }
const ( const (
@ -341,26 +364,32 @@ type rbacIntention struct {
} }
func (r *rbacIntention) FlattenPrincipal(localInfo rbacLocalInfo) *envoy_rbac_v3.Principal { func (r *rbacIntention) FlattenPrincipal(localInfo rbacLocalInfo) *envoy_rbac_v3.Principal {
var principal *envoy_rbac_v3.Principal
if !localInfo.expectXFCC { if !localInfo.expectXFCC {
return r.flattenPrincipalFromCert() principal = r.flattenPrincipalFromCert()
} else if r.Source.Peer == "" { } else if r.Source.Peer == "" {
// NOTE: ixnSourceMatches should enforce that all of Source and NotSources // NOTE: ixnSourceMatches should enforce that all of Source and NotSources
// are peered or not-peered, so we only need to look at the Source element. // are peered or not-peered, so we only need to look at the Source element.
return r.flattenPrincipalFromCert() // intention is not relevant to peering principal = r.flattenPrincipalFromCert() // intention is not relevant to peering
} else {
// If this intention is an L7 peered one, then it is exclusively resolvable
// using XFCC, rather than the TLS SAN field.
fromXFCC := r.flattenPrincipalFromXFCC()
// Use of the XFCC one is gated on coming directly from our own gateways.
gwIDPattern := makeSpiffeMeshGatewayPattern(localInfo.trustDomain, localInfo.partition)
principal = andPrincipals([]*envoy_rbac_v3.Principal{
authenticatedPatternPrincipal(gwIDPattern),
fromXFCC,
})
} }
// If this intention is an L7 peered one, then it is exclusively resolvable if len(r.jwtInfos) == 0 {
// using XFCC, rather than the TLS SAN field. return principal
fromXFCC := r.flattenPrincipalFromXFCC() }
// Use of the XFCC one is gated on coming directly from our own gateways. return addJWTPrincipal(principal, r.jwtInfos)
gwIDPattern := makeSpiffeMeshGatewayPattern(localInfo.trustDomain, localInfo.partition)
return andPrincipals([]*envoy_rbac_v3.Principal{
authenticatedPatternPrincipal(gwIDPattern),
fromXFCC,
})
} }
func (r *rbacIntention) flattenPrincipalFromCert() *envoy_rbac_v3.Principal { func (r *rbacIntention) flattenPrincipalFromCert() *envoy_rbac_v3.Principal {
@ -417,17 +446,47 @@ type rbacPermission struct {
ComputedPermission *envoy_rbac_v3.Permission ComputedPermission *envoy_rbac_v3.Permission
} }
// Flatten ensure the permission rules, not-rules, and jwt validation rules are merged into a single computed permission.
//
// Details on JWTInfo section:
// For each JWTInfo (AKA provider required), this builds 1 single permission that validates that the jwt has
// the right issuer (`iss`) field and validates the claims (if any).
//
// After generating a single permission per info, it combines all the info permissions into a single OrPermission.
// This orPermission is then attached to initial computed permission for jwt payload and claims validation.
func (p *rbacPermission) Flatten() *envoy_rbac_v3.Permission { func (p *rbacPermission) Flatten() *envoy_rbac_v3.Permission {
if len(p.NotPerms) == 0 { computedPermission := p.Perm
return p.Perm if len(p.NotPerms) == 0 && len(p.jwtInfos) == 0 {
return computedPermission
} }
parts := make([]*envoy_rbac_v3.Permission, 0, len(p.NotPerms)+1) if len(p.NotPerms) != 0 {
parts = append(parts, p.Perm) parts := make([]*envoy_rbac_v3.Permission, 0, len(p.NotPerms)+1)
for _, notPerm := range p.NotPerms { parts = append(parts, p.Perm)
parts = append(parts, notPermission(notPerm)) for _, notPerm := range p.NotPerms {
parts = append(parts, notPermission(notPerm))
}
computedPermission = andPermissions(parts)
} }
return andPermissions(parts)
if len(p.jwtInfos) == 0 {
return computedPermission
}
var jwtPerms []*envoy_rbac_v3.Permission
for _, info := range p.jwtInfos {
payloadKey := buildPayloadInMetadataKey(info.Provider.Name)
claimsPermission := jwtInfosToPermission(info.Provider.VerifyClaims, payloadKey)
issuerPermission := segmentToPermission(pathToSegments([]string{"iss"}, payloadKey), info.Issuer)
perm := andPermissions([]*envoy_rbac_v3.Permission{
issuerPermission, claimsPermission,
})
jwtPerms = append(jwtPerms, perm)
}
jwtPerm := orPermissions(jwtPerms)
return andPermissions([]*envoy_rbac_v3.Permission{computedPermission, jwtPerm})
} }
// simplifyNotSourceSlice will collapse NotSources elements together if any element is // simplifyNotSourceSlice will collapse NotSources elements together if any element is
@ -526,7 +585,8 @@ func makeRBACRules(
localInfo rbacLocalInfo, localInfo rbacLocalInfo,
isHTTP bool, isHTTP bool,
peerTrustBundles []*pbpeering.PeeringTrustBundle, peerTrustBundles []*pbpeering.PeeringTrustBundle,
) *envoy_rbac_v3.RBAC { providerMap map[string]*structs.JWTProviderConfigEntry,
) (*envoy_rbac_v3.RBAC, error) {
// TODO(banks,rb): Implement revocation list checking? // TODO(banks,rb): Implement revocation list checking?
// TODO(peering): mkeeler asked that these maps come from proxycfg instead of // TODO(peering): mkeeler asked that these maps come from proxycfg instead of
@ -546,7 +606,10 @@ func makeRBACRules(
} }
// First build up just the basic principal matches. // First build up just the basic principal matches.
rbacIxns := intentionListToIntermediateRBACForm(intentions, localInfo, isHTTP, trustBundlesByPeer) rbacIxns, err := intentionListToIntermediateRBACForm(intentions, localInfo, isHTTP, trustBundlesByPeer, providerMap)
if err != nil {
return nil, err
}
// Normalize: if we are in default-deny then all intentions must be allows and vice versa // Normalize: if we are in default-deny then all intentions must be allows and vice versa
intentionDefaultAction := intentionActionFromBool(intentionDefaultAllow) intentionDefaultAction := intentionActionFromBool(intentionDefaultAllow)
@ -574,10 +637,6 @@ func makeRBACRules(
var principalsL4 []*envoy_rbac_v3.Principal var principalsL4 []*envoy_rbac_v3.Principal
for i, rbacIxn := range rbacIxns { for i, rbacIxn := range rbacIxns {
var infos []*JWTInfo
if isHTTP {
infos = collectJWTInfos(rbacIxn)
}
if rbacIxn.Action == intentionActionLayer7 { if rbacIxn.Action == intentionActionLayer7 {
if len(rbacIxn.Permissions) == 0 { if len(rbacIxn.Permissions) == 0 {
panic("invalid state: L7 intention has no permissions") panic("invalid state: L7 intention has no permissions")
@ -587,10 +646,6 @@ func makeRBACRules(
} }
rbacPrincipals := optimizePrincipals([]*envoy_rbac_v3.Principal{rbacIxn.ComputedPrincipal}) rbacPrincipals := optimizePrincipals([]*envoy_rbac_v3.Principal{rbacIxn.ComputedPrincipal})
if len(infos) > 0 {
claimsPrincipal := jwtInfosToPrincipals(infos)
rbacPrincipals = combineBasePrincipalWithJWTPrincipals(rbacPrincipals, claimsPrincipal)
}
// For L7: we should generate one Policy per Principal and list all of the Permissions // For L7: we should generate one Policy per Principal and list all of the Permissions
policy := &envoy_rbac_v3.Policy{ policy := &envoy_rbac_v3.Policy{
Principals: rbacPrincipals, Principals: rbacPrincipals,
@ -603,11 +658,6 @@ func makeRBACRules(
} else { } else {
// For L4: we should generate one big Policy listing all Principals // For L4: we should generate one big Policy listing all Principals
principalsL4 = append(principalsL4, rbacIxn.ComputedPrincipal) principalsL4 = append(principalsL4, rbacIxn.ComputedPrincipal)
// Append JWT principals to list of principals
if len(infos) > 0 {
claimsPrincipal := jwtInfosToPrincipals(infos)
principalsL4 = combineBasePrincipalWithJWTPrincipals(principalsL4, claimsPrincipal)
}
} }
} }
if len(principalsL4) > 0 { if len(principalsL4) > 0 {
@ -620,59 +670,74 @@ func makeRBACRules(
if len(rbac.Policies) == 0 { if len(rbac.Policies) == 0 {
rbac.Policies = nil rbac.Policies = nil
} }
return rbac return rbac, nil
} }
// combineBasePrincipalWithJWTPrincipals ensure each RBAC/Network principal is associated with // addJWTPrincipal ensure the passed RBAC/Network principal is associated with
// the JWT principal // a JWT principal when JWTs validation is required.
func combineBasePrincipalWithJWTPrincipals(p []*envoy_rbac_v3.Principal, cp *envoy_rbac_v3.Principal) []*envoy_rbac_v3.Principal { //
res := make([]*envoy_rbac_v3.Principal, 0) // For each jwtInfo, this builds a first principal that validates that the jwt has the right issuer (`iss`).
// It collects all the claims principal and combines them into a single principal using jwtClaimsToPrincipals.
// It then combines the issuer principal and the claims principal into a single principal.
//
// After generating a single principal per info, it combines all the info principals into a single jwt OrPrincipal.
// This orPrincipal is then attached to the RBAC/NETWORK principal for jwt payload validation.
func addJWTPrincipal(principal *envoy_rbac_v3.Principal, infos []*JWTInfo) *envoy_rbac_v3.Principal {
if len(infos) == 0 {
return principal
}
jwtPrincipals := make([]*envoy_rbac_v3.Principal, 0, len(infos))
for _, info := range infos {
payloadKey := buildPayloadInMetadataKey(info.Provider.Name)
for _, principal := range p { // build jwt provider issuer principal
if principal != nil && cp != nil { segments := pathToSegments([]string{"iss"}, payloadKey)
p := andPrincipals([]*envoy_rbac_v3.Principal{principal, cp}) p := segmentToPrincipal(segments, info.Issuer)
res = append(res, p)
// add jwt provider claims principal if any
if cp := jwtClaimsToPrincipals(info.Provider.VerifyClaims, payloadKey); cp != nil {
p = andPrincipals([]*envoy_rbac_v3.Principal{p, cp})
} }
jwtPrincipals = append(jwtPrincipals, p)
} }
return res
// make jwt principals into 1 single principal
jwtFinalPrincipal := orPrincipals(jwtPrincipals)
if principal == nil {
return jwtFinalPrincipal
}
return andPrincipals([]*envoy_rbac_v3.Principal{principal, jwtFinalPrincipal})
} }
// collectJWTInfos extracts all the collected JWTInfos top level infos func jwtClaimsToPrincipals(claims []*structs.IntentionJWTClaimVerification, payloadkey string) *envoy_rbac_v3.Principal {
// and permission level infos and returns them as a single array
func collectJWTInfos(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 {
ps := make([]*envoy_rbac_v3.Principal, 0) ps := make([]*envoy_rbac_v3.Principal, 0)
for _, jwtInfo := range c { for _, claim := range claims {
if jwtInfo != nil { ps = append(ps, jwtClaimToPrincipal(claim, payloadkey))
for _, claim := range jwtInfo.Claims { }
ps = append(ps, jwtClaimToPrincipal(claim, jwtInfo.MetadataPayloadKey)) switch len(ps) {
} case 0:
} return nil
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, // jwtClaimToPrincipal takes in a payloadkey which is the metadata key. This key is generated by using provider name
// permission index with a jwt_payload prefix. See buildPayloadInMetadataKey in agent/xds/jwt_authn.go // and a jwt_payload prefix. See buildPayloadInMetadataKey in agent/xds/jwt_authn.go
// //
// This uniquely generated payloadKey is the first segment in the path to validate the JWT claims. The subsequent segments // This uniquely generated payloadKey is the first segment in the path to validate the JWT claims. The subsequent segments
// come from the Path included in the IntentionJWTClaimVerification param. // come from the Path included in the IntentionJWTClaimVerification param.
func jwtClaimToPrincipal(c *structs.IntentionJWTClaimVerification, payloadKey string) *envoy_rbac_v3.Principal { func jwtClaimToPrincipal(c *structs.IntentionJWTClaimVerification, payloadKey string) *envoy_rbac_v3.Principal {
segments := pathToSegments(c.Path, payloadKey) segments := pathToSegments(c.Path, payloadKey)
return segmentToPrincipal(segments, c.Value)
}
func segmentToPrincipal(segments []*envoy_matcher_v3.MetadataMatcher_PathSegment, v string) *envoy_rbac_v3.Principal {
return &envoy_rbac_v3.Principal{ return &envoy_rbac_v3.Principal{
Identifier: &envoy_rbac_v3.Principal_Metadata{ Identifier: &envoy_rbac_v3.Principal_Metadata{
Metadata: &envoy_matcher_v3.MetadataMatcher{ Metadata: &envoy_matcher_v3.MetadataMatcher{
@ -682,7 +747,41 @@ func jwtClaimToPrincipal(c *structs.IntentionJWTClaimVerification, payloadKey st
MatchPattern: &envoy_matcher_v3.ValueMatcher_StringMatch{ MatchPattern: &envoy_matcher_v3.ValueMatcher_StringMatch{
StringMatch: &envoy_matcher_v3.StringMatcher{ StringMatch: &envoy_matcher_v3.StringMatcher{
MatchPattern: &envoy_matcher_v3.StringMatcher_Exact{ MatchPattern: &envoy_matcher_v3.StringMatcher_Exact{
Exact: c.Value, Exact: v,
},
},
},
},
},
},
}
}
func jwtInfosToPermission(claims []*structs.IntentionJWTClaimVerification, payloadkey string) *envoy_rbac_v3.Permission {
ps := make([]*envoy_rbac_v3.Permission, 0, len(claims))
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 segmentToPermission(segments, c.Value)
}
func segmentToPermission(segments []*envoy_matcher_v3.MetadataMatcher_PathSegment, v string) *envoy_rbac_v3.Permission {
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: v,
}, },
}, },
}, },
@ -837,22 +936,32 @@ func countWild(src rbacService) int {
} }
func andPrincipals(ids []*envoy_rbac_v3.Principal) *envoy_rbac_v3.Principal { func andPrincipals(ids []*envoy_rbac_v3.Principal) *envoy_rbac_v3.Principal {
return &envoy_rbac_v3.Principal{ switch len(ids) {
Identifier: &envoy_rbac_v3.Principal_AndIds{ case 1:
AndIds: &envoy_rbac_v3.Principal_Set{ return ids[0]
Ids: ids, default:
return &envoy_rbac_v3.Principal{
Identifier: &envoy_rbac_v3.Principal_AndIds{
AndIds: &envoy_rbac_v3.Principal_Set{
Ids: ids,
},
}, },
}, }
} }
} }
func orPrincipals(ids []*envoy_rbac_v3.Principal) *envoy_rbac_v3.Principal { func orPrincipals(ids []*envoy_rbac_v3.Principal) *envoy_rbac_v3.Principal {
return &envoy_rbac_v3.Principal{ switch len(ids) {
Identifier: &envoy_rbac_v3.Principal_OrIds{ case 1:
OrIds: &envoy_rbac_v3.Principal_Set{ return ids[0]
Ids: ids, default:
return &envoy_rbac_v3.Principal{
Identifier: &envoy_rbac_v3.Principal_OrIds{
OrIds: &envoy_rbac_v3.Principal_Set{
Ids: ids,
},
}, },
}, }
} }
} }
@ -1206,3 +1315,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,
},
},
}
}
}

View File

@ -451,10 +451,11 @@ func TestRemoveIntentionPrecedence(t *testing.T) {
for name, tt := range tests { for name, tt := range tests {
t.Run(name, func(t *testing.T) { 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) intentionDefaultAction := intentionActionFromBool(tt.intentionDefaultAllow)
rbacIxns = removeIntentionPrecedence(rbacIxns, intentionDefaultAction, testLocalInfo) rbacIxns = removeIntentionPrecedence(rbacIxns, intentionDefaultAction, testLocalInfo)
require.NoError(t, err)
require.Equal(t, tt.expect, rbacIxns) require.Equal(t, tt.expect, rbacIxns)
}) })
} }
@ -529,6 +530,10 @@ func TestMakeRBACNetworkAndHTTPFilters(t *testing.T) {
{Path: []string{"perms", "role"}, Value: "admin"}, {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{ jwtRequirement = &structs.IntentionJWTRequirement{
Providers: []*structs.IntentionJWTProvider{ Providers: []*structs.IntentionJWTProvider{
&oktaWithClaims, &oktaWithClaims,
@ -922,7 +927,7 @@ func TestMakeRBACNetworkAndHTTPFilters(t *testing.T) {
}) })
}) })
t.Run("http filter", func(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) require.NoError(t, err)
t.Run("current", func(t *testing.T) { 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 ( var (
firstClaim = structs.IntentionJWTClaimVerification{ firstClaim = structs.IntentionJWTClaimVerification{
Path: []string{"perms"}, Path: []string{"perms"},
@ -1234,7 +1239,7 @@ func TestJwtClaimToPrincipal(t *testing.T) {
Identifier: &envoy_rbac_v3.Principal_Metadata{ Identifier: &envoy_rbac_v3.Principal_Metadata{
Metadata: &envoy_matcher_v3.MetadataMatcher{ Metadata: &envoy_matcher_v3.MetadataMatcher{
Filter: jwtEnvoyFilter, Filter: jwtEnvoyFilter,
Path: pathToSegments(secondClaim.Path, "second-key"), Path: pathToSegments(secondClaim.Path, payloadKey),
Value: &envoy_matcher_v3.ValueMatcher{ Value: &envoy_matcher_v3.ValueMatcher{
MatchPattern: &envoy_matcher_v3.ValueMatcher_StringMatch{ MatchPattern: &envoy_matcher_v3.ValueMatcher_StringMatch{
StringMatch: &envoy_matcher_v3.StringMatcher{ StringMatch: &envoy_matcher_v3.StringMatcher{
@ -1249,38 +1254,21 @@ func TestJwtClaimToPrincipal(t *testing.T) {
} }
) )
tests := map[string]struct { tests := map[string]struct {
jwtInfos []*JWTInfo claims []*structs.IntentionJWTClaimVerification
expected *envoy_rbac_v3.Principal metadataPayloadKey string
expected *envoy_rbac_v3.Principal
}{ }{
"single-jwt-info": { "single-claim": {
jwtInfos: []*JWTInfo{ claims: []*structs.IntentionJWTClaimVerification{&firstClaim},
{ metadataPayloadKey: payloadKey,
Claims: []*structs.IntentionJWTClaimVerification{&firstClaim}, expected: &firstPrincipal,
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},
},
},
},
}, },
"multiple-jwt-info": { "multiple-claims": {
jwtInfos: []*JWTInfo{ claims: []*structs.IntentionJWTClaimVerification{&firstClaim, &secondClaim},
{ metadataPayloadKey: payloadKey,
Claims: []*structs.IntentionJWTClaimVerification{&firstClaim},
MetadataPayloadKey: payloadKey,
},
{
Claims: []*structs.IntentionJWTClaimVerification{&secondClaim},
MetadataPayloadKey: "second-key",
},
},
expected: &envoy_rbac_v3.Principal{ expected: &envoy_rbac_v3.Principal{
Identifier: &envoy_rbac_v3.Principal_OrIds{ Identifier: &envoy_rbac_v3.Principal_AndIds{
OrIds: &envoy_rbac_v3.Principal_Set{ AndIds: &envoy_rbac_v3.Principal_Set{
Ids: []*envoy_rbac_v3.Principal{&firstPrincipal, &secondPrincipal}, Ids: []*envoy_rbac_v3.Principal{&firstPrincipal, &secondPrincipal},
}, },
}, },
@ -1291,7 +1279,7 @@ func TestJwtClaimToPrincipal(t *testing.T) {
for name, tt := range tests { for name, tt := range tests {
tt := tt tt := tt
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
principal := jwtInfosToPrincipals(tt.jwtInfos) principal := jwtClaimsToPrincipals(tt.claims, tt.metadataPayloadKey)
require.Equal(t, principal, tt.expected) require.Equal(t, principal, tt.expected)
}) })
} }

View File

@ -3,9 +3,9 @@
"typedConfig": { "typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication", "@type": "type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication",
"providers": { "providers": {
"okta_0": { "okta": {
"issuer": "test-issuer", "issuer": "test-issuer",
"payloadInMetadata": "jwt_payload_okta_0", "payloadInMetadata": "jwt_payload_okta",
"remoteJwks": { "remoteJwks": {
"httpUri": { "httpUri": {
"uri": "https://example-okta.com/.well-known/jwks.json", "uri": "https://example-okta.com/.well-known/jwks.json",
@ -21,10 +21,15 @@
"rules": [ "rules": [
{ {
"match": { "match": {
"prefix": "some-special-path" "prefix": "/"
}, },
"requires": { "requires": {
"providerName": "okta_0" "requiresAny": {
"requirements": [
{"providerName": "okta"},
{"allowMissingOrFailed": {}}
]
}
} }
} }
] ]

View File

@ -17,7 +17,12 @@
"prefix": "/" "prefix": "/"
}, },
"requires": { "requires": {
"providerName": "okta" "requiresAny": {
"requirements": [
{"providerName": "okta"},
{"allowMissingOrFailed": {}}
]
}
} }
} }
] ]

View File

@ -17,20 +17,6 @@
} }
} }
}, },
"okta_0": {
"issuer": "test-issuer",
"payloadInMetadata": "jwt_payload_okta_0",
"remoteJwks": {
"httpUri": {
"uri": "https://example-okta.com/.well-known/jwks.json",
"cluster": "jwks_cluster_okta",
"timeout": "1s"
},
"asyncFetch": {
"fastListener": true
}
}
},
"auth0": { "auth0": {
"issuer": "another-issuer", "issuer": "another-issuer",
"payloadInMetadata": "jwt_payload_auth0", "payloadInMetadata": "jwt_payload_auth0",
@ -47,28 +33,32 @@
} }
}, },
"rules": [ "rules": [
{
"match": {
"prefix": "some-special-path"
},
"requires": {
"providerName": "okta_0"
}
},
{ {
"match": { "match": {
"prefix": "/" "prefix": "/"
}, },
"requires": { "requires": {
"providerName": "okta" "requiresAll": {
} "requirements": [
}, {
{ "requiresAny": {
"match": { "requirements": [
"prefix": "/" {"providerName": "okta"},
}, {"allowMissingOrFailed": {}}
"requires": { ]
"providerName": "auth0" }
},
{
"requiresAny": {
"requirements": [
{"providerName": "auth0"},
{"allowMissingOrFailed": {}}
]
}
}
]
}
} }
} }
] ]

View File

@ -0,0 +1 @@
{}

View File

@ -24,7 +24,12 @@
"prefix": "/" "prefix": "/"
}, },
"requires": { "requires": {
"providerName": "okta" "requiresAny": {
"requirements": [
{"providerName": "okta"},
{"allowMissingOrFailed": {}}
]
}
} }
} }
] ]

View File

@ -16,37 +16,21 @@
"fastListener": true "fastListener": true
} }
} }
},
"okta_0": {
"issuer": "test-issuer",
"payloadInMetadata": "jwt_payload_okta_0",
"remoteJwks": {
"httpUri": {
"uri": "https://example-okta.com/.well-known/jwks.json",
"cluster": "jwks_cluster_okta",
"timeout": "1s"
},
"asyncFetch": {
"fastListener": true
}
}
} }
}, },
"rules": [ "rules": [
{
"match": {
"prefix": "some-special-path"
},
"requires": {
"providerName": "okta_0"
}
},
{ {
"match": { "match": {
"prefix": "/" "prefix": "/"
}, },
"requires": { "requires": {
"providerName": "okta" "requiresAny": {
"requirements": [
{"providerName": "okta"},
{"allowMissingOrFailed": {}}
]
}
} }
} }
] ]

View File

@ -7,35 +7,37 @@
"consul-intentions-layer7-0": { "consul-intentions-layer7-0": {
"permissions": [ "permissions": [
{ {
"urlPath": { "andRules": {
"path": { "rules": [
"prefix": "some-path"
}
}
}
],
"principals": [
{
"andIds": {
"ids": [
{ {
"authenticated": { "urlPath": {
"principalName": { "path": {
"safeRegex": { "prefix": "some-path"
"googleRe2": {},
"regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$"
}
} }
} }
}, },
{ {
"orIds": { "andRules": {
"ids": [ "rules": [
{ {
"metadata": { "metadata": {
"filter":"envoy.filters.http.jwt_authn", "filter": "envoy.filters.http.jwt_authn",
"path": [ "path": [
{"key": "jwt_payload_okta_0"}, {"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"} {"key": "roles"}
], ],
"value": { "value": {
@ -51,6 +53,18 @@
] ]
} }
} }
],
"principals": [
{
"authenticated": {
"principalName": {
"safeRegex": {
"googleRe2": {},
"regex": "^spiffe://test.consul/ns/default/dc/[^/]+/svc/web$"
}
}
}
}
] ]
} }
} }

View File

@ -27,8 +27,22 @@
} }
}, },
{ {
"orIds": { "andIds": {
"ids": [ "ids": [
{
"metadata": {
"filter":"envoy.filters.http.jwt_authn",
"path": [
{"key": "jwt_payload_okta"},
{"key": "iss"}
],
"value": {
"stringMatch": {
"exact": "mytest.okta-issuer"
}
}
}
},
{ {
"metadata": { "metadata": {
"filter":"envoy.filters.http.jwt_authn", "filter":"envoy.filters.http.jwt_authn",

View File

@ -7,30 +7,112 @@
"consul-intentions-layer7-0": { "consul-intentions-layer7-0": {
"permissions": [ "permissions": [
{ {
"urlPath": { "andRules": {
"path": { "rules": [
"exact": "/v1/secret" {
} "urlPath": {
"path": {
"exact": "/v1/secret"
}
}
},
{
"andRules": {
"rules": [
{
"metadata": {
"filter": "envoy.filters.http.jwt_authn",
"path": [
{"key": "jwt_payload_auth0"},
{"key": "iss"}
],
"value": {
"stringMatch": {
"exact": "mytest.auth0-issuer"
}
}
}
},
{
"metadata": {
"filter": "envoy.filters.http.jwt_authn",
"path": [
{"key": "jwt_payload_auth0"},
{"key": "perms"},
{"key": "role"}
],
"value": {
"stringMatch": {
"exact": "admin"
}
}
}
}
]
}
}
]
} }
}, },
{ {
"andRules": { "andRules": {
"rules": [ "rules": [
{ {
"urlPath": { "andRules": {
"path": { "rules": [
"exact": "/v1/admin" {
} "urlPath": {
"path": {
"exact": "/v1/admin"
}
}
},
{
"notRule": {
"urlPath": {
"path": {
"exact": "/v1/secret"
}
}
}
}
]
} }
}, },
{ {
"notRule": { "andRules": {
"urlPath": { "rules": [
"path": { {
"exact": "/v1/secret" "metadata": {
} "filter": "envoy.filters.http.jwt_authn",
} "path": [
} {"key": "jwt_payload_auth0"},
{"key": "iss"}
],
"value": {
"stringMatch": {
"exact": "mytest.auth0-issuer"
}
}
}
},
{
"metadata": {
"filter": "envoy.filters.http.jwt_authn",
"path": [
{"key": "jwt_payload_auth0"},
{"key": "perms"},
{"key": "role"}
],
"value": {
"stringMatch": {
"exact": "admin"
}
}
}
}
]
}
} }
] ]
} }
@ -53,8 +135,22 @@
} }
}, },
{ {
"orIds": { "andIds": {
"ids": [ "ids": [
{
"metadata": {
"filter":"envoy.filters.http.jwt_authn",
"path": [
{"key": "jwt_payload_okta"},
{"key": "iss"}
],
"value": {
"stringMatch": {
"exact": "mytest.okta-issuer"
}
}
}
},
{ {
"metadata": { "metadata": {
"filter":"envoy.filters.http.jwt_authn", "filter":"envoy.filters.http.jwt_authn",
@ -68,36 +164,6 @@
} }
} }
} }
},
{
"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"
}
}
}
} }
] ]
} }

View File

@ -7,10 +7,51 @@
"consul-intentions-layer7-0": { "consul-intentions-layer7-0": {
"permissions": [ "permissions": [
{ {
"urlPath": { "andRules": {
"path": { "rules": [
"exact": "/v1/secret" {
} "urlPath": {
"path": {
"exact": "/v1/secret"
}
}
},
{
"andRules": {
"rules": [
{
"metadata": {
"filter": "envoy.filters.http.jwt_authn",
"path": [
{"key": "jwt_payload_auth0"},
{"key": "iss"}
],
"value": {
"stringMatch": {
"exact": "mytest.auth0-issuer"
}
}
}
},
{
"metadata": {
"filter": "envoy.filters.http.jwt_authn",
"path": [
{"key": "jwt_payload_auth0"},
{"key": "perms"},
{"key": "role"}
],
"value": {
"stringMatch": {
"exact": "admin"
}
}
}
}
]
}
}
]
} }
}, },
{ {
@ -53,8 +94,22 @@
} }
}, },
{ {
"orIds": { "andIds": {
"ids": [ "ids": [
{
"metadata": {
"filter":"envoy.filters.http.jwt_authn",
"path": [
{"key": "jwt_payload_okta"},
{"key": "iss"}
],
"value": {
"stringMatch": {
"exact": "mytest.okta-issuer"
}
}
}
},
{ {
"metadata": { "metadata": {
"filter":"envoy.filters.http.jwt_authn", "filter":"envoy.filters.http.jwt_authn",
@ -68,21 +123,6 @@
} }
} }
} }
},
{
"metadata": {
"filter":"envoy.filters.http.jwt_authn",
"path": [
{"key": "jwt_payload_auth0_0"},
{"key": "perms"},
{"key": "role"}
],
"value": {
"stringMatch": {
"exact": "admin"
}
}
}
} }
] ]
} }

View File

@ -76,8 +76,8 @@ func TestJWTAuthConnectService(t *testing.T) {
configureIntentions(t, cluster) configureIntentions(t, cluster)
baseURL := fmt.Sprintf("http://localhost:%d", clientPort) baseURL := fmt.Sprintf("http://localhost:%d", clientPort)
// fails without jwt headers // TODO(roncodingenthusiast): update test to reflect jwt-auth filter in metadata mode
doRequest(t, baseURL, http.StatusUnauthorized, "") doRequest(t, baseURL, http.StatusOK, "")
// succeeds with jwt // succeeds with jwt
doRequest(t, baseURL, http.StatusOK, jwt) doRequest(t, baseURL, http.StatusOK, jwt)
} }