This is the vulnerability-reporting policy. For operational security guidance — deployment tiers, mTLS setup, gRPC authorization, and audit logging — see docs/reference/security.md.
| Version | Supported |
|---|---|
| 0.x | Yes (current development) |
Daemon security support follows the Linux-only platform support contract.
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-adjacent<config>.commit-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 v3 authority. A retired v2 locator or locator-free v1/v2 journal makes v0.65.0 and every later release refuse boot before candidate mutation, and remains untouched for recovery with rustbgpd v0.64.0. -
No
unsafecode in shipped paths outside three scoped modules. Every workspace crate root carries#![deny(unsafe_code)], sounsafecannot enter a crate without a visible, reviewed opt-out. Three modules hold one:crates/transport'ssocket_optsmodule, opted out by a module-level#[allow(unsafe_code)]in the crate root and a per-function#[allow(unsafe_code, reason = ...)]on each site inside it. Every site is Linux socket-option ABI with no safe Rust wrapper:TCP_MD5SIG/TCP_MD5SIG_EXT, TCP-AO key installation, deletion, inspection and RNext selection, GTSM (IP_MINTTL/IPV6_MINHOPCOUNT), and thesockaddr_in/sockaddr_in6encoding those options take.crates/api'sruntime_config_settlementmodule: the ambiguous-configuration watchdog's terminal boundary is#[cfg(not(test))] fn terminate_process(), whose entire body isunsafe { libc::_exit(70) }. It is exit-only by construction, and that is enforced rather than asserted — a unit test parses the production source and requires the body to be exactly that call and to contain no allocation, lock, drop, panic, logging, or metric term, so nothing runs between the fence decision and process exit.- The daemon binary's
bfd_runtimemodule: one rawsetsockoptenablingIP_RECVTTL/IPV6_RECVHOPLIMIT, whichsocket2does not expose. BFD needs the received TTL delivered as ancillary data to enforce the RFC 5881 exact-255 receive check.
The daemon binary carries the same unconditional crate-root deny with its default
jemalloc, opt-indhat-heap, and no-default-feature configurations. Registering either dependency-provided allocator introduces nounsafeblock orunsafe implin this crate. The three shipped boundaries above remain visible, reviewed opt-outs with recorded reasons.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 (crate benches and tests, plus 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
three documented, scoped modules.
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.
Six fuzz crates (crates/wire, crates/policy, crates/mrt,
crates/evpn, crates/bfd, crates/rpki) carry fuzz targets covering
the message decoders, the policy frontend, structure-aware policy-chain
compilation and explain-walk agreement, MRT snapshot and warm-bundle
readers, EVPN parsing, the BFD control-packet decoder, and the RTR PDU
decoder. 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. In both nightly and hosted campaigns,
the complete-message wire target accepts 65,535-byte RFC 8654 messages and
the body-only UPDATE and ROUTE-REFRESH targets accept the corresponding
65,516-byte bodies. Pre-negotiation OPEN bodies remain capped at the legacy
4,077-byte body boundary. 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.
Every wire target has a tracked seed. The nightly workflow may reuse a bounded
main-lineage corpus, but it restores into runner-temporary staging and
validates the exact reviewed target-directory set, regular-file shape,
per-target length bounds, SHA-256 manifest, 20,000-file ceiling, and 16 MiB
ceiling before copying anything into the live corpus. Cache misses and
cache-service outages use only the tracked seeds; matched content that fails
validation stops the campaign.
The cache contains public test inputs only and is not a place for credentials
or other private data.
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/reference/security.md and docs/reference/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 docs/explanation/architecture.md for the channel policy).