Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 200 additions & 26 deletions docs/proposals/0059-AccessPolicyTargetRefs.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Date: 3rd February 2026<br/>
Date: 3rd April 2026<br/>
Authors: haiyanmeng<br/>
Status: Provisional<br/>

Expand All @@ -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

Expand All @@ -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
Expand All @@ -61,39 +101,72 @@ 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;
classDef policy fill:#fff3e0,stroke:#ef6c00,stroke-width:2px,stroke-dasharray: 5 5;

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;
```


### Evaluation Flow

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.*
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this "does not matter" is meant to mean "in the way its specified" or "to users"?

I think its probably important to users the order; usually if they are chaining, they are using external auth not just to give a allow/deny but to augment the request with additional information.

However I agree its a very challenging problem to solve if we allow multiple across resources 🙂 just that I would expect that eventually we see demand for ordering

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think its probably important to users the order; usually if they are chaining, they are using external auth not just to give a allow/deny but to augment the request with additional information.

Good point.

Before this PR, the ordering is: the policies 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.

Maybe we can keep this ordering for both ExternalAuth and ALLOW/InlineTools policies (strictly speaking, the evaluation order of ALLOW policies really does not matter). WDYT?

Copy link
Copy Markdown

@mikemorris mikemorris Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually noticed this reading through https://github.com/kubernetes-sigs/kube-agentic-networking/blob/main/docs/proposals/0059-AccessPolicyTargetRefs.md and felt the timestamp behavior was a bit unexpected - in Gateway API timestamps are mostly used to resolve conflicts during the evaluation of a resource by a controller (deciding whether to accept or reject it, such as if only a certain number of Gateway-targeted policies were allowed), and aren't typically relevant during evaluation/enforcement of a resource or policy by the dataplane at runtime.

First, gateway-policy-audit is evaluated (earlier creation timestamp).
Next, gateway-policy-region is evaluated.

I can see how, as @howardjohn mentions, mutations which augment a request could be relevant for subsequent policy decisions, but I think a more explicit ordered configuration would be preferable there, and I'm not sure if the mutating scope extends beyond current expectations for AccessPolicy and starts looking more like https://github.com/kubernetes-sigs/wg-ai-gateway/blob/main/proposals/7-payload-processing.md, which does have specific expectations around ordering and request mutation?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mikemorris , thanks for the feedback. Folk seems in favor of limiting the number of ExternalAuth policies/rules per target to 1 (see the GH discussion).

I will update this PR after we finalize the GH dicussion regarding this.

- **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

Expand All @@ -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`.
Copy link
Copy Markdown
Contributor Author

@haiyanmeng haiyanmeng Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another reason for this is to keep the complexity of the API under control and make it easier for vendors to implement this.

If folk are okay with this restriction, I will add the proposed API change.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the difference between combining InlineTools and ExternalAuth in the same AccessPolicy versus in different ones? I could argue that, if you have both options, it's easier to manage when they are declared in the same object.

Additionally, should we accept that ExternalAuth is only for when targeting a Gateway now? If we want Backend-level ExternalAuth to trigger first and not to mix with Backend-level InlineTools, then why not setting ExternalAuth up in the HTTPRoute instead? (I can only think of one reason that is probably not a very common use case: triggering multiple callouts.)


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
Expand All @@ -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
Comment thread
guicassolato marked this conversation as resolved.

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 |
Comment on lines +253 to +258
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly the same as Kuadrant AuthPolicy when the default merge strategy1 applies.

Footnotes

  1. With the overrides merge strategy, it's the exact reverse.


#### 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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another example of permit-overrides behaviour, although it doesn't seem to support explicit deny – just like the other two examples we have (Kubernetes RBAC and NetworkPolicy).

cc @howardjohn


* 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).
Loading