Problem/Motivation
The interaction bearer doorway is the one route Orchestra answers without asking who is calling. A capability link carries an HMAC over the instance, optionally the branch, and an expiry, signed with the site's private key and hash salt, and holding the link is the authorization: that is the point, because the party it is for is a payer at a provider, someone filling a webform, a reviewer opening a step from an email, none of whom has an account here.
Nothing rate limits it. A request with a wrong signature costs the same work as one with a right signature, up to and including the HMAC, and there is no counter anywhere that notices a caller making them by the thousand. The project uses core's flood service nowhere at all, so this is not an oversight in one route so much as a question never asked.
Guessing a handle is not the worry: it is an HMAC, and an attacker who could guess one could forge anything. The worry is cost. An open route that does per-request cryptographic work is a way to spend a site's CPU from outside, and the doorway's own resolution (loading the instance, the token, the node, the interaction plugin) costs more than the HMAC does.
Proposed resolution
Count requests to the doorway and refuse past a limit, with the count taken before the expensive half of the request rather than after. kessai already solves this shape and Orchestra follows it rather than inventing a second one: a small guard service over core's flood service, holding no limits of its own, taking the event name, the caller and the two limits from whichever address invoked it, and counting nothing at all when both limits are zero. Keeping the guard free of its own settings is what lets each address read its limits from its own module's configuration.
The limits are configurable down to completely off, and off means off: with no limit set, nothing is counted, nothing is read, and core's flood storage is not touched at all, so a site fronted by a reverse proxy or a WAF that already does this pays nothing for a mechanism it is not using. It also ships off, so nothing counts until an operator decides Orchestra should be the one counting, and a site with a limiter in front never has two of them. The notice route proposed in #3623524 wants the same guard and calls it.
What to count on is the question worth settling first. Per source address is the obvious answer and the weakest: a payer coming back from a provider arrives from their own address, and two people behind one office NAT are one address. Per link is stronger for the case that matters, because a caller hammering one link is hammering one instance, and a caller trying many different links is asking about runs it cannot name: each one is fresh, so the per-link count never sees it. So two tiers, counted in one period: per link, and across the doorway as a whole. Neither counts the source address.
The two tiers are not independent, and getting that wrong is what makes a limit worse than none. A request the per-link tier has already refused is not charged to the total, because otherwise one link being hammered would use up the number everybody shares and shut the doorway for every other visitor, which is the thing the per-link tier exists to prevent. The total still sees a flood spread across many links, because each of those is inside its own limit. It is one number for everybody, so when it is reached the doorway refuses every visitor until the period rolls: that is the choice it offers, shut rather than overwhelmed, and it wants to sit well above what the site answers on a busy day.
Every request is counted, not only the ones that fail. Counting only failures was the first answer and it is the weaker one: what is being bounded is the work done before a request is refused, so a request about to be turned away has already cost the thing this exists to limit; and a caller holding one link that does verify could hammer the doorway for free. Counting everything makes the per-link number the one an operator sizes, with room for a person reloading their own page and for the return trips an off-site step makes, and the per-link tier is what keeps one link being worn out from closing the door on anybody else.
Remaining tasks
- A guard service over core's flood service that takes its caller and its two limits from whichever address invoked it, and touches nothing when no limit is set.
- Count and refuse before the doorway resolves the instance, the token and the interaction, which is the work worth protecting.
- Keep a caller the per-link tier has caught from spending the shared ceiling, and record the log throttle once a period rather than once a refusal, so a flood costs one write per caller rather than one per request.
- Expose the two limits in settings, shipped at zero, under one site-wide switch and one period, so an operator can turn every limit off in one place without retyping the numbers.
- Say on the status report that nothing is capping the doorway, since a protection nobody can see is one nobody turns on.
- Kernel coverage that the shipped configuration counts nothing at all, that a run of requests on one link is refused once its limit is reached, that another link keeps working while one is being hammered, that many different links are caught by the total, that the refusal says nothing about whether the link was any good, and that the link itself never reaches the flood service or the log.
- Say in the interaction documentation what the limits are, how to turn them on and off, and what a refused caller sees.
User interface changes
An Interaction settings page holding the two limits, and a Request limits section on the Orchestra settings page holding the switch and the period.
API changes
None to the doorway: its routes and the handle's shape are unchanged. Orchestra gains one public class, the guard, for a module that puts its own public address on a site.
Data model changes
None of Orchestra's own: the counters live in core's flood storage, which core's cron already cleans.
AI-Generated: Yes (Claude Code was used to help draft this issue summary and to write the code. I reviewed both before posting.)
Issue fork orchestra-3623602
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
mably commentedComment #4
mably commentedComment #5
mably commentedComment #6
mably commentedComment #8
mably commented