# Azure Key Vault Integration Guide
**Cold Bore Ballistics LLC — Confidential**
Last updated: 2026-06-06

> How to wire a new CBB service to Azure Key Vault for secret management. Follow this guide when standing up any new hosted service that requires credentials at runtime. For vault inventory and access policy details see `Azure_Key_Vault_Architecture_Reference.md`. For day-to-day rotation and access procedures see `Azure_Key_Vault_Operating_Runbook.md`.

---

## Overview

CBB uses two Key Vaults:

| Vault | URI | Use for |
|---|---|---|
| `kv-cbb-shared-prod` | `https://kv-cbb-shared-prod.vault.azure.net/` | Infrastructure-level secrets (IPsec PSK, VM credentials) — Terraform only |
| `kv-scope-dope-prod` | `https://kv-scope-dope-prod.vault.azure.net/` | ScopeDOPE application secrets — runtime service access |

New services almost always belong in `kv-scope-dope-prod`. A new vault is only created when a new product line (separate cost center, separate acquisition unit) is introduced — not for every new service within ScopeDOPE.

---

## Integration Pattern

CBB services access Key Vault secrets at runtime using **system-assigned managed identity** — no credentials are embedded in code, configuration files, or environment variables. The pattern is:

1. Service is deployed with a system-assigned managed identity
2. Managed identity is granted `Key Vault Secrets User` on the appropriate vault
3. Secrets are referenced by name in the service's environment configuration
4. The Azure platform resolves the secret value at runtime — the service never handles a raw credential

---

## Step-by-Step: Adding a New Service

### Step 1 — Name and document the secrets

Before writing any Terraform or touching the vault, decide on the secret names the new service will need and add them to the Secret Inventory table in `Azure_Key_Vault_Architecture_Reference.md`. Do this first — names are part of the interface between the vault and the service.

**Naming convention:**

| Pattern | Example |
|---|---|
| `<service>-<credential-type>` | `club-saas-db-password` |
| Generic shared secrets reuse existing names | `database-url`, `storage-connection-string` |

Keep secret names lowercase, hyphen-separated.

---

### Step 2 — Enable managed identity in Terraform

In the service's Container App (or VM, or other compute resource) Terraform resource, add a system-assigned identity block:

```hcl
identity {
  type = "SystemAssigned"
}
```

Output the principal ID so it can be used to grant vault access:

```hcl
output "principal_id" {
  value = azurerm_container_app.main.identity[0].principal_id
}
```

---

### Step 3 — Grant vault access via the key_vault module

Pass the new service's managed identity principal ID to the `secrets_user_principal_ids` variable on the key_vault module call in `services/scope-dope/main.tf`:

```hcl
module "key_vault" {
  source              = "../../modules/key_vault"
  name                = "kv-scope-dope-prod"
  resource_group_name = local.resource_group_name
  location            = local.location
  tags                = var.tags
  secrets_user_principal_ids = [
    var.feedback_service_principal_id,
    var.new_service_principal_id,   # ← add here
  ]
}
```

Add a corresponding variable:

```hcl
variable "new_service_principal_id" {
  type        = string
  description = "Managed identity object ID for <service-name> — grants Key Vault Secrets User on kv-scope-dope-prod"
  default     = ""
}
```

Use the same two-step bootstrap pattern as `feedback-service`:
1. First apply with the variable empty — creates the service and its managed identity
2. Get the principal ID from `terraform output`
3. Set the variable, re-apply — grants vault access

---

### Step 4 — Reference secrets in the service configuration

For **Azure Container Apps**, reference Key Vault secrets using the `key_vault_secret_id` pattern in the Terraform resource. The Container App pulls the value at startup via its managed identity — no plaintext value is ever in Terraform state.

```hcl
secret {
  name                = "my-secret"
  key_vault_secret_id = "${var.key_vault_uri}secrets/<secret-name>"
  identity            = "System"
}
```

Then expose it as an environment variable:

```hcl
env {
  name        = "MY_SECRET_ENV_VAR"
  secret_name = "my-secret"
}
```

For **non-Container App services** (e.g., a VM-hosted process), use the Azure SDK or CLI to retrieve secrets at startup:

```python
# Python — requires azure-identity and azure-keyvault-secrets packages
from azure.identity import ManagedIdentityCredential
from azure.keyvault.secrets import SecretClient

credential = ManagedIdentityCredential()
client = SecretClient(vault_url="https://kv-scope-dope-prod.vault.azure.net/", credential=credential)
secret_value = client.get_secret("my-secret").value
```

```bash
# Shell — requires Azure CLI authenticated via managed identity
az keyvault secret show \
  --vault-name kv-scope-dope-prod \
  --name my-secret \
  --query value \
  --output tsv
```

---

### Step 5 — Populate secrets in Key Vault

After Terraform has been applied and the vault exists, populate each secret via the CLI. The vault must exist before secrets can be written into it.

```bash
az keyvault secret set \
  --vault-name kv-scope-dope-prod \
  --name <secret-name> \
  --value "<secret-value>"
```

Run this once per secret. After populating, update the **Populated** column in `Azure_Key_Vault_Architecture_Reference.md` from `Pending` to the date populated (e.g. `2026-06-06`).

---

### Step 6 — Update documentation

After applying Terraform and populating secrets, confirm:

1. **`Azure_Key_Vault_Architecture_Reference.md`** — Secret Inventory Populated column updated; new principal added to the Access table
2. **`ONEDRIVE_ORGANIZATION_GUIDE.md`** — no change needed unless a new vault is created
3. **`Azure_Architecture_Reference.md`** — add the new service's managed identity to the resource inventory if it is a new top-level Azure resource

---

## What NOT to Do

| Do not | Why |
|---|---|
| Pass secrets as Terraform variables | Values end up in `terraform.tfstate` in plaintext |
| Hardcode secrets in application code | Exposed in git history; can't rotate without a redeploy |
| Store secrets in `.env` files committed to git | Same as above |
| Create a new vault per service | Vault proliferation; unnecessary cost separation within one product line |
| Use legacy Key Vault access policies | CBB vaults use RBAC only — access policies are disabled |
| Grant `Key Vault Administrator` to a service identity | Services need `Key Vault Secrets User` (read-only) only — never admin |

---

## Checklist — New Service Integration

**Before any Terraform:**
- [ ] Secret names decided and added to Secret Inventory in `Azure_Key_Vault_Architecture_Reference.md`
- [ ] Managed identity block added to Terraform resource
- [ ] `principal_id` output added to module/service
- [ ] `secrets_user_principal_ids` updated in key_vault module call
- [ ] Two-step bootstrap pattern documented in `terraform.tfvars.example` comments
- [ ] Secrets referenced via `key_vault_secret_id` (Container Apps) or SDK (other)
- [ ] No plaintext secret values in any `.tf`, `.tfvars`, or application config file

**After Terraform apply (vault exists):**
- [ ] Secrets populated via `az keyvault secret set`
- [ ] Populated column updated in `Azure_Key_Vault_Architecture_Reference.md`
- [ ] Access table in Architecture Reference updated with new principal

---

## Document Control

| Field | Value |
|---|---|
| **Document owner** | `jschwefel@coldboreballisticsllc.com` |
| **Classification** | Confidential (03_-_Confidential/) |
| **Location** | `OneDrive/CBB_Corporate/03_-_Confidential/Infrastructure/Azure_Key_Vault/Azure_Key_Vault_Integration_Guide.md` |
| **Cross-references** | `Azure_Key_Vault_Architecture_Reference.md`; `Azure_Key_Vault_Operating_Runbook.md` |
| **Created** | 2026-06-06 |
| **Last updated** | 2026-06-06 |
| **Next full review due** | 2027-06-06 |

---

## Revision Control Block

| Rev | Date | Changed By | Reviewed By | Approved By | Summary |
|-----|------|------------|-------------|-------------|---------|
| 0.9 | 2026-06-06 | jschwefel | — | — | Initial release |
