This is the vulnerability-reporting policy. For operational security guidance — deployment tiers, mTLS setup, gRPC authorization, and audit logging — see docs/SECURITY.md.
| Version | Supported |
|---|---|
| 0.x | Yes (current development) |
Report security vulnerabilities via GitHub private vulnerability reporting, which is enabled on this repository.
Do not open a public issue for security vulnerabilities.
- Remote crash (panic on malformed BGP input)
- Session hijacking or injection
- Denial of service via resource exhaustion
- Memory safety violations
- Authentication bypass (TCP MD5, GTSM)
- Acknowledgment: Within 48 hours
- Critical vulnerabilities (remote crash, session hijack): Patched and released within 72 hours of confirmation
- Other vulnerabilities: Patched in the next milestone release
Qualifying vulnerabilities are published as GitHub Security Advisories once a fix is released, and a CVE ID is requested for each through GitHub's GHSA-to-CVE assignment flow.
The wire decoder is the primary attack surface. It processes untrusted input from the network. It runs under continuous fuzzing in CI.
-
No panics on malformed input. Every input from the network is untrusted. A panic on malformed BGP data is a denial-of-service vulnerability.
-
No unbounded allocations. All channels are bounded. Per-peer prefix limits enforced at insertion. UPDATE attribute sizes enforced at decode time.
-
Commit-confirm state is authenticated local authority. Production writes v3 in durability order: secret-bearing normalized configuration at
<runtime_state_dir>/commit-confirm-v3-prior.toml, confidential provenance and file-identity metadata atcommit-confirm-v3-metadata.json, then the confidential config-adjacentcommit-confirm-locator.json. Metadata and the locator contain paths and digests, not raw TOML; the locator is the sole boot authority. All are bounded daemon-owned regular0600files, and a writer or present pending object requires daemon-owned real parents that are not group- or world-writable. Reads are descriptor-relative, no-follow, same-FD operations; unsafe, changed, oversized, or mismatched state fails closed before candidate contents are opened or candidate/backup mutation. Durable locator unlink plus parentfsyncis terminal; only later verified exact metadata/raw cleanup and pending-directoryfsynccan fail warning-only. Locator absence carries no v2/v3 authority; locator-free v1 remains boot authority. Startup retains the v2 locator and locator-free v1 readers without converting or dual-writing either format. -
No
unsafecode in shipped paths, with two scoped exceptions. Every workspace crate root carries#![deny(unsafe_code)], sounsafecannot enter a crate without a visible, reviewed opt-out. There are two:crates/transport'ssocket_optsmodule carries a documented#[allow(unsafe_code)]for socket-option FFI (TCP_MD5SIG,IP_MINTTL, TCP-AO) that has no safe Rust API. This is the onlyunsafein any library crate.- The daemon binary's deny is
cfg_attr-gated off when thejemalloc(the default) ordhat-heapfeature is on, because registering a#[global_allocator]is itself anunsafeconstruct. Building with--no-default-featuresre-arms the deny; the daemon writes nounsafeblocks of its own under either configuration.
Test and benchmark targets are separate compilation units and are not covered by a crate root's deny. Several carry justified
unsafe: allocation-trackingGlobalAllocwrappers used by the memory-profile receipts (crates/mrtbenches,crates/ribtests, the standalonebench/scaleharnesses) andlibcsocket/netns helpers in the privilegedcrates/evpn-linuxtests. None of that code ships in the daemon or in any published library crate. -
Structured errors, not strings. Every failure produces a machine-parseable event for forensic analysis.
rustbgpd is written in Rust, consistent with product-security guidance
published by CISA and partner agencies recommending memory-safe languages
for software that processes untrusted network input. The enforcement
mechanism is the crate-root #![deny(unsafe_code)] invariant described
under Design Principles above: no unsafe in shipped paths beyond the
two documented, scoped exceptions.
One concrete bound this pins: AS_PATH matching is length-bounded under
RFC 8654 Extended Messages. The policy engine matches the rendered path
string with no fixed-capacity per-ASN buffer anywhere in the match path;
crates/policy/tests/as_path_extended_message_bound.rs decodes a
maximum-size (65,533-byte) UPDATE and proves regex and length matching
see the full ~16,000-ASN path.
Four fuzz crates (crates/wire, crates/policy, crates/mrt,
crates/evpn) carry 17 fuzz targets covering the message decoders, the
policy frontend, MRT snapshot and warm-bundle readers, and EVPN parsing.
A nightly CI campaign (.github/workflows/fuzz.yml) runs every target
in each crate against tracked seed corpora and fails loudly if target
enumeration returns nothing. A ClusterFuzzLite workflow builds all
targets with AddressSanitizer for on-demand campaigns. The target
inventory is fail-closed: scripts/check_fuzz_target_inventory.py runs
in CI and fails when the inventory drifts from the reviewed list.
Integrators documenting rustbgpd's upstream open-source handling can point at:
- This coordinated disclosure policy, including the response timelines above.
- Private vulnerability intake via GitHub private vulnerability reporting, enabled on the repository.
- CVE assignment for qualifying vulnerabilities via the GHSA flow.
- Exact dependency inventory in the committed
Cargo.lockfor every tagged source tree. - MIT or Apache-2.0 licensing; software and release artifacts are provided as-is under those licenses.
- TCP MD5 (RFC 2385): Supported. Linux only.
- GTSM (RFC 5082): Supported. Configurable per peer.
- TCP-AO (RFC 5925): Supported for static neighbors and direct dynamic-prefix listener keys on Linux (ADR-0062), including ordered startup keyrings, fail-closed accepted-socket owned-union validation, and live API/CLI health. SIGHUP can append non-preferred successor keys to unchanged owners, then select the successor (observation-gated deprecation of the predecessor) and later delete deprecated/unselected keys across further SIGHUP generations. Key edits/reordering, selected or non-deprecated-key deletion, and protected-owner changes remain restart-required. Static-exact selection precedes dynamic longest-prefix-match; overlapping protected owners require directionally disjoint KeyIDs, AO/plaintext or AO/MD5 overlaps are rejected, and listener inventories are capped at 4,096 MKTs per address family.
- gRPC: Unix domain socket by default (local-only). TCP listeners
are opt-in via config. Per-listener bearer-token authentication is
available via
token_file. Native mTLS terminates in-process on TCP listeners via tonic + rustls/ring — configuretls_cert_file,tls_key_file, andtls_client_ca_filetogether on[global.telemetry.grpc_tcp](partial config is rejected at config load). SIGHUP re-reads credential bytes from unchanged startup-captured paths and publishes one atomic generation across all listeners before later config reconciliation. New RPCs, including on existing HTTP/2 connections, use the new bearer token; new TLS accepts use the new mTLS material, while existing streams and TLS connections survive. Listener, path, auth-mode, principal, role, and access changes remain restart-required. An mTLS proxy front-end (seeexamples/envoy-mtls/) remains a valid alternative for multi-host fan-out. Per-RPC authorization tiers (ADR-0064, enforced by default since v0.24.0) classify each method on top of the listener split; see docs/SECURITY.md and docs/grpc-method-inventory.md.
- Inbound connections from source addresses matching no configured static
neighbor and no
[[dynamic_neighbors]]range are dropped immediately after TCP accept. - Dynamic-neighbor admission is capped by
dynamic_neighbor_limit(restart-pinned); connections beyond the cap are dropped and counted. - An opt-in per-source accept-rate limiter (
[inbound_admission], ADR-0120, default off) token-buckets sources that match a[[dynamic_neighbors]]range, keyed by aggregated source address (per-host for IPv4, per-/64 for IPv6 by default; both configurable). Over-rate connections are dropped immediately after accept and counted inbgp_inbound_connections_dropped_total{reason="rate_limited"}. Tracking state is a fixed-capacity LRU table, so limiter memory is bounded regardless of offered load. Statically configured neighbor addresses are exempt so a flapping legitimate peer cannot lock itself out of re-establishment. - The message-processing path between peer sessions and the RIB uses bounded channels, so a fast or abusive peer parks on backpressure instead of growing daemon memory (see Design Invariant #3 in ARCHITECTURE.md for the channel policy).