HomeIncident library
Incident library

INCIDENT REPLAY · 2022-06-21

Cloudflare: policy terms reordered in 19 data centres

Policy terms reordered; a reject that matches everything moved ahead of the term advertising site-local prefixes.

Hand-built replaysNot classified (hand-built)Catalog confidence: high

A reconstruction, not the operator's network. Hammerhead's authors rebuilt the change from the public record as a small network of their own. No configuration here is the operator's. What follows is what the tool reported on that reconstruction; it is not a claim that the tool would have prevented the outage.

What happened

The summary at the top is Hammerhead's own one-line description of the replay; the sources below are the record. Hammerhead's catalog rates its confidence in this account high.

Sources

What the public record says

The network

  • 1. Nineteen data centres ran the architecture that failed. CF22, Background: "Over the last 18 months, Cloudflare has been working to convert all of our busiest locations to a more flexible and resilient architecture. In this time, we've converted 19 of our data centers to this architecture, internally called Multi-Colo PoP (MCP): Amsterdam, Atlanta, Ashburn, Chicago, Frankfurt, London, Los Angeles, Madrid, Manchester, Miami, Milan, Mumbai, Newark, Osaka, São Paulo, San Jose, Singapore, Sydney, Tokyo."
  • 2. It is a Clos fabric with a spine layer. CF22: "A critical part of this new architecture, which is designed as a Clos network, is an added layer of routing that creates a mesh of connections. ... This layer is represented by the spines in the following diagram."
  • 3. CF22-FIG1 draws, top to bottom: Internet; two routers labelled edge01 and edge02; three labelled spine01, spine02, spine03, each connected to both edges and not to each other; under each spine two switches labelled sw01 and sw02; under each switch a stack labelled Metals.
  • 4. Policies are ordered. CF22, Incident timeline and impact: "operators define policies which decide which prefixes ... are advertised to peers ..., or accepted from peers. These policies have individual components, which are evaluated sequentially. The end result is that any given prefixes will either be advertised or not advertised." CF22-FIG2 draws a "BGP Policy Evaluator" between "Network Prefix" and "Peers", containing "Term 1 / Match X → Accept", "Term 2 / Match Y → Accept", "Term n / Match Z → Reject".
  • 5. What the site-local prefixes are for. CF22: "These prefixes allow our metals to communicate with each other, as well as connect to customer origins."

The change

  • 6. Its purpose. CF22, Technical description: "we were rolling out a change to standardize the BGP communities we attach to a subset of the prefixes we advertise. Specifically, we were adding informational communities to our site-local prefixes."
  • 7. Its process. CF22: "a Change Request ticket was created, which includes a dry-run of the change, as well as a stepped rollout procedure. Before it was allowed to go out, it was also peer reviewed by multiple engineers. Unfortunately, in this case, the steps weren't small enough to catch the error before it hit all of our spines."
  • 8. What it looked like on a router that was NOT a spine ("The change looked like this on one of the routers"), four stanzas of this shape:
[edit policy-options policy-statement 4-COGENT-TRANSIT-OUT term ADV-SITELOCAL then]
+      community add STATIC-ROUTE;
+      community add SITE-LOCAL-ROUTE;
+      community add TLL01;
+      community add EUROPE;

and the same four lines under 4-PUBLIC-PEER-ANYCAST-OUT, 6-COGENT-TRANSIT-OUT and 6-PUBLIC-PEER-ANYCAST-OUT, each in term ADV-SITELOCAL then. CF22: "This was harmless, and just added some additional information to these prefix advertisements."

  • 9. What it looked like on the spines ("The change on the spines was the following"):
[edit policy-options policy-statement AGGREGATES-OUT]
term 6-DISABLED_PREFIXES { ... }
!    term 6-ADV-TRAFFIC-PREDICTOR { ... }
!    term 4-ADV-TRAFFIC-PREDICTOR { ... }
!    term ADV-FREE { ... }
!    term ADV-PRO { ... }
!    term ADV-BIZ { ... }
!    term ADV-ENT { ... }
!    term ADV-DNS { ... }
!    term REJECT-THE-REST { ... }
!    term 4-ADV-SITE-LOCALS { ... }
!    term 6-ADV-SITE-LOCALS { ... }
[edit policy-options policy-statement AGGREGATES-OUT term 4-ADV-SITE-LOCALS then]
community delete NO-EXPORT { ... }
+      community add STATIC-ROUTE;
+      community add SITE-LOCAL-ROUTE;
+      community add AMS07;
+      community add EUROPE;
[edit policy-options policy-statement AGGREGATES-OUT term 6-ADV-SITE-LOCALS then]
community delete NO-EXPORT { ... }
+      community add STATIC-ROUTE;
+      community add SITE-LOCAL-ROUTE;
+      community add AMS07;
+      community add EUROPE;
  • 10. How to read it. CF22: "In this diff format, the exclamation marks in front of the terms indicate a re-ordering of the terms. In this case, multiple terms moved up, and two terms were added to the bottom. Specifically, the 4-ADV-SITE-LOCALS and 6-ADV-SITE-LOCALS terms moved from the top to the bottom. These terms were now behind the REJECT-THE-REST term, and as might be clear from the name, this term is an explicit reject:"
term REJECT-THE-REST {
    then reject;
}
  • 11. The summary sentence. CF22: "While deploying a change to our prefix advertisement policies, a re-ordering of terms caused us to withdraw a critical subset of prefixes."

The failure

  • 12. The direct effect. CF22: "As this term is now before the site-local terms, we immediately stopped advertising our site-local prefixes, removing our direct access to all the impacted locations, as well as removing the ability of our servers to reach origin servers."
  • 13. The secondary effect. CF22: "the removal of these site-local prefixes also caused our internal load balancing system Multimog (a variation of our Unimog load-balancer) to stop working, as it could no longer forward requests between the servers in our MCPs. This meant that our smaller compute clusters in an MCP received the same amount of traffic as our largest clusters, causing the smaller ones to overload."
  • 14. Scope. CF22: "Even though these locations are only 4% of our total network, the outage impacted 50% of total requests."
  • 15. Timeline, all UTC, from CF22: - "03:56 UTC: We deploy the change to our first location. None of our locations are impacted by the change, as these are using our older architecture." - "06:17: The change is deployed to our busiest locations, but not the locations with the MCP architecture." - "06:27: The rollout reached the MCP-enabled locations, and the change is deployed to our spines. This is when the incident started, as this swiftly took these 19 locations offline." - "06:32: Internal Cloudflare incident declared." - "06:51: First change made on a router to verify the root cause." - "06:58: Root cause found and understood. Work begins to revert the problematic change." - "07:42: The last of the reverts has been completed. This was delayed as network engineers walked over each other's changes, reverting the previous reverts, causing the problem to re-appear sporadically." - "08:00: Incident closed."
  • 16. Getting back in was hard. CF22: "Due to this withdrawal, Cloudflare engineers experienced added difficulty in reaching the affected locations to revert the problematic change. We have backup procedures for handling such an event and used them to take control of the affected locations."
  • 17. It was not an attack. CF22: "This was our error and not the result of an attack or malicious activity."

What Cloudflare said it would change

  • 18. Process. CF22: "While we did use a stagger procedure for this change, the stagger policy did not include an MCP data center until the final step. Change procedures and automation need to include MCP-specific test and deploy procedures".
  • 19. Architecture. CF22: "Ultimately the policy statement that caused the incorrect routing advertisement will be redesigned to prevent an unintentional incorrect ordering."
  • 20. Automation. CF22: "we will be concentrating on automation improvements that enforce an improved stagger policy for rollouts of network configuration and provide an automated "commit-confirm" rollback."

What the reconstruction assumed

Decided by Hammerhead's authors to build a network that runs. None of it describes the operator's network.

None of the following is on the public record. Each is a choice we made so that the corpus parses, converges and runs in seconds. If one of them is wrong, the replay is wrong in that respect.

  • A1. Vendor and syntax of the spines and edges. CF22 never names a vendor. Its excerpts use [edit policy-options policy-statement ...], term, then reject; and community add, which is Junos configuration syntax, so the spines, the edges and the stand-in transit router are written in Junos brace form. That is an inference from syntax, not a statement by Cloudflare.
  • A2. Every term body. CF22 prints the term NAMES and elides every body except REJECT-THE-REST (fact 10) and the community delete NO-EXPORT line inside the two site-local terms (fact 9). What each term matches is ours: every IPv4 term matches one prefix-list, every 6- term matches one IPv6 route-filter. The names, the two orders and then reject; are the only parts of AGGREGATES-OUT that come from the record.
  • A3. The "before" order. CF22 prints the order AFTER the change and says the two site-local terms "moved from the top to the bottom" (fact 10). 6-DISABLED_PREFIXES carries no ! marker, so we leave it first and put 4-ADV-SITE-LOCALS, 6-ADV-SITE-LOCALS directly after it, ahead of the product terms. Their exact original position among "the top" is not stated.
  • A4. `after/` is the reorder and nothing else. The real commit also added four community add lines to each site-local term (fact 9). We leave them out of after/ so that the only difference between the snapshots is term order, and we run the additions alone as a control (generate.py --variant intended, see RESULT.md). Community names STATIC-ROUTE, SITE-LOCAL-ROUTE and EUROPE in that control are CF22's; their numeric values, and POD0n in place of the site code, are ours.
  • A5. Topology. Two edges, three spines, that naming, and the edge-to-every-spine wiring are CF22-FIG1. We draw one switch per spine where the figure draws two, name them s1-sw01 to s3-sw01 because hostnames must be unique in a snapshot, and model the metals as a connected subnet on the switch, not as devices. transit01 stands in for the cloud labelled "Internet"; one router with one "customer origin" subnet is ours.
  • A6. Switch vendor. CF22 says nothing about the switches. They are written in Arista EOS syntax for a modelling reason, not a historical one: the anycast service prefixes have to be originated in every pod, the topology builder joins devices that have interfaces in the same subnet into one segment (RESULT.md, "Odd things", item 5), and this build does not originate a Junos static route into BGP (RESULT.md, gap G3). EOS network statements over Null0 statics avoid both. ip routing is omitted from those files because the EOS parser leaves it unclassified (RESULT.md, "Odd things").
  • A7. BGP layout. CF22 does not say which sessions exist or whether they are internal or external. We run eBGP on every link with one private AS per tier or pod (edges 65000, spines 65101 to 65103, switches 65201 to 65203, transit 64496), no IGP, and no route reflection. AGGREGATES-OUT is the spines' export policy toward the edges; that it faces the edges is our reading of "stopped advertising" plus the figure, not a quotation. The edges' 4-TRANSIT-OUT borrows the term name ADV-SITELOCAL from fact 8, which CF22 shows on a non-MCP router.
  • A8. What "site-local" contains. Fact 5 says the prefixes serve metal-to-metal and metal-to-origin traffic; fact 12 says losing them removed "our direct access to all the impacted locations". We therefore put three things in each pod's site-local list: the metals subnet, the spine loopback and the switch loopback. This choice decides whether the reachability sweep sees the failure, because the sweep probes one loopback per device and never a metals subnet. RESULT.md runs the corpus both ways (--variant no-infra-loopbacks) and reports both.
  • A9. How the site-locals reach the spine. The community name STATIC-ROUTE (fact 9) hints that the real ones were static routes. In the committed corpus the switch announces them to the spine over eBGP and the spine's term matches them by prefix-list, because that is a shape this build evaluates. The static shape is run as a variant (--variant static-origin) and its result is in RESULT.md.
  • A10. Addresses. Everything is from documentation or private space: anycast service prefixes are /27s of 192.0.2.0/24, site-locals are /26s and /32s of 198.51.100.0/24, the customer origin is 203.0.113.0/24, links and the remaining loopbacks are 10.0.0.0/8, IPv6 is 2001:db8::/32. Real anycast prefixes are not /27s. No address here is Cloudflare's.
  • A11. IPv6. The 6- terms are present, in CF22's order, and inert: this build models no IPv6 forwarding, and an IPv6 entry inside a Junos prefix-list is a fatal parse error (RESULT.md), so the terms match with route-filter and no interface carries an IPv6 address.
  • A12. Not modelled at all. Timing, the staggered rollout (facts 7, 15, 18), the load balancer and the overload of small clusters (fact 13), the loss of management access as an operational event (fact 16), and engineers reverting each other (fact 15). The simulator computes converged routing state for two snapshots and nothing else.

What Hammerhead said

Each run below is pinned by Hammerhead's replay tests at commit 0ee9732: the verdict and the quoted text are checked against the tool's output, and a run that disagrees fails the test. Reason lines are copied verbatim; a pinned line can be part of a longer reason. Every preflight run uses --max-blast-radius 100, so a change is never counted as caught merely because it touched many devices of a small network.

Hammerhead's own first-versus-now record

First verdict
fail on lost flows (truthful), but five variants of the same reorder passed, and no verdict mentioned BGP policy.
Verdict now
fail, now also naming the moved match-all term on each spine
Fixed?
Yes for the change itself. The structural check, Junos next term, route-filter, from protocol and community delete (which acted as replace) are fixed.

Quoted from docs/replays/README.md. “First” is the tool before the fixes this replay prompted.

This replay's runs are pinned in Hammerhead's hand-written Rust tests rather than a manifest, so the record above is the only account quoted here.

What changed in Hammerhead because of it

From the “What was fixed” table in Hammerhead's replay notes, where each fix landed after a failing test. “Found by” is copied as written; a family name means the batch this incident belongs to.

External advertisement gate (--allow-advertisement-changes)

A prefix withdrawn from a peer outside the snapshot loses no modeled flow. New announcements are reported, never gated.

Found by: Facebook, Cloudflare 2022

Junos next term

Was a no-op, so a tag-and-continue term accepted the route and skipped every later reject.

Found by: Cloudflare 2022

Policy-widening gate (--allow-policy-widening)

A clause that lost its last match, or a match-all clause that moved earlier.

Found by: Cloudflare 2020, 2022

Junos from protocol and origination through it

from protocol static|direct|ospf was never true and origination followed only prefix-list matches, so a Junos site written the usual way (from { protocol static; route-filter ...; } then accept) announced NOTHING in the model and the advertisement gate was blind on it. The export lift now originates from statics as well as connected and OSPF routes, follows route-filter and from protocol, and records the protocol each prefix came from so the policy engine can decide the match. A bare then accept still originates nothing.

Found by: Cloudflare 2022, CenturyLink

Junos route-filter

The qualifier was dropped and the prefix looked up as an ACL name, so every route-filter term matched nothing at 100% coverage.

Found by: Cloudflare 2022, CenturyLink

Junos aggregates, community delete, well-known communities

routing-options aggregate was not parsed, so a site announcing only a summary announced nothing; community delete replaced every other tag and added the one it meant to remove; NO_EXPORT / NO_ADVERTISE / NO_EXPORT_SUBCONFED had no effect. Each now does what the device does.

Found by: Cloudflare 2022, CenturyLink

Limits

  • The configurations are a reconstruction. Where the record is silent, the assumptions above fill the gap; if one is wrong, the replay is wrong in that respect.
  • A verdict describes the reconstructed network's converged state. Timing, traffic load, and anything a device does that no configuration states, such as software reacting to a health check or a firmware bug, are outside it.
  • A pass is reported as a pass. A fail says a gate fired on the reconstruction, nothing more.

Not fixed: Junos aggregate modifiers

routing-options aggregate { route X; } originates, but modifiers inside the braces change what the announcement carries and stay unparsed.

Recorded against: Cloudflare 2022, CenturyLink

Imported from docs/replays/cloudflare-2022/FACTS.md and the replay manifests in the Hammerhead repository at commit 0ee9732.