Skip to content

Commit

Permalink
Use JWT-auth filter in metadata mode & Delegate validation to RBAC fi…
Browse files Browse the repository at this point in the history
…lter
  • Loading branch information
roncodingenthusiast committed Jul 10, 2023
1 parent f4b0804 commit 69b9e47
Show file tree
Hide file tree
Showing 14 changed files with 619 additions and 367 deletions.
118 changes: 74 additions & 44 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 @@ -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) {
Expand Down Expand Up @@ -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)
}
}
}
Expand All @@ -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

Expand Down Expand Up @@ -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 {
Expand Down
99 changes: 0 additions & 99 deletions agent/xds/jwt_authn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions agent/xds/listeners.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 69b9e47

Please sign in to comment.