All posts
11 minBankingReplay attacksn8n

Stopping replay attacks in banking automation

A captured payment request, sent a second time, looks exactly like the first. A step-by-step look at a synthetic payment-exception workflow in n8n, and why the boundary refused the byte-for-byte replay.

· Article

Banks automate more every quarter: payment exception handling, reconciliation, transfer approvals, customer notifications. A growing share of that work runs through workflow tools such as n8n, calling internal APIs on a schedule or a trigger, with no human in the loop at the moment of the call.

Most of those calls are authorized by something the workflow holds: a bearer token, an API key, a client secret. That is fine until one request is captured in transit, from a proxy log, or from a debugging session. From then on the attacker does not need to break anything. They only need to send the same request again.

This post walks through a recorded demo that tests exactly that, step by step, and explains what the boundary checked, why the replay failed, and what the audit record shows afterwards.

Why a replay is so hard to spot

A replayed request is not malformed. It has the right headers, the right credential, and the right body. Its signature, if it has one, is still mathematically valid, because nothing about it was changed. To an API that only asks "is this caller authorized?", the copy is indistinguishable from the original.

In payment workflows that matters more than almost anywhere else. A request that submits a report, releases a payment, or marks an exception as resolved has a side effect. Executing it twice is not a harmless duplicate; it is a second action nobody approved.

Short token lifetimes shrink the window, but they do not close it. Within those minutes, the same credential authorizes every copy of the request an attacker cares to send. Idempotency keys help only if every API implements them correctly, and they are a data-integrity measure, not an authorization decision.

  • Signature valid: yes. Nothing was altered.
  • Credential valid: yes. It has not expired.
  • Request authorized a second time: it should not be, and with a bearer model it usually is.

The demo: a synthetic payment-exception review

The scenario was recorded by Medhamsh, who built Trustplane, to show the fit for banking. It is a synthetic workflow on synthetic data, not a customer deployment, and it is deliberately harmless: no money moves.

The workflow runs in n8n, with Trustplane authenticating and signing every request the workflow makes. The execution is slowed down in the recording so each node is visible as it runs.

Step 1: the workflow does its job

The workflow reads six payment exceptions, groups them by amount and reason, and saves a review report. Opening the final node shows the real response from the upstream service: the report has been stored and is waiting for manual review.

Nothing here is unusual, and that is the point. This is the kind of routine back-office automation banks run all day. Every one of those calls went through the Trustplane boundary, and every one carried its own proof.

Step 2: the replay test

Next the demo tests a replay attack using the same persistent workload identity. Three requests are sent:

  • The original signed request. Accepted.
  • The exact same signed request, sent again byte for byte, with the same passport nonce and the same signature. Rejected with HTTP 403.
  • A fresh request from the same identity, with a new proof. Accepted.

Step 3: proving nothing was changed

A fair objection to any replay demo is that the second request might have been tampered with, so the rejection proves nothing. The demo answers that directly: the transcript hashes of the original and the replay match. The two requests are identical.

In n8n the test node shows green. That is not the replay succeeding; it is the test confirming the expected outcome. The replay itself was denied.

The third request matters just as much. It shows the refusal was not a blanket lockout of the caller. The same identity, making a new request with a fresh proof, is admitted immediately. The boundary refused one specific request, not the workload.

Why the replay was refused

Every request through Trustplane carries a short-lived passport bound to the caller, the action, the audience, the route, and a moment in time. It includes a unique identifier — the JTI — and a nonce. The boundary checks caller, action, audience, route, freshness, and replay locally, with no hosted call in the request path. See how the boundary works for the full sequence.

The first time a passport arrives, its identifier has not been seen, so the request is verified and allowed. The second time, the identifier has already been spent. Every other check still passes — the signature is valid, the caller is real, the route is allowed — but the replay check fails, and one failed check is enough to refuse.

That is the line the demo ends on: a valid signature is not permission to reuse a request. Authenticity says who produced the request. It says nothing about whether this particular copy should be honoured.

Each request is judged on its own. The original is allowed; the byte-identical replay is refused and recorded.

Step 4: the audit record

Trustplane control records the denial independently of the workflow, with the reason code JTI replay. The workflow's own view and the boundary's record agree, but they do not depend on each other; the evidence exists even if the calling system's logs are lost or altered.

The audit detail also confirms that the replayed request never reached the upstream service. The refusal happened at the boundary, before the API saw anything. There is nothing to roll back, because nothing was executed.

  • Decision: deny.
  • Reason code: JTI replay.
  • Upstream reached: no.
  • Recorded by: the boundary, independently of the caller.

What this means for a bank

For security teams, replay protection stops being a property each API has to implement and becomes a property of the boundary in front of all of them. It is enforced the same way for every workflow, and it does not depend on developers remembering idempotency keys.

For risk and compliance, every allow and every deny is a record with a reason code: which workload, which action, which route, and why. Refusals are recorded on the same terms as approvals, which is what an auditor reviewing an incident actually needs.

For platform teams, the workflow is not rewritten. The boundary runs as a container in front of the existing API, and which workloads may call it is declared as a trust anchor rather than handed out as a credential. Revoking a workload is one anchor edit.

Beyond payment exceptions

The same mechanics apply anywhere an automated request has a side effect: releasing or holding a payment, changing a beneficiary, raising a limit, freezing an account, or pushing a customer notification. Each of those is a request that should happen once, on purpose, by a workload you can name.

They apply equally to AI agents. An agent calling a banking tool through MCP is another machine making requests at speed; the boundary judges each one the same way, whatever produced it.

See it on your own workflows

If you run payment or banking automation and want to see this against your own APIs, book a custom demo. More scenarios are on the use cases page.

Every request proves itself — no keys, anywhere.

Put the boundary in front of one API and measure it yourself.