iam: evaluate trust policies deny-by-default (#11513)

* iam: evaluate trust policies deny-by-default

EvaluateTrustPolicy seeded its result with the engine's DefaultEffect,
so a non-matching trust-policy statement set still resolved to Allow
when the IAM config sets policy.defaultEffect=Allow. A caller holding
a validly signed token from a registered provider could then assume a
role its trust policy does not admit.

Trust policies now start from implicit deny, matching AWS semantics and
the pre-d751623 behavior of evaluateTrustPolicy; DefaultEffect still
governs identity-policy evaluation.

Upgrade note: deployments on defaultEffect=Allow whose trust policies do
not match their callers will see those assumptions refused.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* iam: cover trust policy implicit deny under DefaultEffect=Allow

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Chris Lu
2026-09-29 11:36:01 +08:00
committed by GitHub
co-authored by Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent a901c1a5e2
commit 62d4f9152a
2 changed files with 85 additions and 2 deletions
+5 -2
View File
@@ -473,8 +473,11 @@ func (e *PolicyEngine) EvaluateTrustPolicy(ctx context.Context, trustPolicy *Pol
return nil, fmt.Errorf("trust policy cannot be nil")
}
// A trust policy is deny-by-default, whatever the engine's DefaultEffect:
// a role may be assumed only by a principal a trust statement explicitly
// allows.
result := &EvaluationResult{
Effect: Effect(e.config.DefaultEffect),
Effect: EffectDeny,
EvaluationDetails: &EvaluationDetails{
Principal: evalCtx.Principal,
Action: evalCtx.Action,
@@ -511,7 +514,7 @@ func (e *PolicyEngine) EvaluateTrustPolicy(ctx context.Context, trustPolicy *Pol
// AWS IAM evaluation logic:
// 1. If there's an explicit Deny, the result is Deny
// 2. If there's an Allow and no Deny, the result is Allow
// 3. Otherwise, use the default effect
// 3. Otherwise, the implicit deny stands
if explicitDeny {
result.Effect = EffectDeny
} else if hasAllow {
@@ -0,0 +1,80 @@
package policy
import (
"context"
"encoding/json"
"testing"
)
const subjectBoundTrustPolicy = `{"Version":"2012-10-17","Statement":[{"Effect":"Allow",
"Principal":{"Federated":"https://oidc.example"},"Action":["sts:AssumeRoleWithWebIdentity"],
"Condition":{"StringEquals":{"oidc:sub":"spiffe://example.org/ns/app/sa/app"}}}]}`
func webIdentityContext(sub string) *EvaluationContext {
return &EvaluationContext{
Principal: "web-identity-user",
Action: "sts:AssumeRoleWithWebIdentity",
Resource: "arn:aws:iam::role/app",
RequestContext: map[string]interface{}{
"aws:FederatedProvider": "https://oidc.example",
"oidc:iss": "https://oidc.example",
"oidc:sub": sub,
},
}
}
// A trust policy is deny-by-default. The engine's DefaultEffect decides
// requests no identity policy speaks to; it must not let a principal the trust
// policy does not allow assume the role.
func TestEvaluateTrustPolicyIsImplicitDenyWhateverTheDefaultEffect(t *testing.T) {
var trust PolicyDocument
if err := json.Unmarshal([]byte(subjectBoundTrustPolicy), &trust); err != nil {
t.Fatalf("parse test trust policy: %v", err)
}
for _, defaultEffect := range []string{"Allow", "Deny"} {
t.Run("defaultEffect="+defaultEffect, func(t *testing.T) {
engine := NewPolicyEngine()
if err := engine.Initialize(&PolicyEngineConfig{DefaultEffect: defaultEffect, StoreType: "memory"}); err != nil {
t.Fatalf("initialize policy engine: %v", err)
}
res, err := engine.EvaluateTrustPolicy(context.Background(), &trust, webIdentityContext("spiffe://example.org/ns/other/sa/x"))
if err != nil {
t.Fatalf("evaluate trust policy: %v", err)
}
if res.Effect != EffectDeny {
t.Errorf("a subject the trust policy does not allow got %s", res.Effect)
}
res, err = engine.EvaluateTrustPolicy(context.Background(), &trust, webIdentityContext("spiffe://example.org/ns/app/sa/app"))
if err != nil {
t.Fatalf("evaluate trust policy: %v", err)
}
if res.Effect != EffectAllow {
t.Errorf("the subject the trust policy allows got %s", res.Effect)
}
})
}
}
func TestEvaluateTrustPolicyExplicitDenyWins(t *testing.T) {
var trust PolicyDocument
doc := `{"Version":"2012-10-17","Statement":[
{"Effect":"Allow","Principal":{"Federated":"https://oidc.example"},"Action":["sts:AssumeRoleWithWebIdentity"]},
{"Effect":"Deny","Principal":{"Federated":"https://oidc.example"},"Action":["sts:AssumeRoleWithWebIdentity"],
"Condition":{"StringEquals":{"oidc:sub":"spiffe://example.org/ns/app/sa/app"}}}]}`
if err := json.Unmarshal([]byte(doc), &trust); err != nil {
t.Fatalf("parse test trust policy: %v", err)
}
engine := NewPolicyEngine()
if err := engine.Initialize(&PolicyEngineConfig{DefaultEffect: "Allow", StoreType: "memory"}); err != nil {
t.Fatalf("initialize policy engine: %v", err)
}
res, err := engine.EvaluateTrustPolicy(context.Background(), &trust, webIdentityContext("spiffe://example.org/ns/app/sa/app"))
if err != nil {
t.Fatalf("evaluate trust policy: %v", err)
}
if res.Effect != EffectDeny {
t.Errorf("explicit Deny did not win: %s", res.Effect)
}
}