RPKI and BGP Security

A route hijack is not an attack on BGP. It is BGP working exactly as specified, because the protocol has no idea who owns a prefix. RPKI is the fix, and in the router this module reads it fails open twice: with no cache configured, validation is simply off, and with a stale one every prefix comes back in a fourth state the RFC never names. There is no enforce switch. Enforcement is a route-map you write.

Launch Simulator →

What you'll explore

  1. 01

    A Hijack Is Just a Better Route

    The word hijack implies a break-in, and that framing is the main obstacle to understanding the problem. Nothing is broken into. BGP carries reachability claims and has no field, no check and no concept for authority over a prefix: an announcement is simply believed because a neighbour sent it, and when two announcements cover the same address space the more specific one wins on longest-prefix match without any comparison of the senders. An operator who announces someone else's /24 out of their /16 has not exploited a flaw; they have used the protocol as designed, and every router downstream has behaved correctly. That is why the fix had to be bolted on from outside rather than patched in, and why the bolt-on has the shape it does. Now the part that is genuinely surprising when you read the router rather than the explainer. Enabling RPKI does not, by itself, change anything about which route wins. FRR's own documentation states it plainly: 'By default, enabling RPKI does not change best path selection. In particular, invalid prefixes will still be considered during best path selection' (doc/user/rpki.rst:41-42). The best-path comparator in this tree never consults RPKI at all — validation state reaches the decision only through the route-map hook, where route_match() calls rpki_validate_prefix() and compares the result against the state the rule was compiled for (bgpd/bgp_rpki.c:247-262, with route_match_compile() at :264-278). So RPKI is a source of a MATCH, and what you do on that match is policy you write. There is no enforce mode: the only lever is a route-map clause, and what you do on the match is policy you write rather than a setting you select. The next sentence of that same documentation says 'However, the router can be configured to ignore all invalid prefixes' (:43) — and the documentation then lists no such command, anywhere in its own reference. A grep for an RPKI best-path flag across bgpd returns nothing: no BGP_FLAG, no bestpath sub-command, no global drop switch. The only lever that exists is the route-map, and the worked example the documentation itself provides does not drop anything — it permits invalid routes and sets their local-preference to 10, keeping them eligible and merely unattractive (:168-190). That is a real posture and a defensible one, but it is not enforcement, and a deployment that copied the documented example while reporting 'RPKI enforced' upstream would be wrong in a way no configuration audit keyed on the word rpki would catch. This is why the first control in this module offers three POSTURES rather than three values of a setting. Disabled is not a mode you select; it is what you get when no cache server is configured, and start() says so in its own words before returning an error (:930-933). Monitor is FRR's default behaviour with a working cache and no policy: every prefix is validated, the result is available to show commands and route-maps, and nothing is filtered. Enforce is a clause you author — match rpki invalid, then deny — and its absence from the source is the point, because it means the question 'is RPKI enforced here' can only be answered by reading route-maps, never by reading whether RPKI is on. Two consequences follow for anyone debugging a hijack in progress. First, 'we have RPKI' and 'that announcement would have been rejected' are independent facts, and the gap between them is where most of the real-world deployment sits. Second, because the decision is a route-map match rather than a protocol rule, it is evaluated per neighbour and per address family like any other policy — so RPKI can be enforced on one session and ignored on the next, in the same router, with no warning that the two disagree.

  2. 02

    Three Names, Four States

    RFC 6811 defines three validation outcomes and everyone teaches those three: valid, invalid, not found. The implementation has four, and the fourth is RPKI_NOT_BEING_USED: enum rpki_states is RPKI_NOT_BEING_USED, RPKI_VALID, RPKI_NOTFOUND, RPKI_INVALID (bgpd/bgp_rpki.h:13-18), and the declaration order matters because it puts the extra state at zero — which is how it becomes the value returned by a bare 'return 0' on a guard that failed, in a function whose other exits use the named constants. Start with the three you expect, and be precise about what separates them, because the difference between two of them is the whole of this module's practical advice. VALID means a ROA covers this prefix and both of its conditions hold: the announcing origin AS matches the ROA's ASN, and the announced length is within the ROA's authorised range. INVALID means a ROA covers the prefix and one of those conditions fails — wrong origin AS, or a length longer than the ROA allows. NOT FOUND means no ROA covers the prefix at all. The asymmetry worth internalising is that invalid is a POSITIVE assertion of a conflict, while not-found is an absence of information: it does not distinguish a prefix whose holder never registered a ROA from one that was never allocated. That is why enforcement on not-found is a different and much more dangerous proposition than enforcement on invalid, and why no sensible policy treats them alike even though both are 'not valid'. The router's view of a ROA is strikingly thin, and reading it dispels a lot of mystique. print_record() formats exactly three things: the prefix, a min_len to max_len range, and the origin ASN (bgpd/bgp_rpki.c:456-470); pfx_record_to_prefix() takes the prefix length from min_len (:582); and the show-prefix path filters candidate records with 'record->max_len >= prefix->prefixlen' plus an ASN comparison (:2259-2260). A tuple, a range, a number. No signature reaches this code, which is exactly why the RTR protocol can be plain TCP and why the trust in the whole system sits in the cache server rather than in the router. Then the fourth state, and the reason it exists. rpki_validate_prefix() (:1142) is mostly guards. If there is no bgp instance it returns 0 (:1153); if there is no VRF, 0 (:1157); if there is no RPKI instance for that VRF or it is not yet synchronised, 0 again (:1164-1165) — and zero is RPKI_NOT_BEING_USED. The origin AS is then extracted, and this is worth following because it is where a subtlety hides: with no AS_PATH the route came from iBGP and the router's own AS is used, the rightmost ASN of the last AS_SEQUENCE segment is taken as the origin in the normal case (:1181-1182), a confederation segment again substitutes the local AS (:1183-1186), and anything else returns RPKI_NOTFOUND with a comment citing the RFC's 'distinguished value NONE' (:1187-1190). An unsupported address family returns RPKI_NOT_BEING_USED (:1207-1208), rtr_mgr_validate() is finally called (:1212-1213), and a result outside the three known enums falls through to RPKI_NOT_BEING_USED one last time (:1232-1238). So the fourth state is not a validation outcome at all. It is the state that means no validation happened, and every path to it is a plumbing failure rather than a routing fact. That distinction is the single most useful thing in this module, because of how the consumers treat it: as no opinion. The OVS extended community is attached only when the state is not RPKI_NOT_BEING_USED (bgpd/bgp_route.c:3085-3091), and the show paths print validation state only under the same condition (:12876-12883). A prefix that was never validated therefore looks, everywhere you would go to check, exactly like a prefix with nothing wrong with it. One curiosity for anyone reading the function alongside this text: the synchronisation check appears twice in a row, and the second copy is unreachable — :1164-1165 has already returned by the time :1167-1168 could run. It is harmless, and it is a good reminder that source is written by people under deadlines, which is precisely why claims about it should be read rather than assumed.

  3. 03

    Flat Tuples Over a Socketpair

    The architecture has three tiers and the router is the thinnest of them. Certificate authorities publish resource certificates and signed ROA objects; relying-party software fetches, validates and flattens those into prefix-to-ASN tuples; the router subscribes to the result over the RTR protocol and looks things up. FRR configures the subscription as one or more cache servers with an explicit failover preference — 'rpki cache tcp <HOST> <PORT> [source A.B.C.D] preference (1-255)' and an SSH variant alongside it (bgpd/bgp_rpki.c:1919 and :1974) — which get_groups() turns into rtrlib's group structures (:503) before start() hands them to rtr_mgr_init() together with the three timers (:950-953). The timers are where published advice and running code part company, so take the real numbers from the source. POLLING_PERIOD_DEFAULT is 3600, EXPIRE_INTERVAL_DEFAULT is 7200, RETRY_INTERVAL_DEFAULT is 600 (:58-60), assigned when the instance is allocated (:838-840) and restored when the configuration is removed (:1731-1733). The CLI accepts polling_period (1-86400) (:1786), expire_interval (600-172800) (:1829) and retry_interval (1-7200) (:1877), and one ordering rule is enforced at set time: an expire interval shorter than the polling period is refused with 'Expiry interval must be polling period or larger' (:1844-1849), which is the code preventing you from configuring a cache that expires its data before it would next ask for more. Now the documentation divergence, because it bites exactly the number people reach for first. doc/user/rpki.rst:114-119 documents polling_period as (1-3600) with a default of 300 seconds. The code says (1-86400), default 3600. Both the bound and the default are wrong in the doc, at this same commit, while the expire (:121-125) and retry (:127-132) entries match the code precisely — so this is a stale line rather than a systematically unreliable document, which is the more dangerous kind, since nine correct neighbours lend it credibility. The code is the authority here, and this module's freshness control spans 0 to 7200 for that reason: three breakpoints, each a real constant. At 600 seconds the cache is past RETRY_INTERVAL_DEFAULT but not yet due a scheduled refresh, and the data is still perfectly valid — note what that age does NOT prove, because the retry interval governs retries after a failed sync rather than establishing one: it is equally consistent with a healthy cache part-way through its polling period and with one retrying a failure. At 3600 a refresh is overdue by one full polling period. At 7200 the data expires. Treating one hour as the moment of crisis stops a full refresh period short of anything actually happening. The transport between the RTR thread and the routing daemon is the detail that decides what happens at scale, and it is a socketpair. rpki_init_sync_socket() creates a PF_LOCAL SOCK_DGRAM pair (:794); rpki_update_cb_sync_rtr() write()s one pfx_record per callback (:781-782); and when that write returns EAGAIN or EWOULDBLOCK it sets an atomic overflow flag (:783-785, the flag declared at :103). While that flag is set the producer returns early without writing at all (:776-779) — so records arriving inside the overflow window are DISCARDED, not queued. On the other side, bgpd_sync_callback() normally reads a single record per event and revalidates that one prefix (:697-706), but with the flag set it drains the socket in a loop, revalidating each record it manages to read, logs 'Socket overflow detected (%zu), revalidating affected prefixes' and clears the flag (:674-694). Read those two halves together and the behaviour at full-table scale is specific: a large cache push degrades incremental, per-prefix revalidation into a bulk drain, and some updates are dropped on the floor to make that possible. The prefixes they described are not revalidated until something else triggers it. That is the citable scale story, and it is a more useful one than a memory figure for a validator, because it is a correctness consequence rather than a capacity-planning one — the router can be quietly out of step with the cache it is connected to, and nothing in the routing table says so.

  4. 04

    Origin Only, and the Protocol That Is Not in the Router

    Everything in this module so far validates one thing: the ORIGIN. rpki_validate_prefix() extracts a single AS number — the rightmost ASN of the last AS_SEQUENCE segment (bgpd/bgp_rpki.c:1181-1182) — and asks whether that AS is authorised to announce this prefix. Nothing else about the path is examined. Every AS between you and that origin could be fabricated, reordered or omitted, and a valid ROA would still return RPKI_VALID, because the only claim RPKI makes is about the announcement's far end. That gap is the whole motivation for BGPsec, which RFC 8205 specifies as per-hop cryptographic signing: each AS signs its own addition to the path, so a receiver can verify the sequence of ASes rather than merely its last element. And BGPsec is not in this router. A case-insensitive grep for bgpsec across the entire sparse checkout — bgpd/, lib/, ospfd/ — returns zero hits: no implementation, no stub, no comment, no configuration knob. That is a negative citation, reproducible in one command, and it deserves to be stated as flatly as any positive one, because the deployment literature discusses BGPsec in a register that implies availability. The same sweep settles the other mechanism usually named in the same breath: ASPA has zero occurrences here, and ASPA is additionally not an RFC at all — it exists as IETF SIDROPS drafts — so a claim about how a router behaves with ASPA enabled could not be cited from anywhere in this project. What FRR does implement for path security is RFC 9234, and it is a genuinely different idea: instead of proving the path cryptographically, constrain it by relationship. Each session is configured with a role — ROLE_PROVIDER, ROLE_RS_SERVER, ROLE_RS_CLIENT, ROLE_CUSTOMER, ROLE_PEER, plus ROLE_UNDEFINED at 255 (bgpd/bgpd.h:1602-1607) — and routes carry an Only-To-Customer attribute parsed by bgp_attr_otc() (bgpd/bgp_attr.c:4036-4065, which rejects a zero ASN at :4055). On egress, bgp_otc_egress() stamps the attribute (bgpd/bgp_route.c:2052-2066, invoked at :2784); on ingress, bgp_otc_filter() rejects a route whose OTC marking is inconsistent with the configured relationship (:2033-2050), and the rejection surfaces with the reason string 'failing otc validation' (:5966-5967). Note the condition carefully, because it is where the mechanism's limits live: the ingress check fires when 'peer->local_role == ROLE_PEER && attr->otc != peer->as' (:2039). Role configuration is what gives the attribute meaning; leave the role unset and OTC is inert, present on the wire and enforcing nothing. So the honest summary of path security in this router is: origin validation that covers one AS, plus a relationship-based leak check that works only where somebody declared the relationships, plus an absence where the cryptographic answer would go. There is one more mechanism worth knowing because it is routinely mistaken for propagation of validation state. RFC 8097 defines an Origin Validation State extended community, and FRR attaches it — but only for iBGP peers and EBGP-OAD peers, only when the PEER_FLAG_SEND_EXT_COMMUNITY_RPKI flag is set, and only when the state is not RPKI_NOT_BEING_USED (bgpd/bgp_route.c:3081-3095). The community is non-transitive by design. Validation state therefore does not cross an eBGP boundary: your neighbour's 'valid' is not evidence of anything, and every AS that wants origin validation must perform it itself against its own cache. That is not a limitation of FRR's implementation, it is the design — and it is the reason the answer to 'why must everyone filter' is a property of the attribute rather than a matter of diligence.

  5. 05

    Fails Open, and the Only Fail-Closed Knob Refuses to Peer

    Collect the failure paths from the rest of this module into one place and a pattern appears that should change how you monitor RPKI. It fails open, silently, at every level. At the packaging level it is not even present by default: RPKI is a loadable module, FRR's documentation notes that 'You must install frr-rpki-rtrlib additional package for RPKI support, otherwise bgpd daemon won't startup' (doc/user/rpki.rst:57-58), and bgpd must be launched with '-M rpki' in bgpd_options or the rpki configuration commands do not exist (:73-80). At the configuration level, entering the rpki block achieves nothing on its own — 'Executing this command alone does not activate prefix validation' (:69) — and start() confirms it from the other side, logging 'No caches were found in config. Prefix validation is off.' and returning an error when the cache list is empty (bgpd/bgp_rpki.c:930-933). At runtime, rpki_validate_prefix() returns before validating anything while the cache is not synchronised (:1164-1165), and rtr_is_synced is set in exactly one place, by sync_expired() once rtr_mgr_conf_in_sync() reports true, re-arming itself every BGP_RPKI_CACHE_SERVER_SYNC_RETRY_TIMEOUT seconds — 3 — until it does (:61, :901-916). FRR documents the intent explicitly: after a connect timeout 'the router will process routes without prefix origin validation' (rpki.rst:36-39). And at the consumption level, as the validation section set out, RPKI_NOT_BEING_USED is treated as no opinion by every consumer (bgpd/bgp_route.c:3085-3091, :12876-12883). Stack those together and the operational conclusion is uncomfortable: a router with RPKI configured, a dead cache server and a hijacked prefix in its table looks identical, from every show command that reports validation state, to a router with RPKI configured and nothing wrong. There is exactly one mechanism in this tree that fails closed, and it does not drop routes — it declines to have the session at all. 'neighbor PEER rpki strict' sets PEER_FLAG_RPKI_STRICT, default disabled (rpki.rst:161-166), and the FSM checks it: if the flag is set and bgp_rpki_cache_connected() is false, bgpd logs 'RPKI strict mode enabled, but RPKI cache is not connected', records PEER_DOWN_RPKI_DOWN and returns BGP_FSM_FAILURE (bgpd/bgp_fsm.c:2510-2518). That is a real guarantee and a blunt one: rather than accept unvalidated routes, the router has no peer. Worth noticing where it lives — in the finite state machine, not in validation — because it means the trade it offers is reachability against validation, with no middle setting. And it has a subtlety that is easy to miss and important to state: bgp_rpki_cache_connected() is only a hook dispatch (bgp_fsm.c:49, :51-56, declared at bgp_fsm.h:173), resolved by bgp_rpki_is_connected() inside the loadable module (bgp_rpki.c:550). With the rpki module not loaded there is no subscriber, so the strict check cannot fail closed — the one fail-closed mechanism depends on the module whose absence is the first failure mode in the list. What to take from this into your own network. Monitor the cache session, not the configuration: the configuration is static and will keep claiming RPKI is on long after validation stopped happening. Alert on the synchronisation state and the age of the last successful refresh, since those are the quantities that decide whether any judgement is being made, and remember that a stale cache is not a source of false positives — it is a source of silence. Decide deliberately between monitor and a dropping route-map, and write down which one you chose, because nothing in the running config distinguishes them at a glance. And treat strict mode as the deliberate, narrow tool it is: correct when a session must never carry unvalidated routes, wrong as a default, and dependent on a module being loaded for its guarantee to mean anything at all.

Validated against ONE pinned tree, and the choice of which one is the first thing to understand about this module. RPKI is usually explained from the validator's side — Routinator, OctoRPKI, rpki-client — and that is the half this module deliberately does NOT teach, because no validator is pinned in this project and an uncited implementation claim is worth nothing. What is pinned is FRRouting frr-10.6.1 at commit 71da51b, and FRR implements RFC 6811 prefix origin validation itself in bgpd/bgp_rpki.c, 2356 lines of it. So every claim here is cited to the ROUTER THAT ACTS on RPKI rather than to the validator that computes it, which is also the more useful half: the router is the thing in your network, and the router is where the surprising behaviour lives. That split is not a convenience, it is the architecture. THE ROUTER NEVER SEES A SIGNATURE. RPKI's cryptography — the resource certificates, the CMS-signed ROA objects, the manifests — is done entirely by a relying-party cache server, which hands the router a flat table of tuples over the RTR protocol. You can read the shape of that tuple straight off FRR's own printer: a prefix, a min_len to max_len range, and an origin ASN (print_record, bgpd/bgp_rpki.c:456-470). No key, no certificate, no signature, nothing to verify. The router's whole contribution is a lookup. Which leads to the second boundary, and it is a dependency boundary rather than a layer boundary: THE LOOKUP PRIMITIVE IS NOT IN THIS TREE EITHER. rtr_mgr_validate(), rtr_mgr_init() and struct pfx_record all come from rtrlib, an external library FRR links and Debian packages separately as frr-rpki-rtrlib — without which, FRR's own documentation notes, bgpd will not even start (doc/user/rpki.rst:57-58). Every citation below is therefore FRR's CALL SITE or FRR's own surrounding logic, never rtrlib's internals, the same discipline this library applies to the strongSwan control-plane / kernel-XFRM data-plane split. Three further things about this tree are worth stating before any number below is read. First, THE DOCUMENTATION CONTRADICTS THE CODE AT THIS SAME PIN, in two places and in two different ways. doc/user/rpki.rst:114-119 documents 'rpki polling_period (1-3600)' with a default of 300 seconds; the code says (1-86400) with POLLING_PERIOD_DEFAULT 3600 (bgpd/bgp_rpki.c:1786 and :58) — both the range and the default disagree, while the expire and retry rows in the same doc do match. And rpki.rst:41-43 asserts that 'the router can be configured to ignore all invalid prefixes' while listing no such command anywhere in its own command reference, and the source carries no RPKI best-path flag at all. The code wins both times — a running implementation outranks its own manual — and where the two differ this module says so rather than quietly picking one. Second, TWO FEATURES THE EPIC PROMPT ASKED FOR ARE IN NO PINNED TREE. Case-insensitive greps of the whole sparse checkout — bgpd/, lib/, ospfd/ — return ZERO hits for bgpsec and zero for aspa as a word. BGPsec therefore appears here as a protocol specified by RFC 8205 and as an absence verified in source, never as an implementation; and ASPA, which has no RFC at all and only IETF SIDROPS drafts, was replaced by the path-security mechanism FRR actually ships: RFC 9234 BGP roles with the Only-To-Customer attribute (bgp_otc_filter, bgpd/bgp_route.c:2033-2050). Third, NOTHING ABOUT ANY REAL INCIDENT IS IN THIS TREE. The 2018 redirection of Amazon's Route 53 prefixes is a documented external event, cited as history and never as source; what the tree can tell you is the mechanism — that a more-specific announcement wins on longest-prefix match whatever its origin, and that whether a ROA makes such an announcement invalid turns entirely on its max_len. frr-10.6.1 @ 71da51b (bgpd RFC 6811 origin validation, RTR cache plumbing, RFC 8097 OVS ext-community, RFC 9234 roles/OTC) — bgpd/bgp_rpki.c and friends. Every simulated behavior cites a real source function, and a server-side correlation engine computes how each layer's state cascades into the next. See the methodology →

This module is part of zerohop Pro — it unlocks alongside the full kata library, progress tracking, and weak-area analysis.