Skip to main content

vti_secrets/
config.rs

1//! Secret-store configuration.
2//!
3//! `SecretsConfig` selects which [`SeedStore`](crate::SeedStore) backend
4//! [`create_seed_store`](crate::create_seed_store) builds, plus the
5//! per-backend connection parameters. It is deserialised from the
6//! `[secrets]` table of a service's config file; `vta-service` re-exports
7//! this type so its `AppConfig` keeps a `secrets: SecretsConfig` field.
8
9use serde::{Deserialize, Serialize};
10
11/// Which seed-store backend to build, stated explicitly.
12///
13/// Mirrors `vtc_service::config::SecretBackend` and the wizard's
14/// `SecretsBackendInput` discriminator, so operators meet one vocabulary
15/// across the setup TOML, the generated `config.toml`, and the VTC.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
17#[serde(rename_all = "snake_case")]
18pub enum SecretBackend {
19    /// OS keyring (libsecret / Keychain / Credential Manager).
20    Keyring,
21    /// Hex-encoded seed inlined in `[secrets] seed` (config-seed feature).
22    ConfigSeed,
23    /// AWS Secrets Manager.
24    Aws,
25    /// GCP Secret Manager.
26    Gcp,
27    /// Azure Key Vault.
28    Azure,
29    /// HashiCorp Vault (KV v2).
30    Vault,
31    /// Kubernetes `Secret`.
32    Kubernetes,
33    /// Plaintext file under the data dir — NOT secure, dev/test only.
34    Plaintext,
35}
36
37/// `#[non_exhaustive]`: this struct has grown a field per backend since it was
38/// introduced and will keep growing. Marking it here makes a future field an
39/// additive change rather than a breaking one — at the cost of forbidding
40/// struct-literal (and functional-update) construction outside this crate.
41/// Build one with [`SecretsConfig::default`] and assign the fields you need.
42#[derive(Debug, Clone, Deserialize, Serialize)]
43#[non_exhaustive]
44pub struct SecretsConfig {
45    /// Explicit backend selector. When set it wins outright:
46    /// [`create_seed_store`](crate::create_seed_store) builds exactly this
47    /// backend and validates its required fields, rather than inferring the
48    /// backend from whichever selector field happens to be populated.
49    ///
50    /// Omit to keep the legacy implicit priority chain. Setting it is the
51    /// only way to reach a backend that sits *below* a compiled-in one in
52    /// that chain — `plaintext` in particular is unreachable implicitly on
53    /// any build with `keyring` compiled in (the default), because the
54    /// keyring arm matches unconditionally.
55    #[serde(default, skip_serializing_if = "Option::is_none")]
56    pub backend: Option<SecretBackend>,
57    /// Hex-encoded BIP-32 seed (config-seed feature)
58    pub seed: Option<String>,
59    /// AWS Secrets Manager secret name (aws-secrets feature)
60    pub aws_secret_name: Option<String>,
61    /// AWS region override (aws-secrets feature)
62    pub aws_region: Option<String>,
63    /// GCP project ID (gcp-secrets feature)
64    pub gcp_project: Option<String>,
65    /// GCP secret name (gcp-secrets feature)
66    pub gcp_secret_name: Option<String>,
67    /// Azure Key Vault URL (azure-secrets feature)
68    pub azure_vault_url: Option<String>,
69    /// Azure Key Vault secret name (azure-secrets feature)
70    pub azure_secret_name: Option<String>,
71    /// OS keyring service name (keyring feature).
72    /// Change this to run multiple VTA instances on the same machine.
73    #[serde(default = "default_keyring_service")]
74    pub keyring_service: String,
75    /// HashiCorp Vault server URL (vault-secrets feature). Setting this
76    /// activates the Vault backend.
77    pub vault_addr: Option<String>,
78    /// KV v2 mount path (vault-secrets feature). Default `secret`.
79    #[serde(default = "default_vault_kv_mount")]
80    pub vault_kv_mount: String,
81    /// KV v2 secret path under the mount, e.g. `vta/master-seed`
82    /// (vault-secrets feature).
83    pub vault_secret_path: Option<String>,
84    /// Field name within the KV v2 secret that holds the hex-encoded
85    /// seed (vault-secrets feature). Default `seed`.
86    #[serde(default = "default_vault_secret_key")]
87    pub vault_secret_key: String,
88    /// Vault Enterprise namespace, if any (vault-secrets feature).
89    pub vault_namespace: Option<String>,
90    /// Auth method: `kubernetes` (default), `token`, or `approle`
91    /// (vault-secrets feature).
92    #[serde(default = "default_vault_auth_method")]
93    pub vault_auth_method: String,
94    /// Kubernetes auth role name (vault-secrets feature, kubernetes
95    /// auth method).
96    pub vault_k8s_role: Option<String>,
97    /// Kubernetes auth mount path (vault-secrets feature). Default
98    /// `kubernetes`.
99    #[serde(default = "default_vault_k8s_mount")]
100    pub vault_k8s_mount: String,
101    /// File holding the ServiceAccount JWT presented to Vault
102    /// (vault-secrets feature, kubernetes auth method). Default is the
103    /// kubelet-mounted projected volume path.
104    #[serde(default = "default_vault_k8s_jwt_path")]
105    pub vault_k8s_jwt_path: String,
106    /// Static token (vault-secrets feature, token auth method). Prefer
107    /// the `VAULT_TOKEN` env var over hard-coding here.
108    pub vault_token: Option<String>,
109    /// AppRole role_id (vault-secrets feature, approle auth method).
110    pub vault_approle_role_id: Option<String>,
111    /// AppRole secret_id (vault-secrets feature, approle auth method).
112    pub vault_approle_secret_id: Option<String>,
113    /// AppRole mount path (vault-secrets feature). Default `approle`.
114    #[serde(default = "default_vault_approle_mount")]
115    pub vault_approle_mount: String,
116    /// Skip TLS certificate verification — dev/test only
117    /// (vault-secrets feature).
118    #[serde(default)]
119    pub vault_skip_verify: bool,
120    /// Kubernetes `Secret` name holding the hex-encoded seed
121    /// (k8s-secrets feature). Setting this activates the Kubernetes
122    /// backend.
123    pub k8s_secret_name: Option<String>,
124    /// Kubernetes namespace the `Secret` lives in (k8s-secrets feature).
125    /// When unset, the in-cluster ServiceAccount namespace (or the
126    /// kubeconfig context namespace) is used, falling back to `default`.
127    pub k8s_namespace: Option<String>,
128    /// Key within the `Secret`'s `data` map that holds the hex-encoded
129    /// seed (k8s-secrets feature). Default `seed`.
130    #[serde(default = "default_k8s_secret_key")]
131    pub k8s_secret_key: String,
132    /// Opt in to the **plaintext file** seed-store fallback. Off by
133    /// default: when no secure backend (keyring / cloud / Vault /
134    /// config-seed) is compiled-in *and* configured, `create_seed_store`
135    /// errors rather than silently writing the BIP-32 master seed to a
136    /// file in clear. Set `true` only for dev/test where that is
137    /// acceptable. (P0.9 — closes the "one wrong TOML key → master seed
138    /// on disk in cleartext" footgun.)
139    #[serde(default)]
140    pub allow_plaintext: bool,
141    /// How long a successfully-read seed may be reused from memory before the
142    /// backend is consulted again, in seconds. `0` disables caching entirely —
143    /// every read hits the backend, which is how this crate behaved before the
144    /// cache existed.
145    ///
146    /// Why this exists: the seed is read on **every** key-touching request
147    /// (`load_seed_bytes`, ~25 call sites, including every signature), and on
148    /// the cloud backends each read is a remote call — for AWS one
149    /// `GetSecretValue`, which is in turn one billed KMS `Decrypt`. Uncached,
150    /// KMS request volume scales with request volume; cached, it scales with
151    /// wall-clock time.
152    ///
153    /// Why a bounded TTL rather than load-once: the seed for a generation is
154    /// immutable, and the only writer is in-process rotation (which invalidates
155    /// the cache explicitly), so a stale read is already near-impossible. The
156    /// TTL is the outer bound for anything that gets past that, and it caps how
157    /// long the master seed sits resident in process memory (P0.7).
158    #[serde(default = "default_cache_ttl_secs")]
159    pub cache_ttl_secs: u64,
160}
161
162/// Default seed cache TTL. Short enough that an out-of-band seed change is
163/// picked up within a minute, long enough that a VTA under load makes one
164/// backend read per minute instead of one per request.
165fn default_cache_ttl_secs() -> u64 {
166    60
167}
168
169fn default_keyring_service() -> String {
170    "vta".to_string()
171}
172
173fn default_vault_kv_mount() -> String {
174    "secret".to_string()
175}
176
177fn default_vault_secret_key() -> String {
178    "seed".to_string()
179}
180
181fn default_vault_auth_method() -> String {
182    "kubernetes".to_string()
183}
184
185fn default_vault_k8s_mount() -> String {
186    "kubernetes".to_string()
187}
188
189fn default_vault_k8s_jwt_path() -> String {
190    "/var/run/secrets/kubernetes.io/serviceaccount/token".to_string()
191}
192
193fn default_k8s_secret_key() -> String {
194    "seed".to_string()
195}
196
197fn default_vault_approle_mount() -> String {
198    "approle".to_string()
199}
200
201impl Default for SecretsConfig {
202    fn default() -> Self {
203        Self {
204            backend: None,
205            seed: None,
206            aws_secret_name: None,
207            aws_region: None,
208            gcp_project: None,
209            gcp_secret_name: None,
210            azure_vault_url: None,
211            azure_secret_name: None,
212            keyring_service: default_keyring_service(),
213            vault_addr: None,
214            vault_kv_mount: default_vault_kv_mount(),
215            vault_secret_path: None,
216            vault_secret_key: default_vault_secret_key(),
217            vault_namespace: None,
218            vault_auth_method: default_vault_auth_method(),
219            vault_k8s_role: None,
220            vault_k8s_mount: default_vault_k8s_mount(),
221            vault_k8s_jwt_path: default_vault_k8s_jwt_path(),
222            vault_token: None,
223            vault_approle_role_id: None,
224            vault_approle_secret_id: None,
225            vault_approle_mount: default_vault_approle_mount(),
226            vault_skip_verify: false,
227            k8s_secret_name: None,
228            k8s_namespace: None,
229            k8s_secret_key: default_k8s_secret_key(),
230            allow_plaintext: false,
231            cache_ttl_secs: default_cache_ttl_secs(),
232        }
233    }
234}