RPKI ROA Setup for BGP: Create ROAs, Validate Routes in BIRD2 and FRR
Create IPv4 and IPv6 ROAs in the RIPE NCC portal, install Routinator as an RTR cache, configure RPKI route origin validation in BIRD2 and FRRouting, and verify prefix status with bgp.tools and RIPE Stat.
Without RPKI, any ASN can originate any prefix. A Route Origin Authorization (ROA) binds your prefix to your ASN cryptographically, and route origin validation (ROV) lets your router reject invalid announcements. This tutorial covers the full chain: create ROAs in the RIPE NCC portal, run Routinator as the local RPKI cache, configure validation in BIRD2 and FRRouting, and verify everything works.
Prerequisites: a registered ASN, allocated IP prefixes (PA or PI), a working BGP session on a Linux VPS (BGP with your own IP on a VPS), and either BIRD2 (BIRD2 BGP configuration) or FRR (FRR BGP configuration) already running.
What is RPKI and why does your BGP prefix need a ROA?
Resource Public Key Infrastructure (RPKI) is a cryptographic framework defined in RFC 6480 that ties Internet number resources (IP prefixes, ASNs) to their legitimate holders through X.509 certificates issued by the Regional Internet Registries. A Route Origin Authorization (ROA) is a signed object that states "ASN X is authorized to originate prefix Y with a maximum prefix length of Z." Validators fetch these ROAs and feed the result to routers via the RPKI-to-Router (RTR) protocol (RFC 8210).
When a router receives a BGP update, it checks the origin ASN and prefix against its local ROA table. Each prefix gets one of three validation states:
| State | Meaning | Recommended action |
|---|---|---|
| Valid | A ROA exists and matches the origin ASN and prefix length | Accept |
| Invalid | A ROA exists but the origin ASN or prefix length does not match | Reject |
| Not Found | No ROA covers this prefix | Accept (optionally with lower local-pref) |
Without a ROA, your prefixes show as "Not Found." That is better than "Invalid," but networks performing ROV will prefer "Valid" routes from your peers over your "Not Found" announcements when both are available. Creating ROAs is the first step. Validating inbound routes protects your network from accepting hijacked prefixes.
How do you create IPv4 and IPv6 ROAs in the RIPE NCC portal?
Log in to the RIPE NCC portal, navigate to Resources, then RPKI Dashboard. If you have not initialized RPKI yet, select "Hosted" certificate authority. RIPE NCC manages the CA and signing for you. Once the Hosted CA is active, switch to the BGP Announcements tab. RIPE pre-fills ROA suggestions based on your current BGP announcements.
- Click Create ROA (or + New ROA in the ROA tab).
- Set Origin ASN to your AS number (e.g.,
AS213279). - Set Prefix to your IPv4 allocation (e.g.,
192.0.2.0/24). - Set Maximum Length equal to the prefix length (
/24). Do not increase it. See the maxLength section below. - Click Publish.
- Repeat for your IPv6 prefix (e.g.,
2001:db8::/48with max length/48).
Verify in the RPKI Dashboard that the ROA status shows "Published." Propagation to validators typically takes 10 to 20 minutes, depending on their refresh interval.
Dual-stack reminder: Create one ROA per prefix. If you announce 192.0.2.0/24 and 2001:db8::/48, you need two ROAs. If you announce additional more-specifics (a /25 carved from the /24), each one needs its own ROA with its own ASN binding.
How do you install Routinator as an RPKI-RTR cache on Linux?
Routinator is an RPKI Relying Party (validator) developed by NLnet Labs. It fetches and validates ROAs from all five RIR trust anchors, then serves Validated ROA Payloads (VRPs) to your router over the RTR protocol. Current stable version: 0.15.1.
Install from the NLnet Labs repository
On Debian 12 or Ubuntu 24.04:
sudo apt update
sudo apt install -y ca-certificates curl gnupg lsb-release
Add the NLnet Labs signing key and repository:
curl -fsSL https://packages.nlnetlabs.nl/aptkey.asc | sudo gpg --dearmor -o /usr/share/keyrings/nlnetlabs-archive-keyring.gpg
For Debian:
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/nlnetlabs-archive-keyring.gpg] https://packages.nlnetlabs.nl/linux/debian $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/nlnetlabs.list > /dev/null
For Ubuntu:
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/nlnetlabs-archive-keyring.gpg] https://packages.nlnetlabs.nl/linux/ubuntu $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/nlnetlabs.list > /dev/null
Install Routinator:
sudo apt update
sudo apt install -y routinator
The package installs a systemd service that starts automatically. Routinator runs as the routinator user.
Verify the service
sudo systemctl status routinator
You should see active (running). The first sync takes 2 to 5 minutes while Routinator fetches all trust anchor certificates and validates the global ROA set.
Check that the initial sync completed by querying the HTTP API (the packaged version logs to syslog with minimal detail, so the HTTP API is the reliable source):
curl -s http://127.0.0.1:8323/api/v1/status | python3 -c "import sys,json; d=json.load(sys.stdin); print(f'IPv4 VRPs: {d[\"payload\"][\"routeOriginsIPv4\"][\"final\"]}, IPv6 VRPs: {d[\"payload\"][\"routeOriginsIPv6\"][\"final\"]}')"
If the sync is still running, the counts will be zero. Wait 2 to 5 minutes and try again. Once you see non-zero counts (hundreds of thousands for IPv4, tens of thousands for IPv6), the initial sync is done.
Configuration
The package config lives at /etc/routinator/routinator.conf. The defaults are secure: RTR listens on 127.0.0.1:3323 and HTTP on 127.0.0.1:8323. Both bind to localhost only.
Key settings:
| Option | Default | Purpose |
|---|---|---|
rtr-listen |
["127.0.0.1:3323"] |
RTR server for routers |
http-listen |
["127.0.0.1:8323"] |
HTTP UI and API |
refresh |
600 | Seconds between RPKI syncs |
retry |
600 | Seconds before retrying after a failed sync |
expire |
7200 | Seconds before cached VRPs are considered stale |
If BIRD2 or FRR runs on the same machine (typical for a VPS BGP setup), keep the default 127.0.0.1 binding. No firewall changes needed.
If you run Routinator on a separate server, bind to a private IP and restrict access:
sudo ufw allow from 10.0.0.0/24 to any port 3323 proto tcp comment "RTR from routers"
Check the HTTP interface:
curl -s http://127.0.0.1:8323/api/v1/status | head -20
This returns JSON with the current VRP count, last sync time, and validator status.
How do you configure RPKI validation in BIRD2?
BIRD2 has native RPKI support through the rpki protocol (available since BIRD 2.0; Ubuntu 24.04 ships BIRD 2.14). It connects to Routinator over RTR, populates ROA tables, and provides the roa_check() function for import filters. No external libraries needed.
Add the following to your BIRD2 configuration (typically /etc/bird/bird.conf):
Define ROA tables
roa4 table roa_v4;
roa6 table roa_v6;
Configure the RPKI protocol
protocol rpki rpki_routinator {
roa4 { table roa_v4; };
roa6 { table roa_v6; };
remote 127.0.0.1 port 3323;
retry keep 90;
refresh keep 600;
expire keep 7200;
}
The keep keyword tells BIRD to prefer the server-provided timer values but fall back to the specified defaults. retry 90 means BIRD reconnects 90 seconds after losing the RTR session.
Add ROA validation to your import filter
filter bgp_import_v4 {
if (roa_check(roa_v4, net, bgp_path.last_nonaggregated) = ROA_INVALID) then {
print "RPKI INVALID: ", net, " from AS", bgp_path.last;
reject;
}
if (roa_check(roa_v4, net, bgp_path.last_nonaggregated) = ROA_VALID) then {
bgp_local_pref = 200;
}
accept;
}
filter bgp_import_v6 {
if (roa_check(roa_v6, net, bgp_path.last_nonaggregated) = ROA_INVALID) then {
print "RPKI INVALID: ", net, " from AS", bgp_path.last;
reject;
}
if (roa_check(roa_v6, net, bgp_path.last_nonaggregated) = ROA_VALID) then {
bgp_local_pref = 200;
}
accept;
}
bgp_path.last_nonaggregated is safer than bgp_path.last because it skips AS_SET entries from aggregation. Invalid routes get rejected. Valid routes get a higher local-pref. Not Found routes pass through at the default local-pref.
Apply the filter to your BGP peer
protocol bgp upstream_v4 {
local as 213279;
neighbor 198.51.100.1 as 64500;
ipv4 {
import filter bgp_import_v4;
import table;
export where source ~ [RTS_STATIC, RTS_BGP];
};
}
The import table directive is important. It makes BIRD re-evaluate filtered routes when the ROA table changes, without needing a full session reset.
Reload and verify
sudo birdc configure
Check the RPKI session:
sudo birdc show protocols all rpki_routinator
Look for Established in the output. Then check ROA table contents:
sudo birdc show route table roa_v4 count
You should see hundreds of thousands of entries (the global ROA table has over 800,000 VRPs as of early 2026).
Check a specific prefix validation:
sudo birdc show route 192.0.2.0/24 all
The output includes a ROA field showing valid, invalid, or unknown (not found).
How do you configure RPKI validation in FRRouting?
FRRouting supports RPKI through the rpki module, which uses librtr under the hood (Ubuntu 24.04 ships FRR 8.4.4; FRR 9.x+ and 10.x also supported). The module connects to Routinator's RTR server and integrates with BGP route-maps.
Install the RPKI module
On Debian/Ubuntu with FRR already installed:
sudo apt install -y frr-rpki-rtrlib
Enable the module
Edit /etc/frr/daemons and add -M rpki to the bgpd options:
bgpd_options=" -A 127.0.0.1 -M rpki"
Restart FRR:
sudo systemctl restart frr
Verify bgpd loaded the module:
sudo vtysh -c "show rpki cache-server"
If the command runs without error (output may be empty before configuring a cache), the module loaded correctly. If you get % Unknown command, the -M rpki flag is missing or frr-rpki-rtrlib is not installed.
Configure the RTR cache
Enter vtysh and configure:
sudo vtysh
configure terminal
rpki
rpki cache 127.0.0.1 3323 preference 1
rpki polling_period 300
rpki expire_interval 7200
rpki retry_interval 600
exit
Note: FRR 9.x+ uses the syntax rpki cache tcp 127.0.0.1 3323 preference 1 (with explicit tcp keyword). FRR 8.x uses rpki cache 127.0.0.1 3323 preference 1 without it. Check your version with vtysh -c "show version".
Create route-maps for RPKI states
route-map rpki-filter permit 10
match rpki valid
set local-preference 200
exit
route-map rpki-filter deny 20
match rpki invalid
exit
route-map rpki-filter permit 30
match rpki notfound
exit
This accepts valid routes with elevated local-pref, drops invalid routes, and accepts not-found routes at default local-pref.
Apply the route-map to your BGP neighbor
router bgp 213279
neighbor 198.51.100.1 remote-as 64500
address-family ipv4 unicast
neighbor 198.51.100.1 route-map rpki-filter in
neighbor 198.51.100.1 soft-reconfiguration inbound
exit-address-family
address-family ipv6 unicast
neighbor 2001:db8::1 route-map rpki-filter in
neighbor 2001:db8::1 soft-reconfiguration inbound
exit-address-family
exit
soft-reconfiguration inbound is required. Without it, FRR cannot re-evaluate existing routes when the RPKI cache updates. FRR stores the unmodified routes from the peer and re-applies the route-map when VRPs change.
Write the config:
write memory
end
Verify
Check the RTR connection:
sudo vtysh -c "show rpki cache-connection"
Look for (connected) in the output. Then check the prefix table:
sudo vtysh -c "show rpki prefix-table" | head -20
Filter BGP routes by validation state:
sudo vtysh -c "show bgp ipv4 unicast rpki valid" | head -20
sudo vtysh -c "show bgp ipv4 unicast rpki invalid"
The invalid output should show routes you are actively rejecting.
BIRD2 vs FRR RPKI configuration at a glance
| Feature | BIRD2 | FRR |
|---|---|---|
| Module install | Built-in (no extra package) | frr-rpki-rtrlib package + -M rpki flag |
| RTR config | protocol rpki block with remote |
rpki cache command in vtysh |
| ROA tables | Explicit roa4/roa6 tables |
Internal, not directly exposed |
| Filter mechanism | roa_check() in import filter |
match rpki in route-map |
| Auto re-evaluation | import table directive |
soft-reconfiguration inbound |
| Show ROA count | birdc show route table roa_v4 count |
vtysh show rpki prefix-table |
| Show validation | birdc show route ... all (ROA field) |
vtysh show bgp rpki valid/invalid/notfound |
How do you verify your prefix RPKI status?
After creating ROAs and configuring validation, verify from multiple vantage points.
Local verification
On the router itself, check that your own prefix shows as valid:
BIRD2:
sudo birdc show route 192.0.2.0/24 all | grep -i roa
FRR:
sudo vtysh -c "show rpki prefix-table 192.0.2.0/24"
Both should show your ASN as the authorized origin.
Routinator HTTP API
curl -s "http://127.0.0.1:8323/api/v1/validity/AS213279/192.0.2.0/24"
Returns JSON with the validation state, matching VRPs, and the source trust anchor.
bgp.tools
Open https://bgp.tools/prefix/192.0.2.0/24 in a browser. The RPKI column shows a green shield for Valid, red for Invalid, or grey for Not Found. Allow 15 to 30 minutes after ROA creation for external tools to pick up the change.
RIPE Stat
Query the RPKI validation API:
curl -s "https://stat.ripe.net/data/rpki-validation/data.json?resource=AS213279&prefix=192.0.2.0/24" | python3 -m json.tool
Look for "status": "valid" in the response. RIPE Stat also shows which ROAs cover the prefix and whether maxLength matches.
Repeat for IPv6
Run the same checks with your IPv6 prefix. Every command above accepts IPv6 prefixes. Replace 192.0.2.0/24 with 2001:db8::/48 and verify both address families are covered.
Why should you avoid setting maxLength longer than your prefix?
Set maxLength equal to your prefix length. This is the recommendation from RFC 9319 (which updates and extends RFC 7115).
When you set maxLength to /24 on a /20 ROA, you authorize your ASN to originate the /20 and every more-specific up to /24. That means 16 /24s are now covered. An attacker who hijacks one of those /24s with your ASN in the origin will pass RPKI validation as "Valid." The hijacked more-specific wins longest-match routing, and the ROA cannot help you because you authorized that length.
This is called a forged-origin sub-prefix hijack. Measurements from 2017 cited in RFC 9319 found that 84% of ROAs using maxLength were vulnerable to this attack.
Concrete example:
| ROA | What it authorizes |
|---|---|
192.0.2.0/20, maxLength /20 |
Only 192.0.2.0/20 from your ASN. Safe. |
192.0.2.0/20, maxLength /24 |
/20, /21, /22, /23, /24 from your ASN. Any /24 can be hijacked by spoofing your origin ASN. |
When is maxLength > prefix length acceptable? Only when you genuinely deaggregate in production (e.g., announcing both a /20 and specific /24s for traffic engineering) and you need each deaggregated prefix to validate. In that case, create individual ROAs for each announced prefix instead of one ROA with a wide maxLength. One ROA per announcement is the safest pattern.
DDoS mitigation exception: If you use a service like a scrubbing center that re-announces your more-specifics from your ASN, you may need maxLength to cover those prefixes. Document this exception and audit it regularly.
What happens when the RPKI cache goes down?
When Routinator stops or becomes unreachable, your router's behavior depends on the expire timer.
BIRD2 keeps the last known ROA table in memory for the duration of expire (default 7200 seconds / 2 hours). During this window, validation continues normally with stale data. After expiry, BIRD removes all ROA entries and every route falls back to "Not Found." No routes are rejected for being invalid, but no routes get the valid local-pref boost either.
FRR behaves similarly. The rpki expire_interval controls how long cached VRPs remain usable after the RTR connection drops.
Reduce the risk
Run a second Routinator instance or use a different validator (StayRTR, Fort) on a separate machine. Configure both as RTR sources.
BIRD2 supports multiple protocol rpki blocks:
protocol rpki rpki_backup {
roa4 { table roa_v4; };
roa6 { table roa_v6; };
remote 10.0.0.2 port 3323;
retry keep 90;
refresh keep 600;
expire keep 7200;
}
FRR supports multiple cache servers with different preferences:
rpki
rpki cache 127.0.0.1 3323 preference 1
rpki cache 10.0.0.2 3323 preference 2
exit
Lower preference values are tried first. FRR falls back to the secondary if the primary goes down.
Monitor Routinator health. Check systemctl status routinator and the HTTP API status endpoint with your monitoring system. Alert on VRP count drops (a sudden drop to zero means sync failed) and on RTR connection losses visible in journalctl -u routinator.
Troubleshooting
ROA shows "Not Found" after creation. Propagation takes 10 to 20 minutes. Routinator syncs every 10 minutes by default (refresh = 600). Force a sync restart: sudo systemctl restart routinator, then wait for the initial sync to complete.
birdc shows 0 entries in ROA table. Check birdc show protocols all rpki_routinator. If the state is not "Established," verify Routinator is running and listening on port 3323: ss -tlnp | grep 3323.
FRR "Unknown command" for rpki. The -M rpki flag is missing from /etc/frr/daemons or frr-rpki-rtrlib is not installed. Install the package, add the flag, restart FRR.
Routes not re-evaluated after ROA change. In BIRD2, add import table; to the BGP channel. In FRR, enable soft-reconfiguration inbound on the neighbor.
All routes show Invalid. Your ROA may have the wrong ASN or prefix. Double-check in the RIPE portal. Also verify your router's own ASN matches what you put in the ROA.
Next steps: combine RPKI validation with prefix-list and AS-path filters for defense in depth . Monitor RPKI invalid alerts for your prefixes with BGPalerter .
Copyright 2026 Virtua.Cloud. All rights reserved. This content is original work by the Virtua.Cloud team. Reproduction, republication, or redistribution without written permission is prohibited.
Ready to try it yourself?
Deploy your own server in seconds. Linux, Windows, or FreeBSD.
See VPS Plans