diff --git a/docs/proposals/0059-AccessPolicyTargetRefs.md b/docs/proposals/0059-AccessPolicyTargetRefs.md index e8090bab..66f60050 100644 --- a/docs/proposals/0059-AccessPolicyTargetRefs.md +++ b/docs/proposals/0059-AccessPolicyTargetRefs.md @@ -1,4 +1,4 @@ -Date: 3rd February 2026
+Date: 3rd April 2026
Authors: haiyanmeng
Status: Provisional
@@ -8,19 +8,53 @@ Currently, the [AccessPolicy](https://github.com/kubernetes-sigs/kube-agentic-ne This has scalability issues when a given Tool Authorization policy needs to be enforced for all the traffic managed by a [Gateway](https://gateway-api.sigs.k8s.io/api-types/gateway/) object. -This proposal allows `AccessPolicy` to target `Gateway` objects, in addition to `Backend` objects with following restrictions: +This proposal allows `AccessPolicy` to target `Gateway` objects, in addition to `Backend` objects with the following rules and restrictions: -1. A single `AccessPolicy` object targeting a Gateway and a Backend at the same time is NOT allowed. +## Restrictions and Evaluation Order -1. It is allowed to have `AccessPolicy` objects targeting a `Gateway` object and `AccessPolicy` objects targeting a `Backend` object behind the `Gateway` object. In this case, the `AccessPolicy` objects targeting the `Gateway` object will be evaluated first. Among the `AccessPolicy` objects targeting the `Gateway` object, the ones with earlier creationTimestamp will be evaluated first. For the policies with the same creationTimestamp, the ones appearing first in alphabetical order by `{namespace}/{name}` will be evaluated first. +### Restrictions +- **Mutually Exclusive Targets**: A single `AccessPolicy` object targeting a Gateway and a Backend at the same time is **NOT** allowed. - * If any of the `AccessPolicy` objects targeting the `Gateway` object denies the access, the HTTP request will be denied. The `AccessPolicy` objects targeting the `Backend` object will NOT be evaluated in this case. +### Coexistence and Evaluation Flow +It is allowed to have `AccessPolicy` objects targeting a `Gateway` object and `AccessPolicy` objects targeting a `Backend` object behind the `Gateway` object. The evaluation follows a strict hierarchy: - * If all the `AccessPolicy` objects targeting the `Gateway` object allow the access, the `AccessPolicy` objects targeting the `Backend` object will be evaluated. Among the `AccessPolicy` objects targeting the `Backend` object, the ones with earlier creationTimestamp will be evaluated first. For the policies with the same creationTimestamp, the ones appearing first in alphabetical order by `{namespace}/{name}` will be evaluated first. +#### 1. Gateway-Level Evaluation (First) +The `AccessPolicy` objects targeting the `Gateway` object are evaluated first. - * if any of the `AccessPolicy` objects targeting the `Backend` object denies the access, the HTTP request will be denied. +- **[ExternalAuth-type](https://github.com/kubernetes-sigs/kube-agentic-networking/blob/cf8c85b85d067657a5dce7b87270f8099f1e302c/api/v0alpha0/accesspolicy_types.go#L169) Rules**: + - Evaluated first. + - If **any** ExternalAuth rule matching the request returns `deny`, the request is denied immediately. +- **[InlineTools-type](https://github.com/kubernetes-sigs/kube-agentic-networking/blob/cf8c85b85d067657a5dce7b87270f8099f1e302c/api/v0alpha0/accesspolicy_types.go#L165) Rules**: + - Evaluated only if all ExternalAuth rules (if any) allowed the request. + - If there are **no** InlineTools rules matching the request, proceed to Backend-level evaluation. + - If **any** InlineTools rule matching the request exists and allows it, proceed to Backend-level evaluation. + - Otherwise, the request is denied immediately. - * if all the `AccessPolicy` objects targeting the `Backend` object allow the access, the HTTP request will be allowed. +If the request is denied at the gateway level, evaluation stops, and Backend-level policies are **NOT** evaluated. + +#### 2. Backend-Level Evaluation (Second) +If the request is allowed at the gateway level, the `AccessPolicy` objects targeting the `Backend` object are evaluated using the same logic: + +- **[ExternalAuth-type](https://github.com/kubernetes-sigs/kube-agentic-networking/blob/cf8c85b85d067657a5dce7b87270f8099f1e302c/api/v0alpha0/accesspolicy_types.go#L169) Rules**: + - Evaluated first. + - If **any** ExternalAuth rule matching the request returns `deny`, the request is denied immediately. +- **[InlineTools-type](https://github.com/kubernetes-sigs/kube-agentic-networking/blob/cf8c85b85d067657a5dce7b87270f8099f1e302c/api/v0alpha0/accesspolicy_types.go#L165) Rules**: + - Evaluated only if all ExternalAuth rules (if any) allowed the request. + - If there are **no** InlineTools rules matching the request, the request is allowed. + - If **any** InlineTools rule matching the request exists and allows it, the request is allowed. + - Otherwise, the request is denied. + +## Summary Table + +The following table summarizes the evaluation results for different combinations of policy checks. Note that evaluation short-circuits as soon as a definitive Deny or Allow is reached. + +| Gateway ExternalAuth | Gateway InlineTools | Backend ExternalAuth | Backend InlineTools | Final Result | +| :--- | :--- | :--- | :--- | :--- | +| **Deny** (Any denies) | - | - | - | **Deny** | +| **Allow** (All allow) | **Deny** (none allows) | - | - | **Deny** | +| **Allow** (All allow) | **Allow** (At least one allows) | **Deny** (Any denies) | - | **Deny** | +| **Allow** (All allow) | **Allow** (At least one allows) | **Allow** (All allow) | **Deny** (none allows) | **Deny** | +| **Allow** (All allow) | **Allow** (At least one allows) | **Allow** (All allow) | **Allow** (At least one allows) | **Allow** | ## Example @@ -34,20 +68,26 @@ We have a Gateway, an HTTPRoute and a Backend: We also have the following AccessPolicies applied: -1. `gateway-policy-audit` (Targets `prod-gateway`). Created at T1. -2. `gateway-policy-region` (Targets `prod-gateway`). Created at T2 (T1 < T2). -3. `backend-policy-admin` (Targets `payment-service`). +1. `gateway-policy-external-auth-1` (Targets `prod-gateway`). +2. `gateway-policy-external-auth-2` (Targets `prod-gateway`). +3. `gateway-policy-inline-tools-1` (Targets `prod-gateway`). +4. `gateway-policy-inline-tools-2` (Targets `prod-gateway`). +5. `backend-policy-external-auth-1` (Targets `payment-service`). +6. `backend-policy-external-auth-2` (Targets `payment-service`). +7. `backend-policy-inline-tools-1` (Targets `payment-service`). +8. `backend-policy-inline-tools-2` (Targets `payment-service`). The graph shows the relationships between these resources: ```mermaid graph TD %% 1. Policies at the Top - subgraph PolicyLayer [Policy Attachments] + subgraph PolicyLayer [Gateway-Level Policy Attachments] direction LR - GPA1(AccessPolicy: gateway-policy-audit) - GPR1(AccessPolicy: gateway-policy-region) - BPA1(AccessPolicy: backend-policy-admin) + GPA1(AccessPolicy: gateway-policy-external-auth-1) + GPA2(AccessPolicy: gateway-policy-external-auth-2) + GPR1(AccessPolicy: gateway-policy-inline-tools-1) + GPR2(AccessPolicy: gateway-policy-inline-tools-2) end %% 2. Infrastructure & Routing on the same line @@ -61,13 +101,35 @@ graph TD %% 3. Vertical Arrows (Policies pointing DOWN) GPA1 -. "TargetRefs" .-> GW + GPA2 -. "TargetRefs" .-> GW GPR1 -. "TargetRefs" .-> GW - BPA1 -. "TargetRefs" .-> BE + GPR2 -. "TargetRefs" .-> GW %% 4. Horizontal Arrows (Routing logic) Route -- "ParentRefs" --> GW Route -- "BackendRefs" --> BE + %% 5. Backend Policy at the Bottom + subgraph BackendPolicyLayer [Backend-Level Policy Attachments] + direction LR + BPA1(AccessPolicy: backend-policy-external-auth-1) + BPA2(AccessPolicy: backend-policy-external-auth-2) + BPI1(AccessPolicy: backend-policy-inline-tools-1) + BPI2(AccessPolicy: backend-policy-inline-tools-2) + end + + %% Target arrows + BPA1 -. "TargetRefs" .-> BE + BPA2 -. "TargetRefs" .-> BE + BPI1 -. "TargetRefs" .-> BE + BPI2 -. "TargetRefs" .-> BE + + %% Force BackendPolicyLayer below BE + BE ~~~ BPA1 + BE ~~~ BPA2 + BE ~~~ BPI1 + BE ~~~ BPI2 + %% --- STYLING --- classDef infra fill:#e3f2fd,stroke:#1565c0,stroke-width:2px; classDef routing fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px; @@ -75,8 +137,9 @@ graph TD class GW,BE infra; class Route routing; - class GPA1,GPR1,BPA1 policy; + class GPA1,GPR1,GPA2,GPR2,BPA1,BPA2,BPI1,BPI2 policy; style PolicyLayer fill:none,stroke:#ccc,stroke-dasharray: 5 5; + style BackendPolicyLayer fill:none,stroke:#ccc,stroke-dasharray: 5 5; ``` @@ -84,16 +147,26 @@ graph TD When a request comes to `payment-service` through `prod-gateway`: -1. **Gateway Level Checks:** - * First, `gateway-policy-audit` is evaluated (earlier creation timestamp). - * Next, `gateway-policy-region` is evaluated. - * **Rule:** ALL Gateway policies must allow the request. If `gateway-policy-audit` denies it, the request is rejected immediately, and subsequent policies are skipped. - -2. **Backend Level Checks:** - * If (and only if) all Gateway policies allow the request, `backend-policy-admin` is evaluated. - * **Rule:** Backend policies must also allow the request. +#### 1. Gateway Level Checks +- **ExternalAuth Policies**: `gateway-policy-external-auth-1` and `gateway-policy-external-auth-2` are evaluated. + - If **any** denies, the request is rejected immediately. + - If **all** allow, proceed. + - *Note: Evaluation order among these policies does not matter.* +- **InlineTools Policies**: `gateway-policy-inline-tools-1` and `gateway-policy-inline-tools-2` are evaluated. + - If **any** allows, proceed to Backend level. + - If **none** allows, the request is rejected immediately. + - *Note: Evaluation order among these policies does not matter.* + +#### 2. Backend Level Checks +- **ExternalAuth Policies**: `backend-policy-external-auth-1` and `backend-policy-external-auth-2` are evaluated. + - If **any** denies, the request is rejected immediately. + - If **all** allow, proceed. + - *Note: Evaluation order among these policies does not matter.* +- **InlineTools Policies**: `backend-policy-inline-tools-1` and `backend-policy-inline-tools-2` are evaluated. + - If **any** allows, the request is allowed. + - If **none** allows, the request is denied. + - *Note: Evaluation order among these policies does not matter.* -**Final Result:** The request is allowed ONLY if it is allowed by all three policies. ## API Changes @@ -116,6 +189,8 @@ type AccessPolicySpec struct { } ``` +In addition, to make it easy for users to understand why a tool access is allowed or denied, we will disallow the combination of `InlineTools` and `ExternalAuth` in the same `AccessPolicy`. + Currently, the `InlineTools` type of [AuthorizationRule](https://github.com/kubernetes-sigs/kube-agentic-networking/blob/main/docs/proposals/0017-DynamicAuth.md) supports a list of tool names, which works well for `AccessPolicy` targeting `Backend` objects. However, it does not work well for `AccessPolicy` targeting `Gateway` objects, because there could be tool name conflicts between different backends behind the same `Gateway`. We will address this in a separate proposal. ## Support requirements in implementation @@ -126,3 +201,102 @@ Currently, the `InlineTools` type of [AuthorizationRule](https://github.com/kube * `AccessPolicy` objects targeting `Backend` objects * If an implementation supports allowing `AccessPolicy` to target both `Gateway` and `Backend` objects, it MUST support the evaluation flow described above. + +## Future Considerations: Support for DENY Policies + +In the future, if `AccessPolicy` is extended to support explicit `DENY` policies (in addition to the current `ExternalAuth` and `InlineTools` mechanisms), the evaluation order should follow the following pattern to ensure safe defaults: + +1. **ExternalAuth Policies**: Evaluated first. If any `ExternalAuth` policy matching the request exists and denies it, the request is denied immediately. +2. **DENY Policies**: Evaluated second. If any `DENY` policy matching the request exists, the request is denied immediately. +3. **InlineTools Policies**: Evaluated last. If any `InlineTools` policy matching the request exists and allows it, the request is allowed. Otherwise, the request is denied by default. + +This ensures that explicit denials take precedence over allows, and external authorization has the first say. + +### Summary Table + +The following table summarizes the evaluation results for different combinations of policy checks including future DENY policies. Note that evaluation short-circuits as soon as a definitive Deny or Allow is reached. + +| Gateway ExternalAuth | Gateway DENY | Gateway InlineTools | Backend ExternalAuth | Backend DENY | Backend InlineTools | Final Result | +| :--- | :--- | :--- | :--- | :--- | :--- | :--- | +| **Deny** (Any denies) | - | - | - | - | - | **Deny** | +| **Allow** (All allow) | **Deny** (Any matches) | - | - | - | - | **Deny** | +| **Allow** (All allow) | **Allow** (No matches) | **Deny** (All deny) | - | - | - | **Deny** | +| **Allow** (All allow) | **Allow** (No matches) | **Allow** (At least one allows) | **Deny** (Any denies) | - | - | **Deny** | +| **Allow** (All allow) | **Allow** (No matches) | **Allow** (At least one allows) | **Allow** (All allow) | **Deny** (Any matches) | - | **Deny** | +| **Allow** (All allow) | **Allow** (No matches) | **Allow** (At least one allows) | **Allow** (All allow) | **Allow** (No matches) | **Deny** (All deny) | **Deny** | +| **Allow** (All allow) | **Allow** (No matches) | **Allow** (At least one allows) | **Allow** (All allow) | **Allow** (No matches) | **Allow** (At least one allows) | **Allow** | + +## Prior Art + +### Istio Authorization Policy + +[Istio Authorization Policy](https://istio.io/latest/docs/reference/config/security/authorization-policy/) supports CUSTOM, DENY and ALLOW actions for access control. When CUSTOM, DENY and ALLOW actions are used for a workload at the same time, the CUSTOM action is evaluated first, then the DENY action, and finally the ALLOW action. The evaluation is determined by the following rules: + +* If there are any CUSTOM policies that match the request, evaluate and deny the request if the evaluation result is deny. +* If there are any DENY policies that match the request, deny the request. +* If there are no ALLOW policies for the workload, allow the request. +* If any of the ALLOW policies match the request, allow the request. +* Deny the request. + +### Envoy Gateway Security Policy + +[Envoy Gateway Security Policy](https://gateway.envoyproxy.io/docs/api/extension_types/#securitypolicy) supports external authorization and internal authorization. When both are configured, the evaluation is determined by the following rules: + +* If the external authorization rule denies the request, deny the request. If the external authorization rule allows the request, proceed to the next evaluation phase. +* The internal authorization rules are checked from top to bottom as they appear in the `SecurityPolicy` resource. The first rule that matches the request is applied, and all subsequent rules are ignored. +* If no intenral authorization rules match, the gateway applies the `defaultAction` (which is `Deny` by default). + +#### The Precedence Hierarchy + +When multiple `SecurityPolicy` resources apply to the same request, Envoy Gateway determines which one takes effect based on where they are attached: + +| Level | Target Resource | Precedence | +|-------|-----------------|------------| +| Highest | Route Rule (via `sectionName`) | 1 (Wins all) | +| High | `HTTPRoute` / `GRPCRoute` | 2 | +| Medium | `Gateway` Listener (via `sectionName`) | 3 | +| Lowest | `Gateway` (Entire resource) | 4 | + +#### Handling Conflicts (Same Level) + +If two different `SecurityPolicy` objects target the exact same resource (e.g., two policies both targeting `HTTPRoute: my-service`), Envoy Gateway uses "Tie-breaking" rules: + +* Oldest Wins: The policy with the earliest `creationTimestamp` is applied. + +* Alphabetical Sorting: If the timestamps are identical, the policy whose name comes first alphabetically wins. + +* Status Reporting: The "losing" policies will show a status of Overridden=True or Conflicted=True in their Kubernetes status field. + +### Linkerd Authorization Policy + +[Linkerd Authorization Policy](https://linkerd.io/2-edge/reference/authorization-policy/) evaluation follows a "Specific-to-General" hierarchy that centers around the concept of a `Server` resource. + +Linkerd's proxy makes an authorization decision for every incoming request using these checks in order: + +1. Check for a `Server` resource: + + * NO `Server` exists: The proxy uses the Default Policy (see below). + + * YES `Server` exists: Proceed to step 2. + +2. Check for an `HTTPRoute`: + + * If the request matches a defined `HTTPRoute` (e.g., a specific path like `/admin`), only `AuthorizationPolicy` targeting that specific route are checked. + + * If no `HTTPRoute` matches (or none are defined), Linkerd checks policies targeting the `Server` itself. + +3. Final Verdict: + + * If any matching policy allows the request: ALLOW. + + * If no policies match or the client fails authentication: DENY (HTTP 403 for HTTP traffic, or TCP connection refusal). + +When no `Server` resource is defined for a port, Linkerd looks at the `config.linkerd.io/default-inbound-policy` annotation (checked from Pod → Namespace → Cluster level). + +#### Multi-Policy Interaction (AND vs. OR) + +One of the most important rules is how Linkerd handles multiple authentication requirements: + +* OR (Between Policies): If you have two different AuthorizationPolicy objects targeting the same Server, the request is allowed if either one is satisfied. + +* AND (Within a Policy): If a single AuthorizationPolicy has multiple entries in its requiredAuthenticationRefs list, the client must satisfy all of them (e.g., must have a specific Identity AND come from a specific IP range). \ No newline at end of file