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

[NET-3092] JWT Verify claims handling #17452

Merged
merged 10 commits into from
May 30, 2023
3 changes: 3 additions & 0 deletions .changelog/17452.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:feature
mesh: Support configuring JWT authentication in Envoy.
```
95 changes: 69 additions & 26 deletions agent/xds/jwt_authn.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@ import (
"google.golang.org/protobuf/types/known/wrapperspb"
)

const (
jwtEnvoyFilter = "envoy.filters.http.jwt_authn"
jwtMetadataKeyPrefix = "jwt_payload"
)

// 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) {
providers := map[string]*envoy_http_jwt_authn_v3.JwtProvider{}
var rules []*envoy_http_jwt_authn_v3.RequirementRule
Expand All @@ -24,38 +36,41 @@ func makeJWTAuthFilter(pCE map[string]*structs.JWTProviderConfigEntry, intention
if intention.JWT == nil && !hasJWTconfig(intention.Permissions) {
continue
}
for _, jwtReq := range collectJWTRequirements(intention) {
if _, ok := providers[jwtReq.Name]; ok {
for _, jwtReq := range collectJWTAuthnProviders(intention) {
if _, ok := providers[jwtReq.ComputedName]; ok {
continue
}

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

if !ok {
return nil, fmt.Errorf("provider specified in intention does not exist. Provider name: %s", jwtReq.Name)
return nil, fmt.Errorf("provider specified in intention does not exist. Provider name: %s", jwtReq.Provider.Name)
}
envoyCfg, err := buildJWTProviderConfig(jwtProvider)
// 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)
if err != nil {
return nil, err
}
providers[jwtReq.Name] = envoyCfg
providers[jwtReq.ComputedName] = envoyCfg
}

for _, perm := range intention.Permissions {
for k, perm := range intention.Permissions {
if perm.JWT == nil {
continue
}
for _, prov := range perm.JWT.Providers {
rule := buildRouteRule(prov, perm, "/")
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.
// TODO(roncodingenthusiast): Handle provider.VerifyClaims
rule := buildRouteRule(provider, nil, "/")
rule := buildRouteRule(provider, nil, "/", 0)
rules = append(rules, rule)
}
}
Expand All @@ -70,37 +85,65 @@ func makeJWTAuthFilter(pCE map[string]*structs.JWTProviderConfigEntry, intention
Providers: providers,
Rules: rules,
}
return makeEnvoyHTTPFilter("envoy.filters.http.jwt_authn", cfg)
return makeEnvoyHTTPFilter(jwtEnvoyFilter, cfg)
}

func collectJWTRequirements(i *structs.Intention) []*structs.IntentionJWTProvider {
var jReqs []*structs.IntentionJWTProvider
func collectJWTAuthnProviders(i *structs.Intention) []*jwtAuthnProvider {
var reqs []*jwtAuthnProvider

if i.JWT != nil {
jReqs = append(jReqs, i.JWT.Providers...)
for _, prov := range i.JWT.Providers {
reqs = append(reqs, &jwtAuthnProvider{Provider: prov, ComputedName: makeComputedProviderName(prov.Name, nil, 0)})
}
}

jReqs = append(jReqs, getPermissionsProviders(i.Permissions)...)
reqs = append(reqs, getPermissionsProviders(i.Permissions)...)

return jReqs
return reqs
}

func getPermissionsProviders(p []*structs.IntentionPermission) []*structs.IntentionJWTProvider {
intentionProviders := []*structs.IntentionJWTProvider{}
for _, perm := range p {
func getPermissionsProviders(p []*structs.IntentionPermission) []*jwtAuthnProvider {
var reqs []*jwtAuthnProvider
for k, perm := range p {
if perm.JWT == nil {
continue
}
intentionProviders = append(intentionProviders, perm.JWT.Providers...)
for _, prov := range perm.JWT.Providers {
reqs = append(reqs, &jwtAuthnProvider{Provider: prov, ComputedName: makeComputedProviderName(prov.Name, perm, k)})
}
}

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)
}

return intentionProviders
// 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.
//
// 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) (*envoy_http_jwt_authn_v3.JwtProvider, error) {
func buildJWTProviderConfig(p *structs.JWTProviderConfigEntry, metadataKeySuffix string) (*envoy_http_jwt_authn_v3.JwtProvider, error) {
envoyCfg := envoy_http_jwt_authn_v3.JwtProvider{
Issuer: p.Issuer,
Audiences: p.Audiences,
Issuer: p.Issuer,
Audiences: p.Audiences,
PayloadInMetadata: buildPayloadInMetadataKey(metadataKeySuffix, nil, 0),
}

if p.Forwarding != nil {
Expand Down Expand Up @@ -216,15 +259,15 @@ func buildJWTRetryPolicy(r *structs.JWKSRetryPolicy) *envoy_core_v3.RetryPolicy
return &pol
}

func buildRouteRule(provider *structs.IntentionJWTProvider, perm *structs.IntentionPermission, defaultPrefix string) *envoy_http_jwt_authn_v3.RequirementRule {
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: provider.Name,
ProviderName: makeComputedProviderName(provider.Name, perm, permIdx),
},
},
},
Expand Down
Loading