swarm-queue-client: install aws-lc-rs as the process rustls provider

rustls is built with both `ring` (async-nats's `ring` feature) and
`aws-lc-rs` (reqwest's `rustls` feature), so it cannot pick a
process-level default by itself. Since the queue started requiring TLS
(1d261b3f), async-nats builds its config with `ClientConfig::builder()`,
which panics without an installed default. The panic kills the async-nats
connector task, and every queue client (swarm-controller, hive-c0re, all
hive-agents) has sat in `Pending` since the 2026-09-25 23:04Z deploy.

Add `swarm_queue_client::install_crypto_provider()`, which installs
aws-lc-rs and ignores the "already installed" error. It is called first in
`main` of every binary that links async-nats: hive-agent, hive-c0re,
swarm-controller, swarm-nats-auth. `connect()` also calls it, so a new
binary that dials through this crate is covered without remembering to.

aws-lc-rs because reqwest already falls back to it when no default is
installed, so HTTPS in these processes keeps its current provider. The
other rustls users in the tree reach it only through reqwest, which never
panics here.

Closes #4738
This commit is contained in:
atlas 2026-09-26 19:57:01 +02:00 • committed by mara
commit 0f58cdbde2
8 changed files with 34 additions and 0 deletions

1
Cargo.lock generated
View file

@ -4987,6 +4987,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"async-nats", "async-nats",
"reqwest", "reqwest",
"rustls",
"serde", "serde",
"serde_json", "serde_json",
"strum", "strum",

View file

@ -254,6 +254,13 @@ async-nats = { version = "0.50", default-features = false, features = [
"nkeys", "nkeys",
"ring", "ring",
] } ] }
# Named only to install the process-wide provider (`swarm-queue-client`'s
# `install_crypto_provider`). `aws-lc-rs` is the provider reqwest already
# falls back to, so installing it leaves every HTTPS client unchanged.
rustls = { version = "0.23", default-features = false, features = [
"aws-lc-rs",
"std",
] }
data-encoding = "2" data-encoding = "2"
# The nkey *format* - ed25519 + base32 + CRC16. The primitives are already in # The nkey *format* - ed25519 + base32 + CRC16. The primitives are already in
# the tree; the format is not, and hand-rolling a key format on an auth path # the tree; the format is not, and hand-rolling a key format on an auth path

View file

@ -104,6 +104,7 @@ struct Cli {
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
hive_log::init(); hive_log::init();
swarm_queue_client::install_crypto_provider();
let cli = Cli::parse(); let cli = Cli::parse();
serve_main::<AgentSurface>(&cli.socket, cli.poll_ms).await serve_main::<AgentSurface>(&cli.socket, cli.poll_ms).await

View file

@ -144,6 +144,7 @@ enum Cmd {
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
hive_log::init(); hive_log::init();
swarm_queue_client::install_crypto_provider();
let cli = Cli::parse(); let cli = Cli::parse();
match cli.cmd { match cli.cmd {

View file

@ -2242,6 +2242,7 @@ fn keep_forge_for_state(
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
hive_log::init(); hive_log::init();
swarm_queue_client::install_crypto_provider();
let path = socket_path(); let path = socket_path();

View file

@ -147,6 +147,7 @@ fn read_secret(path: &std::path::Path) -> anyhow::Result<String> {
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
swarm_queue_client::install_crypto_provider();
tracing_subscriber::fmt() tracing_subscriber::fmt()
.with_env_filter( .with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env() tracing_subscriber::EnvFilter::try_from_default_env()

View file

@ -45,6 +45,7 @@ async-nats.workspace = true
# feature's comment above), and `cargo check -p swarm-queue-client` alone # feature's comment above), and `cargo check -p swarm-queue-client` alone
# must not depend on what else is in the build. # must not depend on what else is in the build.
reqwest = { workspace = true, features = ["blocking"] } reqwest = { workspace = true, features = ["blocking"] }
rustls.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
strum.workspace = true strum.workspace = true

View file

@ -656,6 +656,18 @@ pub fn max_payload(client: &async_nats::Client) -> usize {
client.server_info().max_payload client.server_info().max_payload
} }
/// Install `aws-lc-rs` as the process-wide rustls crypto provider. Call it
/// first thing in `main` of every binary that dials the queue.
///
/// rustls is built with both `ring` and `aws-lc-rs`; without an installed
/// default, the first TLS handshake panics (async-nats builds its config with
/// `ClientConfig::builder()`) and the client never connects. `aws-lc-rs`
/// because it is what reqwest uses when nothing is installed.
pub fn install_crypto_provider() {
// `Err` only means a provider is already installed, which is all this needs.
let _already_installed = rustls::crypto::aws_lc_rs::default_provider().install_default();
}
/// Connect to the swarm queue, presenting a token on each connection attempt /// Connect to the swarm queue, presenting a token on each connection attempt
/// and minting a fresh one only when the cached one is near expiry. /// and minting a fresh one only when the cached one is near expiry.
/// ///
@ -664,6 +676,7 @@ pub fn max_payload(client: &async_nats::Client) -> usize {
/// token expires, the controller keeps serving, its status data quietly stops /// token expires, the controller keeps serving, its status data quietly stops
/// updating, and nothing says so until someone reads a dashboard. /// updating, and nothing says so until someone reads a dashboard.
pub async fn connect(cfg: QueueConfig) -> Result<async_nats::Client, Error> { pub async fn connect(cfg: QueueConfig) -> Result<async_nats::Client, Error> {
install_crypto_provider();
// Trusts `cfg.ca_file` when set (the swarm's own CA, when the token // Trusts `cfg.ca_file` when set (the swarm's own CA, when the token
// endpoint is signed by it) — see `build_http_client`. A timeout too, // endpoint is signed by it) — see `build_http_client`. A timeout too,
// because this client runs INSIDE the auth callback: a token endpoint // because this client runs INSIDE the auth callback: a token endpoint
@ -771,6 +784,14 @@ pub async fn connect(cfg: QueueConfig) -> Result<async_nats::Client, Error> {
mod tests { mod tests {
use super::*; use super::*;
#[test]
fn a_tls_client_config_builds_once_the_provider_is_installed() {
install_crypto_provider();
let _config = rustls::ClientConfig::builder()
.with_root_certificates(rustls::RootCertStore::empty())
.with_no_client_auth();
}
/// The all-unset case is the common one — most hosts do not run the queue. /// The all-unset case is the common one — most hosts do not run the queue.
/// ///
/// Uses a prefix no deployment sets, so it cannot pass vacuously by /// Uses a prefix no deployment sets, so it cannot pass vacuously by