Skip to content

fix(backendsecuritypolicy): enforce ReferenceGrant for static credentials - #2783

Merged
nacx merged 8 commits into
theagentrouter:mainfrom
abdulaziz-dev-lab:fix/bsp-static-secret-referencegrant
Oct 5, 2026
Merged

nacx merged 8 commits into
theagentrouter:mainfrom
abdulaziz-dev-lab:fix/bsp-static-secret-referencegrant

Conversation

@abdulaziz-dev-lab

@abdulaziz-dev-lab abdulaziz-dev-lab commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Description

For the static credential types (APIKey, AzureAPIKey, AnthropicAPIKey, and AWSCredentials with credentialsFile), bspToFilterAPIBackendAuth read the Secret from the BackendSecurityPolicy's own namespace. It ignored secretRef.namespace and never checked a ReferenceGrant. Meanwhile the Secret watch index and ReferenceGrantController resolve the namespace with backendSecurityPolicySecretRef. A cross-namespace reference therefore failed with "not found", or silently used a same-named Secret from the policy's namespace.

The Gateway controller now resolves the Secret with backendSecurityPolicySecretRef too, and calls validateSecretReference before reading it, as the rotation paths have done since #2746. Without a matching ReferenceGrant the read fails and the backend is left out of the filter config. The BackendSecurityPolicy controller makes the same check for these types, so the policy is marked NotAccepted with the "not permitted" error. It still syncs the targeted backends before returning that error, as AIGatewayRoute has done for a rejected backendRef since #2750, so a policy changed to point at a Secret it isn't granted stops publishing the credential it had before. Same-namespace references are unchanged and need no grant.

Standalone aigw now loads ReferenceGrants from its configuration into the fake client the controllers read, and still writes them to the Envoy Gateway resources. A cross-namespace secretRef works there when the configuration includes the grant. Without one, aigw panics at startup with the "not permitted" error, as it does for other reconcile errors, instead of silently leaving the backend out.

Error messages from getSecretData and for an unset secretRef now include the namespace. The examples, the connect-providers docs, and the e2e and aigw test manifests no longer pin secretRef.namespace: default, so they work in whatever namespace they are applied. The OIDC clientSecret.namespace pins stay, because the OIDC token provider requires that field. The connect-providers docs now explain that a Secret in another namespace needs a ReferenceGrant in the Secret's namespace.

Tests:

  • TestGatewayController_bspToFilterAPIBackendAuth_CrossNamespaceSecret runs the four static types with the namespace unset and set to the policy's own. It also covers cross-namespace refs with no grant, a matching grant, and grants with the wrong from-kind or from-namespace. Every case has a same-named Secret with a different value in the policy's namespace, and the denied cases assert that no Secret was read.
  • Two filter-config tests check that a denied backend is left out instead of published without auth, and that a cancelled grant lookup aborts the reconcile.
  • TestBackendSecurityPolicyController_Reconcile_StaticCredentialCrossNamespace checks, for each static type, NotAccepted without a grant and Accepted with one, and that the targeted backend is synced in both cases.
  • TestRunCmdContext_writeEnvoyResourcesAndRunExtProc_crossNamespaceSecret runs an aigw configuration with a cross-namespace API key Secret. With the grant, the filter config carries the key and Envoy Gateway still receives the grant. Without it, aigw fails with the "not permitted" error.

Related Issues/PRs (if applicable)

Fixes #2778

Special notes for reviewers (if applicable)

Like #2746, this changes behavior for some existing configs and may be worth a release note. A static policy whose secretRef.namespace names another namespace, but whose Secret is in the policy's namespace, only worked because the namespace was ignored. It now needs the Secret in the referenced namespace and a ReferenceGrant there, or the namespace field removed.

…ials

bspToFilterAPIBackendAuth read the Secret for APIKey, AzureAPIKey,
AnthropicAPIKey and AWS credentialsFile policies from the policy's own
namespace. It ignored secretRef.namespace and never checked a
ReferenceGrant, while the Secret watch index resolves the referenced
namespace with backendSecurityPolicySecretRef. A cross-namespace
reference failed with "not found", or silently used a same-named Secret
from the policy's namespace.

Resolve the Secret with backendSecurityPolicySecretRef and check
validateSecretReference before reading, as the rotation paths have
done since theagentrouter#2746. Without a grant the read fails and the backend is
skipped. getSecretData errors now include the namespace, and the
connect-providers docs no longer pin secretRef examples to the default
namespace.

Fixes theagentrouter#2778

Signed-off-by: Abdulaziz Alharbi <246710009+abdulaziz-dev-lab@users.noreply.github.com>
@abdulaziz-dev-lab
abdulaziz-dev-lab requested a review from a team as a code owner October 4, 2026 16:40
@netlify

netlify Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for theagentrouter ready!

Name Link
🔨 Latest commit a3a138e
🔍 Latest deploy log https://app.netlify.com/projects/theagentrouter/deploys/6ac41473c857e90008ad8e00
😎 Deploy Preview https://deploy-preview-2783--theagentrouter.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@nacx nacx left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

should static types also set NotAccepted on the policy status, or keep that for a separate PR?

Yes, please. Make the change aprt of this PR as well.

Comment thread internal/controller/gateway.go Outdated
func (c *GatewayController) getBSPSecretRefData(ctx context.Context, bsp *aigv1b1.BackendSecurityPolicy, dataKey string) (string, error) {
name, namespace, ok := backendSecurityPolicySecretRef(bsp)
if !ok {
return "", fmt.Errorf("secretRef is not set for policy %s", bsp.Name)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Worth namespacing this message like the two you just changed above. With policies of the same name in several namespaces, 'secretRef is not set for policy bsp' does not identify the object.

Suggested change
return "", fmt.Errorf("secretRef is not set for policy %s", bsp.Name)
return "", fmt.Errorf("secretRef is not set for policy %s/%s", bsp.Namespace, bsp.Name)

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.

Done in b02a549, along with the test that checks this message.

if !ok {
return "", fmt.Errorf("secretRef is not set for policy %s", bsp.Name)
}
if err := c.referenceGrantValidator.validateSecretReference(ctx, bsp.Namespace, namespace, name); err != nil {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The standalone aigw does not support ReferenceGrants, and this change makes it more evident. In standalone mode, this method will use the fake client, that has no ReferenceGrants populated, and cross-secrets in the aigw config will silently drop.

I know this is a gap that was already there, but it will become more evident now. Worth fixing in this PR?

It should be a small shange. Something like adding somethign like case "ReferenceGrant": mustExtractAndAppend(obj, &referenceGrants) to collectObjects and create the collected grants in the fake client in translateCustomResourceObjects, alongside the user-defined Secrets.

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.

Done in d6cc9fa. collectObjects now returns ReferenceGrants and still writes them to the Envoy Gateway resources, since EG needs them for HTTPRoute→Backend refs. translateCustomResourceObjects creates them in the controller-runtime fake client before anything is reconciled, because that is where the validator lists grants from (the user Secrets are in the client-go fake clientset). TestRunCmdContext_writeEnvoyResourcesAndRunExtProc_crossNamespaceSecret covers it with and without the grant.

With the NotAccepted change (5898814), an ungranted cross-namespace secretRef in an aigw config now makes aigw panic at startup with the "not permitted" error, like any other reconcile error there, instead of silently dropping the backend. Let me know if you'd rather it only log in standalone mode.

Not changed: collectObjects dedups on kind/name, so two objects with the same name in different namespaces (e.g. two grants named allow-bsp) collapse into one. I can fix that here or in a follow-up.

apiKey:
secretRef:
name: openai-secret
namespace: default

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Dropping namespace: default here is right, but the same pattern is still in the repo's own manifests: examples/basic/openai.yaml:52, anthropic.yaml:52, cohere.yaml:53, tars.yaml:54, typesafe.yaml:53 and tests/e2e/testdata/testupstream.yaml:152. They work today only because those policies also live in default; applied into any other namespace they become grant-requiring cross-namespace references under the new rule. Since this PR is behaviour-changing for exactly that shape, it would be worth sweeping them in the same change, and adding a sentence here saying that a secretRef.namespace different from the policy's namespace now requires a ReferenceGrant in the Secret's namespace.

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.

Done in 0c3ef3a. Besides the six files you listed, I removed the same pin from examples/basic/bitdeer.yaml, examples/basic/azure_openai.yaml (clientSecretRef) and the two aigw translate test inputs. I kept the OIDC clientSecret.namespace pins (the GCP Workload Identity Federation example on this page and the crdcel gcp_oidc fixture), because the OIDC token provider errors when that field is unset. versioned_docs are untouched. The new "Secrets in another namespace" section under "Authentication Types" explains the ReferenceGrant, with the same example as the v1.2 upgrade guidance.

@codecov

codecov Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.85714% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/controller/backend_security_policy.go 81.81% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

Policies with the same name can exist in several namespaces, so the name
alone does not identify the object. Also cover the error for a Secret
that lacks the expected key.

Signed-off-by: Abdulaziz Alharbi <246710009+abdulaziz-dev-lab@users.noreply.github.com>
The controllers check cross-namespace references against ReferenceGrants
from the controller-runtime client, but standalone aigw never created
any there. A BackendSecurityPolicy whose Secret is in another namespace
was therefore left out of the filter config even when the config file
contained a matching grant.

collectObjects now collects ReferenceGrants, and still writes them to
the Envoy Gateway resources, which need them for their own
cross-namespace references. translateCustomResourceObjects creates them
in the fake client before reconciling anything.

Signed-off-by: Abdulaziz Alharbi <246710009+abdulaziz-dev-lab@users.noreply.github.com>
…s grant is missing

The Gateway controller already leaves out a backend whose APIKey,
AzureAPIKey, AnthropicAPIKey or AWS credentialsFile Secret is in another
namespace without a ReferenceGrant, but the policy still reported
Accepted. The BackendSecurityPolicy controller now makes the same check,
so the policy is marked NotAccepted with the "not permitted" error, as
the rotation-based types are.

The targeted backends are still synced before the error is returned,
like AIGatewayRoute does for a rejected backendRef. A policy changed to
point at a Secret it isn't granted then stops publishing the credential
it had before.

In standalone aigw the error stops startup, as other reconcile errors
do, instead of the backend being dropped silently.

Signed-off-by: Abdulaziz Alharbi <246710009+abdulaziz-dev-lab@users.noreply.github.com>
The examples and test manifests set namespace: default on secretRef and
clientSecretRef next to policies that are also in default. Applied in
any other namespace, those become cross-namespace references that need
a ReferenceGrant. Leave the namespace out so the Secret is looked up
next to the policy.

The OIDC clientSecret pins stay, because the OIDC token provider
requires that namespace.

Document that a Secret in another namespace needs a ReferenceGrant in
the Secret's namespace, with an example.

Signed-off-by: Abdulaziz Alharbi <246710009+abdulaziz-dev-lab@users.noreply.github.com>
@abdulaziz-dev-lab

Copy link
Copy Markdown
Contributor Author

should static types also set NotAccepted on the policy status, or keep that for a separate PR?

Yes, please. Make the change aprt of this PR as well.

Thanks for the review @nacx. Changes since your review:

  • 5898814: the BackendSecurityPolicy controller now runs the ReferenceGrant check for static types too, so a missing grant marks the policy NotAccepted with the same "not permitted" message as the rotation types. It still syncs the targeted backends before returning the error, as AIGatewayRoute does since fix(gateway): require ReferenceGrant for cross-namespace backends in filterconfig #2750, so a policy changed to point at a Secret it isn't granted stops publishing its old credential.
  • b02a549, d6cc9fa, 0c3ef3a: the error message, aigw and examples/docs changes, answered in the threads above.

I updated the PR description to match.

@abdulaziz-dev-lab
abdulaziz-dev-lab requested a review from nacx October 5, 2026 17:11

@nacx nacx left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thank you!

@nacx
nacx enabled auto-merge (squash) October 5, 2026 17:26
@nacx

nacx commented Oct 5, 2026

Copy link
Copy Markdown
Member

/retest

1 similar comment
@nacx

nacx commented Oct 5, 2026

Copy link
Copy Markdown
Member

/retest

@missBerg

missBerg commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

/retest

kind: BackendSecurityPolicy
namespace: default
to:
- group: ""

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.

nit: add the specific secret name (realistically we don't want all secrets from being accessible)

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.

Good point. Since this PR is already merged, I'll scope it to shared-openai-apikey in a follow-up PR. The example in the new "Secrets in another namespace" section of connect-providers.md has the same grant, so I'll name the Secret there too.

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.

Done in #2807.

@nacx

nacx commented Oct 5, 2026

Copy link
Copy Markdown
Member

/retest

@nacx
nacx merged commit 01142d5 into theagentrouter:main Oct 5, 2026
62 of 65 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fully support referenceGrant for BackendSecurityPolicy

4 participants