RPKI ROA Setup for BGP: Create ROAs, Validate Routes in BIRD2 and FRR

12 min read·Matthieu·RPKIROABGPBIRD2FRRRoutinatorRIPE NCCIPv6|

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.

  1. Click Create ROA (or + New ROA in the ROA tab).
  2. Set Origin ASN to your AS number (e.g., AS213279).
  3. Set Prefix to your IPv4 allocation (e.g., 192.0.2.0/24).
  4. Set Maximum Length equal to the prefix length (/24). Do not increase it. See the maxLength section below.
  5. Click Publish.
  6. Repeat for your IPv6 prefix (e.g., 2001:db8::/48 with 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