# ByteLyst — Azure Key Vault & Secrets Rotation

> **Purpose:** Centralize all secrets in Azure Key Vault and establish a repeatable rotation process.  
> **Vault:** `kv-mywisprai` in `rg-mywisprai` (East US) — _legacy resource names, not yet renamed_  
> **Last updated:** 2026-03-21

---

## Overview

All ByteLyst products share a **single Key Vault**: `kv-mywisprai` (legacy name). Secrets are prefixed by product (e.g., `lysnr-*`, `mindlyst-*`).

### Goals

- Azure Key Vault is the **source of truth** for all secrets (no secrets in docs, code, or git).
- Secret rotation is **low/no-downtime** using primary/secondary keys where available.
- All services resolve secrets at startup with **env var fallback**.

### Non-Goals (for now)

- Automated rotation via Azure Event Grid (manual runbooks for now).
- Client-side secret usage (mobile apps should not embed service keys).

---

## Current State

| Product              | Prefix       | Secrets in KV | Status                                                        |
| -------------------- | ------------ | ------------- | ------------------------------------------------------------- |
| **LysnrAI**          | `lysnr-*`    | 13            | ✅ Seeded (2026-02-15)                                        |
| **MindLyst**         | `mindlyst-*` | 12            | Fully populated                                               |
| **LysnrAI (legacy)** | `wispr-*`    | 5             | Legacy desktop secrets (pre-rebrand, superseded by `lysnr-*`) |

**Total secrets:** 30 (13 LysnrAI + 12 MindLyst + 5 legacy)

### Code Integration Status

All components have AKV resolution implemented:

| Component                | Integration                                   | File                                                          |
| ------------------------ | --------------------------------------------- | ------------------------------------------------------------- |
| platform-service         | `resolveKeyVaultSecrets()` at startup         | `services/platform-service/src/server.ts`                     |
| extraction-service       | `resolveKeyVaultSecrets()` at startup         | `services/extraction-service/src/server.ts`                   |
| Admin dashboard          | Next.js `instrumentation.ts` hook             | `dashboards/admin-web/src/instrumentation.ts`                 |
| User dashboard           | Next.js `instrumentation.ts` hook             | `user-dashboard-web/src/instrumentation.ts` (in LysnrAI repo) |
| Tracker dashboard        | Next.js `instrumentation.ts` hook             | `dashboards/tracker-web/src/instrumentation.ts`               |
| Python backend (LysnrAI) | `SecretResolver` + pydantic `model_validator` | `backend-python/src/secrets/keyvault.py` (in LysnrAI repo)    |
| Desktop app (LysnrAI)    | `SecretResolver` + pydantic `model_validator` | `src/secrets/keyvault.py` (in LysnrAI repo)                   |

### Admin Secrets Manager

The admin dashboard at **`/ops/secrets`** provides live CRUD access to Key Vault:

- List all secrets with metadata (status, expiry, last updated)
- Read secret values (masked, with copy-to-clipboard)
- Create new secrets
- Edit existing secret values
- Rotate secrets (auto-generates random value)
- Delete secrets (soft-delete with confirmation)

---

## Secret Inventory (Canonical)

### LysnrAI — `lysnr-*` (13 secrets, code in `packages/config/src/keyvault.ts`)

| KV Secret Name                 | Env Var                        | Used By                  | Priority |
| ------------------------------ | ------------------------------ | ------------------------ | -------- |
| `lysnr-cosmos-key`             | `COSMOS_KEY`                   | All services, dashboards | Critical |
| `lysnr-cosmos-endpoint`        | `COSMOS_ENDPOINT`              | All services, dashboards | Critical |
| `lysnr-jwt-secret`             | `JWT_SECRET`                   | All services, dashboards | Critical |
| `lysnr-stripe-secret-key`      | `STRIPE_SECRET_KEY`            | platform-service         | Critical |
| `lysnr-stripe-webhook-secret`  | `STRIPE_WEBHOOK_SECRET`        | platform-service         | Critical |
| `lysnr-billing-internal-key`   | `BILLING_INTERNAL_KEY`         | platform-service         | High     |
| `lysnr-blob-connection-string` | `AZURE_BLOB_CONNECTION_STRING` | platform-service         | Critical |
| `lysnr-blob-account-key`       | `AZURE_BLOB_ACCOUNT_KEY`       | platform-service         | Critical |
| `lysnr-gemini-api-key`         | `GEMINI_API_KEY`               | extraction-service       | Critical |
| `lysnr-seed-secret`            | `SEED_SECRET`                  | admin dashboard          | Medium   |
| `lysnr-azure-speech-key`       | `AZURE_SPEECH_KEY`             | desktop, backend         | High     |
| `lysnr-azure-openai-key`       | `AZURE_OPENAI_KEY`             | extraction-service       | High     |
| `lysnr-azure-openai-endpoint`  | `AZURE_OPENAI_ENDPOINT`        | extraction-service       | High     |

### MindLyst — `mindlyst-*` (12 secrets, all populated)

| KV Secret Name                                | Env Var                                 | Notes                |
| --------------------------------------------- | --------------------------------------- | -------------------- |
| `mindlyst-cosmos-endpoint`                    | `COSMOS_ENDPOINT`                       | Config               |
| `mindlyst-cosmos-key`                         | `COSMOS_KEY`                            | Rotate via key1/key2 |
| `mindlyst-cosmos-database`                    | `COSMOS_DATABASE`                       | Config (`mindlyst`)  |
| `mindlyst-openai-endpoint`                    | `AZURE_OPENAI_ENDPOINT`                 | Config               |
| `mindlyst-openai-key`                         | `AZURE_OPENAI_KEY`                      | Rotate via key1/key2 |
| `mindlyst-openai-deployment`                  | `AZURE_OPENAI_DEPLOYMENT`               | Config               |
| `mindlyst-openai-api-version`                 | `AZURE_OPENAI_API_VERSION`              | Config               |
| `mindlyst-speech-key`                         | `AZURE_SPEECH_KEY`                      | Rotate via key1/key2 |
| `mindlyst-speech-region`                      | `AZURE_SPEECH_REGION`                   | Config               |
| `mindlyst-blob-connection-string`             | `AZURE_BLOB_CONNECTION_STRING`          | Rotate via key1/key2 |
| `mindlyst-notification-hub-connection-string` | `ANH_CONNECTION_STRING`                 | Rotate SAS keys      |
| `mindlyst-appinsights-connection-string`      | `APPLICATIONINSIGHTS_CONNECTION_STRING` | Treat as sensitive   |

---

## Immediate Action Items

### 1. Seed LysnrAI Secrets (BLOCKING)

All 13 `lysnr-*` secrets must be added to the Key Vault:

```bash
# Option A: Use the seed script (reads from .env)
cp .env.example .env  # fill in real values
./scripts/seed-keyvault.sh

# Option B: Use the admin dashboard Secrets Manager UI
# Navigate to /ops/secrets → Add Secret for each

# Option C: Use az cli directly
az keyvault secret set --vault-name kv-mywisprai --name lysnr-cosmos-key --value "<value>" -o none  # vault name is legacy
```

### 2. Rotate Leaked Keys (DEFERRED)

Secrets have appeared in git history. Rotate after seeding:

- [ ] Cosmos DB keys (`cosmos-mywisprai` — legacy name) → update `lysnr-cosmos-key` + `mindlyst-cosmos-key`
- [ ] Storage account keys (`bytelystblobs`) → update blob connection strings
- [ ] Azure OpenAI keys (`mywisprai-openai-sweden` — legacy name) → update OpenAI key secrets
- [ ] Speech keys (`mywisprai-speech` — legacy name) → update speech key secrets
- [ ] Notification Hub SAS keys → update `mindlyst-notification-hub-connection-string`

### 3. Enable KV Diagnostics

- [ ] Enable Key Vault diagnostics to Log Analytics
- [ ] Review Key Vault access policies / RBAC assignments

---

## Rotation Runbooks

**General rule:** Rotate by switching to the **secondary** key first (if supported), regenerate the primary, then switch back.

### Cosmos DB Key Rotation

Supports primary/secondary keys.

1. Update KV secrets to the **secondary** Cosmos key
2. Restart all services that use Cosmos
3. Verify reads/writes: `GET /health` on platform-service + extraction-service
4. Regenerate the Cosmos **primary** key in Azure Portal
5. (Optional) Switch KV back to new primary + restart again

**Secrets to update:** `lysnr-cosmos-key`, `mindlyst-cosmos-key`  
**Rollback:** Switch KV secret back to the other key and restart.

### Storage Account Key Rotation

Supports key1/key2.

1. Update KV blob connection strings to use the **other** storage key
2. Restart platform-service
3. Regenerate the original key in Azure Portal

**Secrets to update:** `lysnr-blob-connection-string`, `lysnr-blob-account-key`, `mindlyst-blob-connection-string`

### Azure OpenAI Key Rotation

Supports key1/key2.

1. Update KV OpenAI key secrets to use the **other** key
2. Verify extraction endpoints work
3. Regenerate the old key in Azure Portal

**Secrets to update:** `lysnr-azure-openai-key`, `mindlyst-openai-key`

### Speech Key Rotation

Supports key1/key2.

1. Update KV speech key secrets to the **other** key
2. Verify STT/TTS pipeline
3. Regenerate the old key

**Secrets to update:** `lysnr-azure-speech-key`, `mindlyst-speech-key`

### Stripe Keys Rotation (External)

1. Rotate keys in Stripe dashboard
2. Update KV secrets
3. Restart platform-service

**Secrets to update:** `lysnr-stripe-secret-key`, `lysnr-stripe-webhook-secret`

### Recommended Rotation Schedule

| Secret Type               | Frequency             | Method                                    |
| ------------------------- | --------------------- | ----------------------------------------- |
| Cosmos DB keys            | Quarterly             | Azure Portal → rotate → update KV         |
| JWT secret                | Quarterly             | Generate new → update KV → rolling deploy |
| Stripe keys               | Annually or on breach | Stripe Dashboard → update KV              |
| API keys (Gemini, OpenAI) | Annually or on breach | Provider portal → update KV               |
| Blob storage keys         | Quarterly             | Azure Portal → rotate → update KV         |

---

## Access Model

### Current: Access Policies (simple)

- Access policies grant explicit secret permissions to the deployer user
- Apps use `DefaultAzureCredential` (managed identity in prod, `az cli` locally)

### Future: Key Vault RBAC (recommended for production)

- Enable `enableRbacAuthorization=true` on the vault
- Grant humans `Key Vault Secrets Officer`
- Grant app managed identities `Key Vault Secrets User` (read-only)

---

## Related Files

| File                                                      | Purpose                                               |
| --------------------------------------------------------- | ----------------------------------------------------- |
| `packages/config/src/keyvault.ts`                         | `resolveKeyVaultSecrets()` + `LYSNR_SECRETS` constant |
| `scripts/seed-keyvault.sh`                                | Seed KV from `.env` values                            |
| `docs/devops/AZURE_RESOURCE_INVENTORY.md`                 | Complete Azure resource inventory                     |
| `docs/devops/AZURE_PORTAL_SETUP.md`                       | Step-by-step provisioning guide                       |
| `docs/devops/ENVIRONMENT_VARIABLES_AND_KEYVAULT_AUDIT.md` | Full env var + KV gap analysis                        |

---

## Security Guardrails

- [x] Husky pre-commit: `scripts/secret-scan-staged.sh` blocks `AccountKey=` / `SharedAccessKey=`
- [x] Husky pre-push: `scripts/secret-scan-repo.sh` scans tracked files
- [ ] CI secret scanning (gitleaks) — not yet configured
- [ ] (Optional) Rewrite git history to remove old secrets
