use crate::config::AppConfig;
use crate::error::AppError;
#[cfg(feature = "aws-secrets")]
pub use vti_secrets::seed_store::AwsSeedStore as AwsSecretStore;
#[cfg(feature = "azure-secrets")]
pub use vti_secrets::seed_store::AzureSeedStore as AzureSecretStore;
#[cfg(feature = "config-secret")]
pub use vti_secrets::seed_store::ConfigSeedStore as ConfigSecretStore;
#[cfg(feature = "gcp-secrets")]
pub use vti_secrets::seed_store::GcpSeedStore as GcpSecretStore;
#[cfg(feature = "k8s-secrets")]
pub use vti_secrets::seed_store::K8sSeedStore as K8sSecretStore;
#[cfg(feature = "keyring")]
pub use vti_secrets::seed_store::KeyringSeedStore as KeyringSecretStore;
pub use vti_secrets::seed_store::PlaintextSeedStore as PlaintextSecretStore;
pub use vti_common::seed_store::SeedStore as SecretStore;
const VTC_PLAINTEXT_FILENAME: &str = "secret.plaintext";
#[allow(unused_variables)]
pub fn create_secret_store(config: &AppConfig) -> Result<Box<dyn SecretStore>, AppError> {
#[cfg(feature = "aws-secrets")]
if config.secrets.aws_secret_name.is_some() {
let store = AwsSecretStore::new(
config.secrets.aws_secret_name.clone().unwrap(),
config.secrets.aws_region.clone(),
);
return Ok(Box::new(store));
}
#[cfg(not(feature = "aws-secrets"))]
if config.secrets.aws_secret_name.is_some() {
return Err(AppError::Config(
"secrets.aws_secret_name is set but this binary was built without the \
'aws-secrets' feature"
.into(),
));
}
#[cfg(feature = "gcp-secrets")]
if config.secrets.gcp_secret_name.is_some() {
let project = config.secrets.gcp_project.clone().ok_or_else(|| {
AppError::Config(
"secrets.gcp_project is required when secrets.gcp_secret_name is set".into(),
)
})?;
let store = GcpSecretStore::new(project, config.secrets.gcp_secret_name.clone().unwrap());
return Ok(Box::new(store));
}
#[cfg(not(feature = "gcp-secrets"))]
if config.secrets.gcp_secret_name.is_some() {
return Err(AppError::Config(
"secrets.gcp_secret_name is set but this binary was built without the \
'gcp-secrets' feature"
.into(),
));
}
#[cfg(feature = "azure-secrets")]
if config.secrets.azure_vault_url.is_some() {
let vault_url = config.secrets.azure_vault_url.clone().unwrap();
let secret_name = config
.secrets
.azure_secret_name
.clone()
.unwrap_or_else(|| "vtc-secret".to_string());
let store = AzureSecretStore::new(vault_url, secret_name);
return Ok(Box::new(store));
}
#[cfg(not(feature = "azure-secrets"))]
if config.secrets.azure_vault_url.is_some() {
return Err(AppError::Config(
"secrets.azure_vault_url is set but this binary was built without the \
'azure-secrets' feature"
.into(),
));
}
#[cfg(feature = "vault-secrets")]
if config.secrets.vault_addr.is_some() {
let s = &config.secrets;
let store =
vti_secrets::seed_store::vault_from_params(&vti_secrets::seed_store::VaultParams {
addr: s.vault_addr.as_deref(),
namespace: s.vault_namespace.as_deref(),
skip_verify: s.vault_skip_verify,
secret_path: s.vault_secret_path.as_deref(),
secret_key: &s.vault_secret_key,
kv_mount: &s.vault_kv_mount,
auth_method: &s.vault_auth_method,
k8s_role: s.vault_k8s_role.as_deref(),
k8s_mount: &s.vault_k8s_mount,
k8s_jwt_path: &s.vault_k8s_jwt_path,
token: s.vault_token.as_deref(),
approle_role_id: s.vault_approle_role_id.as_deref(),
approle_secret_id: s.vault_approle_secret_id.as_deref(),
approle_mount: &s.vault_approle_mount,
})?;
return Ok(Box::new(store));
}
#[cfg(not(feature = "vault-secrets"))]
if config.secrets.vault_addr.is_some() {
return Err(AppError::Config(
"secrets.vault_addr is set but this binary was built without the \
'vault-secrets' feature"
.into(),
));
}
#[cfg(feature = "k8s-secrets")]
if config.secrets.k8s_secret_name.is_some() {
let store = K8sSecretStore::new(
config.secrets.k8s_secret_name.clone().unwrap(),
config.secrets.k8s_namespace.clone(),
config.secrets.k8s_secret_key.clone(),
);
return Ok(Box::new(store));
}
#[cfg(not(feature = "k8s-secrets"))]
if config.secrets.k8s_secret_name.is_some() {
return Err(AppError::Config(
"secrets.k8s_secret_name is set but this binary was built without the \
'k8s-secrets' feature"
.into(),
));
}
#[cfg(feature = "config-secret")]
if config.secrets.secret.is_some() {
let store = ConfigSecretStore::new(config.secrets.secret.clone().unwrap());
return Ok(Box::new(store));
}
#[cfg(not(feature = "config-secret"))]
if config.secrets.secret.is_some() {
return Err(AppError::Config(
"secrets.secret is set but this binary was built without the \
'config-secret' feature"
.into(),
));
}
#[cfg(feature = "keyring")]
{
let store = KeyringSecretStore::new(&config.secrets.keyring_service, "vtc_secret");
return Ok(Box::new(store));
}
#[allow(unreachable_code)]
{
tracing::warn!(
"no secure secret store backend available — falling back to plaintext file storage"
);
let store =
PlaintextSecretStore::with_filename(&config.store.data_dir, VTC_PLAINTEXT_FILENAME);
Ok(Box::new(store))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn base_config() -> AppConfig {
toml::from_str("").expect("empty config parses")
}
fn assert_config_err_mentions(config: &AppConfig, needle: &str) {
match create_secret_store(config) {
Err(AppError::Config(msg)) => {
assert!(
msg.contains(needle),
"error should mention {needle:?}: {msg}"
)
}
Err(other) => panic!("expected Config error, got {other:?}"),
Ok(_) => panic!("expected a Config error, got a store (did the feature leak on?)"),
}
}
#[test]
fn aws_set_without_feature_is_config_error() {
let mut config = base_config();
config.secrets.aws_secret_name = Some("prod/vtc-secret".into());
assert_config_err_mentions(&config, "aws-secrets");
}
#[test]
fn gcp_set_without_feature_is_config_error() {
let mut config = base_config();
config.secrets.gcp_secret_name = Some("vtc-secret".into());
assert_config_err_mentions(&config, "gcp-secrets");
}
#[test]
fn azure_set_without_feature_is_config_error() {
let mut config = base_config();
config.secrets.azure_vault_url = Some("https://v.vault.azure.net".into());
assert_config_err_mentions(&config, "azure-secrets");
}
#[test]
fn config_secret_set_without_feature_is_config_error() {
let mut config = base_config();
config.secrets.secret = Some("ab".repeat(32));
assert_config_err_mentions(&config, "config-secret");
}
#[test]
fn k8s_set_without_feature_is_config_error() {
let mut config = base_config();
config.secrets.k8s_secret_name = Some("vtc-master-seed".into());
assert_config_err_mentions(&config, "k8s-secrets");
}
#[test]
fn vault_set_without_feature_is_config_error() {
let mut config = base_config();
config.secrets.vault_addr = Some("https://vault.internal:8200".into());
assert_config_err_mentions(&config, "vault-secrets");
}
#[test]
fn no_backend_set_falls_through_to_the_default() {
let config = base_config();
assert!(create_secret_store(&config).is_ok());
}
}