|
| 1 | +# NORA Secrets Management Roadmap |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +Trait-based secrets architecture for secure credential management. |
| 6 | + |
| 7 | +``` |
| 8 | +┌─────────────────────────────────────────────────────────────┐ |
| 9 | +│ SecretsProvider Trait │ |
| 10 | +├─────────────────────────────────────────────────────────────┤ |
| 11 | +│ get_secret(key) -> ProtectedString │ |
| 12 | +│ get_secret_optional(key) -> Option<ProtectedString> │ |
| 13 | +│ provider_name() -> &'static str │ |
| 14 | +└─────────────────────────────────────────────────────────────┘ |
| 15 | + │ |
| 16 | + ┌────────────────────┼────────────────────┐ |
| 17 | + │ │ │ |
| 18 | + ▼ ▼ ▼ |
| 19 | + ┌──────────┐ ┌──────────┐ ┌──────────┐ |
| 20 | + │ ENV │ │ AWS │ │ Vault │ |
| 21 | + │ Provider │ │ Secrets │ │ Provider │ |
| 22 | + │ v0.3.0 │ │ v0.4.0 │ │ v0.5.0 │ |
| 23 | + └──────────┘ └──────────┘ └──────────┘ |
| 24 | +``` |
| 25 | + |
| 26 | +--- |
| 27 | + |
| 28 | +## Phase 1: ENV Provider (v0.3.0) ✅ DONE |
| 29 | + |
| 30 | +### Features |
| 31 | +- [x] SecretsProvider trait |
| 32 | +- [x] EnvProvider implementation |
| 33 | +- [x] ProtectedString with zeroize |
| 34 | +- [x] Redacted Debug impl |
| 35 | +- [x] SecretsConfig in config.toml |
| 36 | +- [x] ENV overrides |
| 37 | +- [x] 16 unit tests |
| 38 | + |
| 39 | +### Files |
| 40 | +``` |
| 41 | +nora-registry/src/secrets/ |
| 42 | +├── mod.rs # Trait + factory |
| 43 | +├── env.rs # EnvProvider |
| 44 | +└── protected.rs # ProtectedString, S3Credentials |
| 45 | +``` |
| 46 | + |
| 47 | +### Config |
| 48 | +```toml |
| 49 | +[secrets] |
| 50 | +provider = "env" |
| 51 | +clear_env = false |
| 52 | +``` |
| 53 | + |
| 54 | +### ENV Variables |
| 55 | +```bash |
| 56 | +NORA_SECRETS_PROVIDER=env |
| 57 | +NORA_SECRETS_CLEAR_ENV=false |
| 58 | +``` |
| 59 | + |
| 60 | +--- |
| 61 | + |
| 62 | +## Phase 2: AWS + K8s (v0.4.0) |
| 63 | + |
| 64 | +### AWS Secrets Manager |
| 65 | + |
| 66 | +```rust |
| 67 | +// src/secrets/aws.rs |
| 68 | +pub struct AwsSecretsProvider { |
| 69 | + client: SecretsManagerClient, |
| 70 | + secret_name: String, |
| 71 | + region: String, |
| 72 | +} |
| 73 | + |
| 74 | +#[async_trait] |
| 75 | +impl SecretsProvider for AwsSecretsProvider { |
| 76 | + async fn get_secret(&self, key: &str) -> Result<ProtectedString, SecretsError> { |
| 77 | + let response = self.client |
| 78 | + .get_secret_value() |
| 79 | + .secret_id(&self.secret_name) |
| 80 | + .send() |
| 81 | + .await?; |
| 82 | + |
| 83 | + let secrets: HashMap<String, String> = |
| 84 | + serde_json::from_str(&response.secret_string.unwrap())?; |
| 85 | + |
| 86 | + secrets.get(key) |
| 87 | + .map(|v| ProtectedString::new(v.clone())) |
| 88 | + .ok_or_else(|| SecretsError::NotFound(key.to_string())) |
| 89 | + } |
| 90 | +} |
| 91 | +``` |
| 92 | + |
| 93 | +### Config |
| 94 | +```toml |
| 95 | +[secrets] |
| 96 | +provider = "aws-secrets" |
| 97 | + |
| 98 | +[secrets.aws] |
| 99 | +secret_name = "nora/production" |
| 100 | +region = "us-east-1" |
| 101 | +``` |
| 102 | + |
| 103 | +### Kubernetes Secrets |
| 104 | + |
| 105 | +```rust |
| 106 | +// src/secrets/k8s.rs |
| 107 | +pub struct K8sSecretsProvider { |
| 108 | + mount_path: PathBuf, |
| 109 | +} |
| 110 | + |
| 111 | +#[async_trait] |
| 112 | +impl SecretsProvider for K8sSecretsProvider { |
| 113 | + async fn get_secret(&self, key: &str) -> Result<ProtectedString, SecretsError> { |
| 114 | + let path = self.mount_path.join(key); |
| 115 | + let value = tokio::fs::read_to_string(&path) |
| 116 | + .await |
| 117 | + .map_err(|_| SecretsError::NotFound(key.to_string()))?; |
| 118 | + Ok(ProtectedString::new(value.trim().to_string())) |
| 119 | + } |
| 120 | +} |
| 121 | +``` |
| 122 | + |
| 123 | +### Config |
| 124 | +```toml |
| 125 | +[secrets] |
| 126 | +provider = "k8s" |
| 127 | + |
| 128 | +[secrets.k8s] |
| 129 | +mount_path = "/var/run/secrets/nora" |
| 130 | +``` |
| 131 | + |
| 132 | +### Kubernetes Deployment |
| 133 | +```yaml |
| 134 | +apiVersion: v1 |
| 135 | +kind: Secret |
| 136 | +metadata: |
| 137 | + name: nora-secrets |
| 138 | +type: Opaque |
| 139 | +data: |
| 140 | + AWS_ACCESS_KEY_ID: QUtJQS4uLg== |
| 141 | + AWS_SECRET_ACCESS_KEY: d0phbHIuLi4= |
| 142 | +--- |
| 143 | +apiVersion: apps/v1 |
| 144 | +kind: Deployment |
| 145 | +spec: |
| 146 | + template: |
| 147 | + spec: |
| 148 | + containers: |
| 149 | + - name: nora |
| 150 | + volumeMounts: |
| 151 | + - name: secrets |
| 152 | + mountPath: /var/run/secrets/nora |
| 153 | + readOnly: true |
| 154 | + volumes: |
| 155 | + - name: secrets |
| 156 | + secret: |
| 157 | + secretName: nora-secrets |
| 158 | +``` |
| 159 | +
|
| 160 | +### Tasks |
| 161 | +- [ ] Add `aws-sdk-secretsmanager` dependency |
| 162 | +- [ ] Implement AwsSecretsProvider |
| 163 | +- [ ] Implement K8sSecretsProvider |
| 164 | +- [ ] Add auto-refresh for rotation |
| 165 | +- [ ] Integration tests with localstack |
| 166 | +- [ ] Update factory function |
| 167 | +- [ ] Documentation |
| 168 | + |
| 169 | +--- |
| 170 | + |
| 171 | +## Phase 3: HashiCorp Vault (v0.5.0+) |
| 172 | + |
| 173 | +### Vault Provider |
| 174 | + |
| 175 | +```rust |
| 176 | +// src/secrets/vault.rs |
| 177 | +pub struct VaultProvider { |
| 178 | + client: VaultClient, |
| 179 | + mount_path: String, |
| 180 | + secret_path: String, |
| 181 | +} |
| 182 | +
|
| 183 | +#[async_trait] |
| 184 | +impl SecretsProvider for VaultProvider { |
| 185 | + async fn get_secret(&self, key: &str) -> Result<ProtectedString, SecretsError> { |
| 186 | + let secret: HashMap<String, String> = self.client |
| 187 | + .kv2(&self.mount_path) |
| 188 | + .read(&format!("{}/{}", self.secret_path, key)) |
| 189 | + .await?; |
| 190 | +
|
| 191 | + secret.get("value") |
| 192 | + .map(|v| ProtectedString::new(v.clone())) |
| 193 | + .ok_or_else(|| SecretsError::NotFound(key.to_string())) |
| 194 | + } |
| 195 | +} |
| 196 | +``` |
| 197 | + |
| 198 | +### Auth Methods |
| 199 | + |
| 200 | +**Kubernetes Auth:** |
| 201 | +```rust |
| 202 | +let jwt = tokio::fs::read_to_string( |
| 203 | + "/var/run/secrets/kubernetes.io/serviceaccount/token" |
| 204 | +).await?; |
| 205 | +
|
| 206 | +client.auth().kubernetes(&role, &jwt).await?; |
| 207 | +``` |
| 208 | + |
| 209 | +**AppRole Auth:** |
| 210 | +```rust |
| 211 | +let role_id = env::var("VAULT_ROLE_ID")?; |
| 212 | +let secret_id = env::var("VAULT_SECRET_ID")?; |
| 213 | +
|
| 214 | +client.auth().approle(&role_id, &secret_id).await?; |
| 215 | +``` |
| 216 | + |
| 217 | +### Config |
| 218 | +```toml |
| 219 | +[secrets] |
| 220 | +provider = "vault" |
| 221 | +
|
| 222 | +[secrets.vault] |
| 223 | +address = "https://vault.company.com" |
| 224 | +mount_path = "secret" |
| 225 | +secret_path = "nora/production" |
| 226 | +auth_method = "kubernetes" # or "approle", "token" |
| 227 | +role = "nora-production" |
| 228 | +``` |
| 229 | + |
| 230 | +### Tasks |
| 231 | +- [ ] Add `vaultrs` dependency |
| 232 | +- [ ] Implement VaultProvider |
| 233 | +- [ ] Kubernetes auth |
| 234 | +- [ ] AppRole auth |
| 235 | +- [ ] Token auth |
| 236 | +- [ ] Lease renewal |
| 237 | +- [ ] Integration tests |
| 238 | +- [ ] Documentation |
| 239 | + |
| 240 | +--- |
| 241 | + |
| 242 | +## Security Checklist |
| 243 | + |
| 244 | +### Code |
| 245 | +- [x] Zeroize for all secrets (ProtectedString) |
| 246 | +- [x] Redacted Debug impl |
| 247 | +- [x] No secret logging |
| 248 | +- [x] Clear ENV after read option |
| 249 | +- [ ] TLS verification for Vault/AWS |
| 250 | + |
| 251 | +### Deployment |
| 252 | +- [x] .gitignore for secrets |
| 253 | +- [ ] Kubernetes Secrets encrypted at rest |
| 254 | +- [ ] AWS KMS for Secrets Manager |
| 255 | +- [ ] Least privilege IAM/RBAC |
| 256 | +- [ ] Audit logging |
| 257 | + |
| 258 | +--- |
| 259 | + |
| 260 | +## Comparison |
| 261 | + |
| 262 | +| Provider | Complexity | Security | Use Case | |
| 263 | +|----------|------------|----------|----------| |
| 264 | +| ENV | Low | Medium | Dev, small prod | |
| 265 | +| AWS Secrets | Medium | High | AWS infrastructure | |
| 266 | +| K8s Secrets | Medium | Good | Kubernetes | |
| 267 | +| Vault | High | Maximum | Enterprise | |
| 268 | + |
| 269 | +--- |
| 270 | + |
| 271 | +## NOT Implementing |
| 272 | + |
| 273 | +### Vault Sidecar Pattern |
| 274 | +- Too complex (90% teams do it wrong) |
| 275 | +- NORA uses native Vault client instead |
| 276 | +- Simpler deployment |
| 277 | + |
| 278 | +### Custom Secrets Manager (СЕВА) |
| 279 | +- Focus on NORA core first |
| 280 | +- May add later if demand exists |
| 281 | + |
| 282 | +--- |
| 283 | + |
| 284 | +## Timeline |
| 285 | + |
| 286 | +| Phase | Version | ETA | |
| 287 | +|-------|---------|-----| |
| 288 | +| ENV Provider | v0.3.0 | ✅ Done | |
| 289 | +| AWS + K8s | v0.4.0 | 1-2 weeks | |
| 290 | +| Vault | v0.5.0 | 2-3 weeks | |
| 291 | + |
| 292 | +--- |
| 293 | + |
| 294 | +## Expert Panel Recommendations |
| 295 | + |
| 296 | +**Linus Torvalds:** |
| 297 | +> "ENV variables — правильно. Это UNIX way. Но zeroize обязателен." |
| 298 | + |
| 299 | +**Werner Vogels (AWS):** |
| 300 | +> "AWS Secrets Manager интеграция критична для AWS workloads." |
| 301 | + |
| 302 | +**Kelsey Hightower:** |
| 303 | +> "K8s Secrets через volume mount — проще чем sidecar." |
| 304 | + |
| 305 | +**Mitchell Hashimoto:** |
| 306 | +> "Native Vault client лучше sidecar. Проще деплоить." |
| 307 | + |
| 308 | +**DHH:** |
| 309 | +> "95% пользователей используют ENV. Не делайте Vault обязательным." |
0 commit comments