What is it for?
Somewhere in the world, a router announces that your network goes through it. It isn’t a rare typo: it has happened to Google, to Cloudflare, to whole chunks of banking traffic rerouted for hours toward networks that had nothing to do with them. BGP, the protocol that holds the Internet together, trusts: if an operator says “that network is mine,” the others believe it.
RPKI dismantles this blind trust with cryptographic signatures. Whoever owns a block of addresses publishes a signed record — a ROA — that says “only this AS may announce these IPs.” An external validator verifies those signatures and hands your router, over RTR, the already-validated list; the router, before accepting a BGP route, checks whether the announcer’s origin is on that list and drops whoever doesn’t add up. This isn’t theory for big carriers only: on RouterOS 7 it takes two menus, and in this howto we watch it stop a hijack live, in the lab.
What a ROA is (and why BGP needs one)
BGP announces prefixes (blocks of addresses) and, for each one, the origin AS: the number of the operator that first puts it on the net. The problem is that nobody verifies the origin. Anyone can announce 198.51.100.0/24 under their own name, and if they do it “more specific” than the others, they win: the traffic rains down on them.
The ROA (Route Origin Authorization) is a record signed by the address holder that states three things: which prefix, up to which length (the maxLength) and which AS may announce it. ROAs live in the RIR repositories (RIPE, ARIN…), signed; a validator collects them, verifies them and keeps a list of “prefix → authorized AS” pairs (the VRPs, Validated ROA Payloads).
RouterOS is not a validator: it doesn’t collect ROAs on its own. It connects to an external validator with a lightweight protocol, RTR (RPKI-to-Router, RFC 8210), downloads the VRPs and uses them to judge routes. Free validators: Routinator, rpki-client, StayRTR.
The four verdicts
Comparing a route (prefix + origin AS) against the database, RouterOS assigns one of four states:
- valid — a ROA exists and the origin is the right one.
- invalid — a ROA exists, but the origin is wrong (or the prefix is more specific than the
maxLength). This is the signature of a hijack. - unknown — no ROA covers that prefix. It isn’t a judgment: it’s “I don’t know.” Still a large slice of the Internet, though a shrinking one.
- unverified — no RTR session of the group has synced the database yet. It handles the “validator unreachable” case without dropping everything by mistake.
The lab: a hijack in three prefixes
Let’s set up the smallest possible scene to see all three useful cases. Three pieces:
- a validator (StayRTR) on
10.99.99.1, with a fake but consistent VRP database; - gw1 (AS 65001), our border router, on
10.99.99.2: it does RPKI and filters; - gw2 (AS 65002), a “neighbor” on
10.99.99.3that announces three prefixes, one of them stolen.
The validator’s database holds two ROAs (the addresses are the documentation ones, RFC 5737):
203.0.113.0/24 maxLength 24 AS65002 # gw2's legitimate announcement
198.51.100.0/24 maxLength 24 AS64500 # belongs to AS64500, NOT gw2
For 192.0.2.0/24 there is no ROA: it will stay unknown. The trick is in the second line: gw2 will announce 198.51.100.0/24 under its own name (AS65002), but the ROA says that network belongs to AS64500. That is the hijack, and RPKI must catch it.
The validator
StayRTR is a single binary (grab it from the GitHub releases). We give it a JSON file with the two ROAs and have it serve over RTR on port 8282:
# vrps.json
{
"metadata": { "buildtime": "2026-10-22T00:00:00Z" },
"roas": [
{ "prefix": "203.0.113.0/24", "maxLength": 24, "asn": "AS65002" },
{ "prefix": "198.51.100.0/24", "maxLength": 24, "asn": "AS64500" }
]
}
./stayrtr -cache ./vrps.json -checktime=false -bind 10.99.99.1:8282
In the lab the validator runs on a Debian; in production, instead of the hand-made JSON, StayRTR (or Routinator) fetches the real ROAs from the RIRs. Nothing changes for the router: same RTR port, same configuration.
gw2, the neighbor that announces (including the stolen prefix)
On RouterOS 7 a prefix is originated by putting it in an address-list and telling the BGP session to announce that list; network-blackhole=yes creates an active blackhole route for each one, so the prefix really goes out.
/ip address add address=10.99.99.3/24 interface=ether1
/ip firewall address-list add list=bgp-nets address=203.0.113.0/24
/ip firewall address-list add list=bgp-nets address=198.51.100.0/24
/ip firewall address-list add list=bgp-nets address=192.0.2.0/24
/routing/bgp/instance add name=as65002 as=65002
/routing/bgp/connection add name=to-gw1 remote.address=10.99.99.2 instance=as65002 \
remote.as=65001 local.role=ebgp-peer output.network=bgp-nets output.network-blackhole=yes
gw1, the border that validates
First the RTR session to the validator, then the BGP rule. We call the RPKI group lab:
/ip address add address=10.99.99.2/24 interface=ether1
/routing/rpki add group=lab address=10.99.99.1 port=8282 refresh-interval=60
The filter itself lives in the routing rules: a first rule tags every route with the RPKI verdict of the lab group, the second drops the invalid ones and accepts the rest.
/routing/filter/rule add chain=bgp_in rule="rpki-verify lab"
/routing/filter/rule add chain=bgp_in rule="if (rpki invalid) { reject } else { accept }"
And the session toward gw2, with that filter applied on input:
/routing/bgp/instance add name=as65001 as=65001
/routing/bgp/connection add name=to-gw2 remote.address=10.99.99.3 instance=as65001 \
remote.as=65002 local.role=ebgp-peer input.filter=bgp_in
The proof: checking a verdict by hand
As soon as the RTR session is synced, on gw1 we can query the database for a single prefix with rpki-check. Mind a difference between versions: on RouterOS 7.24.5 the parameter is called prefix (the newer manual writes prfx).
[admin@gw1] > /routing/rpki rpki-check group=lab prefix=203.0.113.0/24 origin-as=65002
valid
[admin@gw1] > /routing/rpki rpki-check group=lab prefix=198.51.100.0/24 origin-as=65002
invalid
[admin@gw1] > /routing/rpki rpki-check group=lab prefix=198.51.100.0/24 origin-as=64500
valid
[admin@gw1] > /routing/rpki rpki-check group=lab prefix=192.0.2.0/24 origin-as=65002
unknown
All as expected: the same prefix 198.51.100.0/24 is valid from the right AS (64500) and invalid from the wrong one (65002). And 192.0.2.0/24, with no ROA, is unknown.
The proof that matters: the hijack dropped in BGP
This is the point. On the neighbor gw2, all three prefixes really go out toward gw1 — including the stolen one:
[admin@gw2] > /routing/bgp/advertisements print
DST PEER AFI NEXTHOP
192.0.2.0/24 to-gw1-1 ip 10.99.99.3
198.51.100.0/24 to-gw1-1 ip 10.99.99.3
203.0.113.0/24 to-gw1-1 ip 10.99.99.3
But on the border gw1, only two make it into the routing table:
[admin@gw1] > /ip route print where bgp
Flags: D - DYNAMIC; A - ACTIVE; b - BGP
DST-ADDRESS GATEWAY DISTANCE
DAb 192.0.2.0/24 10.99.99.3 20
DAb 203.0.113.0/24 10.99.99.3 20
The valid one (203.0.113.0/24) got in. So did the unknown one (192.0.2.0/24): RPKI doesn’t drop what it doesn’t know, it lets it through. The invalid one — 198.51.100.0/24 announced by someone with no right to it — isn’t there: the filter stopped it before it touched the table. The hijack never made it inside.
maxLength: the “loose” ROA that leaves a crack open
There’s a detail that makes the difference between a tight ROA and one that leaves the door ajar. The ROA also states up to what length a prefix is allowed. Ours is maxLength 24: it authorizes /24 and nothing more. If someone announces a more specific piece, it’s invalid even if the AS were the right one:
[admin@gw1] > /routing/rpki rpki-check group=lab prefix=203.0.113.128/25 origin-as=65002
invalid
Why does it matter? Because the classic hijack trick is to announce a more specific prefix than the legitimate one, which in BGP always wins. If your ROA has a wide maxLength (say /24 on a block you only ever use as a /24), you are authorizing all the more-specifics up to there yourself: an attacker can announce a /24 inside your block and stay valid. Golden rule: set maxLength equal to the length you actually announce, not wider.
The honest ending: the origin yes, the path no
What you’ve seen so far — origin validation, based on ROAs — checks who first announces a prefix. It does not check the path: it can’t tell whether the AS-path is plausible or whether someone in the middle is lying while keeping the origin correct. An attacker who inserts themselves as “transit” without forging the origin gets through anyway.
The missing piece — validating the path too — is called ASPA (Autonomous System Provider Authorization), and it is itself an RPKI object: each network declares who its legitimate providers are, so the AS-path can be checked for sense. It’s still early (it covers a small slice of routing), but it’s the direction. For today: by validating the origin you accept less junk, and origin hijacks — the most common kind — you really do stop. That’s not nothing.
To get hands-on with ASPA — see the published ASPAs, who declares whom as a provider, and verify an AS-path live — there’s ASPA Explorer, a free and open-source service from the DEI at the University of Padua: aspa.ping6.it.
Frequently asked questions
Can RouterOS act as a validator on its own?
No. RouterOS only speaks RTR toward an external validator (Routinator, rpki-client, StayRTR). The validator collects and verifies the ROAs; RouterOS uses the result in its filters.
If the validator goes down, does the router drop every route?
No, if you write the filter well. Until no session of the group is synced the state is unverified, not invalid: you can handle it on purpose (if (rpki unverified) { accept }) so you don’t isolate yourself when the validator is unreachable.
Can I block an attack based on RPKI alone?
RPKI is great for dropping the invalid ones. But the unknown ones are still a large slice of the Internet (today about a third of all routes: ROA coverage has now passed 65%): dropping them would cut you off from too much legitimate traffic. Drop the invalid, accept valid and unknown.
Why is my route invalid even though the AS is right?
Almost always it’s the maxLength: you’re announcing a prefix more specific than the ROA allows. Check with rpki-check and align the ROA’s maxLength with what you actually announce.
What’s the difference between origin validation and ASPA?
They are two checks of the same RPKI. Origin validation (based on ROAs) checks who announces the prefix; ASPA checks the path (whether the ASes crossed are the expected ones). They’re complementary: origin validation is mature and widespread, ASPA is the new piece that adds the path check.
Tested in the lab on PNETLab with CHR RouterOS 7.24.5 (stable) and the StayRTR validator: RTR synchronization, the four verdicts with rpki-check and the BGP filter dropping the invalid route while letting valid and unknown through. The outputs are the real ones from the lab.



