2023-03-28 18:39:22 +00:00
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
2020-08-27 17:20:58 +00:00
package xds
import (
"fmt"
"sort"
2020-10-06 22:09:13 +00:00
"strings"
2020-08-27 17:20:58 +00:00
2021-02-26 22:23:15 +00:00
envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
envoy_rbac_v3 "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3"
envoy_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
2023-01-06 17:13:40 +00:00
envoy_http_header_to_meta_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/header_to_metadata/v3"
2021-02-26 22:23:15 +00:00
envoy_http_rbac_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/rbac/v3"
envoy_http_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
envoy_network_rbac_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/rbac/v3"
envoy_matcher_v3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
2021-02-22 21:00:15 +00:00
2021-09-10 00:24:43 +00:00
"github.com/hashicorp/consul/agent/connect"
2020-08-27 17:20:58 +00:00
"github.com/hashicorp/consul/agent/structs"
2023-02-17 21:14:46 +00:00
"github.com/hashicorp/consul/proto/private/pbpeering"
2020-08-27 17:20:58 +00:00
)
2022-06-10 21:15:22 +00:00
func makeRBACNetworkFilter (
2023-04-20 16:16:04 +00:00
intentions structs . SimplifiedIntentions ,
2022-06-10 21:15:22 +00:00
intentionDefaultAllow bool ,
2022-06-29 15:29:54 +00:00
localInfo rbacLocalInfo ,
2022-06-21 02:47:14 +00:00
peerTrustBundles [ ] * pbpeering . PeeringTrustBundle ,
2022-06-10 21:15:22 +00:00
) ( * envoy_listener_v3 . Filter , error ) {
2022-07-14 18:45:51 +00:00
rules := makeRBACRules ( intentions , intentionDefaultAllow , localInfo , false , peerTrustBundles )
2020-08-27 17:20:58 +00:00
2021-02-26 22:23:15 +00:00
cfg := & envoy_network_rbac_v3 . RBAC {
2020-08-27 17:20:58 +00:00
StatPrefix : "connect_authz" ,
Rules : rules ,
}
2021-02-22 21:00:15 +00:00
return makeFilter ( "envoy.filters.network.rbac" , cfg )
2020-08-27 17:20:58 +00:00
}
2022-06-10 21:15:22 +00:00
func makeRBACHTTPFilter (
2023-04-20 16:16:04 +00:00
intentions structs . SimplifiedIntentions ,
2022-06-10 21:15:22 +00:00
intentionDefaultAllow bool ,
2022-06-29 15:29:54 +00:00
localInfo rbacLocalInfo ,
2022-06-21 02:47:14 +00:00
peerTrustBundles [ ] * pbpeering . PeeringTrustBundle ,
2022-06-10 21:15:22 +00:00
) ( * envoy_http_v3 . HttpFilter , error ) {
2022-07-14 18:45:51 +00:00
rules := makeRBACRules ( intentions , intentionDefaultAllow , localInfo , true , peerTrustBundles )
2020-08-27 17:20:58 +00:00
2021-02-26 22:23:15 +00:00
cfg := & envoy_http_rbac_v3 . RBAC {
2020-08-27 17:20:58 +00:00
Rules : rules ,
}
return makeEnvoyHTTPFilter ( "envoy.filters.http.rbac" , cfg )
}
2022-06-10 21:15:22 +00:00
func intentionListToIntermediateRBACForm (
2023-04-20 16:16:04 +00:00
intentions structs . SimplifiedIntentions ,
2022-06-29 15:29:54 +00:00
localInfo rbacLocalInfo ,
2022-06-10 21:15:22 +00:00
isHTTP bool ,
trustBundlesByPeer map [ string ] * pbpeering . PeeringTrustBundle ,
) [ ] * rbacIntention {
2020-10-06 22:09:13 +00:00
sort . Sort ( structs . IntentionPrecedenceSorter ( intentions ) )
// Omit any lower-precedence intentions that share the same source.
intentions = removeSameSourceIntentions ( intentions )
rbacIxns := make ( [ ] * rbacIntention , 0 , len ( intentions ) )
for _ , ixn := range intentions {
2022-06-10 21:15:22 +00:00
// trustBundle is only applicable to imported services
trustBundle , ok := trustBundlesByPeer [ ixn . SourcePeer ]
if ixn . SourcePeer != "" && ! ok {
// If the intention defines a source peer, we expect to
// see a trust bundle. Otherwise the config snapshot may
// not have yet received the bundles and we fail silently
continue
}
2022-06-29 15:29:54 +00:00
rixn := intentionToIntermediateRBACForm ( ixn , localInfo , isHTTP , trustBundle )
2020-10-06 22:09:13 +00:00
rbacIxns = append ( rbacIxns , rixn )
}
return rbacIxns
}
2022-06-29 15:29:54 +00:00
func removeSourcePrecedence ( rbacIxns [ ] * rbacIntention , intentionDefaultAction intentionAction , localInfo rbacLocalInfo ) [ ] * rbacIntention {
2020-10-06 22:09:13 +00:00
if len ( rbacIxns ) == 0 {
return nil
}
// Remove source precedence:
//
// First walk backwards and add each intention to all subsequent statements
// (via AND NOT $x).
//
// If it is L4 and has the same action as the default intention action then
// mark the rule itself for erasure.
numRetained := 0
for i := len ( rbacIxns ) - 1 ; i >= 0 ; i -- {
for j := i + 1 ; j < len ( rbacIxns ) ; j ++ {
if rbacIxns [ j ] . Skip {
continue
}
// [i] is the intention candidate that we are distributing
// [j] is the thing to maybe NOT [i] from
if ixnSourceMatches ( rbacIxns [ i ] . Source , rbacIxns [ j ] . Source ) {
rbacIxns [ j ] . NotSources = append ( rbacIxns [ j ] . NotSources , rbacIxns [ i ] . Source )
}
}
if rbacIxns [ i ] . Action == intentionDefaultAction {
// Lower precedence intentions that match the default intention
// action are skipped, since they're handled by the default
// catch-all.
rbacIxns [ i ] . Skip = true // mark for deletion
} else {
numRetained ++
}
}
// At this point precedence doesn't matter for the source element.
// Remove skipped intentions and also compute the final Principals for each
// intention.
out := make ( [ ] * rbacIntention , 0 , numRetained )
for _ , rixn := range rbacIxns {
if rixn . Skip {
continue
}
2022-06-29 15:29:54 +00:00
rixn . ComputedPrincipal = rixn . FlattenPrincipal ( localInfo )
2020-10-06 22:09:13 +00:00
out = append ( out , rixn )
}
return out
}
2022-06-29 15:29:54 +00:00
func removeIntentionPrecedence ( rbacIxns [ ] * rbacIntention , intentionDefaultAction intentionAction , localInfo rbacLocalInfo ) [ ] * rbacIntention {
2020-10-06 22:09:13 +00:00
// Remove source precedence. After this completes precedence doesn't matter
// between any two intentions.
2022-06-29 15:29:54 +00:00
rbacIxns = removeSourcePrecedence ( rbacIxns , intentionDefaultAction , localInfo )
2020-10-06 22:09:13 +00:00
2021-07-15 15:09:00 +00:00
numRetained := 0
2020-10-06 22:09:13 +00:00
for _ , rbacIxn := range rbacIxns {
// Remove permission precedence. After this completes precedence
// doesn't matter between any two permissions on this intention.
rbacIxn . Permissions = removePermissionPrecedence ( rbacIxn . Permissions , intentionDefaultAction )
2021-07-15 15:09:00 +00:00
if rbacIxn . Action == intentionActionLayer7 && len ( rbacIxn . Permissions ) == 0 {
// All of the permissions must have had the default action type and
// were removed. Mark this for removal below.
rbacIxn . Skip = true
} else {
numRetained ++
}
2020-10-06 22:09:13 +00:00
}
2021-07-15 15:09:00 +00:00
if numRetained == len ( rbacIxns ) {
return rbacIxns
}
// We previously used the absence of permissions (above) as a signal to
// mark the entire intention for removal. Now do the deletions.
out := make ( [ ] * rbacIntention , 0 , numRetained )
for _ , rixn := range rbacIxns {
if ! rixn . Skip {
out = append ( out , rixn )
}
}
return out
2020-10-06 22:09:13 +00:00
}
func removePermissionPrecedence ( perms [ ] * rbacPermission , intentionDefaultAction intentionAction ) [ ] * rbacPermission {
if len ( perms ) == 0 {
return nil
}
// First walk backwards and add each permission to all subsequent
// statements (via AND NOT $x).
//
// If it has the same action as the default intention action then mark the
// permission itself for erasure.
numRetained := 0
for i := len ( perms ) - 1 ; i >= 0 ; i -- {
for j := i + 1 ; j < len ( perms ) ; j ++ {
if perms [ j ] . Skip {
continue
}
// [i] is the permission candidate that we are distributing
// [j] is the thing to maybe NOT [i] from
perms [ j ] . NotPerms = append (
perms [ j ] . NotPerms ,
perms [ i ] . Perm ,
)
}
if perms [ i ] . Action == intentionDefaultAction {
// Lower precedence permissions that match the default intention
// action are skipped, since they're handled by the default
// catch-all.
perms [ i ] . Skip = true // mark for deletion
} else {
numRetained ++
}
}
// Remove skipped permissions and also compute the final Permissions for each item.
out := make ( [ ] * rbacPermission , 0 , numRetained )
for _ , perm := range perms {
if perm . Skip {
continue
}
perm . ComputedPermission = perm . Flatten ( )
out = append ( out , perm )
}
return out
}
2022-06-29 15:29:54 +00:00
func intentionToIntermediateRBACForm (
ixn * structs . Intention ,
localInfo rbacLocalInfo ,
isHTTP bool ,
bundle * pbpeering . PeeringTrustBundle ,
) * rbacIntention {
2020-10-06 22:09:13 +00:00
rixn := & rbacIntention {
2022-06-10 21:15:22 +00:00
Source : rbacService {
ServiceName : ixn . SourceServiceName ( ) ,
Peer : ixn . SourcePeer ,
2022-06-29 15:29:54 +00:00
TrustDomain : localInfo . trustDomain ,
2022-06-10 21:15:22 +00:00
} ,
2020-10-06 22:09:13 +00:00
Precedence : ixn . Precedence ,
}
2022-06-10 21:15:22 +00:00
// imported services will have addition metadata used to override SpiffeID creation
if bundle != nil {
rixn . Source . ExportedPartition = bundle . ExportedPartition
rixn . Source . TrustDomain = bundle . TrustDomain
}
2020-10-06 22:09:13 +00:00
if len ( ixn . Permissions ) > 0 {
if isHTTP {
rixn . Action = intentionActionLayer7
rixn . Permissions = make ( [ ] * rbacPermission , 0 , len ( ixn . Permissions ) )
for _ , perm := range ixn . Permissions {
rixn . Permissions = append ( rixn . Permissions , & rbacPermission {
Definition : perm ,
Action : intentionActionFromString ( perm . Action ) ,
Perm : convertPermission ( perm ) ,
} )
}
} else {
// In case L7 intentions slip through to here, treat them as deny intentions.
rixn . Action = intentionActionDeny
}
} else {
rixn . Action = intentionActionFromString ( ixn . Action )
}
return rixn
}
type intentionAction int
const (
intentionActionDeny intentionAction = iota
intentionActionAllow
intentionActionLayer7
)
func intentionActionFromBool ( v bool ) intentionAction {
if v {
return intentionActionAllow
} else {
return intentionActionDeny
}
}
func intentionActionFromString ( s structs . IntentionAction ) intentionAction {
if s == structs . IntentionActionAllow {
return intentionActionAllow
}
return intentionActionDeny
}
2022-06-10 21:15:22 +00:00
type rbacService struct {
structs . ServiceName
// Peer, ExportedPartition, and TrustDomain are
// only applicable to imported services and are
// used to override SPIFFEID fields.
Peer string
ExportedPartition string
TrustDomain string
}
2020-08-27 17:20:58 +00:00
type rbacIntention struct {
2022-06-10 21:15:22 +00:00
Source rbacService
NotSources [ ] rbacService
2020-10-06 22:09:13 +00:00
Action intentionAction
Permissions [ ] * rbacPermission
Precedence int
// Skip is field used to indicate that this intention can be deleted in the
// final pass. Items marked as true should generally not escape the method
// that marked them.
Skip bool
2021-02-26 22:23:15 +00:00
ComputedPrincipal * envoy_rbac_v3 . Principal
2020-08-27 17:20:58 +00:00
}
2022-06-29 15:29:54 +00:00
func ( r * rbacIntention ) FlattenPrincipal ( localInfo rbacLocalInfo ) * envoy_rbac_v3 . Principal {
if ! localInfo . expectXFCC {
return r . flattenPrincipalFromCert ( )
} else if r . Source . Peer == "" {
// 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.
return r . flattenPrincipalFromCert ( ) // intention is not relevant to peering
}
// 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 )
return andPrincipals ( [ ] * envoy_rbac_v3 . Principal {
authenticatedPatternPrincipal ( gwIDPattern ) ,
fromXFCC ,
} )
}
func ( r * rbacIntention ) flattenPrincipalFromCert ( ) * envoy_rbac_v3 . Principal {
2020-08-27 17:20:58 +00:00
r . NotSources = simplifyNotSourceSlice ( r . NotSources )
2020-10-06 22:09:13 +00:00
if len ( r . NotSources ) == 0 {
return idPrincipal ( r . Source )
}
2021-02-26 22:23:15 +00:00
andIDs := make ( [ ] * envoy_rbac_v3 . Principal , 0 , len ( r . NotSources ) + 1 )
2020-10-06 22:09:13 +00:00
andIDs = append ( andIDs , idPrincipal ( r . Source ) )
for _ , src := range r . NotSources {
andIDs = append ( andIDs , notPrincipal (
idPrincipal ( src ) ,
) )
}
return andPrincipals ( andIDs )
}
2022-06-29 15:29:54 +00:00
func ( r * rbacIntention ) flattenPrincipalFromXFCC ( ) * envoy_rbac_v3 . Principal {
r . NotSources = simplifyNotSourceSlice ( r . NotSources )
if len ( r . NotSources ) == 0 {
return xfccPrincipal ( r . Source )
}
andIDs := make ( [ ] * envoy_rbac_v3 . Principal , 0 , len ( r . NotSources ) + 1 )
andIDs = append ( andIDs , xfccPrincipal ( r . Source ) )
for _ , src := range r . NotSources {
andIDs = append ( andIDs , notPrincipal (
xfccPrincipal ( src ) ,
) )
}
return andPrincipals ( andIDs )
}
2020-10-06 22:09:13 +00:00
type rbacPermission struct {
Definition * structs . IntentionPermission
Action intentionAction
2021-02-26 22:23:15 +00:00
Perm * envoy_rbac_v3 . Permission
NotPerms [ ] * envoy_rbac_v3 . Permission
2020-10-06 22:09:13 +00:00
// Skip is field used to indicate that this permission can be deleted in
// the final pass. Items marked as true should generally not escape the
// method that marked them.
Skip bool
2021-02-26 22:23:15 +00:00
ComputedPermission * envoy_rbac_v3 . Permission
2020-10-06 22:09:13 +00:00
}
2021-02-26 22:23:15 +00:00
func ( p * rbacPermission ) Flatten ( ) * envoy_rbac_v3 . Permission {
2020-10-06 22:09:13 +00:00
if len ( p . NotPerms ) == 0 {
return p . Perm
}
2021-02-26 22:23:15 +00:00
parts := make ( [ ] * envoy_rbac_v3 . Permission , 0 , len ( p . NotPerms ) + 1 )
2020-10-06 22:09:13 +00:00
parts = append ( parts , p . Perm )
for _ , notPerm := range p . NotPerms {
parts = append ( parts , notPermission ( notPerm ) )
}
return andPermissions ( parts )
2020-08-27 17:20:58 +00:00
}
2021-09-02 18:33:56 +00:00
// simplifyNotSourceSlice will collapse NotSources elements together if any element is
// a subset of another.
// For example "default/web" is a subset of "default/*" because it is covered by the wildcard.
2022-06-10 21:15:22 +00:00
func simplifyNotSourceSlice ( notSources [ ] rbacService ) [ ] rbacService {
2020-08-27 17:20:58 +00:00
if len ( notSources ) <= 1 {
return notSources
}
// Sort, keeping the least wildcarded elements first.
2021-09-02 18:33:56 +00:00
// More specific elements have a higher precedence over more wildcarded elements.
2020-08-27 17:20:58 +00:00
sort . SliceStable ( notSources , func ( i , j int ) bool {
return countWild ( notSources [ i ] ) < countWild ( notSources [ j ] )
} )
2022-06-10 21:15:22 +00:00
keep := make ( [ ] rbacService , 0 , len ( notSources ) )
2020-08-27 17:20:58 +00:00
for i := 0 ; i < len ( notSources ) ; i ++ {
si := notSources [ i ]
remove := false
for j := i + 1 ; j < len ( notSources ) ; j ++ {
sj := notSources [ j ]
if ixnSourceMatches ( si , sj ) {
remove = true
break
}
}
if ! remove {
keep = append ( keep , si )
}
}
return keep
}
2022-06-29 15:29:54 +00:00
type rbacLocalInfo struct {
trustDomain string
datacenter string
partition string
expectXFCC bool
}
2020-08-27 17:20:58 +00:00
// makeRBACRules translates Consul intentions into RBAC Policies for Envoy.
//
// Consul lets you define up to 9 different kinds of intentions that apply at
// different levels of precedence (this is limited to 4 if not using Consul
// Enterprise). Each intention in this flat list (sorted by precedence) can either
// be an allow rule or a deny rule. Here’ s a concrete example of this at work:
//
2022-10-21 19:58:06 +00:00
// intern/trusted-app => billing/payment-svc : ALLOW (prec=9)
// intern/* => billing/payment-svc : DENY (prec=8)
// */* => billing/payment-svc : ALLOW (prec=7)
// ::: ACL default policy ::: : DENY (prec=N/A)
2020-08-27 17:20:58 +00:00
//
// In contrast, Envoy lets you either configure a filter to be based on an
// allow-list or a deny-list based on the action attribute of the RBAC rules
// struct.
//
// On the surface it would seem that the configuration model of Consul
// intentions is incompatible with that of Envoy’ s RBAC engine. For any given
// destination service Consul’ s model requires evaluating a list of rules and
// short circuiting later rules once an earlier rule matches. After a rule is
// found to match then we decide if it is allow/deny. Envoy on the other hand
// requires the rules to express all conditions to allow access or all conditions
// to deny access.
//
// Despite the surface incompatibility it is possible to marry these two
// models. For clarity I’ ll rewrite the earlier example intentions in an
// abbreviated form:
//
2022-10-21 19:58:06 +00:00
// A : ALLOW
// B : DENY
// C : ALLOW
// <default> : DENY
2020-08-27 17:20:58 +00:00
//
2022-10-21 19:58:06 +00:00
// 1. Given that the overall intention default is set to deny, we start by
// choosing to build an allow-list in Envoy (this is also the variant that I find
// easier to think about).
// 2. Next we traverse the list in precedence order (top down) and any DENY
// intentions are combined with later intentions using logical operations.
// 3. Now that all of the intentions result in the same action (allow) we have
// successfully removed precedence and we can express this in as a set of Envoy
// RBAC policies.
2020-08-27 17:20:58 +00:00
//
// After this the earlier A/B/C/default list becomes:
//
2022-10-21 19:58:06 +00:00
// A : ALLOW
// C AND NOT(B) : ALLOW
// <default> : DENY
2020-08-27 17:20:58 +00:00
//
// Which really is just an allow-list of [A, C AND NOT(B)]
2022-06-10 21:15:22 +00:00
func makeRBACRules (
2023-04-20 16:16:04 +00:00
intentions structs . SimplifiedIntentions ,
2022-06-10 21:15:22 +00:00
intentionDefaultAllow bool ,
2022-06-29 15:29:54 +00:00
localInfo rbacLocalInfo ,
2022-06-10 21:15:22 +00:00
isHTTP bool ,
2022-06-21 02:47:14 +00:00
peerTrustBundles [ ] * pbpeering . PeeringTrustBundle ,
2022-07-14 18:45:51 +00:00
) * envoy_rbac_v3 . RBAC {
2020-08-27 17:20:58 +00:00
// TODO(banks,rb): Implement revocation list checking?
2022-06-21 02:47:14 +00:00
// TODO(peering): mkeeler asked that these maps come from proxycfg instead of
// being constructed in xds to save memory allocation and gc pressure. Low priority.
trustBundlesByPeer := make ( map [ string ] * pbpeering . PeeringTrustBundle , len ( peerTrustBundles ) )
for _ , ptb := range peerTrustBundles {
trustBundlesByPeer [ ptb . PeerName ] = ptb
}
2022-06-29 15:29:54 +00:00
if isHTTP && len ( peerTrustBundles ) > 0 {
for _ , ixn := range intentions {
if ixn . SourcePeer != "" {
localInfo . expectXFCC = true
break
}
}
}
2020-08-27 17:20:58 +00:00
// First build up just the basic principal matches.
2022-06-29 15:29:54 +00:00
rbacIxns := intentionListToIntermediateRBACForm ( intentions , localInfo , isHTTP , trustBundlesByPeer )
2020-08-27 17:20:58 +00:00
// Normalize: if we are in default-deny then all intentions must be allows and vice versa
2020-10-06 22:09:13 +00:00
intentionDefaultAction := intentionActionFromBool ( intentionDefaultAllow )
2020-08-27 17:20:58 +00:00
2021-02-26 22:23:15 +00:00
var rbacAction envoy_rbac_v3 . RBAC_Action
2020-08-27 17:20:58 +00:00
if intentionDefaultAllow {
// The RBAC policies deny access to principals. The rest is allowed.
// This is block-list style access control.
2021-02-26 22:23:15 +00:00
rbacAction = envoy_rbac_v3 . RBAC_DENY
2020-08-27 17:20:58 +00:00
} else {
// The RBAC policies grant access to principals. The rest is denied.
// This is safe-list style access control. This is the default type.
2021-02-26 22:23:15 +00:00
rbacAction = envoy_rbac_v3 . RBAC_ALLOW
2020-08-27 17:20:58 +00:00
}
2020-10-06 22:09:13 +00:00
// Remove source and permissions precedence.
2022-06-29 15:29:54 +00:00
rbacIxns = removeIntentionPrecedence ( rbacIxns , intentionDefaultAction , localInfo )
2020-08-27 17:20:58 +00:00
2020-10-06 22:09:13 +00:00
// For L4: we should generate one big Policy listing all Principals
// For L7: we should generate one Policy per Principal and list all of the Permissions
2021-02-26 22:23:15 +00:00
rbac := & envoy_rbac_v3 . RBAC {
2020-10-06 22:09:13 +00:00
Action : rbacAction ,
2021-02-26 22:23:15 +00:00
Policies : make ( map [ string ] * envoy_rbac_v3 . Policy ) ,
2020-10-06 22:09:13 +00:00
}
2020-08-27 17:20:58 +00:00
2021-02-26 22:23:15 +00:00
var principalsL4 [ ] * envoy_rbac_v3 . Principal
2020-10-06 22:09:13 +00:00
for i , rbacIxn := range rbacIxns {
2021-07-15 15:09:00 +00:00
if rbacIxn . Action == intentionActionLayer7 {
if len ( rbacIxn . Permissions ) == 0 {
panic ( "invalid state: L7 intention has no permissions" )
}
2020-10-06 22:09:13 +00:00
if ! isHTTP {
panic ( "invalid state: L7 permissions present for TCP service" )
}
2021-07-15 15:09:00 +00:00
2020-10-06 22:09:13 +00:00
// For L7: we should generate one Policy per Principal and list all of the Permissions
2021-02-26 22:23:15 +00:00
policy := & envoy_rbac_v3 . Policy {
2022-06-29 15:29:54 +00:00
Principals : optimizePrincipals ( [ ] * envoy_rbac_v3 . Principal { rbacIxn . ComputedPrincipal } ) ,
2021-02-26 22:23:15 +00:00
Permissions : make ( [ ] * envoy_rbac_v3 . Permission , 0 , len ( rbacIxn . Permissions ) ) ,
2020-08-27 17:20:58 +00:00
}
2020-10-06 22:09:13 +00:00
for _ , perm := range rbacIxn . Permissions {
policy . Permissions = append ( policy . Permissions , perm . ComputedPermission )
}
rbac . Policies [ fmt . Sprintf ( "consul-intentions-layer7-%d" , i ) ] = policy
2020-08-27 17:20:58 +00:00
} else {
2020-10-06 22:09:13 +00:00
// For L4: we should generate one big Policy listing all Principals
principalsL4 = append ( principalsL4 , rbacIxn . ComputedPrincipal )
2020-08-27 17:20:58 +00:00
}
}
2020-10-06 22:09:13 +00:00
if len ( principalsL4 ) > 0 {
2021-02-26 22:23:15 +00:00
rbac . Policies [ "consul-intentions-layer4" ] = & envoy_rbac_v3 . Policy {
2022-06-29 15:29:54 +00:00
Principals : optimizePrincipals ( principalsL4 ) ,
2021-02-26 22:23:15 +00:00
Permissions : [ ] * envoy_rbac_v3 . Permission { anyPermission ( ) } ,
2020-08-27 17:20:58 +00:00
}
}
2020-10-06 22:09:13 +00:00
if len ( rbac . Policies ) == 0 {
rbac . Policies = nil
}
2022-07-14 18:45:51 +00:00
return rbac
2020-08-27 17:20:58 +00:00
}
2022-06-29 15:29:54 +00:00
func optimizePrincipals ( orig [ ] * envoy_rbac_v3 . Principal ) [ ] * envoy_rbac_v3 . Principal {
// If they are all ORs, then OR them together.
var orIds [ ] * envoy_rbac_v3 . Principal
for _ , p := range orig {
or , ok := p . Identifier . ( * envoy_rbac_v3 . Principal_OrIds )
if ! ok {
return orig
}
orIds = append ( orIds , or . OrIds . Ids ... )
}
return [ ] * envoy_rbac_v3 . Principal { orPrincipals ( orIds ) }
}
2021-09-02 18:12:51 +00:00
// removeSameSourceIntentions will iterate over intentions and remove any lower precedence
// intentions that share the same source. Intentions are sorted by descending precedence
// so once a source has been seen, additional intentions with the same source can be dropped.
//
// Example for the default/web service:
// input: [(backend/* -> default/web), (backend/* -> default/*)]
// output: [(backend/* -> default/web)]
//
// (backend/* -> default/*) was dropped because it is already known that any service
// in the backend namespace can target default/web.
2023-04-20 16:16:04 +00:00
func removeSameSourceIntentions ( intentions structs . SimplifiedIntentions ) structs . SimplifiedIntentions {
2020-08-27 17:20:58 +00:00
if len ( intentions ) < 2 {
return intentions
}
var (
2023-04-20 16:16:04 +00:00
out = make ( structs . SimplifiedIntentions , 0 , len ( intentions ) )
2020-08-27 17:20:58 +00:00
changed = false
2022-06-10 21:15:22 +00:00
seenSource = make ( map [ structs . PeeredServiceName ] struct { } )
2020-08-27 17:20:58 +00:00
)
for _ , ixn := range intentions {
2022-06-10 21:15:22 +00:00
psn := structs . PeeredServiceName {
ServiceName : ixn . SourceServiceName ( ) ,
Peer : ixn . SourcePeer ,
}
if _ , ok := seenSource [ psn ] ; ok {
2020-08-27 17:20:58 +00:00
// A higher precedence intention already used this exact source
// definition with a different destination.
changed = true
continue
}
2022-06-10 21:15:22 +00:00
seenSource [ psn ] = struct { } { }
2020-08-27 17:20:58 +00:00
out = append ( out , ixn )
}
if ! changed {
return intentions
}
return out
}
2022-06-10 21:15:22 +00:00
// ixnSourceMatches determines if the 'tester' service name is matched by the
2020-08-27 17:20:58 +00:00
// 'against' service name via wildcard rules.
//
// For instance:
2021-09-02 16:43:56 +00:00
// - (web, api) => false, because these have no wildcards
// - (web, *) => true, because "all services" includes "web"
// - (default/web, default/*) => true, because "all services in the default NS" includes "default/web"
// - (default/*, */*) => true, "any service in any NS" includes "all services in the default NS"
// - (default/default/*, other/*/*) => false, "any service in "other" partition" does NOT include services in the default partition"
2022-06-10 21:15:22 +00:00
//
// Peer and partition must be exact names and cannot be compared with wildcards.
func ixnSourceMatches ( tester , against rbacService ) bool {
2020-08-27 17:20:58 +00:00
// We assume that we can't have the same intention twice before arriving
// here.
numWildTester := countWild ( tester )
numWildAgainst := countWild ( against )
if numWildTester == numWildAgainst {
return false
} else if numWildTester > numWildAgainst {
return false
}
2022-06-10 21:15:22 +00:00
matchesAP := tester . PartitionOrDefault ( ) == against . PartitionOrDefault ( )
matchesPeer := tester . Peer == against . Peer
2020-08-27 17:20:58 +00:00
matchesNS := tester . NamespaceOrDefault ( ) == against . NamespaceOrDefault ( ) || against . NamespaceOrDefault ( ) == structs . WildcardSpecifier
matchesName := tester . Name == against . Name || against . Name == structs . WildcardSpecifier
2022-06-10 21:15:22 +00:00
return matchesAP && matchesPeer && matchesNS && matchesName
2020-08-27 17:20:58 +00:00
}
// countWild counts the number of wildcard values in the given namespace and name.
2022-06-10 21:15:22 +00:00
func countWild ( src rbacService ) int {
2021-09-02 16:43:56 +00:00
// If Partition is wildcard, panic because it's not supported
if src . PartitionOrDefault ( ) == structs . WildcardSpecifier {
panic ( "invalid state: intention references wildcard partition" )
}
2022-06-10 21:15:22 +00:00
if src . Peer == structs . WildcardSpecifier {
panic ( "invalid state: intention references wildcard peer" )
}
2021-09-02 16:43:56 +00:00
2020-08-27 17:20:58 +00:00
// If NS is wildcard, it must be 2 since wildcards only follow exact
if src . NamespaceOrDefault ( ) == structs . WildcardSpecifier {
return 2
}
// Same reasoning as above, a wildcard can only follow an exact value
// and an exact value cannot follow a wildcard, so if name is a wildcard
// we must have exactly one.
if src . Name == structs . WildcardSpecifier {
return 1
}
return 0
}
2021-02-26 22:23:15 +00:00
func andPrincipals ( ids [ ] * envoy_rbac_v3 . Principal ) * envoy_rbac_v3 . Principal {
return & envoy_rbac_v3 . Principal {
Identifier : & envoy_rbac_v3 . Principal_AndIds {
AndIds : & envoy_rbac_v3 . Principal_Set {
2020-08-27 17:20:58 +00:00
Ids : ids ,
} ,
} ,
}
}
2022-06-29 15:29:54 +00:00
func orPrincipals ( ids [ ] * envoy_rbac_v3 . Principal ) * envoy_rbac_v3 . Principal {
return & envoy_rbac_v3 . Principal {
Identifier : & envoy_rbac_v3 . Principal_OrIds {
OrIds : & envoy_rbac_v3 . Principal_Set {
Ids : ids ,
} ,
} ,
}
}
2021-02-26 22:23:15 +00:00
func notPrincipal ( id * envoy_rbac_v3 . Principal ) * envoy_rbac_v3 . Principal {
return & envoy_rbac_v3 . Principal {
Identifier : & envoy_rbac_v3 . Principal_NotId {
2020-08-27 17:20:58 +00:00
NotId : id ,
} ,
}
}
2022-06-10 21:15:22 +00:00
func idPrincipal ( src rbacService ) * envoy_rbac_v3 . Principal {
pattern := makeSpiffePattern ( src )
2022-06-29 15:29:54 +00:00
return authenticatedPatternPrincipal ( pattern )
}
2020-08-27 17:20:58 +00:00
2022-06-29 15:29:54 +00:00
func authenticatedPatternPrincipal ( pattern string ) * envoy_rbac_v3 . Principal {
2021-02-26 22:23:15 +00:00
return & envoy_rbac_v3 . Principal {
Identifier : & envoy_rbac_v3 . Principal_Authenticated_ {
Authenticated : & envoy_rbac_v3 . Principal_Authenticated {
PrincipalName : & envoy_matcher_v3 . StringMatcher {
MatchPattern : & envoy_matcher_v3 . StringMatcher_SafeRegex {
2020-08-27 17:20:58 +00:00
SafeRegex : makeEnvoyRegexMatch ( pattern ) ,
} ,
} ,
} ,
} ,
}
}
2021-09-10 00:24:43 +00:00
2022-06-29 15:29:54 +00:00
func xfccPrincipal ( src rbacService ) * envoy_rbac_v3 . Principal {
// Same match we normally would use.
idPattern := makeSpiffePattern ( src )
// Remove the leading ^ and trailing $.
idPattern = idPattern [ 1 : len ( idPattern ) - 1 ]
// Anchor to the first XFCC component
pattern := ` ^[^,]+;URI= ` + idPattern + ` (?:,.*)?$ `
// By=spiffe://8c7db6d3-e4ee-aa8c-488c-dbedd3772b78.consul/gateway/mesh/dc/dc2;
// Hash=2a2db78ac351a05854a0abd350631bf98cc0eb827d21f4ed5935ccd287779eb6;
// Cert="-----BEGIN%20CERTIFICATE-----<SNIP>";
// Chain="-----BEGIN%20CERTIFICATE-----<SNIP>";
// Subject="";
// URI=spiffe://5583c38e-c1c0-fd1e-2079-170bb2f396ad.consul/ns/default/dc/dc1/svc/pong,
return & envoy_rbac_v3 . Principal {
Identifier : & envoy_rbac_v3 . Principal_Header {
Header : & envoy_route_v3 . HeaderMatcher {
Name : "x-forwarded-client-cert" ,
HeaderMatchSpecifier : & envoy_route_v3 . HeaderMatcher_StringMatch {
StringMatch : & envoy_matcher_v3 . StringMatcher {
MatchPattern : & envoy_matcher_v3 . StringMatcher_SafeRegex {
SafeRegex : makeEnvoyRegexMatch ( pattern ) ,
} ,
} ,
} ,
} ,
} ,
}
}
2022-06-10 21:15:22 +00:00
const anyPath = ` [^/]+ `
2023-01-06 17:13:40 +00:00
const trustDomain = anyPath + "." + anyPath
// downstreamServiceIdentityMatcher needs to match XFCC headers in two cases:
// 1. Requests to cluster peered services through a mesh gateway. In this case, the XFCC header looks like the following (I added a new line after each ; for readability)
// By=spiffe://950df996-caef-ddef-ec5f-8d18a153b7b2.consul/gateway/mesh/dc/alpha;
// Hash=...;
// Cert=...;
// Chain=...;
// Subject="";
// URI=spiffe://c7e1d24a-eed8-10a3-286a-52bdb6b6a6fd.consul/ns/default/dc/primary/svc/s1,By=spiffe://950df996-caef-ddef-ec5f-8d18a153b7b2.consul/ns/default/dc/alpha/svc/s2;
// Hash=...;
// Cert=...;
// Chain=...;
// Subject="";
// URI=spiffe://950df996-caef-ddef-ec5f-8d18a153b7b2.consul/gateway/mesh/dc/alpha
//
// 2. Requests directly to another service
// By=spiffe://ae9dbea8-c1dd-7356-b211-c564f7917100.consul/ns/default/dc/primary/svc/s2;
// Hash=396218588ebc1655d32a49b68cedd6b66b9de7b3d69d0c0451bc5818132377d0;
// Cert=...;
// Chain=...;
// Subject="";
// URI=spiffe://ae9dbea8-c1dd-7356-b211-c564f7917100.consul/ns/default/dc/primary/svc/s1
//
// In either case, the regex matches the downstream service's spiffe id because mesh gateways use a different spiffe id format.
// Envoy requires us to include the trailing and leading .* to properly extract the properly submatch.
const downstreamServiceIdentityMatcher = ".*URI=spiffe://(" + trustDomain +
")(?:/ap/(" + anyPath +
"))?/ns/(" + anyPath +
")/dc/(" + anyPath +
")/svc/([^/;,]+).*"
func parseXFCCToDynamicMetaHTTPFilter ( ) ( * envoy_http_v3 . HttpFilter , error ) {
var rules [ ] * envoy_http_header_to_meta_v3 . Config_Rule
fields := [ ] struct {
name string
sub string
} {
{
name : "trust-domain" ,
sub : ` \1 ` ,
} ,
{
name : "partition" ,
sub : ` \2 ` ,
} ,
{
name : "namespace" ,
sub : ` \3 ` ,
} ,
{
name : "datacenter" ,
sub : ` \4 ` ,
} ,
{
name : "service" ,
sub : ` \5 ` ,
} ,
}
for _ , f := range fields {
rules = append ( rules , & envoy_http_header_to_meta_v3 . Config_Rule {
Header : "x-forwarded-client-cert" ,
OnHeaderPresent : & envoy_http_header_to_meta_v3 . Config_KeyValuePair {
MetadataNamespace : "consul" ,
Key : f . name ,
RegexValueRewrite : & envoy_matcher_v3 . RegexMatchAndSubstitute {
Pattern : & envoy_matcher_v3 . RegexMatcher {
Regex : downstreamServiceIdentityMatcher ,
EngineType : & envoy_matcher_v3 . RegexMatcher_GoogleRe2 {
GoogleRe2 : & envoy_matcher_v3 . RegexMatcher_GoogleRE2 { } ,
} ,
} ,
Substitution : f . sub ,
} ,
} ,
} )
}
cfg := & envoy_http_header_to_meta_v3 . Config { RequestRules : rules }
return makeEnvoyHTTPFilter ( "envoy.filters.http.header_to_metadata" , cfg )
}
2022-06-10 21:15:22 +00:00
func makeSpiffePattern ( src rbacService ) string {
var (
2022-06-21 02:47:14 +00:00
host = src . TrustDomain
2022-06-10 21:15:22 +00:00
ap = src . PartitionOrDefault ( )
ns = src . NamespaceOrDefault ( )
svc = src . Name
)
// Validate proper wildcarding
if ns == structs . WildcardSpecifier && svc != structs . WildcardSpecifier {
panic ( fmt . Sprintf ( "not possible to have a wildcarded namespace %q but an exact service %q" , ns , svc ) )
2020-08-27 17:20:58 +00:00
}
2022-06-10 21:15:22 +00:00
if ap == structs . WildcardSpecifier {
2021-09-10 00:24:43 +00:00
panic ( "not possible to have a wildcarded source partition" )
}
2022-06-10 21:15:22 +00:00
if src . Peer == structs . WildcardSpecifier {
panic ( "not possible to have a wildcarded source peer" )
}
2021-09-10 00:24:43 +00:00
// Match on any namespace or service if it is a wildcard, or on a specific value otherwise.
2022-06-10 21:15:22 +00:00
if ns == structs . WildcardSpecifier {
2021-09-10 00:24:43 +00:00
ns = anyPath
}
2022-06-10 21:15:22 +00:00
if svc == structs . WildcardSpecifier {
2021-09-10 00:24:43 +00:00
svc = anyPath
}
2022-06-10 21:15:22 +00:00
// If service is imported from a peer, the SpiffeID must
// refer to its remote partition and trust domain.
if src . Peer != "" {
ap = src . ExportedPartition
host = src . TrustDomain
}
2021-09-10 00:24:43 +00:00
id := connect . SpiffeIDService {
Namespace : ns ,
Service : svc ,
2022-06-10 21:15:22 +00:00
Host : host ,
2021-09-10 00:24:43 +00:00
2022-06-10 21:15:22 +00:00
// Datacenter is not verified by RBAC, so we match on any value.
2021-09-10 00:24:43 +00:00
Datacenter : anyPath ,
// Partition can only ever be an exact value.
2022-06-10 21:15:22 +00:00
Partition : ap ,
2021-09-10 00:24:43 +00:00
}
return fmt . Sprintf ( ` ^%s://%s%s$ ` , id . URI ( ) . Scheme , id . Host , id . URI ( ) . Path )
2020-08-27 17:20:58 +00:00
}
2022-06-29 15:29:54 +00:00
func makeSpiffeMeshGatewayPattern ( gwTrustDomain , gwPartition string ) string {
id := connect . SpiffeIDMeshGateway {
Host : gwTrustDomain ,
Partition : gwPartition ,
// Datacenter is not verified by RBAC, so we match on any value.
Datacenter : anyPath ,
}
return fmt . Sprintf ( ` ^%s://%s%s$ ` , id . URI ( ) . Scheme , id . Host , id . URI ( ) . Path )
}
2021-02-26 22:23:15 +00:00
func anyPermission ( ) * envoy_rbac_v3 . Permission {
return & envoy_rbac_v3 . Permission {
Rule : & envoy_rbac_v3 . Permission_Any { Any : true } ,
2020-08-27 17:20:58 +00:00
}
}
2020-10-06 22:09:13 +00:00
2021-02-26 22:23:15 +00:00
func convertPermission ( perm * structs . IntentionPermission ) * envoy_rbac_v3 . Permission {
2020-10-06 22:09:13 +00:00
// NOTE: this does not do anything with perm.Action
if perm . HTTP == nil {
return anyPermission ( )
}
2021-02-26 22:23:15 +00:00
var parts [ ] * envoy_rbac_v3 . Permission
2020-10-06 22:09:13 +00:00
switch {
case perm . HTTP . PathExact != "" :
2021-02-26 22:23:15 +00:00
parts = append ( parts , & envoy_rbac_v3 . Permission {
Rule : & envoy_rbac_v3 . Permission_UrlPath {
UrlPath : & envoy_matcher_v3 . PathMatcher {
Rule : & envoy_matcher_v3 . PathMatcher_Path {
Path : & envoy_matcher_v3 . StringMatcher {
MatchPattern : & envoy_matcher_v3 . StringMatcher_Exact {
2020-10-06 22:09:13 +00:00
Exact : perm . HTTP . PathExact ,
} ,
} ,
} ,
} ,
} ,
} )
case perm . HTTP . PathPrefix != "" :
2021-02-26 22:23:15 +00:00
parts = append ( parts , & envoy_rbac_v3 . Permission {
Rule : & envoy_rbac_v3 . Permission_UrlPath {
UrlPath : & envoy_matcher_v3 . PathMatcher {
Rule : & envoy_matcher_v3 . PathMatcher_Path {
Path : & envoy_matcher_v3 . StringMatcher {
MatchPattern : & envoy_matcher_v3 . StringMatcher_Prefix {
2020-10-06 22:09:13 +00:00
Prefix : perm . HTTP . PathPrefix ,
} ,
} ,
} ,
} ,
} ,
} )
case perm . HTTP . PathRegex != "" :
2021-02-26 22:23:15 +00:00
parts = append ( parts , & envoy_rbac_v3 . Permission {
Rule : & envoy_rbac_v3 . Permission_UrlPath {
UrlPath : & envoy_matcher_v3 . PathMatcher {
Rule : & envoy_matcher_v3 . PathMatcher_Path {
Path : & envoy_matcher_v3 . StringMatcher {
MatchPattern : & envoy_matcher_v3 . StringMatcher_SafeRegex {
2020-10-06 22:09:13 +00:00
SafeRegex : makeEnvoyRegexMatch ( perm . HTTP . PathRegex ) ,
} ,
} ,
} ,
} ,
} ,
} )
}
for _ , hdr := range perm . HTTP . Header {
2021-02-26 22:23:15 +00:00
eh := & envoy_route_v3 . HeaderMatcher {
2020-10-06 22:09:13 +00:00
Name : hdr . Name ,
}
switch {
case hdr . Exact != "" :
2021-02-26 22:23:15 +00:00
eh . HeaderMatchSpecifier = & envoy_route_v3 . HeaderMatcher_ExactMatch {
2020-10-06 22:09:13 +00:00
ExactMatch : hdr . Exact ,
}
case hdr . Regex != "" :
2021-02-26 22:23:15 +00:00
eh . HeaderMatchSpecifier = & envoy_route_v3 . HeaderMatcher_SafeRegexMatch {
2020-10-06 22:09:13 +00:00
SafeRegexMatch : makeEnvoyRegexMatch ( hdr . Regex ) ,
}
case hdr . Prefix != "" :
2021-02-26 22:23:15 +00:00
eh . HeaderMatchSpecifier = & envoy_route_v3 . HeaderMatcher_PrefixMatch {
2020-10-06 22:09:13 +00:00
PrefixMatch : hdr . Prefix ,
}
case hdr . Suffix != "" :
2021-02-26 22:23:15 +00:00
eh . HeaderMatchSpecifier = & envoy_route_v3 . HeaderMatcher_SuffixMatch {
2020-10-06 22:09:13 +00:00
SuffixMatch : hdr . Suffix ,
}
case hdr . Present :
2021-02-26 22:23:15 +00:00
eh . HeaderMatchSpecifier = & envoy_route_v3 . HeaderMatcher_PresentMatch {
2020-10-06 22:09:13 +00:00
PresentMatch : true ,
}
default :
continue // skip this impossible situation
}
if hdr . Invert {
eh . InvertMatch = true
}
2021-02-26 22:23:15 +00:00
parts = append ( parts , & envoy_rbac_v3 . Permission {
Rule : & envoy_rbac_v3 . Permission_Header {
2020-10-06 22:09:13 +00:00
Header : eh ,
} ,
} )
}
if len ( perm . HTTP . Methods ) > 0 {
methodHeaderRegex := strings . Join ( perm . HTTP . Methods , "|" )
2021-02-26 22:23:15 +00:00
eh := & envoy_route_v3 . HeaderMatcher {
2020-10-06 22:09:13 +00:00
Name : ":method" ,
2021-02-26 22:23:15 +00:00
HeaderMatchSpecifier : & envoy_route_v3 . HeaderMatcher_SafeRegexMatch {
2020-10-06 22:09:13 +00:00
SafeRegexMatch : makeEnvoyRegexMatch ( methodHeaderRegex ) ,
} ,
}
2021-02-26 22:23:15 +00:00
parts = append ( parts , & envoy_rbac_v3 . Permission {
Rule : & envoy_rbac_v3 . Permission_Header {
2020-10-06 22:09:13 +00:00
Header : eh ,
} ,
} )
}
// NOTE: if for some reason we errantly allow a permission to be defined
// with a body of "http{}" then we'll end up treating that like "ANY" here.
return andPermissions ( parts )
}
2021-02-26 22:23:15 +00:00
func notPermission ( perm * envoy_rbac_v3 . Permission ) * envoy_rbac_v3 . Permission {
return & envoy_rbac_v3 . Permission {
Rule : & envoy_rbac_v3 . Permission_NotRule { NotRule : perm } ,
2020-10-06 22:09:13 +00:00
}
}
2021-02-26 22:23:15 +00:00
func andPermissions ( perms [ ] * envoy_rbac_v3 . Permission ) * envoy_rbac_v3 . Permission {
2020-10-06 22:09:13 +00:00
switch len ( perms ) {
case 0 :
return anyPermission ( )
case 1 :
return perms [ 0 ]
default :
2021-02-26 22:23:15 +00:00
return & envoy_rbac_v3 . Permission {
Rule : & envoy_rbac_v3 . Permission_AndRules {
AndRules : & envoy_rbac_v3 . Permission_Set {
2020-10-06 22:09:13 +00:00
Rules : perms ,
} ,
} ,
}
}
}