Skip to content

controlplane: log the served_by and audience refusals at /sts/token - #795

Open
asivanadi0 wants to merge 1 commit into
google:mainfrom
asivanadi0:fix/sts-deny-audit-served-by-audience
Open

asivanadi0 wants to merge 1 commit into
google:mainfrom
asivanadi0:fix/sts-deny-audit-served-by-audience

Conversation

@asivanadi0

Copy link
Copy Markdown
Contributor

Refs #742

Follow-up to the audit gap raised in the review of #750. Two refusals at POST /sts/token return 403 before the caller's Biscuit is verified and wrote no Border Crossing line: the served_by check and the resolveEgressAudience check in HandleSTSToken. A node probing destinations it doesn't serve left no trace in an audit join on the deny lines.

Both now write a deny line in the same shape as the existing /sts/token one: direction=outbound, endpoint=/sts/token, decision=deny, actor_node, destination, reason, and the requested audience (the resolved one doesn't exist yet when the audience check is what failed). Both checks run after the PoP challenge has verified, so actor_node is proven. client_peer_id is empty, since nothing in the Biscuit has been verified at that point. Refusals before the challenge (no node credential, router role, rate limit, bad body, failed challenge) are unchanged. The Mint endpoint row in security-architecture.md now says so.

Test: TestSTSTokenBorderCrossingDeniesBeforeBiscuit, next to TestSTSTokenBorderCrossingNamesTheOriginPeer and using the same go-log pipe. One case calls an EgressDestination whose served_by the egress node doesn't match, the other sends an audience that doesn't match the destination's oidc_federation audience. Each checks for a 403 and exactly one deny line with the calling node, the destination, the requested audience, the refusal reason and an empty client_peer_id. The caller's Biscuit is a Task Biscuit bound to another peer, so a line logged after Biscuit verification would show up as a non-empty client_peer_id. The test fails on main with no line for the served_by case.

go test -race ./internal/controlplane/ and golangci-lint run ./internal/controlplane/... pass.

The served_by check and the resolveEgressAudience check in
HandleSTSToken return 403 before the caller's Biscuit is verified, and
neither wrote a Border Crossing line, so an audit join on the deny lines
could not see a node probing destinations it does not serve.

Both refusals now write a deny line in the same shape as the existing
/sts/token one: direction, endpoint, decision, actor_node (proven by
the PoP challenge), destination, the requested audience and the reason.
client_peer_id is empty because nothing in the Biscuit has been
verified at that point. Refusals before the challenge verifies stay as
they are.

TestSTSTokenBorderCrossingDeniesBeforeBiscuit covers both refusals and
checks for exactly one deny line each.

Refs google#742

Signed-off-by: Anpoo Sivanadi <sivanadi08@gmail.com>
@asivanadi0
asivanadi0 requested a review from aojea as a code owner October 20, 2026 17:46

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request introduces auditing for /sts/token denials that occur before the caller's Biscuit is verified, specifically for served_by and audience refusals. It adds the logSTSDenyBeforeBiscuit helper to log these 'Border Crossing' deny events with an empty client_peer_id, implements a comprehensive integration test (TestSTSTokenBorderCrossingDeniesBeforeBiscuit) to verify this behavior, and updates the security architecture documentation. No review comments were provided, and the changes comply with the repository's style guide, so I have no feedback to provide.

This branch has not been deployed

No deployments
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.

1 participant