# BytelystAI — 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)  
> **Last updated:** 2026-02-15

---

## Overview

All ByteLyst products (LysnrAI, MindLyst, legacy MyWisprAI) share a **single Key Vault**: `kv-mywisprai`. Secrets are prefixed by product.

### 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 |
|---------|--------|---------------|--------|
| **MindLyst** | `mindlyst-*` | 12 | Fully populated |
| **MyWisprAI** (legacy) | `wispr-*` | 5 | Legacy desktop secrets |
| **LysnrAI** | `lysnr-*` | 13 | ✅ Seeded (2026-02-15) |

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

### 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 | `admin-dashboard-web/src/instrumentation.ts` |
| User dashboard | Next.js `instrumentation.ts` hook | `user-dashboard-web/src/instrumentation.ts` |
| Tracker dashboard | Next.js `instrumentation.ts` hook | `tracker-dashboard-web/src/instrumentation.ts` |
| Python backend | `SecretResolver` + pydantic `model_validator` | `backend/src/secrets/keyvault.py` |
| Desktop app | `SecretResolver` + pydantic `model_validator` | `src/secrets/keyvault.py` |

### 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 |

### MyWisprAI (legacy) — `wispr-*` (5 secrets)

| KV Secret Name | Env Var |
|----------------|---------|
| `wispr-azure-openai-endpoint` | `AZURE_OPENAI_ENDPOINT` |
| `wispr-azure-openai-key` | `AZURE_OPENAI_KEY` |
| `wispr-azure-openai-deployment` | `AZURE_OPENAI_DEPLOYMENT` |
| `wispr-azure-speech-key` | `AZURE_SPEECH_KEY` |
| `wispr-azure-speech-region` | `AZURE_SPEECH_REGION` |

---

## Immediate Action Items

### 1. Seed LysnrAI Secrets (BLOCKING)

All 13 `lysnr-*` secrets must be added to `kv-mywisprai`:

```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
```

### 2. Rotate Leaked Keys (DEFERRED)

Secrets have appeared in git history. Rotate after seeding:

- [ ] Cosmos DB keys (`cosmos-mywisprai`) → update `mindlyst-cosmos-key` + `lysnr-cosmos-key`
- [ ] Storage account keys (`bytelystblobs`) → update blob connection strings
- [ ] Azure OpenAI keys (`mywisprai-openai-sweden`) → update OpenAI key secrets
- [ ] Speech keys (`mywisprai-speech`) → 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
