Standards RPKI_STALE_ROA_TRANSFER

Stale ROA After Prefix Transfer: Hijack Window When Origin AS Changes Hands

Published October 10, 2026 Updated October 10, 2026

Stale ROA after prefix transfer: the hijack window when the origin AS changes hands

Summary

A route origin authorization (ROA) is a signed statement that a specific AS may originate a specific prefix. When a prefix changes hands, the ROA naming the former holder's AS often stays in the RPKI repository. As long as it is there, route origin validation keeps marking announcements from that old AS as Valid. That opens a hijack window. A route that should fail validation, or at least look suspicious, passes because a leftover ROA still backs it.

This article covers how stale ROAs arise, how they are exploited or flagged, what they cost, how monitoring catches them, and how to clean them up.

How a stale ROA comes to exist

A ROA binds three values: a prefix, a maxLength, and an origin ASN. The holder signs it under the resource certificate that covers the prefix and publishes it to a repository. The registry does not delete it when a transfer completes. The old holder has to do that, and often doesn't.

Three sequences produce a stale ROA:

  1. The seller never removes its ROA. Brokers or lawyers close the sale, the network team that managed the prefix is disbanded or reassigned, and nobody owns the RPKI cleanup step.
  2. The certificate chain lags the transfer. A validator accepts a ROA only when the prefix sits inside the resources of the certificate that signed it. After a clean certificate update, a leftover ROA under the old holder's certificate should fail. The gap is in transfers where space moves without a clean certificate reissue: cross-registry moves, legacy space, and delegated CAs that the child never updated. In those cases the old ROA stays Valid.
  3. The new holder adds a ROA and leaves the old one. Multiple ROAs may cover the same prefix with different origins. Both origins then validate as Valid. The new holder's dashboard shows green, and the old AS still validates.

The third case is the most common in practice. It is also the hardest to notice, because nothing looks broken.

Why it is a hijack window

Route origin validation checks only the origin AS against the set of covering VRPs. It has no idea who currently operates the prefix. If the old AS is still reachable, whether because the seller still uses it, it sits dormant but not retired, or someone else has re-registered it, it can originate the prefix and receive a Valid state.

Upstreams that enforce route origin validation accept those routes. Traffic for the prefix can then be pulled toward the old AS by normal path preference. The window stays open until three things happen: the ROA is removed, the removal propagates through the RPKI repositories, and validators refresh their caches. In practice that can take hours. With no cleanup owner, it can stay open indefinitely.

The attack doesn't need to hit every route. Partial capture is enough to intercept traffic for specific networks, or to break a domain's mail or web path through a single transit provider.

What it costs

  • Traffic interception and blackholing. Diverted traffic can be dropped, read, or modified. Any unencrypted service is exposed. Encrypted services are exposed to downgrade and certificate-issuance attacks that depend on routing.
  • Email and DNS-adjacent exposure. Mail for the affected domains can be routed through the wrong network. That makes password resets and domain validation a plausible follow-on attack.
  • Audit findings. An auditor who compares VRPs against the organization's current ASN inventory will flag any ROA naming an AS the organization no longer operates. Under internal routing-security controls or MANRS-style commitments, this counts as a failed control with evidence attached.
  • Incident response and trust loss. Working out whether a Valid route was hostile takes time, because the validation result itself says nothing is wrong.

How validation and monitoring catch it

A stale ROA is invisible to the router. You only see it when the validated data is compared against an authoritative statement of who should be originating what.

Inventory comparison. Keep a per-prefix list of allowed origin ASNs in a source of truth. Pull VRPs from at least two validators and diff them against that list. Any VRP for a prefix whose ASN is not in the allowed set is a stale-ROA candidate.

import json

# prefix -> origin ASNs the holder still operates (source of truth)
ALLOWED = {
    "192.0.2.0/24": {"AS64500"},
}

with open("vrps.json") as fh:
    roas = json.load(fh)["roas"]

for roa in roas:
    prefix, asn = roa["prefix"], roa["asn"]
    if asn not in ALLOWED.get(prefix, set()):
        print(f"STALE ROA {prefix} maxLength={roa['maxLength']} origin={asn}")

Run this against validator output on a schedule. Treat any output as a finding until someone signs it off.

BGP origin monitoring. Watch public collectors such as RIPE RIS or RouteViews, or a commercial BGP monitoring service, for announcements of your prefixes. Alert on any origin outside the allowed set, even if the route is RPKI Valid. RPKI tells you the origin is authorized. It does not tell you the origin is right.

Transfer-event alerting. A stale ROA is most likely right after a transfer. Make the transfer runbook (below) trigger a time-boxed check, for example at 7, 30, and 90 days, that confirms the old VRP is gone.

Mitigation and implementation steps

  1. Before closing a transfer, get written confirmation of which ROAs the seller holds for the block. Make the seller's removal either a closing condition or a named post-close task with an owner and a due date.
  2. Create the new ROA for the new origin ASN. Set maxLength to the announced prefix length. Don't use the maximum the allocation allows (see the companion article on maxLength overreach).
  3. Remove the old ROA through the registry portal or the certificate holder's tooling, and record the removal ID.
  4. Verify the VRP is gone from at least two independent validators. A removed object can linger in a cache until the next refresh cycle runs.
  5. Retire ASNs deliberately. When an AS is decommissioned, remove every ROA that names it, and remove it from the inventory. A retired AS with a live ROA is a hijack waiting to happen.
  6. Alert on ROA expiry so an expired ROA gets replaced, not silently dropped. Silent dropping would create a NotFound gap.
  7. Enforce invalid rejection on your routers. The example below is BIRD 2 with two RTR caches, so one cache failure doesn't blank the table:
roa4 table roa4;

protocol rpki rpki_primary {
    roa4 { table roa4; };
    remote "rtr1.example.net" port 323;
    retry keep 90;
    refresh keep 900;
    expire 7200;
}

protocol rpki rpki_secondary {
    roa4 { table roa4; };
    remote "rtr2.example.net" port 323;
    retry keep 90;
    refresh keep 900;
    expire 7200;
}
  1. Add the transfer checklist to the runbook. Confirm the seller's ROA is removed, confirm the new ROA is issued, confirm the VRP is absent in two validators, confirm BGP origin monitoring is updated, and record the evidence for audit.

Key takeaways

  • A ROA outlives the holder that created it unless someone removes it. Transfers without a named cleanup owner leave stale ROAs behind.
  • A leftover ROA makes a hijacking AS Valid, so RPKI validation alone won't catch the attack.
  • Compare validated VRPs against your own current ASN inventory, not just against the validator's verdict.
  • Put ROA removal and verification in the transfer runbook, with evidence kept for audit.
  • Retire ASNs and their ROAs together. A live ROA for a dead AS is the most dangerous kind of leftover.

maxLength overreach: why a permissive ROA enables subprefix hijacks despite valid signatures

Summary

A ROA's maxLength field tells validators how long a more-specific announcement can be and still count as authorized. When maxLength is set well beyond the prefix length the holder actually announces, an attacker can announce a more-specific subprefix and receive a Valid state. Origin-only protection doesn't stop it, because the signature, the origin, and the prefix range all check out.

What maxLength actually authorizes

Take a ROA for 198.51.100.0/20 with maxLength 24 and origin AS64500. It authorizes AS64500 to originate that /20 and any more-specific up to /24. Validation results look like this:

Announcement Origin Result
198.51.100.0/20 AS64500 Valid
198.51.100.0/24 AS64500 Valid
198.51.100.0/24 AS64666 Invalid (wrong origin)
198.51.100.0/25 AS64500 Invalid (longer than maxLength)

The signature is correct in every row. The validator is doing what the standard specifies. The problem is that the ROA authorizes far more than the holder intends to announce.

How overreach happens

Overreach usually comes from convenience, not carelessness:

  • Defaulting to the maximum. An operator creates a ROA through a tool that defaults maxLength to a generous value, or copies one from a template written for a larger allocation.
  • Traffic engineering plans. Someone wants room to split a block later and sets maxLength to /24 on a /16 "for flexibility."
  • Legacy ROAs. A ROA created years ago covers space that now has a different announcement pattern, and nobody revisited it.

The result is a ROA that answers "may this AS announce anything in this block down to /24?" when the operator's real answer is "only this /20."

How an attacker exploits it

Origin validation checks only the origin AS, not the AS path. That leaves two routes for a subprefix hijack.

Direct origination. If the attacker controls the authorized AS, or the authorized AS is dormant and reachable, it can announce a /24 inside the authorized /20. The announcement validates as Valid. Because routers prefer the longest matching prefix, traffic for that /24 moves to the attacker even when the legitimate /20 is announced normally.

Forged origin. The attacker doesn't need the authorized AS at all. It can announce the prefix with the authorized AS as the last hop in the path, so the origin still reads AS64500. Origin validation sees AS64500 as the origin, finds a covering ROA with sufficient maxLength, and returns Valid. Tight maxLength doesn't fully stop this variant, because the attacker can still announce a /24 that the ROA authorizes. It does shrink the surface, though. A ROA limited to /20 would make the forged /24 Invalid.

That's why maxLength discipline is a real control and not a formality. It's also why route origin validation is usually paired with path-based controls, which are still maturing.

Why auditors flag it

An auditor comparing ROAs against announced routes will see a gap between authorized and used space. The standard reference is RFC 9319, The Use of maxLength in the Resource Public Key Infrastructure (RPKI), which recommends keeping maxLength as tight as possible and avoiding values that authorize more-specifics the holder doesn't announce.

A finding typically reads: "ROA authorizes more-specific announcements that the organization does not use. Subprefix hijack surface exceeds documented routing policy."

What it costs

A successful subprefix hijack captures all traffic for the more-specific range. For a /24 inside a larger block, that can be enough to intercept traffic for specific services, customers, or resolvers. Networks that enforce route origin validation accept the route, so the attack travels the normal path of trust. The costs match those of any hijack: interception, blackholing, certificate-issuance risk for domain-validated TLS, and incident-response time. The added cost is that the validator's verdict makes the route look authorized. Responders may spend time checking the RPKI data instead of suspecting the route.

How monitoring catches it

Compute the excess for each VRP: the set of more-specific lengths the ROA allows that the holder doesn't announce.

import ipaddress

def overreach(vrp, announced):
    """True if maxLength authorizes more-specifics than the holder announces."""
    net = ipaddress.ip_network(vrp["prefix"])
    longest = max(
        (n.prefixlen for n in announced if n.subnet_of(net)),
        default=net.prefixlen,
    )
    return vrp["maxLength"] > longest

vrp = {"prefix": "198.51.100.0/20", "maxLength": 24}
announced = [ipaddress.ip_network("198.51.100.0/20")]
print(overreach(vrp, announced))  # True: /24 authorized, only /20 announced

Feed it the holder's own announcement list from BGP monitoring. Flag any ROA where overreach returns True. Also alert on more-specific announcements of your space from any origin, even when they validate as Valid. A new /24 inside your /20 deserves a human look regardless of its RPKI state.

Mitigation and implementation steps

  1. Set maxLength to the announced length. For a block announced as a single /20, use maxLength 20. Add a new ROA when you announce a more-specific.
  2. Audit existing ROAs with the excess check above. Tighten each ROA whose maxLength exceeds its longest announcement.
  3. Sequence changes safely. When tightening, publish the new ROA before removing the old one. Check that the announced routes stay Valid throughout.
  4. Filter customer and peer announcements. Use prefix filters that accept only the more-specifics you expect, so a subprefix from a customer session is rejected before it reaches the RPKI decision.
  5. Watch for more-specific announcements of your space from any origin, including Valid ones, and treat them as events to review.
  6. Document the policy. Record the intended announced prefix set for each ROA in the inventory, so the excess check has a source of truth.
  7. Plan for path-based controls. Track ASPA and similar path-validation work. Origin validation alone can't stop forged-origin announcements, so don't treat a valid ROA as proof that a route is legitimate.

Key takeaways

  • A valid signature proves authorization for the ranges the ROA names, not for the prefixes the holder actually uses.
  • maxLength set far above the announced length lets a subprefix hijack validate as Valid.
  • Origin validation ignores the AS path, so forged-origin announcements can also validate when maxLength is loose.
  • Tighten maxLength to the announced prefix length, following RFC 9319, and update it whenever announcements change.
  • Compare ROA excess against your own announcement list, and alert on every more-specific of your space, whatever its validation state.

Validator fallback and unknown state: attacker-induced downgrade when repositories are unreachable

Summary

RPKI origin validation returns one of three states: Valid, Invalid, or NotFound. Most networks reject Invalid routes and accept NotFound ones, since many prefixes have no ROA at all. That policy depends on the validator having current data. If an attacker can disrupt the validator or its repositories, covered prefixes can fall back to NotFound. An Invalid hijack then looks like an unremarkable route, and a lenient policy accepts it.

The three states and the policy gap

RFC 6811 defines the states:

  • Valid: a covering VRP authorizes the origin.
  • Invalid: a covering VRP exists, but it names a different origin or the announcement is longer than maxLength.
  • NotFound: no covering VRP exists.

A typical policy drops Invalid, prefers Valid, and accepts NotFound. That policy assumes NotFound means "this prefix has no ROA." It can also mean "the data that would have shown this prefix has a ROA is missing." The router can't tell those apart, and that ambiguity is the attack surface.

How fallback happens

Validated data reaches routers through an RTR session with a validator, such as Routinator, rpki-client, or a hosted RTR cache. Two things can break that session.

Loss of the RTR session. Routers keep the last data they received, but only until their expire timers run out. After expiry, the router drops the VRPs, and every prefix becomes NotFound. Relying on one cache is a single point of failure.

Loss of repository access. A validator fetches ROAs from publication points over rsync or RRDP. When it can't reach a point, it can keep cached objects only while their manifests and CRLs are current. Once those expire, most validators drop the objects. A ROA that was Valid this morning can be NotFound after a long enough outage.

An attacker doesn't need to break cryptography. It only needs to make the data path fail for long enough. Possible vectors include:

  • Flooding the RTR cache or the validator host.
  • Disrupting routes to the repository host. The attacker can do this with a more-specific hijack of the repository's own prefix. It's a real case where one hijack disables the defense against others.
  • Exploiting a dependency outage the operator didn't monitor, such as a DNS or TLS failure in the fetch path.

The attacker's target is the NotFound state for the victim prefix, not the validator itself.

How the hijack works

Assume a victim prefix 192.0.2.0/24 with a ROA naming AS64500. Under normal operation, an announcement from AS64666 is Invalid and dropped. During a fallback, the same announcement returns NotFound, and the policy accepts it. The attacker then wins traffic through ordinary path selection, with no RPKI signal to alert anyone.

The window lasts as long as the validator is stale or the router has no VRPs for the prefix. It closes when the data path recovers. An attacker that can keep the fallback going can keep the window open.

Why it matters for compliance

An auditor who asks "Do you enforce RPKI route origin validation?" will accept a yes only if enforcement holds under failure. A deployment that silently accepts NotFound for covered prefixes during a validator outage doesn't meet that standard. The control exists on paper but fails open in practice.

What it costs

The costs match a normal hijack: interception, blackholing, and exposure of services that rely on routing for integrity. The extra cost is detection. The validator reports that it's running, the routes show NotFound instead of Invalid, and the alarm that would normally fire on an Invalid route never goes off. Responders may spend hours on the wrong layer.

How monitoring catches it

Monitor the validator, not just the routes.

  • VRP count. Track the total count per validator and alert on a sharp drop between refreshes. A fallback often shows up as a count collapse before any route changes.
  • RTR session state. Alert when any RTR session is down or hasn't refreshed within its refresh interval.
  • Manifest and CRL expiry. Alert days before expiry on any publication point you depend on.
  • Previously covered prefixes. Keep an inventory of prefixes you know have ROAs. Alert when any of them appears as NotFound in a router's table or in a public BGP collector's view.
  • Cross-validator comparison. Two validators fed from different paths should agree. A disagreement is an early sign of an outage on one path.
import json

EXPECTED_COVERED = {"192.0.2.0/24", "198.51.100.0/20"}  # from your ROA inventory

with open("vrps.json") as fh:
    covered = {roa["prefix"] for roa in json.load(fh)["roas"]}

missing = EXPECTED_COVERED - covered
if missing:
    print(f"ALERT: expected ROAs missing from validator output: {sorted(missing)}")

Run this against each validator's output. A missing expected ROA is either a real revocation, which should have a matching inventory event, or a fallback in progress.

Mitigation and implementation steps

  1. Use at least two RTR caches from independent operators or hosts, and configure routers to use both. A single cache failure shouldn't change the table.
  2. Set expire timers deliberately. They should be long enough to survive a short outage and short enough that stale data doesn't persist forever. Example BIRD 2 configuration:
roa4 table roa4;

protocol rpki rpki_primary {
    roa4 { table roa4; };
    remote "rtr1.example.net" port 323;
    retry keep 90;
    refresh keep 900;
    expire 7200;
}

protocol rpki rpki_secondary {
    roa4 { table roa4; };
    remote "rtr2.example.net" port 323;
    retry keep 90;
    refresh keep 900;
    expire 7200;
}
  1. Fail closed for known-covered prefixes. Reject NotFound for prefixes in your inventory that you know have ROAs. Use a prefix set so the policy applies only to space you can vouch for:
filter rpki_policy {
    if roa_check(roa4) = ROA_INVALID then reject;
    if net ~ [ 192.0.2.0/24{24,24}, 198.51.100.0/20{20,24} ]
       && roa_check(roa4) = ROA_UNKNOWN then reject;
    accept;
}

Test this in a lab before production. A bad prefix set can reject legitimate routes.

  1. Mirror and monitor repositories. Run your own validator that fetches from multiple publication points, and alert on fetch failures and approaching manifest expiry.
  2. Avoid single-path dependencies. If you can, make sure the validator's network path doesn't depend on the same upstream as the routes it protects.
  3. Write a runbook. When VRP count drops or an RTR session fails, the first step is to confirm the data state, not to assume the routes are fine.

Key takeaways

  • NotFound is ambiguous. It can mean "no ROA" or "the validator lost the ROA."
  • An attacker who disrupts the validator or its repositories can turn Invalid hijacks into NotFound routes that lenient policies accept.
  • Expire timers and cache redundancy decide how long a fallback lasts, and whether it happens at all.
  • Monitor VRP counts, RTR session state, and manifest expiry, not only the routes themselves.
  • Fail closed for prefixes you know are covered, and keep the prefix set current with your ROA inventory.