Triage a burst of denied calls

MaintenanceIntermediate~10 min

Before you start

Plugin version 1.9.0 or newer, to see refusal reasons on the Activity Log tab. The Activity Log read requires the manage-options capability, so run this from an administrator connection, not from a least-privilege agent user.

Something has been refused a lot, and you want to know what. Maybe you saw a wall of red in the Activity Log, or you turned on the alert for bursts of denied calls and it just fired. Either way, “denied” covers several different things, and they point at different fixes.

This prompt does the sorting an agent is good at: it reads every denied row in your window, separates the kinds, and hands back the tool, user and time range worth a closer look. The why is deliberately left to you, on the Activity Log tab. It reads and reports, and changes nothing.

The prompt
I want to understand a burst of denied calls on this site. Read only, change
nothing.
Read the Activity Log with the status set to denied, newest first, 200 rows a
page, and keep paging until created_at (it is UTC) is older than [48 hours
ago]. There is no date filter on this read, so that is the only way to bound
it. If it takes more than five pages, stop and tell me how far back you got.
The total the read reports covers the whole retention window, so do not quote
it as the count for my window.
Split the rows into three groups. Front-door refusals are rows whose ability is
"(transport)": the request reached WordPress but was refused before any tool
ran. Sign-in refusals are rows whose ability starts with "oauth:", and I want
those listed on their own. Tool denials are the rest. A few of those may be
permission checks that crashed, and their detail then names an exception class,
so list any of those separately. Also leave out review actions
("aafm/review-publish" and "aafm/review-discard"), rows whose detail starts
with "Blocked " (an attempt to switch an ability on while a lock was on), and
denied rows with no acting user (an ability left out of the catalog). They are
not agent calls.
For the front-door group, show me each row's timestamp, acting user if there is
one, and detail.
For the tool denials, count them by ability, by acting user, and by hour in
UTC, busiest first, so I can see where the burst is. Show the detail on a
sample of rows from the busiest cluster. It describes what the call tried to
do, not why it was refused.
Finish with a short list I can take to wp-admin: for each cluster, the tool,
the user and the time range. Do not guess at the reason a call was refused.
Say plainly if you cannot tell.

Why this is safe to run

One ability, and it is a read. It reports denials that already happened and cannot rewrite or clear a row.

The agent’s read leaves out the one thing you most want to know. Since 1.9.0 a denied call records why it was refused, and the ability does not return that reason, because the same read is available to any administrator credential, an agent’s included. So the prompt asks for clusters and leaves the reason to you, rather than letting the agent guess at one.

Then open the Activity Log tab

Take the agent’s list to the Activity Log tab in wp-admin (the walkthrough of the filters, denied rows and CSV covers each control). Click Denied, then put the tool in the Tool box and the login in User, and set From and To in UTC around the burst.

Three columns now answer what the agent could not:

  • Detail carries the reason on a denied row, after what the call attempted.
  • Connection names the app that made the call.
  • IP is the address the request came from. Behind a proxy or CDN that is the proxy’s address.

The reasons come from a short fixed list, so they read the same on every site. In the plugin’s own wording:

  • “rate limit reached”: the connection went over the per-minute cap on the Settings tab.
  • “WordPress capability check failed”: the user the agent is bound to lacks the capability the tool needs.
  • “ability is not in this role or connection list”: that tool is not allowed for the role or connection.
  • “item missing or not editable by this user”: the post or term the call named does not exist, or the user cannot edit it.
  • “a required argument was missing”: the agent called the tool without an input it needs.
  • “the source plugin check refused”: for a tool bridged from another plugin, that plugin’s own permission check said no.
  • “key is not on the allowlist”, “key is on the deny list”, “key is protected or reserved” or “key is reserved by WordPress”: a custom field the call named is off limits to agents.

Denials on one tool from one connection point at a single agent reaching for something it was not given. Denials spread across users, or front-door rows, point you at credentials and your IP allowlist first. The new connection check shows what a connection can reach before it starts trying things, and if the agent cannot connect at all, tell a blocked connection apart from a plugin problem.

Already connected? Skip the paste. From an admin connection, ask your agent for the “triage a burst of denied calls” recipe, or just ask it to sort out why so many calls were refused today, and it runs the same read on your own log.

How it works

  1. Read only the denied rows, newest first, paging until they pass the start of your window.
  2. Separate front-door refusals (rows named (transport)) and sign-in refusals (rows starting oauth:) from denials of a specific tool.
  3. Group the tool denials by ability, acting user and hour so the burst shows itself.
  4. Report which tool, user and time range to look at, without guessing at the reason.
  5. Open the Activity Log tab, filter to Denied and those values, and read the reason and IP columns yourself.

Questions

Why can't the agent just tell me why the calls were denied?
Because from 1.9.0 the ability it reads leaves the refusal reason out. Any administrator credential, an agent's included, can call that ability, and a reason such as "key is not on the allowlist" would let an agent map the rules that every other path keeps opaque. The reason is recorded and shown on the Activity Log tab, in the Detail column of the denied row. The message the agent itself sees is unchanged.
Is a pile of denied calls a sign something is wrong?
Not by itself. A denied call is the plugin doing its job: an agent asked for something its user, its connection or your switches do not allow. A handful on a new connection is normal. What deserves a look is a sudden cluster on one tool, a tool nobody expected an agent to try, or denials from an account you do not recognise.
How is this different from the burst alert in Settings?
The alert is optional and off by default. When you turn it on, it tells you that a burst or a rate-limit hit happened, and its email links to the Activity Log without naming a user or an IP. This recipe is what you run after the alert, or without it, to find out who and what.
Do the front-door rows count every failed attempt?
No. Rows named (transport) are capped at five per source address every ten minutes, so a client that keeps retrying leaves a bounded number of rows rather than one for each try. Treat them as evidence that refusals happened, not as a tally.