1
// Copyright Istio Authors
3
// Licensed under the Apache License, Version 2.0 (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
7
// http://www.apache.org/licenses/LICENSE-2.0
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
21
listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
22
rbacpb "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3"
23
rbachttp "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/rbac/v3"
24
hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
25
rbactcp "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/rbac/v3"
26
"github.com/hashicorp/go-multierror"
28
"istio.io/api/annotation"
29
"istio.io/istio/pilot/pkg/model"
30
authzmodel "istio.io/istio/pilot/pkg/security/authz/model"
31
"istio.io/istio/pilot/pkg/security/trustdomain"
32
"istio.io/istio/pilot/pkg/util/protoconv"
33
"istio.io/istio/pkg/maps"
34
"istio.io/istio/pkg/wellknown"
37
var rbacPolicyMatchNever = &rbacpb.Policy{
38
Permissions: []*rbacpb.Permission{{Rule: &rbacpb.Permission_NotRule{
39
NotRule: &rbacpb.Permission{Rule: &rbacpb.Permission_Any{Any: true}},
41
Principals: []*rbacpb.Principal{{Identifier: &rbacpb.Principal_NotId{
42
NotId: &rbacpb.Principal{Identifier: &rbacpb.Principal_Any{Any: true}},
46
// General setting to control behavior
53
// Builder builds Istio authorization policy to Envoy filters.
55
trustDomainBundle trustdomain.Bundle
58
// populated when building for CUSTOM action.
59
customPolicies []model.AuthorizationPolicy
60
extensions map[string]*builtExtAuthz
62
// populated when building for ALLOW/DENY/AUDIT action.
63
denyPolicies []model.AuthorizationPolicy
64
allowPolicies []model.AuthorizationPolicy
65
auditPolicies []model.AuthorizationPolicy
67
// logger emits logs about policies
71
// New returns a new builder for the given workload with the authorization policy.
72
// Returns nil if none of the authorization policies are enabled for the workload.
73
func New(trustDomainBundle trustdomain.Bundle, push *model.PushContext, policies model.AuthorizationPoliciesResult, option Option) *Builder {
74
if option.IsCustomBuilder {
75
if len(policies.Custom) == 0 {
79
customPolicies: policies.Custom,
80
extensions: processExtensionProvider(push),
81
trustDomainBundle: trustDomainBundle,
86
if len(policies.Deny) == 0 && len(policies.Allow) == 0 && len(policies.Audit) == 0 {
90
denyPolicies: policies.Deny,
91
allowPolicies: policies.Allow,
92
auditPolicies: policies.Audit,
93
trustDomainBundle: trustDomainBundle,
98
// BuildHTTP returns the HTTP filters built from the authorization policy.
99
func (b Builder) BuildHTTP() []*hcm.HttpFilter {
100
b.logger = &AuthzLogger{}
101
defer b.logger.Report()
102
if b.option.IsCustomBuilder {
103
// Use the DENY action so that a HTTP rule is properly handled when generating for TCP filter chain.
104
if configs := b.build(b.customPolicies, rbacpb.RBAC_DENY, false); configs != nil {
105
b.logger.AppendDebugf("built %d HTTP filters for CUSTOM action", len(configs.http))
111
var filters []*hcm.HttpFilter
112
if configs := b.build(b.auditPolicies, rbacpb.RBAC_LOG, false); configs != nil {
113
b.logger.AppendDebugf("built %d HTTP filters for AUDIT action", len(configs.http))
114
filters = append(filters, configs.http...)
116
if configs := b.build(b.denyPolicies, rbacpb.RBAC_DENY, false); configs != nil {
117
b.logger.AppendDebugf("built %d HTTP filters for DENY action", len(configs.http))
118
filters = append(filters, configs.http...)
120
if configs := b.build(b.allowPolicies, rbacpb.RBAC_ALLOW, false); configs != nil {
121
b.logger.AppendDebugf("built %d HTTP filters for ALLOW action", len(configs.http))
122
filters = append(filters, configs.http...)
127
// BuildTCP returns the TCP filters built from the authorization policy.
128
func (b Builder) BuildTCP() []*listener.Filter {
129
b.logger = &AuthzLogger{}
130
defer b.logger.Report()
131
if b.option.IsCustomBuilder {
132
if configs := b.build(b.customPolicies, rbacpb.RBAC_DENY, true); configs != nil {
133
b.logger.AppendDebugf("built %d TCP filters for CUSTOM action", len(configs.tcp))
139
var filters []*listener.Filter
140
if configs := b.build(b.auditPolicies, rbacpb.RBAC_LOG, true); configs != nil {
141
b.logger.AppendDebugf("built %d TCP filters for AUDIT action", len(configs.tcp))
142
filters = append(filters, configs.tcp...)
144
if configs := b.build(b.denyPolicies, rbacpb.RBAC_DENY, true); configs != nil {
145
b.logger.AppendDebugf("built %d TCP filters for DENY action", len(configs.tcp))
146
filters = append(filters, configs.tcp...)
148
if configs := b.build(b.allowPolicies, rbacpb.RBAC_ALLOW, true); configs != nil {
149
b.logger.AppendDebugf("built %d TCP filters for ALLOW action", len(configs.tcp))
150
filters = append(filters, configs.tcp...)
155
type builtConfigs struct {
156
http []*hcm.HttpFilter
157
tcp []*listener.Filter
160
func (b Builder) isDryRun(policy model.AuthorizationPolicy) bool {
162
if val, ok := policy.Annotations[annotation.IoIstioDryRun.Name]; ok {
164
dryRun, err = strconv.ParseBool(val)
166
b.logger.AppendError(fmt.Errorf("failed to parse the value of %s: %v", annotation.IoIstioDryRun.Name, err))
172
func shadowRuleStatPrefix(rule *rbacpb.RBAC) string {
173
switch rule.GetAction() {
174
case rbacpb.RBAC_ALLOW:
175
return authzmodel.RBACShadowRulesAllowStatPrefix
176
case rbacpb.RBAC_DENY:
177
return authzmodel.RBACShadowRulesDenyStatPrefix
183
func (b Builder) build(policies []model.AuthorizationPolicy, action rbacpb.RBAC_Action, forTCP bool) *builtConfigs {
184
if len(policies) == 0 {
188
enforceRules := &rbacpb.RBAC{
190
Policies: map[string]*rbacpb.Policy{},
192
shadowRules := &rbacpb.RBAC{
194
Policies: map[string]*rbacpb.Policy{},
197
var providers []string
202
hasEnforcePolicy, hasDryRunPolicy := false, false
203
for _, policy := range policies {
204
var currentRule *rbacpb.RBAC
205
if b.isDryRun(policy) {
206
currentRule = shadowRules
207
hasDryRunPolicy = true
209
currentRule = enforceRules
210
hasEnforcePolicy = true
212
if b.option.IsCustomBuilder {
213
providers = append(providers, policy.Spec.GetProvider().GetName())
215
for i, rule := range policy.Spec.Rules {
216
// The name will later be used by ext_authz filter to get the evaluation result from dynamic metadata.
217
name := policyName(policy.Namespace, policy.Name, i, b.option)
219
b.logger.AppendError(fmt.Errorf("skipped nil rule %s", name))
222
m, err := authzmodel.New(rule, b.option.UseExtendedJwt)
224
b.logger.AppendError(multierror.Prefix(err, fmt.Sprintf("skipped invalid rule %s:", name)))
227
m.MigrateTrustDomain(b.trustDomainBundle)
228
if len(b.trustDomainBundle.TrustDomains) > 1 {
229
b.logger.AppendDebugf("patched source principal with trust domain aliases %v", b.trustDomainBundle.TrustDomains)
231
generated, err := m.Generate(forTCP, !b.option.UseFilterState, action)
233
b.logger.AppendDebugf("skipped rule %s on TCP filter chain: %v", name, err)
236
if generated != nil {
237
currentRule.Policies[name] = generated
238
b.logger.AppendDebugf("generated config from rule %s on %s filter chain successfully", name, filterType)
241
if len(policy.Spec.Rules) == 0 {
242
// Generate an explicit policy that never matches.
243
name := policyName(policy.Namespace, policy.Name, 0, b.option)
244
b.logger.AppendDebugf("generated config from policy %s on %s filter chain successfully", name, filterType)
245
currentRule.Policies[name] = rbacPolicyMatchNever
249
if !hasEnforcePolicy {
252
if !hasDryRunPolicy {
256
return &builtConfigs{tcp: b.buildTCP(enforceRules, shadowRules, providers)}
258
return &builtConfigs{http: b.buildHTTP(enforceRules, shadowRules, providers)}
261
func (b Builder) buildHTTP(rules *rbacpb.RBAC, shadowRules *rbacpb.RBAC, providers []string) []*hcm.HttpFilter {
262
if !b.option.IsCustomBuilder {
263
rbac := &rbachttp.RBAC{
265
ShadowRules: shadowRules,
266
ShadowRulesStatPrefix: shadowRuleStatPrefix(shadowRules),
268
return []*hcm.HttpFilter{
270
Name: wellknown.HTTPRoleBasedAccessControl,
271
ConfigType: &hcm.HttpFilter_TypedConfig{TypedConfig: protoconv.MessageToAny(rbac)},
276
extauthz, err := getExtAuthz(b.extensions, providers)
278
b.logger.AppendError(multierror.Prefix(err, "failed to process CUSTOM action, will generate deny configs for the specified rules:"))
279
rbac := &rbachttp.RBAC{Rules: getBadCustomDenyRules(rules)}
280
return []*hcm.HttpFilter{
282
Name: wellknown.HTTPRoleBasedAccessControl,
283
ConfigType: &hcm.HttpFilter_TypedConfig{TypedConfig: protoconv.MessageToAny(rbac)},
287
// Add the RBAC filter in shadow mode so that it only evaluates the matching rules for CUSTOM action but not enforce it.
288
// The evaluation result is stored in the dynamic metadata keyed by the policy name. And then the ext_authz filter
289
// can utilize these metadata to trigger the enforcement conditionally.
290
// See https://docs.google.com/document/d/1V4mCQCw7mlGp0zSQQXYoBdbKMDnkPOjeyUb85U07iSI/edit#bookmark=kix.jdq8u0an2r6s
292
rbac := &rbachttp.RBAC{
294
ShadowRulesStatPrefix: authzmodel.RBACExtAuthzShadowRulesStatPrefix,
296
return []*hcm.HttpFilter{
298
Name: wellknown.HTTPRoleBasedAccessControl,
299
ConfigType: &hcm.HttpFilter_TypedConfig{TypedConfig: protoconv.MessageToAny(rbac)},
302
Name: wellknown.HTTPExternalAuthorization,
303
ConfigType: &hcm.HttpFilter_TypedConfig{TypedConfig: protoconv.MessageToAny(extauthz.http)},
308
func (b Builder) buildTCP(rules *rbacpb.RBAC, shadowRules *rbacpb.RBAC, providers []string) []*listener.Filter {
309
if !b.option.IsCustomBuilder {
310
rbac := &rbactcp.RBAC{
312
StatPrefix: authzmodel.RBACTCPFilterStatPrefix,
313
ShadowRules: shadowRules,
314
ShadowRulesStatPrefix: shadowRuleStatPrefix(shadowRules),
316
return []*listener.Filter{
318
Name: wellknown.RoleBasedAccessControl,
319
ConfigType: &listener.Filter_TypedConfig{TypedConfig: protoconv.MessageToAny(rbac)},
324
extauthz, err := getExtAuthz(b.extensions, providers)
326
b.logger.AppendError(multierror.Prefix(err, "failed to process CUSTOM action, will generate deny configs for the specified rules:"))
327
rbac := &rbactcp.RBAC{
328
Rules: getBadCustomDenyRules(rules),
329
StatPrefix: authzmodel.RBACTCPFilterStatPrefix,
331
return []*listener.Filter{
333
Name: wellknown.RoleBasedAccessControl,
334
ConfigType: &listener.Filter_TypedConfig{TypedConfig: protoconv.MessageToAny(rbac)},
337
} else if extauthz.tcp == nil {
338
b.logger.AppendDebugf("ignored CUSTOM action with HTTP provider on TCP filter chain")
342
rbac := &rbactcp.RBAC{
344
StatPrefix: authzmodel.RBACTCPFilterStatPrefix,
345
ShadowRulesStatPrefix: authzmodel.RBACExtAuthzShadowRulesStatPrefix,
347
return []*listener.Filter{
349
Name: wellknown.RoleBasedAccessControl,
350
ConfigType: &listener.Filter_TypedConfig{TypedConfig: protoconv.MessageToAny(rbac)},
353
Name: wellknown.ExternalAuthorization,
354
ConfigType: &listener.Filter_TypedConfig{TypedConfig: protoconv.MessageToAny(extauthz.tcp)},
359
func getBadCustomDenyRules(rules *rbacpb.RBAC) *rbacpb.RBAC {
360
rbac := &rbacpb.RBAC{
361
Action: rbacpb.RBAC_DENY,
362
Policies: map[string]*rbacpb.Policy{},
364
for _, key := range maps.Keys(rules.Policies) {
365
rbac.Policies[key+badCustomActionSuffix] = rules.Policies[key]
370
func policyName(namespace, name string, rule int, option Option) string {
372
if option.IsCustomBuilder {
373
prefix = extAuthzMatchPrefix + "-"
375
return fmt.Sprintf("%sns[%s]-policy[%s]-rule[%d]", prefix, namespace, name, rule)