# Azure Infrastructure — Recommendations & Hardening Log

**Cold Bore Ballistics LLC — Confidential**
Last updated: 2026-06-07

> Advisory companion to `Azure_Architecture_Reference.md`. Tracks security/best-practice
> hardening of the `cbb-infra` Terraform. Items here are either **implemented & applied**
> (live in Azure) or **open** (advisory). The authoritative as-built state is the
> Architecture Reference; this doc records *why* and *what's next*.

---

## Context

A best-practices review of `cbb-infra` (2026-06-07) found the baseline already strong:
clean module/service split, per-service state with `use_azuread_auth`, OIDC auth (no
creds in code), Key Vault + managed identity, `prevent_destroy` on the database, RBAC-only
vaults. The items below are hardening refinements, not foundational fixes.

**Status: items 1–5 are all implemented AND applied (live) as of 2026-06-07.** They were
coded while concurrent sessions (DocuSeal Container App, DNS migration) had uncommitted
work; items 1/2/4/5 applied with the coordinated "full initial deployment" apply (commit
`a194be6`), and item 3 applied separately once those landed (commit `b8dfef8`). All were
**in-place** (no resource replacement).

---

## Implemented (code in cbb-infra)

### 1. Tag governance — Azure Policy (inherit-from-RG)
**`modules/tag_policy`, wired into `services/shared`.** azurerm has no provider-level
`default_tags` (that is AWS-only). Instead, the built-in **"Inherit a tag from the resource
group if missing"** policy (modify effect, definition `ea3f2387-9b95-492a-a190-fcdc54f7b070`)
is assigned per mandatory tag (`project`, `owner`, `cost_center`, `managed_by`,
`environment`) scoped to `rg-cbb-prod`. Any resource created without a mandatory tag
inherits it from the RG — **no untagged resources possible**, without the hard-block (deny)
behaviour that would break Azure-auto-created child resources. Each assignment has a
system-assigned identity granted **Tag Contributor** for remediation. The resource groups
were tagged (they were previously untagged) so inheritance has a source.
*Status: applied (commit `a194be6`) — 5 tag-inheritance policy assignments live on rg-cbb-prod.*

### 2. Storage hardening — `modules/storage`
Added (all in-place, no account replacement): `min_tls_version = TLS1_2`,
`https_traffic_only_enabled`, explicit `container_access_type = private`, **blob versioning
+ soft-delete** (14-day, configurable — protects `.dope` uploads from accidental deletion),
and a `shared_access_key_enabled` variable.
- `shared_access_key_enabled` **defaults true** today because the `storage-connection-string`
  KV secret is account-key based. **Flip to `false`** when the feedback-service rework
  (CBB-108) moves blob access to managed identity (`DefaultAzureCredential`).
- `infrastructure_encryption_enabled` (double-encryption) was intentionally **left off** —
  it is create-time-only and would force account replacement. Set it on any future rebuild.
*Status: applied (commit `a194be6`) — TLS1_2 + blob versioning/soft-delete live on stcbbprod.*

### 4. Key Vault network ACLs — `modules/key_vault`
Added a `network_acls` block with `bypass = AzureServices` and a `network_default_action`
variable. **Default `Allow`** (no behaviour change — RBAC still gates access).
- **Finding:** the Container App environment `cae-cbb-prod` is **not VNet-injected** today
  (no infrastructure subnet) — it reaches Key Vault over the *public* endpoint. So flipping
  to **`Deny`** now would break runtime secret reads. It is gated on the DocuSeal session's
  VNet-injection work (`snet-ca-prod`).
- **To enforce later:** set `network_default_action = "Deny"`,
  `network_allowed_subnet_ids = [<Container Apps subnet>]`, and
  `network_allowed_ips = ["184.58.131.229"]` (operator IP for `az` secret seeding).
*Status: applied (commit `a194be6`) with Allow default (no behaviour change). NOTE: the DocuSeal/DNS work has since VNet-injected the Container App env (snet-ca-prod) — so flipping to Deny is now feasible; revisit to enforce.*

### 5. VM hardening — `modules/vm`
Added `boot_diagnostics {}` (in-place, free — aids debugging a VM that won't boot), plus
two configurable knobs:
- `os_disk_type` — **defaults `Standard_LRS`** to avoid a forced VM rebuild now (changing
  the OS disk type forces replacement, which would re-cut the IPsec tunnel + reassign the
  public IP). Set `StandardSSD_LRS` (~+$1/mo) to take effect on the **next legitimate VM
  rebuild**.
- `encryption_at_host_enabled` — **defaults false** because the subscription feature
  `Microsoft.Compute/EncryptionAtHost` is **NotRegistered**. To enable:
  `az feature register --namespace Microsoft.Compute --name EncryptionAtHost` (wait for
  Registered), then set the variable true (forces a rebuild).
*Status: applied (commit `a194be6`) — boot diagnostics live; SSD/encryption knobs available for next rebuild.*

### 3. ACR managed-identity pulls — `modules/container_registry`, `modules/container_app`, `services/cbb`
**The shared ACR admin credential was eliminated.** `crcbbprod` had `admin_enabled = true`
(a shared `crcbbprod` username/password stored as `acr-registry-password` and injected into
the Container App). Now:
- ACR `admin_enabled = false` — no shared registry credential exists.
- The Container App's `registry` block uses `identity = "System"` — it pulls via its
  system-assigned managed identity, granted the **`AcrPull`** role on the registry.
- The `acr-registry-password` KV secrets (both vaults) and the `acr_admin_username` var /
  ACR admin outputs were removed; the orphaned secrets were deleted.
- The `AcrPull` grant lives in `services/cbb` (the only Container App consumer is the
  DocuSeal app today), using the same two-step `principal_id` bootstrap as the KV grant;
  the ACR resource ID is passed via `acr_id` (the ACR is in the `scope-dope` state).

*Status: applied + verified (commit `b8dfef8`) — `adminEnabled=false`, DocuSeal MI holds
AcrPull, the app is healthy pulling via identity. (The admin user was `crcbbprod`, a
registry service credential — not a person; neither `jschwefel@` nor `admin@` was involved.)*

---

## Done (follow-ups, 2026-06-07)

- **Key Vault network ACLs → Deny** — both vaults flipped from Allow to Deny (commit
  `0b89852`). `kv-cbb-shared-prod` allows the operator IP + the Container App env static
  outbound IP (DocuSeal runtime); `kv-scope-dope-prod` allows the operator IP. AzureServices
  bypass on both. Verified: operator seeding works; DocuSeal re-resolves its secrets healthy.
  This was unblocked by the Container App env now being VNet-injected (`snet-ca-prod`).
- **Committed `.terraform.lock.hcl`** (commit `6d42794`) — all four services pin azurerm
  4.76.0; un-ignored for reproducible provider versions.
- **CI lint/scan** (commit `2cf5444`) — `.github/workflows/lint.yml`: fmt-check, validate
  (`-backend=false`, no Azure creds), tflint, tfsec (soft-fail).

## Open / advisory (not yet scheduled)

- **Key Vault private endpoint** — beyond network ACLs, a private endpoint removes the
  public endpoint entirely. Worth considering once the topology fully settles.
- **`terraform.yml` pipeline is stale** — the existing plan/apply workflow still references
  `vm-postgres-prod`, `github_token`/`flask_secret_key` vars (removed), and omits the `dns`
  service from its matrix. Needs updating to match the current services/vars.
- **tfsec → hard-fail** — once the tfsec baseline is reviewed/clean, remove `soft_fail` so
  new security findings block PRs.

---

## Cost impact of the implemented items

| Item | Cost |
|---|---|
| 1 tag policy | $0 (policy assignments are free) |
| 2 storage hardening | ~$0 (blob soft-delete/versioning bills only for retained deleted/old blobs — pennies at `.dope` volume) |
| 4 KV ACLs | $0 |
| 5 VM (boot diag; SSD/encryption deferred) | $0 now (SSD would be ~+$1/mo when applied on next rebuild) |
| **Total now** | **~$0/mo** |

---

## Document Control

| Field | Value |
|---|---|
| **Document owner** | `jschwefel@coldboreballisticsllc.com` |
| **Classification** | Confidential (03_-_Confidential/) |
| **Location** | `OneDrive/CBB_Corporate/03_-_Confidential/Infrastructure/Azure/Azure_Recommendations.md` |
| **Cross-references** | `Azure_Architecture_Reference.md`; `Azure_Operating_Runbook.md`; `Azure_Rebuild_Guide.md` |
| **Created** | 2026-06-07 |
| **Last updated** | 2026-06-07 |
| **Next full review due** | 2027-06-07 |

---

## Revision Control Block

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