Skip to content

fix: guard ref names in billingaccountbinding create policy#69

Open
ecv wants to merge 2 commits into
mainfrom
fix/billingaccountbinding-ref-name-dlq-guard
Open

fix: guard ref names in billingaccountbinding create policy#69
ecv wants to merge 2 commits into
mainfrom
fix/billingaccountbinding-ref-name-dlq-guard

Conversation

@ecv

@ecv ecv commented Jun 27, 2026

Copy link
Copy Markdown

What

Tighten the create-with-refs match in billingaccountbinding-policy.yaml to
require projectRef.name and billingAccountRef.name, the fields its summary
dereferences.

Why

Same DLQ-leak class as milo-os/activity#212. The match guarded only
has(projectRef) / has(billingAccountRef), but the summary derefs their
.name. A create whose refs omit name (rejected by admission, but still
recorded in the audit requestObject) raises CEL no such key: name → event
to DLQ → retries fail.

Fix

Adding the .name leaves to the match routes malformed bindings to the
existing create-fallback rule (created billing account binding <name>)
instead of leaking. Match-based guard, consistent with this file's style.

Remediation

Merge + billing release → the billing-milo-activity-policies Flux
Kustomization re-applies the corrected CR. No kubectl.

Related

The create-with-refs rule matched on has(projectRef) and has(billingAccountRef)
but its summary dereferenced projectRef.name and billingAccountRef.name. A
create whose refs omit name (rejected by admission, but still recorded in the
audit requestObject) raised CEL "no such key: name", sending the event to the
DLQ -- the same leak class as milo-os/activity#212.

Require the .name leaves in the match so malformed bindings fall through to the
create-fallback rule instead of leaking.
create-with-refs reads audit.requestObject.spec refs (always present, safe), but
create-fallback derefs audit.responseObject.metadata.name unconditionally. A
create that misses the refs guard and is rejected returns a Status as
responseObject with no metadata.name, raising "no such key: name" -> DLQ. A
generateName create rejected before naming also has an empty objectRef.name.

Replace with a per-level-guarded fallback chain:
  responseObject.metadata.name -> objectRef.name -> responseObject.details.name -> literal
The details branch is guarded with has(responseObject.details) because some
Status responses carry no details.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ecv ecv marked this pull request as ready for review June 28, 2026 04:57
@ecv ecv enabled auto-merge June 28, 2026 05:00
@ecv ecv requested review from mattdjenkinson and scotwells and removed request for mattdjenkinson June 29, 2026 22:34
@scotwells

Copy link
Copy Markdown
Contributor

@ecv do you have the full audit log that was processed? I would expect it to always have a response object unless it's a non-successful request. We should probably add a check for failed requests and handle those appropriately.

@ecv

ecv commented Jun 30, 2026

Copy link
Copy Markdown
Author

@scotwells the full audit log confirms your intuition: the response object is present, but on a non-successful request it's a Status, not the created object — so the fields the summary derefs aren't there.

The clearest live evidence I have is the same DLQ-leak class on the resourcemanager.miloapis.com-project policy in the prod activity processor (activity-system/activity-processor, core control plane). A create that's rejected (here, a 403 quota denial) still matches the create rule, but responseObject is a Status with empty metadata:

{
  "auditID": "a6d769ee-6417-4620-893a-9364d8b809f9",
  "verb": "create",
  "level": "RequestResponse",
  "requestURI": "/apis/resourcemanager.miloapis.com/v1alpha1/projects",
  "objectRef": { "name": "personal-project-30ea0ba6", "resource": "projects" },
  "responseObject": {
    "apiVersion": "v1",
    "kind": "Status",
    "status": "Failure",
    "reason": "Forbidden",
    "code": 403,
    "message": "... is forbidden: You've reached your quota for this resource type ...",
    "metadata": {},
    "details": { "group": "resourcemanager.miloapis.com", "kind": "projects", "name": "personal-project-30ea0ba6" }
  },
  "responseStatus": { "code": 403, "reason": "Forbidden", "status": "Failure", "metadata": {} }
}

Processor error:

rule 0 summary: summary template evaluation failed:
eval "link(audit.responseObject.metadata.name, audit.objectRef)": no such key: name

For billingaccountbinding the trigger is a rejected create (admission-deny / malformed refs) rather than quota, but the audit shape is identical: matched create rule + Status response with no usable name leaf. The match-based guard in this PR routes those to create-fallback instead of leaking. I agree the durable fix is the one you describe — explicitly handle failed requests (responseStatus.code >= 300 / responseObject.kind == "Status") across the policies rather than guarding ref-by-ref; tracked alongside milo-os/activity#213.

@ecv

ecv commented Jun 30, 2026

Copy link
Copy Markdown
Author

taking your steer on how to address this. set to draft, close, remain?

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.

2 participants