Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use JWT-auth filter in metadata mode & Delegate validation to RBAC filter #18062

Merged
merged 1 commit into from
Jul 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
210 changes: 97 additions & 113 deletions agent/xds/jwt_authn.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -22,129 +23,149 @@ const (
jwksClusterPrefix = "jwks_cluster"
)

// This is an intermediate JWTProvider form used to associate
// unique payload keys to providers
type jwtAuthnProvider struct {
ComputedName string
Provider *structs.IntentionJWTProvider
}

func makeJWTAuthFilter(pCE map[string]*structs.JWTProviderConfigEntry, intentions structs.SimplifiedIntentions) (*envoy_http_v3.HttpFilter, error) {
// makeJWTAuthFilter builds jwt filter for envoy. It limits its use to referenced provider rather than every provider.
//
// Eg. If you have three providers: okta, auth0 and fusionAuth and only okta is referenced in your intentions, then this
// will create a jwt-auth filter containing just okta in the list of providers.
func makeJWTAuthFilter(providerMap 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 jwtRequirements []*envoy_http_jwt_authn_v3.JwtRequirement
roncodingenthusiast marked this conversation as resolved.
Show resolved Hide resolved

for _, intention := range intentions {
if intention.JWT == nil && !hasJWTconfig(intention.Permissions) {
continue
}
for _, jwtReq := range collectJWTAuthnProviders(intention) {
if _, ok := providers[jwtReq.ComputedName]; ok {
for _, p := range collectJWTProviders(intention) {
providerName := p.Name
if _, ok := providers[providerName]; ok {
continue
}

jwtProvider, ok := pCE[jwtReq.Provider.Name]

providerCE, ok := providerMap[providerName]
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
// 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)

envoyCfg, err := buildJWTProviderConfig(providerCE)
if err != nil {
return nil, err
}
providers[jwtReq.ComputedName] = envoyCfg
}

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)
}
providers[providerName] = envoyCfg
reqs := providerToJWTRequirement(providerCE)
jwtRequirements = append(jwtRequirements, reqs)
}
}

if len(intentions) == 0 && len(providers) == 0 {
//do not add jwt_authn filter when intentions don't have JWT
if len(jwtRequirements) == 0 {
//do not add jwt_authn filter when intentions don't have JWTs
return nil, nil
}

cfg := &envoy_http_jwt_authn_v3.JwtAuthentication{
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)
}

func collectJWTAuthnProviders(i *structs.Intention) []*jwtAuthnProvider {
var reqs []*jwtAuthnProvider
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,
roncodingenthusiast marked this conversation as resolved.
Show resolved Hide resolved
}
}

if i.JWT != nil {
for _, prov := range i.JWT.Providers {
reqs = append(reqs, &jwtAuthnProvider{Provider: prov, ComputedName: makeComputedProviderName(prov.Name, nil, 0)})
// 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,
},
},
}
}
}

reqs = append(reqs, getPermissionsProviders(i.Permissions)...)
// 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)
roncodingenthusiast marked this conversation as resolved.
Show resolved Hide resolved

if i.JWT != nil {
// get top level providers
reqs = append(reqs, i.JWT.Providers...)
}

return reqs
}

func getPermissionsProviders(p []*structs.IntentionPermission) []*jwtAuthnProvider {
var reqs []*jwtAuthnProvider
for k, perm := range p {
if perm.JWT == nil {
func getPermissionsProviders(perms []*structs.IntentionPermission) []*structs.IntentionJWTProvider {
var reqs []*structs.IntentionJWTProvider
for _, p := range perms {
if p.JWT == nil {
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
}

// makeComputedProviderName is used to create names for unique provider per permission
// This is to stop jwt claims cross validation across permissions/providers.
//
// eg. If Permission x is the 3rd permission and has a provider of original name okta
// this function will return okta_3 as the computed provider name
func makeComputedProviderName(name string, perm *structs.IntentionPermission, idx int) string {
if perm == nil {
return name
}
return fmt.Sprintf("%s_%d", name, idx)
}

// buildPayloadInMetadataKey is used to create a unique payload key per provider/permissions.
// 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.
// buildPayloadInMetadataKey is used to create a unique payload key per provider.
// 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. 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))
// eg. With a provider named okta will have a payload key of: jwt_payload_okta
func buildPayloadInMetadataKey(providerName string) string {
return jwtMetadataKeyPrefix + "_" + providerName
}

func buildJWTProviderConfig(p *structs.JWTProviderConfigEntry, metadataKeySuffix string) (*envoy_http_jwt_authn_v3.JwtProvider, error) {
func buildJWTProviderConfig(p *structs.JWTProviderConfigEntry) (*envoy_http_jwt_authn_v3.JwtProvider, error) {
envoyCfg := envoy_http_jwt_authn_v3.JwtProvider{
Issuer: p.Issuer,
Audiences: p.Audiences,
PayloadInMetadata: buildPayloadInMetadataKey(metadataKeySuffix, nil, 0),
PayloadInMetadata: buildPayloadInMetadataKey(p.Name),
}

if p.Forwarding != nil {
Expand Down Expand Up @@ -262,43 +283,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 {
Expand Down
Loading