@faizahmed/secret-keystore 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SECURITY.md ADDED
@@ -0,0 +1,505 @@
1
+ # Security
2
+
3
+ This document explains the security model of [`@faizahmed/secret-keystore`](https://www.npmjs.com/package/@faizahmed/secret-keystore) (available on npm) and how it protects your secrets.
4
+
5
+ > **🔐 Key Design Decision:** This package uses **IAM roles by default**. Explicit AWS credentials require opt-in. This ensures production deployments are secure by default.
6
+
7
+ ## Table of Contents
8
+
9
+ - [Overview](#overview)
10
+ - [Threat Scenarios](#threat-scenarios)
11
+ - [Security Layers](#security-layers)
12
+ - [Security-First Dependency Policy](#security-first-dependency-policy)
13
+ - [Nitro Enclave Attestation](#nitro-enclave-attestation)
14
+ - [Full Attestation Flow](#full-attestation-flow)
15
+ - [5-Minute Auto-Refresh](#5-minute-auto-refresh)
16
+ - [AttestationManager API](#attestationmanager-api)
17
+ - [KMS Key Policies](#kms-key-policies)
18
+ - [Authentication](#authentication)
19
+ - [Best Practices](#best-practices)
20
+ - [Security Checklist](#security-checklist)
21
+
22
+ ## Overview
23
+
24
+ The package provides multiple layers of security:
25
+
26
+ | Layer | Protection |
27
+ |-------|------------|
28
+ | **IAM Role Default** | Uses IAM roles by default — no credentials to manage or leak |
29
+ | **Encryption at Rest** | Secrets in config files are KMS-encrypted ciphertext (symmetric: direct; RSA: envelope with encrypted DEK) |
30
+ | **Access Control** | IAM policies + KMS key policies control decryption |
31
+ | **Runtime Isolation** | Decrypted values never in `process.env` |
32
+ | **Memory Protection** | Additional AES-256-GCM encryption in keystore memory |
33
+ | **Attestation** | Optional Nitro Enclave verification for maximum security |
34
+ | **Minimal Dependencies** | Security-sensitive code uses native Node.js modules only |
35
+
36
+ ## Threat Scenarios
37
+
38
+ ### Scenario 1: Attacker gets your config file
39
+
40
+ If an attacker obtains your `.env` or `secrets.yaml` file, they see:
41
+
42
+ ```env
43
+ KMS_KEY_ID=arn:aws:kms:us-east-1:123456789:key/abc-123
44
+ DB_PASSWORD=ENC[AQICAHh2nZPq7x9K3mJ...long-base64-ciphertext...]
45
+ ```
46
+
47
+ **They CANNOT decrypt `DB_PASSWORD` because:**
48
+
49
+ | Barrier | Why It Stops Them |
50
+ |---------|-------------------|
51
+ | **KMS Required** | The ciphertext can ONLY be decrypted by AWS KMS — no offline cracking possible |
52
+ | **IAM Required** | They need valid AWS credentials with `kms:Decrypt` permission |
53
+ | **Key Policy** | The KMS key policy controls exactly who/what can decrypt |
54
+
55
+ ### Scenario 2: Attacker gets server access
56
+
57
+ Even with full server access:
58
+
59
+ | Protection Layer | How It Helps |
60
+ |------------------|--------------|
61
+ | **IAM Role Binding** | Only the specific EC2/ECS/Lambda role can decrypt |
62
+ | **VPC Restrictions** | KMS key policy can restrict to specific VPCs |
63
+ | **No Plaintext on Disk** | Decrypted values exist only in memory |
64
+ | **No `process.env`** | Can't dump secrets via `/proc/[pid]/environ` |
65
+ | **Memory Encryption** | Additional AES-256-GCM encryption in keystore memory |
66
+
67
+ ### Scenario 3: Attacker gets server access + AWS credentials
68
+
69
+ Even with both:
70
+
71
+ | Protection | How It Helps |
72
+ |------------|--------------|
73
+ | **KMS Key Policy** | Can restrict to specific IAM roles only |
74
+ | **VPC Conditions** | Can require requests from specific VPCs |
75
+ | **Attestation** | Can require valid Nitro Enclave attestation |
76
+
77
+ ## Security Layers
78
+
79
+ ```mermaid
80
+ flowchart TB
81
+ subgraph CONFIG["Config File"]
82
+ A["DB_PASSWORD=ENC[AQICAHh...encrypted...]"]
83
+ end
84
+
85
+ subgraph KMS["AWS KMS"]
86
+ B["IAM Policy Check"]
87
+ C["Key Policy Check"]
88
+ D["Attestation Check (optional)"]
89
+ end
90
+
91
+ subgraph MEM["Keystore Memory"]
92
+ E["AES-256-GCM Encrypted Storage"]
93
+ F["keyStore.get() API"]
94
+ end
95
+
96
+ CONFIG -->|"Encrypted ciphertext"| KMS
97
+ KMS -->|"Decrypted plaintext"| MEM
98
+
99
+ style CONFIG fill:#ffcccc
100
+ style KMS fill:#ccffcc
101
+ style MEM fill:#ccccff
102
+ ```
103
+
104
+ ### Layer 1: Encryption at Rest
105
+
106
+ - Secrets encrypted with AWS KMS before storage
107
+ - Only ciphertext exists in config files
108
+ - No offline cracking possible — requires KMS API
109
+ - Encrypted format: `ENC[...]` (base64). With **symmetric** keys, KMS encrypts the value directly; with **asymmetric (RSA)** keys, the library uses envelope encryption (a random DEK encrypts the secret, KMS encrypts only the DEK; the encrypted DEK is stored with the ciphertext so only KMS can recover it).
110
+
111
+ ### Layer 2: Access Control
112
+
113
+ - **IAM Policies**: Control which AWS identities can call KMS
114
+ - **KMS Key Policies**: Additional layer of access control on the key itself
115
+ - **Conditions**: Can restrict by VPC, IP, time, and more
116
+
117
+ ### Layer 3: Runtime Protection
118
+
119
+ - Decrypted values **never** stored in `process.env`
120
+ - Cannot be dumped via `/proc/[pid]/environ`
121
+ - Additional AES-256-GCM encryption in keystore memory
122
+ - Accessible only via `keyStore.get()` API
123
+ - Secure memory wipe on `destroy()`
124
+
125
+ ## Security-First Dependency Policy
126
+
127
+ > **Critical Decision:** This library intentionally avoids third-party dependencies for all security-sensitive operations.
128
+
129
+ ### Rationale
130
+
131
+ When dealing with sensitive information (secrets, encryption keys, attestation documents), we cannot risk vulnerabilities introduced through third-party packages. Supply chain attacks and dependency vulnerabilities have proven to be a significant threat vector.
132
+
133
+ **Example: react2shell (Next.js)**
134
+
135
+ The `react2shell` vulnerability demonstrated how a widely-used framework could be compromised, potentially leaking sensitive data. Such incidents reinforce our decision to minimize external dependencies in security-critical code paths.
136
+
137
+ ### What We Use
138
+
139
+ | Operation | Approach |
140
+ |-----------|----------|
141
+ | **AWS KMS calls** | AWS SDK only (official, maintained by AWS) |
142
+ | **Encryption/Decryption** | Node.js native `crypto` module |
143
+ | **In-memory encryption** | Node.js native `crypto` module |
144
+ | **JSON parsing** | Native `JSON.parse()` |
145
+ | **ENV parsing** | Custom implementation (no dotenv) |
146
+ | **Buffer operations** | Node.js native `Buffer` |
147
+
148
+ ### Dependencies We Allow
149
+
150
+ | Dependency | Justification |
151
+ |------------|---------------|
152
+ | `@aws-sdk/client-kms` | Official AWS SDK, required for KMS operations |
153
+ | `js-yaml` (optional) | Well-audited, widely used, minimal attack surface |
154
+ | `pkijs` / `asn1js` | Required for CMS/EnvelopedData unwrapping in attestation flow. Well-audited PKI libraries maintained by PeculiarVentures. |
155
+ | `pvtsutils` | Utility dependency for PKIjs buffer operations |
156
+
157
+ ### Dependencies We Avoid
158
+
159
+ | Category | Examples | Reason |
160
+ |----------|----------|--------|
161
+ | General utility libraries | lodash, underscore | Unnecessary for our use case |
162
+ | HTTP clients | axios, got, node-fetch | Use native `fetch` or `https` |
163
+ | Crypto wrappers | bcrypt wrappers, crypto-js | Use native `crypto` module |
164
+ | Config parsers | dotenv, config | Custom implementation for control |
165
+
166
+ ## Nitro Enclave Attestation
167
+
168
+ For maximum security, use AWS Nitro Enclaves with attestation. **This library manages the entire attestation lifecycle internally.**
169
+
170
+ ### Why Attestation is the Strongest Protection
171
+
172
+ **With attestation, even root access to the host cannot decrypt secrets** because:
173
+
174
+ 1. KMS key policy requires a valid attestation document
175
+ 2. Attestation proves the request comes from verified, untampered enclave code
176
+ 3. Host OS and other processes cannot generate valid attestation
177
+ 4. PCR values are cryptographically tied to the enclave image
178
+
179
+ ### Full Attestation Flow
180
+
181
+ The library implements the complete attestation flow as required by AWS KMS:
182
+
183
+ ```mermaid
184
+ sequenceDiagram
185
+ participant App as Your App
186
+ participant AM as AttestationManager
187
+ participant Anjuna as Nitro/Anjuna Endpoint
188
+ participant KMS as AWS KMS
189
+
190
+ App->>AM: createSecretKeyStore() with attestation enabled
191
+ AM->>AM: 1. Generate RSA-4096 key pair
192
+ AM->>Anjuna: 2. GET /attestation?public_key=...
193
+ Anjuna-->>AM: Attestation document (contains public key)
194
+ AM->>AM: Cache {document, privateKey, timestamp}
195
+
196
+ App->>AM: keyStore.get('SECRET')
197
+ AM->>KMS: 3. Decrypt + Recipient{AttestationDocument}
198
+ Note over KMS: KMS verifies attestation + PCR values
199
+ KMS-->>AM: 4. CiphertextForRecipient (CMS EnvelopedData)
200
+ AM->>AM: 5. Unwrap CMS with private key (PKIjs/asn1js)
201
+ AM-->>App: Decrypted plaintext
202
+ ```
203
+
204
+ ### Key Components
205
+
206
+ | Component | Purpose |
207
+ |-----------|---------|
208
+ | **Ephemeral RSA-4096 Key Pair** | Generated fresh for each attestation cycle. Public key embedded in attestation doc, private key used to unwrap CMS response. |
209
+ | **Attestation Document** | Fetched from Anjuna/Nitro endpoint. Contains public key + PCR measurements. Valid for 5 minutes. |
210
+ | **CiphertextForRecipient** | KMS returns encrypted plaintext wrapped in CMS EnvelopedData format instead of raw plaintext. |
211
+ | **CMS Unwrapping** | Uses PKIjs/asn1js to decrypt the CMS EnvelopedData using the ephemeral private key. |
212
+
213
+ ### How to Enable Attestation
214
+
215
+ ```javascript
216
+ const { createSecretKeyStore } = require('@faizahmed/secret-keystore');
217
+
218
+ const keyStore = await createSecretKeyStore(
219
+ { type: 'env', content },
220
+ kmsKeyId,
221
+ {
222
+ attestation: {
223
+ enabled: true, // Enable attestation
224
+ required: true, // Fail if attestation unavailable
225
+ endpoint: 'http://localhost:8080/attestation', // Anjuna/Nitro endpoint
226
+ timeout: 10000, // Request timeout (ms)
227
+ userData: 'app-identifier' // Optional user data in attestation
228
+ }
229
+ }
230
+ );
231
+ ```
232
+
233
+ ### 5-Minute Auto-Refresh
234
+
235
+ AWS KMS requires attestation documents to be less than 5 minutes old. The library handles this **automatically**:
236
+
237
+ ```mermaid
238
+ sequenceDiagram
239
+ participant App
240
+ participant AM as AttestationManager
241
+ participant KMS as AWS KMS
242
+ participant Anjuna
243
+
244
+ Note over AM: Document cached (age: 4m 55s)
245
+ App->>AM: decrypt()
246
+ AM->>KMS: Decrypt with cached doc
247
+ KMS-->>AM: ❌ ValidationException (doc expired)
248
+
249
+ AM->>AM: Detect 5-min age limit error
250
+ AM->>AM: Generate NEW RSA-4096 key pair
251
+ AM->>Anjuna: Fetch NEW attestation doc
252
+ Anjuna-->>AM: Fresh attestation document
253
+ AM->>AM: Update cache (timestamp reset)
254
+
255
+ AM->>KMS: Retry decrypt with fresh doc
256
+ KMS-->>AM: CiphertextForRecipient
257
+ AM->>AM: Unwrap CMS
258
+ AM-->>App: ✅ Decrypted plaintext
259
+ ```
260
+
261
+ | Scenario | Library Behavior |
262
+ |----------|------------------|
263
+ | First request | Initialize (generate key pair, fetch doc, cache) |
264
+ | Document < 5 min old | Use cached document and private key |
265
+ | Document expired (KMS rejects) | Regenerate key pair, fetch new doc, retry once |
266
+ | Retry also fails | Throw `ATTESTATION_INIT_FAILED` error |
267
+ | Anjuna/Nitro unavailable | Throw `ATTESTATION_FETCH_FAILED` error |
268
+
269
+ ### AttestationManager API
270
+
271
+ For advanced use cases, use `AttestationManager` directly:
272
+
273
+ ```javascript
274
+ const { createAttestationManager } = require('@faizahmed/secret-keystore');
275
+ const { KMSClient } = require('@aws-sdk/client-kms');
276
+
277
+ // Create and auto-initialize
278
+ const manager = await createAttestationManager({
279
+ endpoint: 'http://localhost:8080/attestation',
280
+ timeout: 10000,
281
+ userData: 'my-app',
282
+ logger: console, // Optional logger
283
+ autoInitialize: true // Default: true
284
+ });
285
+
286
+ // Check status
287
+ console.log(manager.getStatus());
288
+ // {
289
+ // initialized: true,
290
+ // hasError: false,
291
+ // hasDocument: true,
292
+ // hasKeyPair: true,
293
+ // documentAge: 45000, // ms since document was fetched
294
+ // endpoint: 'http://...'
295
+ // }
296
+
297
+ // Decrypt with attestation
298
+ const kmsClient = new KMSClient({ region: 'us-east-1' });
299
+ const plaintext = await manager.decryptWithAttestation(
300
+ kmsClient,
301
+ ciphertextBlob,
302
+ kmsKeyId,
303
+ { encryptionContext: { key: 'value' } }
304
+ );
305
+
306
+ // Force refresh (if needed)
307
+ await manager.reinitialize();
308
+
309
+ // Cleanup
310
+ manager.destroy();
311
+ ```
312
+
313
+ ### Security Considerations
314
+
315
+ | Aspect | Implementation |
316
+ |--------|----------------|
317
+ | **Key Material** | RSA-4096 private key never leaves process memory |
318
+ | **Key Lifetime** | Private key destroyed when document expires or manager is destroyed |
319
+ | **No External Storage** | Attestation materials exist only in memory |
320
+ | **CMS Unwrapping** | Uses PKIjs (well-audited library) for ASN.1/CMS operations |
321
+ | **Mutex Protection** | Concurrent initialization requests share single init promise |
322
+
323
+ ## KMS Key Policies
324
+
325
+ ### Basic Policy: Restrict to Specific IAM Role
326
+
327
+ ```json
328
+ {
329
+ "Version": "2012-10-17",
330
+ "Statement": [
331
+ {
332
+ "Sid": "AllowDecryptFromSpecificRole",
333
+ "Effect": "Allow",
334
+ "Principal": {
335
+ "AWS": "arn:aws:iam::123456789012:role/MyAppRole"
336
+ },
337
+ "Action": "kms:Decrypt",
338
+ "Resource": "*"
339
+ }
340
+ ]
341
+ }
342
+ ```
343
+
344
+ ### Enhanced Policy: Restrict to VPC
345
+
346
+ ```json
347
+ {
348
+ "Version": "2012-10-17",
349
+ "Statement": [
350
+ {
351
+ "Sid": "AllowDecryptFromVPC",
352
+ "Effect": "Allow",
353
+ "Principal": {
354
+ "AWS": "arn:aws:iam::123456789012:role/MyAppRole"
355
+ },
356
+ "Action": "kms:Decrypt",
357
+ "Resource": "*",
358
+ "Condition": {
359
+ "StringEquals": {
360
+ "aws:SourceVpc": "vpc-12345678"
361
+ }
362
+ }
363
+ }
364
+ ]
365
+ }
366
+ ```
367
+
368
+ ### Maximum Security: Nitro Enclave Attestation
369
+
370
+ ```json
371
+ {
372
+ "Version": "2012-10-17",
373
+ "Statement": [
374
+ {
375
+ "Sid": "AllowDecryptFromEnclave",
376
+ "Effect": "Allow",
377
+ "Principal": {
378
+ "AWS": "arn:aws:iam::123456789012:role/EnclaveRole"
379
+ },
380
+ "Action": "kms:Decrypt",
381
+ "Resource": "*",
382
+ "Condition": {
383
+ "StringEqualsIgnoreCase": {
384
+ "kms:RecipientAttestation:PCR0": "EXPECTED_PCR0_VALUE",
385
+ "kms:RecipientAttestation:PCR1": "EXPECTED_PCR1_VALUE",
386
+ "kms:RecipientAttestation:PCR2": "EXPECTED_PCR2_VALUE"
387
+ }
388
+ }
389
+ }
390
+ ]
391
+ }
392
+ ```
393
+
394
+ ## Authentication
395
+
396
+ ### IAM Roles (Default & Recommended)
397
+
398
+ This package uses **IAM roles by default**. This is the recommended and most secure approach:
399
+
400
+ | Environment | IAM Role Type |
401
+ |-------------|---------------|
402
+ | EC2 | Instance Profile |
403
+ | ECS | Task Role |
404
+ | EKS | IAM Roles for Service Accounts (IRSA) |
405
+ | Lambda | Execution Role |
406
+ | Nitro Enclave | Enclave Role + Attestation |
407
+
408
+ **Benefits of IAM roles:**
409
+ - No credentials to manage or rotate
410
+ - No risk of credential leakage
411
+ - Automatic credential rotation by AWS
412
+ - Fine-grained access control via IAM policies
413
+
414
+ ### Explicit Credentials (Not Recommended)
415
+
416
+ Only use explicit credentials when IAM roles are not available (e.g., local development):
417
+
418
+ ```bash
419
+ # CLI: Explicit opt-in required
420
+ npx @faizahmed/secret-keystore encrypt \
421
+ --kms-key-id="alias/my-key" \
422
+ --use-credentials
423
+ ```
424
+
425
+ ```javascript
426
+ // Runtime: Explicit opt-in required
427
+ const keyStore = await createSecretKeyStore(source, kmsKeyId, {
428
+ aws: {
429
+ credentials: {
430
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID,
431
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
432
+ }
433
+ }
434
+ });
435
+ ```
436
+
437
+ > ⚠️ **Warning:** Never commit credentials to version control. Use environment variables for local development only.
438
+
439
+ ## Best Practices
440
+
441
+ ### Infrastructure
442
+
443
+ | Practice | Why |
444
+ |----------|-----|
445
+ | **Use IAM Roles (default)** | This package uses IAM roles by default—no credentials to manage |
446
+ | **Restrict KMS Key Policy** | Limit decrypt to specific roles, VPCs, or enclaves |
447
+ | **Enable CloudTrail** | Log all KMS operations for audit and alerting |
448
+ | **Use Nitro Enclaves** | For sensitive workloads, attestation provides strongest protection |
449
+ | **Separate Keys per Environment** | Use different KMS keys for dev, staging, production |
450
+
451
+ ### Application
452
+
453
+ | Practice | Why |
454
+ |----------|-----|
455
+ | **Don't use explicit credentials** | Rely on IAM roles; explicit credentials are for local dev only |
456
+ | **Set `throwOnMissingKey: true`** | Fail fast in production if secrets are missing |
457
+ | **Validate at Startup** | Ensure all required secrets are available before serving traffic |
458
+ | **Don't Log Secrets** | Never log decrypted values, even in debug mode |
459
+ | **Use TTL with Auto-Refresh** | Enable `access.ttl` and `access.autoRefresh` for long-running services |
460
+ | **Call `destroy()` on Shutdown** | Securely wipe secrets from memory |
461
+
462
+ ### Operations
463
+
464
+ | Practice | Why |
465
+ |----------|-----|
466
+ | **Monitor KMS Usage** | Alert on unexpected decrypt operations |
467
+ | **Review IAM Policies** | Regularly audit who has access to KMS keys |
468
+ | **Test Recovery** | Ensure you can recover if KMS access is lost |
469
+ | **Document Key Policies** | Keep track of what each key policy allows |
470
+ | **Rotate Secrets Regularly** | Re-encrypt with new values periodically |
471
+
472
+ ## Security Checklist
473
+
474
+ ### Basic Security
475
+
476
+ - [ ] KMS key created with appropriate key policy
477
+ - [ ] IAM role configured with minimal permissions
478
+ - [ ] Config files encrypted using CLI (`--kms-key-id` is required)
479
+ - [ ] Reserved keys (`KMS_KEY_ID`, `AWS_REGION`) are the only plaintext values
480
+ - [ ] Application uses `throwOnMissingKey: true` in production
481
+ - [ ] Application calls `keyStore.destroy()` on shutdown
482
+ - [ ] CloudTrail logging enabled for KMS operations
483
+
484
+ ### Runtime Protection
485
+
486
+ - [ ] TTL with auto-refresh enabled for long-running services
487
+ - [ ] Secure memory wipe enabled (default)
488
+ - [ ] In-memory AES-256-GCM encryption enabled (default)
489
+ - [ ] No secrets logged, even in debug mode
490
+
491
+ ### Nitro Enclave Attestation (Maximum Security)
492
+
493
+ - [ ] Anjuna/Nitro attestation endpoint accessible from enclave
494
+ - [ ] KMS key policy includes PCR value conditions
495
+ - [ ] `attestation.enabled: true` and `attestation.required: true` set
496
+ - [ ] `attestation.endpoint` points to correct Anjuna/Nitro URL
497
+ - [ ] VPC restrictions in KMS key policy (optional but recommended)
498
+ - [ ] Fallback disabled in production (`fallbackToStandard: false`)
499
+
500
+ ## Further Reading
501
+
502
+ - [AWS KMS Key Policies](https://docs.aws.amazon.com/kms/latest/developerguide/key-policies.html)
503
+ - [AWS Nitro Enclaves](https://docs.aws.amazon.com/enclaves/latest/user/nitro-enclave.html)
504
+ - [KMS Cryptographic Details](https://docs.aws.amazon.com/kms/latest/cryptographic-details/intro.html)
505
+ - [IAM Roles for Amazon EC2](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html)