Skip to content

Commit 41717c3

Browse files
haasonsaasclaude
andcommitted
Add HashiCorp Vault integration for secret management
Pull API keys from Vault KV v2 instead of env vars or config files. Uses reqwest directly (no new dependencies) for a single GET request. Config fields: vault_addr, vault_token, vault_path, vault_key, vault_mount, vault_namespace. All support env var fallbacks (VAULT_ADDR, VAULT_TOKEN, VAULT_PATH, VAULT_KEY, VAULT_NAMESPACE). CLI flags: --vault-addr, --vault-path, --vault-key Usage: export VAULT_ADDR=https://vault:8200 export VAULT_TOKEN=s.mytoken diffscope review --vault-path diffscope --diff my.diff Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2394913 commit 41717c3

File tree

3 files changed

+573
-0
lines changed

3 files changed

+573
-0
lines changed

src/config.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,30 @@ pub struct Config {
128128
#[serde(default = "default_feedback_suppression_margin")]
129129
pub feedback_suppression_margin: usize,
130130

131+
/// HashiCorp Vault server address (e.g., https://vault.example.com:8200).
132+
#[serde(default)]
133+
pub vault_addr: Option<String>,
134+
135+
/// Vault authentication token.
136+
#[serde(default)]
137+
pub vault_token: Option<String>,
138+
139+
/// Secret path in Vault (e.g., "diffscope" or "ci/diffscope").
140+
#[serde(default)]
141+
pub vault_path: Option<String>,
142+
143+
/// Key within the Vault secret to extract as the API key (default: "api_key").
144+
#[serde(default)]
145+
pub vault_key: Option<String>,
146+
147+
/// Vault KV engine mount point (default: "secret").
148+
#[serde(default)]
149+
pub vault_mount: Option<String>,
150+
151+
/// Vault Enterprise namespace.
152+
#[serde(default)]
153+
pub vault_namespace: Option<String>,
154+
131155
#[serde(default)]
132156
pub plugins: PluginConfig,
133157

@@ -261,6 +285,12 @@ impl Default for Config {
261285
include_fix_suggestions: true,
262286
feedback_suppression_threshold: default_feedback_suppression_threshold(),
263287
feedback_suppression_margin: default_feedback_suppression_margin(),
288+
vault_addr: None,
289+
vault_token: None,
290+
vault_path: None,
291+
vault_key: None,
292+
vault_mount: None,
293+
vault_namespace: None,
264294
plugins: PluginConfig::default(),
265295
exclude_patterns: Vec::new(),
266296
paths: HashMap::new(),
@@ -685,6 +715,31 @@ impl Config {
685715
}
686716
}
687717

718+
/// Try to resolve the API key from Vault if Vault is configured and api_key is not set.
719+
pub async fn resolve_vault_api_key(&mut self) -> Result<()> {
720+
if self.api_key.is_some() {
721+
return Ok(());
722+
}
723+
724+
let vault_config = crate::vault::try_build_vault_config(
725+
self.vault_addr.as_deref(),
726+
self.vault_token.as_deref(),
727+
self.vault_path.as_deref(),
728+
self.vault_key.as_deref(),
729+
self.vault_mount.as_deref(),
730+
self.vault_namespace.as_deref(),
731+
);
732+
733+
if let Some(vc) = vault_config {
734+
tracing::info!("Fetching API key from Vault at {}", vc.addr);
735+
let secret = crate::vault::fetch_secret(&vc).await?;
736+
self.api_key = Some(secret);
737+
tracing::info!("API key loaded from Vault");
738+
}
739+
740+
Ok(())
741+
}
742+
688743
/// Returns true if the configured base_url points to a local/self-hosted server.
689744
pub fn is_local_endpoint(&self) -> bool {
690745
match self.base_url.as_deref() {

src/main.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ mod parsing;
77
mod plugins;
88
mod review;
99
mod server;
10+
mod vault;
1011

1112
use anyhow::Result;
1213
use clap::{Parser, Subcommand};
@@ -81,6 +82,15 @@ struct Cli {
8182
#[arg(long, global = true, help = "Output language (e.g., en, ja, de)")]
8283
output_language: Option<String>,
8384

85+
#[arg(long, global = true, help = "Vault server address (e.g., https://vault:8200)")]
86+
vault_addr: Option<String>,
87+
88+
#[arg(long, global = true, help = "Vault secret path (e.g., diffscope)")]
89+
vault_path: Option<String>,
90+
91+
#[arg(long, global = true, help = "Key within Vault secret to use as API key (default: api_key)")]
92+
vault_key: Option<String>,
93+
8494
#[arg(long, global = true, default_value = "json")]
8595
output_format: OutputFormat,
8696

@@ -315,8 +325,22 @@ async fn main() -> Result<()> {
315325
if let Some(lang) = cli.output_language {
316326
config.output_language = Some(lang);
317327
}
328+
if let Some(vault_addr) = cli.vault_addr {
329+
config.vault_addr = Some(vault_addr);
330+
}
331+
if let Some(vault_path) = cli.vault_path {
332+
config.vault_path = Some(vault_path);
333+
}
334+
if let Some(vault_key) = cli.vault_key {
335+
config.vault_key = Some(vault_key);
336+
}
318337
config.normalize();
319338

339+
// Resolve API key from Vault if configured and api_key is not already set
340+
if let Err(e) = config.resolve_vault_api_key().await {
341+
eprintln!("Warning: Failed to fetch API key from Vault: {:#}", e);
342+
}
343+
320344
match cli.command {
321345
Commands::Review {
322346
diff,

0 commit comments

Comments
 (0)