{"record":{"id":"593fd851bc49c452","repo":"rustfs/rustfs","slug":"connect-registration-exchange-failed","errorCode":null,"errorMessage":"Connect registration exchange failed","messagePattern":"Connect registration exchange failed","errorType":"exception","errorClass":"RegistrationBootstrapError","httpStatus":null,"severity":"error","filePath":"rustfs/src/connect/registration_bootstrap.rs","lineNumber":49,"sourceCode":"#[derive(Debug, PartialEq, Eq)]\npub struct RegistrationBootstrapResult {\n    pub device_uid: String,\n    pub cluster_name: String,\n}\n\n#[derive(Debug, thiserror::Error)]\npub enum RegistrationBootstrapError {\n    #[error(\"the Connect registration token file must be an owner-readable, owner-only regular file\")]\n    TokenFileSecurity,\n    #[error(\"the Connect root CA file must be a trusted, non-shared-writable regular file\")]\n    RootCaFileSecurity,\n    #[error(\"the Connect state path must be an explicit directory, not a symlink\")]\n    StateDirectorySecurity,\n    #[error(\"failed to read protected Connect registration input\")]\n    Input(#[source] io::Error),\n    #[error(\"Connect registration configuration is invalid\")]\n    Configuration,\n    #[error(\"Connect registration exchange failed\")]\n    Exchange,\n    #[error(\"Connect registration bootstrap requires Unix owner and permission guarantees\")]\n    PlatformSecurity,\n    #[error(transparent)]\n    Token(#[from] TokenError),\n}\n\n#[cfg(not(unix))]\npub async fn register_from_protected_input(\n    endpoint: &str,\n    root_ca_file: &Path,\n    state_directory: &Path,\n    token_file: Option<&Path>,\n) -> Result<RegistrationBootstrapResult, RegistrationBootstrapError> {\n    let _ = (endpoint, root_ca_file, state_directory, token_file);\n    Err(RegistrationBootstrapError::PlatformSecurity)\n}\n","sourceCodeStart":31,"sourceCodeEnd":67,"githubUrl":"https://github.com/rustfs/rustfs/blob/201c653dcd34c2a01b9aec5991ed76176b342118/rustfs/src/connect/registration_bootstrap.rs#L31-L67","documentation":"RegistrationBootstrapError::Exchange means the registration exchange with the Connect control plane failed. It is raised at rustfs/src/connect/registration_bootstrap.rs:96 when client.register(...) returns an error (network/TLS failure, rejected or revoked token, pending-registration conflict, store errors — the full ClientError surface in client.rs), and again at :97-98 when the returned credential's name does not equal organizations/{org}/clusters/{cluster}/clusterDevices/{device_uid}. The original ClientError is swallowed by map_err(|_| ...), so diagnose from connectivity and token freshness.","triggerScenarios":"register_from_protected_input with an already-consumed, expired, or revoked registration token; the Connect endpoint unreachable or presenting a certificate not signed by the configured root CA (mTLS handshake failure); the server returns a credential whose resource name does not match the token's organization_uid/cluster_uid; a leftover pending registration in the credential store that fails validation.","commonSituations":"Re-using a one-time registration token after a first successful or partially-successful bootstrap; a corporate proxy intercepting TLS; a staging root CA paired with a production endpoint (or vice versa); significant clock skew on the node.","solutions":["Obtain a fresh registration token and retry the bootstrap","Verify the endpoint/CA pair: openssl s_client -connect host:443 -CAfile ca.pem should end with Verify return code: 0","Check network reachability and DNS for the Connect endpoint from the node","If the error persists, call ConnectClient::register directly (ClientError is preserved there) to see the precise failure, and clear the state directory's credential store if a stale pending registration is suspected"],"exampleFix":"# before: one-shot attempt with a possibly stale token\nRUSTFS_CONNECT_TOKEN_FILE=used-token.txt rustfs ...   # Exchange\n\n# after: verify the CA matches the endpoint issuer, then register with a fresh token\nopenssl s_client -connect connect.example.com:443 -CAfile ca.pem </dev/null | grep 'Verify return code'\nRUSTFS_CONNECT_TOKEN_FILE=fresh-token.txt rustfs ...","handlingStrategy":"retry","validationCode":"// fail fast on an unreadable or unparseable token before the network exchange\nlet token_file = std::fs::File::open(&token_path)?;\nlet _token = RegistrationToken::from_reader(token_file)?; // TokenError surfaces here, not Exchange\n// confirm the endpoint chains to the configured root before registering\nlet probe = std::process::Command::new(\"openssl\")\n    .args([\"s_client\", \"-connect\", &host_port, \"-CAfile\", ca_path])\n    .output()?;\nassert!(String::from_utf8_lossy(&probe.stderr).contains(\"Verify return code: 0\"));","typeGuard":"fn is_exchange_error(e: &RegistrationBootstrapError) -> bool {\n    matches!(e, RegistrationBootstrapError::Exchange)\n}","tryCatchPattern":"// Exchange hides ClientError; treat as transient up to a small bound, then require a fresh token\nlet mut attempt = 0;\nloop {\n    match register_from_protected_input(&endpoint, &ca, &state, token_file.as_deref()).await {\n        Err(RegistrationBootstrapError::Exchange) if attempt < 2 => {\n            attempt += 1;\n            tokio::time::sleep(std::time::Duration::from_secs(1 << attempt)).await;\n        }\n        other => break other,\n    }\n}","preventionTips":["Treat registration tokens as one-time: fetch a new token for each bootstrap attempt instead of replaying","Pair each endpoint with its matching root CA in a single config unit so they cannot drift between environments","Verify TLS reachability (openssl s_client -CAfile) during provisioning, before first registration","Bounded retry with backoff for transient network failures; stop and re-token once the bound is hit"],"tags":["rustfs","connect","registration","network","mtls","token"],"backgroundTag":"device-registration-failed","analyzedSha":"201c653dcd34c2a01b9aec5991ed76176b342118","analyzedAt":"2026-08-23T16:57:04.676Z","contentChangedAt":"2026-08-23T16:57:04.676Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}