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}