consul/agent/xdsv2/rbac_resources.go

487 lines
14 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package xdsv2
import (
"fmt"
"strings"
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"
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"
"github.com/hashicorp/consul/agent/xds/response"
"github.com/hashicorp/consul/proto-public/pbmesh/v2beta1/pbproxystate"
)
const (
baseL4PermissionKey = "consul-intentions-layer4"
baseL7PermissionKey = "consul-intentions-layer7"
)
// MakeRBAC returns the envoy deny and allow rules from the traffic permissions. After calling this function these
// rules can be put into a network rbac filter or http rbac filter depending on the local app port protocol.
func MakeRBAC(trafficPermissions *pbproxystate.TrafficPermissions, makePolicies func([]*pbproxystate.Permission) map[string]*envoy_rbac_v3.Policy) (deny *envoy_rbac_v3.RBAC, allow *envoy_rbac_v3.RBAC, err error) {
var denyRBAC *envoy_rbac_v3.RBAC
var allowRBAC *envoy_rbac_v3.RBAC
if trafficPermissions == nil {
return nil, nil, nil
}
if len(trafficPermissions.DenyPermissions) > 0 {
denyRBAC = &envoy_rbac_v3.RBAC{
Action: envoy_rbac_v3.RBAC_DENY,
Policies: make(map[string]*envoy_rbac_v3.Policy),
}
denyRBAC.Policies = makePolicies(trafficPermissions.DenyPermissions)
}
// Only include the allow RBAC when Consul is in default deny.
if !trafficPermissions.DefaultAllow {
allowRBAC = &envoy_rbac_v3.RBAC{
Action: envoy_rbac_v3.RBAC_ALLOW,
Policies: make(map[string]*envoy_rbac_v3.Policy),
}
allowRBAC.Policies = makePolicies(trafficPermissions.AllowPermissions)
}
return denyRBAC, allowRBAC, nil
}
// MakeRBACNetworkFilters calls MakeL4RBAC and wraps the result in envoy network filters meant for L4 protocols.
func MakeRBACNetworkFilters(trafficPermissions *pbproxystate.TrafficPermissions) ([]*envoy_listener_v3.Filter, error) {
var filters []*envoy_listener_v3.Filter
deny, allow, err := MakeRBAC(trafficPermissions, makeL4RBACPolicies)
if err != nil {
return nil, err
}
if deny != nil {
filter, err := makeRBACFilter(deny)
if err != nil {
return nil, err
}
filters = append(filters, filter)
}
if allow != nil {
filter, err := makeRBACFilter(allow)
if err != nil {
return nil, err
}
filters = append(filters, filter)
}
return filters, nil
}
// MakeRBACHTTPFilters calls MakeL4RBAC and wraps the result in envoy http filters meant for L7 protocols. Eventually
// this will need to also accumulate any L7 traffic permissions when that is implemented.
func MakeRBACHTTPFilters(trafficPermissions *pbproxystate.TrafficPermissions) ([]*envoy_http_v3.HttpFilter, error) {
var httpFilters []*envoy_http_v3.HttpFilter
deny, allow, err := MakeRBAC(trafficPermissions, makeL7RBACPolicies)
if err != nil {
return nil, err
}
if deny != nil {
filter, err := makeRBACHTTPFilter(deny)
if err != nil {
return nil, err
}
httpFilters = append(httpFilters, filter)
}
if allow != nil {
filter, err := makeRBACHTTPFilter(allow)
if err != nil {
return nil, err
}
httpFilters = append(httpFilters, filter)
}
return httpFilters, nil
}
const (
envoyNetworkRBACFilterKey = "envoy.filters.network.rbac"
envoyHTTPRBACFilterKey = "envoy.filters.http.rbac"
)
func makeRBACFilter(rbac *envoy_rbac_v3.RBAC) (*envoy_listener_v3.Filter, error) {
cfg := &envoy_network_rbac_v3.RBAC{
StatPrefix: "connect_authz",
Rules: rbac,
}
return makeEnvoyFilter(envoyNetworkRBACFilterKey, cfg)
}
func makeRBACHTTPFilter(rbac *envoy_rbac_v3.RBAC) (*envoy_http_v3.HttpFilter, error) {
cfg := &envoy_http_rbac_v3.RBAC{
Rules: rbac,
}
return makeEnvoyHTTPFilter(envoyHTTPRBACFilterKey, cfg)
}
func makeL4RBACPolicies(l4Permissions []*pbproxystate.Permission) map[string]*envoy_rbac_v3.Policy {
policies := make(map[string]*envoy_rbac_v3.Policy, len(l4Permissions))
for i, permission := range l4Permissions {
if len(permission.DestinationRules) != 0 {
// This is an L7-only permission
// ports are split out for separate configuration before this point and L7 filters are configured separately
continue
}
policy := makeL4RBACPolicy(permission)
if policy != nil {
policies[l4PolicyLabel(l4Permissions, i)] = policy
}
}
return policies
}
func makeL4RBACPolicy(p *pbproxystate.Permission) *envoy_rbac_v3.Policy {
if p == nil || len(p.Principals) == 0 {
return nil
}
var principals []*envoy_rbac_v3.Principal
for _, p := range p.Principals {
principals = append(principals, toEnvoyPrincipal(p))
}
return &envoy_rbac_v3.Policy{
Principals: principals,
Permissions: []*envoy_rbac_v3.Permission{anyPermission()},
}
}
func l4PolicyLabel(perms []*pbproxystate.Permission, i int) string {
if len(perms) == 1 {
return baseL4PermissionKey
}
return fmt.Sprintf("%s-%d", baseL4PermissionKey, i)
}
func makeL7RBACPolicies(l7Permissions []*pbproxystate.Permission) map[string]*envoy_rbac_v3.Policy {
// sort permissions into those with L7-specific features and those without, to match labeling and behavior
// conventions in V1: https://github.com/hashicorp/consul/blob/4e451f23584473a7eaf7f123145ca85e0a31783a/agent/xds/rbac.go#L647
// this is a somewhat unfortunate carry-over needed for testing v1 vs v2 final config
// and this will break with v1 intentions when multiple L4 permissions are used
var l4Perms []*pbproxystate.Permission
var l7Perms []*pbproxystate.Permission
for _, p := range l7Permissions {
if len(p.DestinationRules) > 0 {
l7Perms = append(l7Perms, p)
} else {
l4Perms = append(l4Perms, p)
}
}
policies := make(map[string]*envoy_rbac_v3.Policy, len(l7Permissions))
// L7 policies first, then L4 per: https://github.com/hashicorp/consul/blob/4e451f23584473a7eaf7f123145ca85e0a31783a/agent/xds/rbac.go#L664
for i, permission := range l7Perms {
policy := makeL7RBACPolicy(permission)
if policy != nil {
policies[fmt.Sprintf("%s-%d", baseL7PermissionKey, i)] = policy
}
}
for i, permission := range l4Perms {
policy := makeL4RBACPolicy(permission)
if policy != nil {
policies[l4PolicyLabel(l4Perms, i)] = policy
}
}
return policies
}
func makeL7RBACPolicy(p *pbproxystate.Permission) *envoy_rbac_v3.Policy {
if p == nil || len(p.Principals) == 0 {
return nil
}
var principals []*envoy_rbac_v3.Principal
for _, p := range p.Principals {
principals = append(principals, toEnvoyPrincipal(p))
}
permissions := permissionsFromDestinationRules(p.DestinationRules)
return &envoy_rbac_v3.Policy{
Principals: principals,
Permissions: permissions,
}
}
func translateRule(dr *pbproxystate.DestinationRule) *envoy_rbac_v3.Permission {
var perms []*envoy_rbac_v3.Permission
// paths
switch {
case dr.PathExact != "":
perms = append(perms, &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{
Exact: dr.PathExact,
},
},
},
},
},
})
case dr.PathPrefix != "":
perms = append(perms, &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{
Prefix: dr.PathPrefix,
},
},
},
},
},
})
case dr.PathRegex != "":
perms = append(perms, &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{
SafeRegex: response.MakeEnvoyRegexMatch(dr.PathRegex),
},
},
},
},
},
})
}
// methods
if len(dr.Methods) > 0 {
methodHeaderRegex := strings.Join(dr.Methods, "|")
eh := &envoy_route_v3.HeaderMatcher{
Name: ":method",
HeaderMatchSpecifier: &envoy_route_v3.HeaderMatcher_StringMatch{
StringMatch: &envoy_matcher_v3.StringMatcher{
MatchPattern: &envoy_matcher_v3.StringMatcher_SafeRegex{
SafeRegex: response.MakeEnvoyRegexMatch(methodHeaderRegex),
},
},
},
}
perms = append(perms, &envoy_rbac_v3.Permission{
Rule: &envoy_rbac_v3.Permission_Header{
Header: eh,
}})
}
// headers
for _, hdr := range dr.DestinationRuleHeader {
eh := &envoy_route_v3.HeaderMatcher{
Name: hdr.Name,
}
switch {
case hdr.Exact != "":
eh.HeaderMatchSpecifier = &envoy_route_v3.HeaderMatcher_StringMatch{
StringMatch: &envoy_matcher_v3.StringMatcher{
MatchPattern: &envoy_matcher_v3.StringMatcher_Exact{
Exact: hdr.Exact,
},
IgnoreCase: false,
},
}
case hdr.Regex != "":
eh.HeaderMatchSpecifier = &envoy_route_v3.HeaderMatcher_StringMatch{
StringMatch: &envoy_matcher_v3.StringMatcher{
MatchPattern: &envoy_matcher_v3.StringMatcher_SafeRegex{
SafeRegex: response.MakeEnvoyRegexMatch(hdr.Regex),
},
IgnoreCase: false,
},
}
case hdr.Prefix != "":
eh.HeaderMatchSpecifier = &envoy_route_v3.HeaderMatcher_StringMatch{
StringMatch: &envoy_matcher_v3.StringMatcher{
MatchPattern: &envoy_matcher_v3.StringMatcher_Prefix{
Prefix: hdr.Prefix,
},
IgnoreCase: false,
},
}
case hdr.Suffix != "":
eh.HeaderMatchSpecifier = &envoy_route_v3.HeaderMatcher_StringMatch{
StringMatch: &envoy_matcher_v3.StringMatcher{
MatchPattern: &envoy_matcher_v3.StringMatcher_Suffix{
Suffix: hdr.Suffix,
},
IgnoreCase: false,
},
}
case hdr.Present:
eh.HeaderMatchSpecifier = &envoy_route_v3.HeaderMatcher_PresentMatch{
PresentMatch: true,
}
default:
continue // skip this impossible situation
}
if hdr.Invert {
eh.InvertMatch = true
}
perms = append(perms, &envoy_rbac_v3.Permission{
Rule: &envoy_rbac_v3.Permission_Header{
Header: eh,
},
})
}
return combineAndPermissions(perms)
}
func permissionsFromDestinationRules(drs []*pbproxystate.DestinationRule) []*envoy_rbac_v3.Permission {
var perms []*envoy_rbac_v3.Permission
for _, dr := range drs {
subPerms := make([]*envoy_rbac_v3.Permission, len(dr.Exclude))
for i, er := range dr.Exclude {
translated := translateRule(&pbproxystate.DestinationRule{
PathExact: er.PathExact,
PathPrefix: er.PathPrefix,
PathRegex: er.PathRegex,
Methods: er.Methods,
DestinationRuleHeader: er.Headers,
})
subPerms[i] = &envoy_rbac_v3.Permission{
Rule: &envoy_rbac_v3.Permission_NotRule{NotRule: translated},
}
}
subPerms = append([]*envoy_rbac_v3.Permission{translateRule(dr)}, subPerms...)
perms = append(perms, combineAndPermissions(subPerms))
}
return perms
}
func combineAndPermissions(perms []*envoy_rbac_v3.Permission) *envoy_rbac_v3.Permission {
switch len(perms) {
case 0:
return anyPermission()
case 1:
return perms[0]
default:
return &envoy_rbac_v3.Permission{
Rule: &envoy_rbac_v3.Permission_AndRules{
AndRules: &envoy_rbac_v3.Permission_Set{
Rules: perms,
},
},
}
}
}
func toEnvoyPrincipal(p *pbproxystate.Principal) *envoy_rbac_v3.Principal {
includePrincipal := principal(p.Spiffe)
if len(p.ExcludeSpiffes) == 0 {
return includePrincipal
}
principals := make([]*envoy_rbac_v3.Principal, 0, len(p.ExcludeSpiffes)+1)
principals = append(principals, includePrincipal)
for _, s := range p.ExcludeSpiffes {
principals = append(principals, negatePrincipal(principal(s)))
}
return andPrincipals(principals)
}
func principal(spiffe *pbproxystate.Spiffe) *envoy_rbac_v3.Principal {
var andIDs []*envoy_rbac_v3.Principal
andIDs = append(andIDs, idPrincipal(spiffe.Regex))
if len(spiffe.XfccRegex) > 0 {
andIDs = append(andIDs, xfccPrincipal(spiffe.XfccRegex))
}
return andPrincipals(andIDs)
}
func negatePrincipal(p *envoy_rbac_v3.Principal) *envoy_rbac_v3.Principal {
return &envoy_rbac_v3.Principal{
Identifier: &envoy_rbac_v3.Principal_NotId{
NotId: p,
},
}
}
func idPrincipal(spiffeID string) *envoy_rbac_v3.Principal {
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{
SafeRegex: response.MakeEnvoyRegexMatch(spiffeID),
},
},
},
},
}
}
func andPrincipals(ids []*envoy_rbac_v3.Principal) *envoy_rbac_v3.Principal {
switch len(ids) {
case 1:
return ids[0]
default:
return &envoy_rbac_v3.Principal{
Identifier: &envoy_rbac_v3.Principal_AndIds{
AndIds: &envoy_rbac_v3.Principal_Set{
Ids: ids,
},
},
}
}
}
func xfccPrincipal(spiffeID string) *envoy_rbac_v3.Principal {
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: response.MakeEnvoyRegexMatch(spiffeID),
},
},
},
},
},
}
}
func anyPermission() *envoy_rbac_v3.Permission {
return &envoy_rbac_v3.Permission{
Rule: &envoy_rbac_v3.Permission_Any{Any: true},
}
}