Skip to content

Commit db57849

Browse files
committed
vault mTLS: add mTLS client cert support to Status Panel agent
- load_mtls_identity() loads client cert from VAULT_CLIENT_CERT/KEY or VAULT_CLIENT_CERT_PATH/KEY_PATH - ReqwestVaultTransport and VaultClient::from_env() both use mTLS identity when configured
1 parent e04494f commit db57849

1 file changed

Lines changed: 58 additions & 11 deletions

File tree

src/security/vault_client.rs

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
5050
use anyhow::{Context, Result};
5151
use async_trait::async_trait;
52-
use reqwest::{Client, StatusCode};
52+
use reqwest::{Client, Identity, StatusCode};
5353
use serde::{Deserialize, Serialize};
5454
use std::collections::HashMap;
5555
use std::fmt;
@@ -369,13 +369,23 @@ trait VaultTransport: Send + Sync {
369369
#[derive(Debug, Default)]
370370
struct ReqwestVaultTransport;
371371

372+
impl ReqwestVaultTransport {
373+
fn build_client() -> Result<Client> {
374+
let mut builder = Client::builder()
375+
.timeout(std::time::Duration::from_secs(10));
376+
377+
if let Some(identity) = load_mtls_identity() {
378+
builder = builder.identity(identity);
379+
}
380+
381+
builder.build().context("creating HTTP client")
382+
}
383+
}
384+
372385
#[async_trait]
373386
impl VaultTransport for ReqwestVaultTransport {
374387
async fn get(&self, url: &str, token: &str) -> Result<VaultHttpResponse> {
375-
let response = Client::builder()
376-
.timeout(std::time::Duration::from_secs(10))
377-
.build()
378-
.context("creating HTTP client")?
388+
let response = Self::build_client()?
379389
.get(url)
380390
.header("X-Vault-Token", token)
381391
.send()
@@ -393,10 +403,7 @@ impl VaultTransport for ReqwestVaultTransport {
393403
token: &str,
394404
payload: &serde_json::Value,
395405
) -> Result<VaultHttpResponse> {
396-
let response = Client::builder()
397-
.timeout(std::time::Duration::from_secs(10))
398-
.build()
399-
.context("creating HTTP client")?
406+
let response = Self::build_client()?
400407
.post(url)
401408
.header("X-Vault-Token", token)
402409
.json(payload)
@@ -410,6 +417,39 @@ impl VaultTransport for ReqwestVaultTransport {
410417
}
411418
}
412419

420+
/// Load mTLS client certificate identity from environment variables.
421+
///
422+
/// Supports two modes:
423+
/// 1. Inline PEM via `VAULT_CLIENT_CERT` and `VAULT_CLIENT_KEY` env vars
424+
/// 2. File paths via `VAULT_CLIENT_CERT_PATH` and `VAULT_CLIENT_KEY_PATH` env vars
425+
///
426+
/// Returns `None` if neither set (mTLS is optional — degradation path).
427+
fn load_mtls_identity() -> Option<Identity> {
428+
let cert_pem = std::env::var("VAULT_CLIENT_CERT").ok()
429+
.or_else(|| {
430+
let path = std::env::var("VAULT_CLIENT_CERT_PATH").ok()?;
431+
std::fs::read_to_string(path).ok()
432+
})?;
433+
434+
let key_pem = std::env::var("VAULT_CLIENT_KEY").ok()
435+
.or_else(|| {
436+
let path = std::env::var("VAULT_CLIENT_KEY_PATH").ok()?;
437+
std::fs::read_to_string(path).ok()
438+
})?;
439+
440+
let identity_pem = format!("{}\n{}", cert_pem, key_pem);
441+
match Identity::from_pem(identity_pem.as_bytes()) {
442+
Ok(identity) => {
443+
debug!("mTLS client identity loaded for Vault connections");
444+
Some(identity)
445+
}
446+
Err(e) => {
447+
warn!("Failed to load mTLS client identity: {}", e);
448+
None
449+
}
450+
}
451+
}
452+
413453
// =============================================================================
414454
// Vault Client Implementation
415455
// =============================================================================
@@ -495,8 +535,15 @@ impl VaultClient {
495535
// Configure HTTP client with security-conscious defaults:
496536
// - 10 second timeout prevents resource exhaustion from hanging connections
497537
// - TLS certificate validation enabled by default (reqwest behavior)
498-
let http_client = Client::builder()
499-
.timeout(std::time::Duration::from_secs(10))
538+
// - mTLS client identity loaded from VAULT_CLIENT_CERT / VAULT_CLIENT_KEY
539+
let mut builder = Client::builder()
540+
.timeout(std::time::Duration::from_secs(10));
541+
542+
if let Some(identity) = load_mtls_identity() {
543+
builder = builder.identity(identity);
544+
}
545+
546+
let http_client = builder
500547
.build()
501548
.context("creating HTTP client")?;
502549

0 commit comments

Comments
 (0)