This repository has been archived by the owner on Jul 11, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 277
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
envoy/rbac: add support for server side RBAC fitler
This change introduces an RBAC filter in the inbound mesh filter chain. Currently, the RBAC filter grants full access to client identities that are permitted by an SMI traffic target policy. HTTP filtering based on HTTP routes still happens within RDS. The RBAC filter is omitted in permissive mode. This change is a part of #1964 and is required by #1521.
- Loading branch information
1 parent
2a09728
commit cc4257e
Showing
4 changed files
with
304 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
package lds | ||
|
||
import ( | ||
"fmt" | ||
|
||
xds_listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" | ||
xds_rbac "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3" | ||
xds_network_rbac "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/rbac/v3" | ||
xds_matcher "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3" | ||
"github.com/envoyproxy/go-control-plane/pkg/wellknown" | ||
|
||
"github.com/openservicemesh/osm/pkg/envoy" | ||
"github.com/openservicemesh/osm/pkg/identity" | ||
"github.com/openservicemesh/osm/pkg/service" | ||
) | ||
|
||
// buildRBACFilter builds an RBAC filter based on SMI TrafficTarget policies. | ||
// The returned RBAC filter has policies that gives downstream principals full access to the local service. | ||
func (lb *listenerBuilder) buildRBACFilter() (*xds_listener.Filter, error) { | ||
networkRBACPolicy, err := lb.buildInboundRBACPolicies() | ||
if err != nil { | ||
log.Error().Err(err).Msgf("Error building inbound RBAC policies for principal %q", lb.svcAccount) | ||
return nil, err | ||
} | ||
|
||
marshalledNetworkRBACPolicy, err := envoy.MessageToAny(networkRBACPolicy) | ||
if err != nil { | ||
log.Error().Err(err).Msgf("Error marshalling RBAC policy: %v", networkRBACPolicy) | ||
return nil, err | ||
} | ||
|
||
rbacFilter := &xds_listener.Filter{ | ||
Name: wellknown.RoleBasedAccessControl, | ||
ConfigType: &xds_listener.Filter_TypedConfig{TypedConfig: marshalledNetworkRBACPolicy}, | ||
} | ||
|
||
return rbacFilter, nil | ||
} | ||
|
||
// buildInboundRBACPolicies builds the RBAC policies based on allowed principals | ||
func (lb *listenerBuilder) buildInboundRBACPolicies() (*xds_network_rbac.RBAC, error) { | ||
// Create an inbound RBAC policy that denies a request by default, unless a policy explicitly allows it | ||
networkRBACPolicy := &xds_network_rbac.RBAC{ | ||
StatPrefix: "RBAC", | ||
Rules: &xds_rbac.RBAC{ | ||
Action: xds_rbac.RBAC_ALLOW, // Allows the request if and only if there is a policy that matches the request | ||
}, | ||
} | ||
|
||
allowsInboundSvcAccounts, err := lb.meshCatalog.ListAllowedInboundServiceAccounts(lb.svcAccount) | ||
if err != nil { | ||
log.Error().Err(err).Msgf("Error listing allowed inbound ServiceAccounts for ServiceAccount %q", lb.svcAccount) | ||
return nil, err | ||
} | ||
|
||
// Each downstream is a principal in the RBAC policy, which will have its own permissions | ||
// based on SMI TrafficTarget policies. | ||
rbacPolicies := make(map[string]*xds_rbac.Policy) | ||
for _, downstreamSvcAccount := range allowsInboundSvcAccounts { | ||
policyName := getPolicyName(downstreamSvcAccount, lb.svcAccount) | ||
principal := identity.GetKubernetesServiceIdentity(downstreamSvcAccount, identity.ClusterLocalTrustDomain) | ||
rbacPolicies[policyName] = buildAllowAllPermissionsPolicy(principal) | ||
} | ||
|
||
networkRBACPolicy.Rules.Policies = rbacPolicies | ||
|
||
return networkRBACPolicy, nil | ||
} | ||
|
||
// buildAllowAllPermissionsPolicy creates an XDS RBAC policy for the given client principal to be granted all access | ||
func buildAllowAllPermissionsPolicy(clientPrincipal identity.ServiceIdentity) *xds_rbac.Policy { | ||
return &xds_rbac.Policy{ | ||
Permissions: []*xds_rbac.Permission{ | ||
{ | ||
// Grant the given principal all access | ||
Rule: &xds_rbac.Permission_Any{Any: true}, | ||
}, | ||
}, | ||
Principals: []*xds_rbac.Principal{ | ||
{ | ||
Identifier: &xds_rbac.Principal_OrIds{ | ||
OrIds: &xds_rbac.Principal_Set{ | ||
Ids: []*xds_rbac.Principal{ | ||
getPrincipalAuthenticated(clientPrincipal.String()), | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
} | ||
} | ||
|
||
// getPolicyName returns a policy name for the policy used to authorize a downstream service account by the upstream | ||
func getPolicyName(downstream, upstream service.K8sServiceAccount) string { | ||
return fmt.Sprintf("%s to %s", downstream, upstream) | ||
} | ||
|
||
func getPrincipalAuthenticated(principalName string) *xds_rbac.Principal { | ||
return &xds_rbac.Principal{ | ||
Identifier: &xds_rbac.Principal_Authenticated_{ | ||
Authenticated: &xds_rbac.Principal_Authenticated{ | ||
PrincipalName: &xds_matcher.StringMatcher{ | ||
MatchPattern: &xds_matcher.StringMatcher_Exact{ | ||
Exact: principalName, | ||
}, | ||
}, | ||
}, | ||
}, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
package lds | ||
|
||
import ( | ||
"fmt" | ||
"testing" | ||
|
||
"github.com/golang/mock/gomock" | ||
"github.com/stretchr/testify/assert" | ||
|
||
xds_rbac "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3" | ||
"github.com/envoyproxy/go-control-plane/pkg/wellknown" | ||
|
||
"github.com/openservicemesh/osm/pkg/catalog" | ||
"github.com/openservicemesh/osm/pkg/service" | ||
) | ||
|
||
func TestBuildInboundRBACPolicies(t *testing.T) { | ||
assert := assert.New(t) | ||
mockCtrl := gomock.NewController(t) | ||
defer mockCtrl.Finish() | ||
|
||
mockCatalog := catalog.NewMockMeshCataloger(mockCtrl) | ||
proxySvcAccount := service.K8sServiceAccount{Name: "sa-1", Namespace: "ns-1"} | ||
|
||
lb := &listenerBuilder{ | ||
meshCatalog: mockCatalog, | ||
svcAccount: proxySvcAccount, | ||
} | ||
|
||
testCases := []struct { | ||
name string | ||
allowedInboundSvcAccounts []service.K8sServiceAccount | ||
expectedPrincipals []string | ||
expectErr bool | ||
}{ | ||
{ | ||
"multiple client allowed", | ||
[]service.K8sServiceAccount{ | ||
{Name: "sa-2", Namespace: "ns-2"}, | ||
{Name: "sa-3", Namespace: "ns-3"}, | ||
}, | ||
[]string{ | ||
"sa-2.ns-2.cluster.local", | ||
"sa-3.ns-3.cluster.local", | ||
}, | ||
false, // no error | ||
}, | ||
{ | ||
"no clients allowed", | ||
[]service.K8sServiceAccount{}, | ||
[]string{}, | ||
false, // no error | ||
}, | ||
} | ||
|
||
for i, tc := range testCases { | ||
t.Run(fmt.Sprintf("Testing test case %d: %s", i, tc.name), func(t *testing.T) { | ||
// Mock the calls to catalog | ||
mockCatalog.EXPECT().ListAllowedInboundServiceAccounts(lb.svcAccount).Return(tc.allowedInboundSvcAccounts, nil).Times(1) | ||
|
||
// Test the RBAC policies | ||
networkRBAC, err := lb.buildInboundRBACPolicies() | ||
assert.Equal(err != nil, tc.expectErr) | ||
|
||
assert.Equal(networkRBAC.Rules.GetAction(), xds_rbac.RBAC_ALLOW) | ||
|
||
rbacPolicies := networkRBAC.Rules.Policies | ||
|
||
// Expect 1 policy per client principal | ||
assert.Len(rbacPolicies, len(tc.expectedPrincipals)) | ||
|
||
// Loop through the policies and ensure there is a policy corresponding to each principal | ||
var actualPrincipals []string | ||
for _, policy := range rbacPolicies { | ||
principalName := policy.Principals[0].GetOrIds().Ids[0].GetAuthenticated().PrincipalName.GetExact() | ||
actualPrincipals = append(actualPrincipals, principalName) | ||
|
||
assert.Len(policy.Permissions, 1) // Any permission | ||
assert.True(policy.Permissions[0].GetAny()) | ||
} | ||
assert.ElementsMatch(tc.expectedPrincipals, actualPrincipals) | ||
}) | ||
} | ||
} | ||
|
||
func TestBuildRBACFilter(t *testing.T) { | ||
assert := assert.New(t) | ||
mockCtrl := gomock.NewController(t) | ||
defer mockCtrl.Finish() | ||
|
||
mockCatalog := catalog.NewMockMeshCataloger(mockCtrl) | ||
proxySvcAccount := service.K8sServiceAccount{Name: "sa-1", Namespace: "ns-1"} | ||
|
||
lb := &listenerBuilder{ | ||
meshCatalog: mockCatalog, | ||
svcAccount: proxySvcAccount, | ||
} | ||
|
||
testCases := []struct { | ||
name string | ||
allowedInboundSvcAccounts []service.K8sServiceAccount | ||
expectErr bool | ||
}{ | ||
{ | ||
"multiple clients allowed", | ||
[]service.K8sServiceAccount{ | ||
{Name: "sa-2", Namespace: "ns-2"}, | ||
{Name: "sa-3", Namespace: "ns-3"}, | ||
}, | ||
false, // no error | ||
}, | ||
{ | ||
"no clients allowed", | ||
[]service.K8sServiceAccount{}, | ||
false, // no error | ||
}, | ||
} | ||
|
||
for i, tc := range testCases { | ||
t.Run(fmt.Sprintf("Testing test case %d", i), func(t *testing.T) { | ||
// Mock the calls to catalog | ||
mockCatalog.EXPECT().ListAllowedInboundServiceAccounts(lb.svcAccount).Return(tc.allowedInboundSvcAccounts, nil).Times(1) | ||
|
||
// Test the RBAC filter | ||
rbacFilter, err := lb.buildRBACFilter() | ||
assert.Equal(err != nil, tc.expectErr) | ||
|
||
assert.Equal(rbacFilter.Name, wellknown.RoleBasedAccessControl) | ||
}) | ||
} | ||
} | ||
|
||
func TestGetPolicyName(t *testing.T) { | ||
assert := assert.New(t) | ||
|
||
testCases := []struct { | ||
downstream service.K8sServiceAccount | ||
upstream service.K8sServiceAccount | ||
expectedName string | ||
}{ | ||
{ | ||
service.K8sServiceAccount{Name: "foo", Namespace: "ns-1"}, | ||
service.K8sServiceAccount{Name: "bar", Namespace: "ns-2"}, | ||
"ns-1/foo to ns-2/bar", | ||
}, | ||
} | ||
|
||
for i, tc := range testCases { | ||
t.Run(fmt.Sprintf("Testing test case %d", i), func(t *testing.T) { | ||
actual := getPolicyName(tc.downstream, tc.upstream) | ||
assert.Equal(actual, tc.expectedName) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,17 @@ | ||
package lds | ||
|
||
import ( | ||
"github.com/openservicemesh/osm/pkg/catalog" | ||
"github.com/openservicemesh/osm/pkg/logger" | ||
"github.com/openservicemesh/osm/pkg/service" | ||
) | ||
|
||
var ( | ||
log = logger.New("envoy/lds") | ||
) | ||
|
||
// listenerBuilder is a type containing data to build the listener configurations | ||
type listenerBuilder struct { | ||
svcAccount service.K8sServiceAccount | ||
meshCatalog catalog.MeshCataloger | ||
} |