# Azure Architecture Reference

# Cold Bore Ballistics, LLC

**Last updated:** 2026-06-08 (Logic App `logic-docuseal-onedrive-prod` deployed; Power Automate flow retired)
**Status:** LIVE — all four Terraform services applied; see per-resource status below
**Subscription:** Cold Bore Ballistics LLC
**Region:** northcentralus (North Central US)

> **Purpose of this document.** This is the authoritative as-built record of the CBB
> Azure environment — what exists, how it is structured, and why. It is written for a
> reader with **no prior exposure to this Azure subscription or to Azure in general** —
> for example, an acquiring company's engineering team operating the system without
> the founder present.
>
> Two companion documents accompany this one:
> - **`Azure_Operating_Runbook.md`** — how to perform recurring tasks (SSH into the VM,
>   check Container App logs, push an image, run a Terraform change, rotate a credential).
> - **`Azure_Rebuild_Guide.md`** — how to reproduce the entire environment from scratch
>   for disaster recovery or due-diligence handoff.

---

## Table of Contents

1. [Orientation — what Azure is](#1-orientation--what-azure-is)
2. [Subscription and Tenant](#2-subscription-and-tenant)
3. [Resource Groups](#3-resource-groups)
4. [Terraform State Backend](#4-terraform-state-backend)
5. [Service Layout](#5-service-layout)
6. [Shared Service — Networking](#6-shared-service--networking)
7. [ScopeDOPE Service](#7-scopedope-service)
8. [CBB Corporate Service](#8-cbb-corporate-service)
   - [8.1 Static Web App — CBB Corporate](#81-static-web-app--cbb-corporate)
   - [8.2 DocuSeal — E-Signature Platform](#82-docuseal--e-signature-platform)
9. [IPsec Site-to-Site VPN](#9-ipsec-site-to-site-vpn)
10. [WireGuard Remote-User VPN](#10-wireguard-remote-user-vpn)
11. [GitHub Actions — OIDC Authentication](#11-github-actions--oidc-authentication)
12. [Access Control — RBAC and Entra ID Groups](#12-access-control--rbac-and-entra-id-groups)
13. [Tagging and Cost Allocation](#13-tagging-and-cost-allocation)
14. [Estimated Monthly Costs](#14-estimated-monthly-costs)
15. [Reference Identifiers](#15-reference-identifiers)
16. [DNS Hostnames](#16-dns-hostnames)
17. [Secrets and Key Material](#17-secrets-and-key-material)

---

## 1. Orientation — what Azure is

If you have never used Azure before, read this section first.

**Microsoft Azure** is a cloud computing platform. Cold Bore Ballistics uses it to host
the backend infrastructure for the ScopeDOPE product and the company website. All CBB
Azure resources live inside a single **subscription** — a billing and access boundary
owned by Cold Bore Ballistics LLC.

Key vocabulary used throughout this document:

- **Subscription** — the top-level billing container. All resources belong to exactly
  one subscription. Costs roll up to the subscription and appear on one invoice.
- **Resource group** — a logical grouping of resources within a subscription. Think of
  it as a folder. Resources in the same group share a lifecycle and can be managed
  together. CBB uses two: one for application resources, one for Terraform state.
- **Region** — the Azure data-center location where resources are created. CBB uses
  `northcentralus` (Illinois). Choosing one region for everything minimises network latency
  between resources and keeps costs predictable.
- **Terraform** — the Infrastructure-as-Code tool CBB uses to create and manage all
  Azure resources. No Azure resources are created manually in the portal — everything
  is defined in the `cbb-infra` git repository and applied via Terraform. This ensures
  the environment is fully reproducible and auditable.
- **Virtual Network (VNet)** — a private network inside Azure. Resources inside the
  same VNet can communicate without going to the internet.
- **Container App** — a serverless compute service for running Docker containers.
  CBB uses it to run DocuSeal (the e-signature platform). It scales to zero when idle,
  so no charge when not serving requests.
- **Static Web App (SWA)** — Azure's hosting service for static sites (HTML, CSS, JS).
  CBB uses it for the ScopeDOPE marketing site and the company website.
- **Azure Container Registry (ACR)** — a private Docker image registry. The CI/CD
  pipeline pushes container images here; the Container App pulls from here.
- **Virtual Machine (VM)** — a full Linux server. CBB uses one (`vm-infra-prod`,
  DNS `infra.coldboreballisticsllc.com`) as the **IPsec site-to-site VPN gateway** linking
  Azure to the local office network, and a general-purpose backend host for future roles.
  The feedback database is the managed PostgreSQL service (below), not on this VM.
- **Azure Database for PostgreSQL Flexible Server** — Azure's managed PostgreSQL. CBB uses
  it (`psql-cbb-prod`) for the feedback database, with managed backups and point-in-time
  restore, private (VNet-only) access.

---

## 2. Subscription and Tenant

| Field | Value |
|---|---|
| Subscription name | Cold Bore Ballistics LLC |
| Subscription ID | `735b4bb4-b179-4ed7-af87-2270bbbde658` |
| Tenant | `coldboreballisticsllc.com` |
| Tenant ID | `d553115d-3f7e-4bb8-8052-946b328cca87` |
| Region | `northcentralus` |
| Support plan | Free |

The subscription is a Pay-As-You-Go account billed to the company credit card. All
invoices are accessible in the Azure portal under **Cost Management + Billing**.

---

## 3. Resource Groups

CBB uses two resource groups, both in `northcentralus`.

| Resource group | Purpose |
|---|---|
| `rg-cbb-prod` | All application resources — VNet, VM, Container Apps, ACR, SWAs, storage |
| `rg-cbb-tfstate` | Terraform state storage only — the storage account that holds `.tfstate` files |

`rg-cbb-tfstate` exists specifically to isolate state storage from application resources.
If application resources are ever bulk-deleted or transferred, state storage remains intact.

---

## 4. Terraform State Backend

Terraform tracks what it has created in **state files** stored in Azure Blob Storage.

| Resource | Name |
|---|---|
| Storage account | `stcbbtfstate` (in `rg-cbb-tfstate`) |
| State container — shared | `shared` |
| State container — ScopeDOPE | `scopedope` |
| State container — CBB corporate | `cbb` |

Each service has its own state file, which means each can be planned and applied
independently. The `scope-dope` and `cbb` state files reference outputs from the
`shared` state file (VNet/subnet IDs) using Terraform's `terraform_remote_state` data source.

Access to the state storage account requires the `Storage Blob Data Contributor` role,
which is granted to the `sp-cbb-github-actions` service principal used by CI/CD.

---

## 5. Service Layout

The infrastructure is divided into four independent Terraform services, applied in order:

```
shared  →  scope-dope  →  cbb (first pass)  →  dns  →  cbb (second pass)
```

The two-pass `cbb` apply is required only when bootstrapping the DocuSeal custom domain: the first pass creates the Container App and outputs its FQDN and domain verification ID; those values are populated into `dns/terraform.tfvars`; dns is applied to create the CNAME and TXT records; the second cbb pass binds the custom domain and provisions the managed TLS certificate.

| Service | Directory | What it creates | Status |
|---|---|---|---|
| shared | `services/shared/` | VNet, subnets (`snet-cbb-prod`, `snet-pg-prod`, `snet-ca-prod`), shared Key Vault, LAN route table, tag policies | **LIVE** |
| scope-dope | `services/scope-dope/` | 3 SWAs, ACR, Storage, VM, scope-dope Key Vault, PostgreSQL | **LIVE** |
| cbb | `services/cbb/` | CBB corporate SWA, DocuSeal Container App + Environment, custom domain, managed cert | **LIVE** |
| dns | `services/dns/` | Azure DNS zones and record sets for all four CBB domains | **LIVE** |

**Acquisition boundary:** If ScopeDOPE is ever sold separately from CBB:
- Transfer `scopedope.tfstate` from `stcbbtfstate`
- Transfer all Azure resources tagged `project = scope-dope` in `rg-cbb-prod`
- `services/shared/` and `services/cbb/` remain with CBB, untouched

---

## 6. Shared Service — Networking

**Status: LIVE**

| Resource | Name | Value |
|---|---|---|
| Virtual Network | `vnet-cbb-prod` | `10.0.0.0/16` |
| Subnet (general) | `snet-cbb-prod` | `10.0.1.0/24` — VM |
| Subnet (PostgreSQL) | `snet-pg-prod` | `10.0.2.0/24` — delegated to `Microsoft.DBforPostgreSQL/flexibleServers` |
| Subnet (Container Apps) | `snet-ca-prod` | `10.0.4.0/23` — delegated to `Microsoft.App/environments`; hosts `cae-cbb-prod` |
| Private DNS zone | `privatelink.postgres.database.azure.com` | linked to the VNet; resolves `psql-cbb-prod` privately |

General application resources use `snet-cbb-prod`. The managed PostgreSQL Flexible Server
(§7.6) requires its **own** delegated subnet (`snet-pg-prod`) — it cannot share the general
subnet. The private DNS zone makes the server's FQDN resolve to a private VNet address.

**Route Table (UDR) — LIVE.** A route table `rt-cbb-prod-lan` is associated with
`snet-cbb-prod` and directs traffic destined for the local LAN subnets
(`172.31.0.0/22`, `172.29.1.0/24`) to the VM (`10.0.1.4`) as a `VirtualAppliance`
next hop. The VM forwards that traffic over the IPsec tunnel (see §9). This is what
lets Azure resources *other than the VM itself* reach the local network. It is defined
in the `modules/vm` Terraform module.

| Route table resource | Value |
|---|---|
| Name | `rt-cbb-prod-lan` |
| Associated subnet | `snet-cbb-prod` |
| Route 1 | `172.31.0.0/22` → VirtualAppliance `10.0.1.4` |
| Route 2 | `172.29.1.0/24` → VirtualAppliance `10.0.1.4` |

---

## 7. ScopeDOPE Service

### 7.1 Azure Container Registry

**Status: LIVE**

| Field | Value |
|---|---|
| Name | `crcbbprod` |
| Login server | `crcbbprod.azurecr.io` |
| SKU | Basic |
| Admin access | **Disabled** — no shared registry credential exists |
| Pull auth | Container App pulls via its system-assigned managed identity (**AcrPull** role) |

The DocuSeal image is mirrored from Docker Hub into ACR and pulled by the Container App from here. Repository path: `crcbbprod.azurecr.io/docuseal/docuseal:latest`. The Container App authenticates to the registry with its managed identity (AcrPull) — the admin user is disabled and there is no `acr-registry-password` secret.

### 7.2 Blob Storage

**Status: LIVE**

| Field | Value |
|---|---|
| Storage account | `stcbbprod` |
| Container — `dope-uploads` | Stores `.dope` files submitted via the feedback service API |
| Container — `docuseal-docs` | Stores signed PDFs from DocuSeal; Logic App `logic-docuseal-onedrive-prod` transfers them to OneDrive via Microsoft Graph |
| Access | Private — no anonymous access; DocuSeal reads account key from `kv-cbb-shared-prod/docuseal-storage-key` |

### 7.3 Static Web Apps — ScopeDOPE and Parked Trademark Domains

**Status: LIVE**

Azure Static Web Apps are not offered in `northcentralus`. All SWAs are deployed in `eastus2`. SWA is edge-served globally so the metadata region does not affect content latency. Custom domains are bound in Terraform via `azurerm_static_web_app_custom_domain` (apex uses dns-txt-token validation, www uses cname-delegation). DNS is managed in `services/dns/` — apex alias A record and www CNAME point at each SWA's default hostname.

> **Free tier limit: 2 custom domains per SWA.** Each domain pair therefore has its own dedicated SWA.

| Name | Default hostname | Custom domains | Purpose |
|---|---|---|---|
| `swa-scopedope-prod` | `delightful-desert-0da2cf60f.7.azurestaticapps.net` | `scope-dope.com` + `www.scope-dope.com` | ScopeDOPE marketing site |
| `swa-ballisticcomputer-prod` | `polite-water-0c537470f.7.azurestaticapps.net` | `ballisticcomputer.com` + `www.ballisticcomputer.com` | Parked trademark — redirects to `www.scope-dope.com` |
| `swa-ballisticscomputer-prod` | `delightful-hill-08994c70f.7.azurestaticapps.net` | `ballisticscomputer.com` + `www.ballisticscomputer.com` | Parked trademark — redirects to `www.scope-dope.com` |

All custom domain bindings are **bound and Ready**.

### 7.4 Container App — Feedback Service

**Status: DEFERRED — removed from Terraform as of 2026-06-06, targeted for v1.1.0 rebuild.**

The feedback service (Flask API for `.dope` file submissions) was removed from the `scope-dope` Terraform service when the VNet-injected Container App Environment was provisioned for DocuSeal. It will be rebuilt as part of the v1.1.0 scope with a dedicated Container App and proper image CI/CD. Tracked in Jira CBB-108.

The Container App Environment `cae-cbb-prod` is managed by the `cbb` service (DocuSeal) and will be shared by the feedback service when it is rebuilt.

### 7.5 Virtual Machine — IPsec gateway / backend host

**Status: LIVE**

`vm-infra-prod` (DNS `infra.coldboreballisticsllc.com`) is the **strongSwan IPsec
site-to-site VPN gateway** linking Azure to the local office network, and a
general-purpose backend host for future roles. It is provisioned entirely by cloud-init
on first boot — no manual steps after Terraform creates it.

> **No database on the VM.** PostgreSQL was moved to the managed **Azure Database for
> PostgreSQL Flexible Server** (`psql-cbb-prod`, §7.6) so the data survives any VM
> lifecycle event and gets managed backups. The VM name still contains "postgres" for
> historical/state-continuity reasons but no longer runs a DB.

| Field | Value |
|---|---|
| Name | `vm-infra-prod` |
| Public DNS | `infra.coldboreballisticsllc.com` → public IP |
| Internal DNS | `infra-int.coldboreballisticsllc.com` → `10.0.1.4` (in Azure DNS; private target, reachable only over the IPsec tunnel/LAN) |
| **Public IP** | **`52.159.73.194`** (`pip-infra-prod`, static; IPsec endpoint) |
| **Private IP** | **`10.0.1.4`** (static, on `snet-cbb-prod`; SSH / route-table next hop) |
| Size | `Standard_B2pts_v2` (ARM64, 2 vCPU, 1 GiB RAM, burstable) |
| OS | Ubuntu 24.04 LTS ARM64 |
| OS disk | 30 GB, Standard LRS (ephemeral — VM holds no persistent data; the DB is managed, §7.6) |
| Admin user | `azureuser` |
| SSH auth | Ed25519 key pair — public key in `kv-cbb-shared-prod/vm-ssh-public-key` |
| NIC | `nic-infra-prod` — static private IP, **IP forwarding enabled** (so it can route VNet↔LAN traffic over the tunnel) |
| NSG | `nsg-infra-prod` |

**NSG rules:**

| Rule | Port | Protocol | Source | Purpose |
|---|---|---|---|---|
| allow-ssh | 22 | TCP | `0.0.0.0/0` (any) | SSH — open to the internet; safe because sshd is **key-only** |
| allow-ipsec-ike | 500, 4500 | UDP | on-prem WAN IP `184.58.131.229/32` | IPsec IKE + NAT-T (ESP rides UDP 4500) |
| allow-wireguard | 51820 | UDP | `0.0.0.0/0` (any) | WireGuard — safe to expose publicly; unknown peers are silently dropped at the protocol level |

SSH is intentionally open to the internet (`allowed_ssh_cidrs = ["0.0.0.0/0"]`). This is
an accepted decision because the VM's sshd enforces **key-only authentication**:
`PasswordAuthentication no`, `KbdInteractiveAuthentication no`,
`PermitRootLogin without-password` (Ed25519 keys only — verified via `sshd -T`). There is
no password surface to brute-force. The IKE rule remains locked to the on-prem WAN IP.
(There is no longer an `allow-postgres` rule — the DB is the managed Flexible Server, §7.6.)

### 7.6 Database — Azure Database for PostgreSQL Flexible Server

**Status: LIVE**

The feedback database is the **managed** Azure Database for PostgreSQL Flexible Server —
chosen over VM-hosted Postgres because data safety is paramount: it provides managed
automated backups, point-in-time restore, and durability fully decoupled from the VM
lifecycle. It is private (VNet-integrated) — no public endpoint.

| Field | Value |
|---|---|
| Name | `psql-cbb-prod` |
| FQDN | `psql-cbb-prod.postgres.database.azure.com` (private; resolves only inside the VNet via the private DNS zone) |
| SKU | `Standard_B1ms` (Burstable, 1 vCPU, 2 GiB) — **12-month free-tier shape** |
| Version | PostgreSQL 16 |
| Storage | 32 GB (free-tier allowance; `auto_grow_enabled = false`) |
| Backups | **35-day** retention, locally-redundant, point-in-time restore (free up to the DB size) |
| Networking | Private — injected into `snet-pg-prod` (10.0.2.0/24, delegated); public access **Disabled** |
| Private DNS | `privatelink.postgres.database.azure.com` (zone in `shared`, §6) |
| Admin login | `cbbadmin` — password in `kv-cbb-shared-prod/vm-db-password` |
| Databases | `feedback` (ScopeDOPE feedback service), `docuseal` (DocuSeal e-signature) |
| TLS | `require_secure_transport = ON` (connections must use SSL; `sslmode=require`) |
| Availability zone | unset — northcentralus is non-zonal for this subscription |
| Lifecycle guard | `prevent_destroy = true` on the server **and** the database |

**Connection:** DocuSeal reads `kv-cbb-shared-prod/docuseal-database-url` = `postgresql://cbbadmin:…@psql-cbb-prod.postgres.database.azure.com:5432/docuseal?sslmode=require`. The feedback service (when rebuilt) will use its own connection string in `kv-scope-dope-prod`. Both resolve the private FQDN over the VNet via the private DNS zone.

> **Cost:** $0/month for the first 12 months under the Azure free-account offer (B1ms 750h
> + 32 GB). After the free window lapses: ~$12–15/month. A reminder is set in Outlook ahead
> of the expiry. The VM-hosted-Postgres approach (and the briefly-considered persistent
> data disk) were superseded by this — managed backups + PITR are the stronger
> data-durability choice.

---

## 8. CBB Corporate Service

**Status: LIVE**

### 8.1 Static Web App — CBB Corporate

| Field | Value |
|---|---|
| Name | `swa-cbb-prod` |
| Region | `eastus2` |
| Default hostname | `ambitious-bush-01991e40f.7.azurestaticapps.net` |
| Custom domains | `coldboreballisticsllc.com` (apex) + `www.coldboreballisticsllc.com` — **bound and Ready** |
| Purpose | Hosts the CBB company website |

DNS in `services/dns/`: apex alias A record + www CNAME → the SWA default hostname.

### 8.2 DocuSeal — E-Signature Platform

**Status: LIVE** — `https://esign.coldboreballisticsllc.com`

DocuSeal is CBB's self-hosted e-signature platform for NDAs, contractor agreements, and vendor agreements. It runs as an Azure Container App in a dedicated VNet-injected environment.

| Field | Value |
|---|---|
| Container App Environment | `cae-cbb-prod` |
| Container App | `ca-docuseal-prod` |
| Environment subnet | `snet-ca-prod` (`10.0.4.0/23`) — delegated to `Microsoft.App/environments` |
| Image | `crcbbprod.azurecr.io/docuseal/docuseal:latest` |
| CPU / Memory | 0.5 vCPU / 1 GiB |
| Min/Max replicas | 0 / 1 (scales to zero when idle) |
| Ingress port | 3000 |
| Azure FQDN | `ca-docuseal-prod.ambitiouswater-c352d6df.northcentralus.azurecontainerapps.io` |
| Custom domain | `esign.coldboreballisticsllc.com` — TLS via Azure Managed Certificate (`cert-docuseal-esign`), `SniEnabled` |
| DNS alias | `sign.coldboreballisticsllc.com` → CNAME → `esign.coldboreballisticsllc.com` |
| Managed identity | System-assigned — principal ID `84f4f44c-1334-4dd8-a6fd-09e3e496d0b1` |
| KV access | Key Vault Secrets User on `kv-cbb-shared-prod` (role assignment ID `1c0d62fc8b514e33971a9611a8403dfb`) |
| Database | `docuseal` in `psql-cbb-prod` |
| Storage | `docuseal-docs` container in `stcbbprod` |
| Terraform service | `services/cbb/` |

**Secrets — read from `kv-cbb-shared-prod` at runtime:**

| Secret name | Description |
|---|---|
| `docuseal-database-url` | PostgreSQL connection string (`docuseal` database, private FQDN, `sslmode=require`) |
| `docuseal-secret-key-base` | 64-byte random hex — Rails `SECRET_KEY_BASE` |
| `docuseal-storage-key` | `stcbbprod` primary access key |
| `docuseal-smtp-password` | SendGrid API key for outbound email |
| `docuseal-admin-password` | DocuSeal web UI admin password |

(Image pulls from `crcbbprod.azurecr.io` use the Container App's managed identity via the AcrPull role — there is no registry password secret.)

**Signed document transfer — Logic App:** `logic-docuseal-onedrive-prod` (Consumption, `northcentralus`) polls `stcbbprod/docuseal-docs` every 1 minute via the `azureblob` managed API connection. On each new blob, it filters for `.pdf`, retrieves the blob content, then PUTs it to Microsoft Graph (`Files.ReadWrite.All`) targeting the CBB SharePoint drive under `CBB_Corporate/03_-_Confidential/Legal_and_Trademarks/Signed_Agreements/`. Auth uses the Logic App's **system-assigned managed identity** — no licensed user, no delegated OAuth. Monitor alert `alert-logic-docuseal-failures-prod` emails `jschwefel@coldboreballisticsllc.com` on any run failure. Fully Terraform-managed (`services/cbb/logic_app.tf`). See `DocuSeal_Operating_Runbook.md` §4 for troubleshooting.

| Field | Value |
|---|---|
| Logic App | `logic-docuseal-onedrive-prod` |
| Managed identity principal ID | `2c299f73-5093-46bb-8549-a0096f1333c3` |
| Graph app role | `Files.ReadWrite.All` (`75359482-378d-4052-8f01-80520e7db3cd`) on Microsoft Graph SP `bccd0938-bf0f-4a65-85f4-9a02054c6a62` |
| Target drive ID | `b!8KZ6l6zqUkWX3yo-OUxmhYeF60xdYy5MvDh1TzvtjdYp4zfMYleGQpITWXd0CY2l` |
| Entra app reg (audit reference only) | `app-docuseal-sharepoint-prod` — client ID `7392de62-40be-406c-86f6-e507f9d69caf`; cert in `kv-cbb-shared-prod/app-docuseal-sharepoint-cert-pfx` (exp 2027-06-08); not used at runtime |

---

## 9. IPsec Site-to-Site VPN

A **strongSwan IPsec** tunnel provides a site-to-site VPN between the Azure VM and the
local office network (UniFi Dream Router). This gives the local network access to Azure
resources (and vice versa) without exposing services to the public internet.

**Mode:** Route-based (VTI), IKEv2, pre-shared-key authentication.

**Topology:**

```
Local network (172.31.0.0/22, 172.29.1.0/24)
        |
   UniFi Dream Router  — WAN 184.58.131.229
        |
   [ IPsec tunnel — IKEv2, UDP 500/4500 ]
        |
   Azure VM (vm-infra-prod) — public 52.159.73.194, private 10.0.1.4
   strongSwan, VTI interface vti0
        |
   Azure VNet (10.0.0.0/16) — LAN routes via route table rt-cbb-prod-lan (§6)
```

**Crypto (must match exactly on both ends):**

| Parameter | Value |
|---|---|
| Key exchange | IKEv2 |
| Phase 1 (IKE) | AES-256 / SHA-256 / DH group 14 (modp2048), lifetime 28800s |
| Phase 2 (ESP) | AES-256 / SHA-256 / DH group 14 (PFS on), lifetime 3600s |
| Authentication | Pre-shared key — `kv-cbb-shared-prod/ipsec-psk` |

**Endpoints / selectors:**

| Side | Value |
|---|---|
| Azure (local) | public `52.159.73.194`, leftsubnet `10.0.1.0/24` |
| On-prem (remote) | UniFi WAN `184.58.131.229`, rightsubnet `172.31.0.0/22, 172.29.1.0/24` |

**Routing notes (route-based VTI specifics):**
- The VM's NIC has **IP forwarding enabled**; the kernel has `net.ipv4.ip_forward=1`.
- strongSwan is set `install_routes=no` so its table-220 routes do not preempt the VTI
  routes in the main table. The `updown` hook builds `vti0` and routes the LAN subnets
  into it.
- **The VTI `local` is the VM's PRIVATE IP `10.0.1.4`, not the public IP** — Azure NATs
  to the public IP externally, so binding the VTI to the public IP breaks packet
  forwarding. This is the single most important detail when rebuilding the VM.
- Azure→LAN routing also depends on the route table `rt-cbb-prod-lan` (§6).

**UniFi configuration** (Dream Router → Settings → VPN → Site-to-Site VPN → IPsec):

| Field | Value |
|---|---|
| VPN type | IPsec, Route Based |
| Pre-shared key | `kv-cbb-shared-prod/ipsec-psk` |
| Local IP (WAN) | `184.58.131.229` |
| Remote IP | `52.159.73.194` (`infra.coldboreballisticsllc.com`) |
| Remote networks | `10.0.1.0/24` |
| Advanced (Manual) | IKEv2, AES-256, SHA256, DH14, PFS on, P1 28800 / P2 3600 |

**Verification:** On the VM, `ipsec status` should show the `azure-lan` SA `ESTABLISHED` /
`INSTALLED`; a ping from the VM to a LAN host (e.g. `172.31.1.200`) should succeed
(~14 ms). Manage the VM via `az vm run-command` while SSH is tunnel-restricted.

---

## 10. WireGuard Remote-User VPN

A **WireGuard** tunnel allows a single remote user (off-network, never on-premises) to
reach `172.31.1.200` (the internal file server) over an encrypted, split-tunnel VPN that
terminates on `vm-infra-prod`. It coexists with the strongSwan IPsec tunnel (§9) without
interference — the two daemons use independent ports and interfaces.

**Mode:** WireGuard kernel module, UDP, pre-shared keypair authentication (no PKI).

**Topology:**

```
Remote user (anywhere on the internet)
        |
   [ WireGuard tunnel — UDP 51820 ]
        |
   Azure VM (vm-infra-prod) — public 52.159.73.194, private 10.0.1.4
   WireGuard interface wg0 — server IP 10.10.0.1/30
        |
   IPsec tunnel (§9)
        |
   LAN → 172.31.1.200 (files.coldboreballisticsllc.com) only
```

**Address space:**

| Role | Address |
|---|---|
| WireGuard server (wg0) | `10.10.0.1/30` |
| Remote user peer | `10.10.0.2/32` |

The `/30` is a dedicated range outside all existing CBB networks (`10.0.0.0/16` Azure VNet,
`172.28.0.0/14` on-premises supernet) — no overlap risk.

**Firewall policy (enforced by iptables on the VM):**

| Direction | Source | Destination | Action |
|---|---|---|---|
| Forward | `10.10.0.2` | `172.31.1.200` | ACCEPT (port 80 and 443 only) |
| Forward | `10.10.0.2` | `172.28.0.0/14` | DROP (all other CBB LAN traffic blocked) |
| Forward | `10.10.0.2` | all other | ACCEPT (internet passthrough not used — split tunnel) |

Because the client uses **split-tunnel** (`AllowedIPs = 172.31.1.200/32`), only traffic
destined for `172.31.1.200` enters the tunnel at all. The iptables rules are a defence-in-depth
backstop in case the client config is ever modified.

**Key material:** Server private key and peer public key are stored in
`kv-cbb-shared-prod/wg-server-private-key` and `kv-cbb-shared-prod/wg-peer-ctimchak-public-key`.
The client `.conf` file (containing the client private key) is distributed out-of-band to the
user and is **never stored on the server or in any repository**.

**NSG rule:** `allow-wireguard` — UDP 51820 inbound from `0.0.0.0/0`.
This is intentional: WireGuard's cryptographic handshake makes the port safe to expose
publicly (unknown peers are silently dropped at the protocol level).

| Field | Value |
|---|---|
| Interface | `wg0` |
| Server listen port | UDP `51820` |
| Server tunnel IP | `10.10.0.1/30` |
| Server public key | `3cUWGLPEuZBZxY/S7y69wgDkMrhMe0SRYzOfZj6voig=` |
| Server private key | `kv-cbb-shared-prod/wg-server-private-key` |
| Config file | `/etc/wireguard/wg0.conf` (on VM) |
| Service | `wg-quick@wg0` (systemd, enabled on boot) |
| NSG rule | `allow-wireguard` — UDP 51820, priority 310, source `*` |

**Peers:**

| Name | Tunnel IP | Public key | KV secret |
|---|---|---|---|
| ctimchak | `10.10.0.2/32` | `/Bw6hKA7Ug7mnKZ+K4V8VIDwjiF9ItxQCVJYo3EKVio=` | `kv-cbb-shared-prod/wg-peer-ctimchak-public-key` |

See `Azure_Operating_Runbook.md §11` for peer management (add/remove/rotate keys).
See `Azure_Rebuild_Guide.md §14` for full reinstallation from scratch.

---

## 11. GitHub Actions — OIDC Authentication

GitHub Actions authenticates to Azure using **OIDC federated identity** — no client
secret exists or is needed. GitHub proves its identity cryptographically; Azure trusts
the proof based on the registered federated credential.

> **⚠ NOT YET IN PLACE (2026-06-06):** The service principal `sp-cbb-github-actions` was
> **deleted during the centralus→northcentralus migration and has not been recreated.**
> Until it is, GitHub Actions cannot run Terraform — all applies are currently done
> **locally** via the operator's `az login` identity. Recreate the SP + federated creds
> by re-running the service-principal section of `bootstrap/bootstrap.sh` before wiring
> CI. The `AZURE_CLIENT_ID` below is the OLD (deleted) value and will change on recreate.

**Service principal (to be recreated):** `sp-cbb-github-actions`

| Scope | Role |
|---|---|
| `rg-cbb-prod` | Contributor |
| `rg-cbb-tfstate` | Contributor |
| `stcbbtfstate` (storage account) | Storage Blob Data Contributor |

**GitHub Actions variables** (set as repository *variables* on `ColdBoreBallistics/cbb-infra`; update `AZURE_CLIENT_ID` after the SP is recreated):

| Variable | Value |
|---|---|
| `AZURE_CLIENT_ID` | *(stale — `87e6c68a-0f36-4d45-a941-e1650a5a645c` was the deleted SP; set to the new appId after recreate)* |
| `AZURE_TENANT_ID` | `d553115d-3f7e-4bb8-8052-946b328cca87` |
| `AZURE_SUBSCRIPTION_ID` | `735b4bb4-b179-4ed7-af87-2270bbbde658` |

**Secrets:** Application secrets are no longer injected as GitHub Actions secrets — they
live in Azure Key Vault and are read by the Container App's managed identity (§7.4, §15).
The only credential GitHub Actions needs is the OIDC federated identity above (no secret).

**Deployment workflow:**
- **Pull request** → GHA plans all three services and posts output as a PR comment.
  This is the review gate — inspect the plan before merging.
- **Merge to `main`** → GHA applies: `shared` first, then `scope-dope` and `cbb` in parallel.
  The `terraform-apply` GHA environment restricts applies to protected branches only.

---

## 12. Access Control — RBAC and Entra ID Groups

Subscription-level access is granted through three **Entra ID security groups** provisioned in the M365 tenant. Each group maps to one Azure built-in RBAC role at the subscription scope (`/subscriptions/735b4bb4-b179-4ed7-af87-2270bbbde658`). Assign individuals to these groups in Entra ID to grant or revoke their Azure access.

| Entra ID group | Object ID | Azure RBAC role | Scope | What it can do |
|---|---|---|---|---|
| `sg-azure-ro` | `0855aaeb-624f-49c2-86de-e371392ffa3a` | Reader | Subscription | Read-only view of all resources — no changes |
| `sg-azure-rw` | `aa465fb5-3c11-404d-9a60-a287418aa2f8` | Contributor | Subscription | Create/modify/delete resources; no role assignments |
| `sg-azure-admin` | `8a33351a-ba76-439d-960d-50a90a51b107` | Owner | Subscription | Full control including role assignments; reserved for infrastructure admins |

Role assignments applied: 2026-06-06 via `az role assignment create`.

> These groups are defined in the M365 tenant — see `M365_Architecture_Reference.md` → Infrastructure access groups. Membership management is done in Entra ID (M365 Admin Center → Teams & groups → Security groups), not in the Azure portal.

---

## 13. Tagging and Cost Allocation

Every Azure resource carries a mandatory tag set. Tags enable cost allocation by product
line in Azure Cost Analysis and support clean asset separation during due diligence.

| Tag | scope-dope resources | cbb resources |
|---|---|---|
| `project` | `scope-dope` | `cbb` |
| `owner` | `cbb` | `cbb` |
| `cost_center` | `scopedope` | `cbb-corp` |
| `managed_by` | `terraform` | `terraform` |
| `environment` | `prod` | `prod` |

**Checking compliance:**

```bash
bash cbb-utils/azure/azure_tags.sh           # report only
bash cbb-utils/azure/azure_tags.sh --enforce # apply missing tags
```

Run after every `terraform apply` and before any due-diligence process.

---

## 14. Estimated Monthly Costs

Costs are approximate and based on the northcentralus region Pay-As-You-Go rates.
All resources are now live; figures should be confirmed against the first full billing cycle.

| Resource | Est. monthly cost | Notes |
|---|---|---|
| VM `Standard_B2pts_v2` | ~$15–20/mo | Live; burstable ARM64, always-on (IPsec gateway) |
| Managed PostgreSQL `psql-cbb-prod` | **$0 for 12 mo**, then ~$12–15/mo | B1ms + 32 GB free-tier; reminder set before expiry |
| Container Apps | Consumption | Scales to zero; charged on requests |
| Logic App `logic-docuseal-onedrive-prod` | ~$0.01–0.05/mo | Consumption plan; ~1 440 trigger polls/day + actions on each signed doc; negligible at CBB volume |
| ACR Basic | ~$5/mo | Plus data transfer |
| Blob Storage `stcbbprod` | Minimal | At current volume |
| Key Vaults (×2) | ~$0 | Per-operation; negligible at CBB volume |
| SWA `swa-scopedope-prod` | Free tier | Static hosting |
| SWA `swa-cbb-prod` | Free tier | Static hosting |
| SWA `swa-ballisticcomputer-prod` | Free tier | Parked trademark domain |
| SWA `swa-ballisticscomputer-prod` | Free tier | Parked trademark domain |
| Azure DNS (4 zones) | ~$2/mo | $0.50/zone/month × 4 |
| VNet / Public IP | ~$3–5/mo | Static public IP on the VM |
| **Total estimate** | ~$29–34/mo | Confirm after first full billing cycle |

Actual costs are visible in the Azure portal under **Cost Management + Billing → Cost Analysis**.
Filter by resource group `rg-cbb-prod` and tag `project` to separate ScopeDOPE vs CBB costs.

---

## 15. Reference Identifiers

| Item | Value |
|---|---|
| Subscription ID | `735b4bb4-b179-4ed7-af87-2270bbbde658` |
| Tenant ID | `d553115d-3f7e-4bb8-8052-946b328cca87` |
| Tenant domain | `coldboreballisticsllc.com` |
| Service principal app ID | `87e6c68a-0f36-4d45-a941-e1650a5a645c` |
| Service principal name | `sp-cbb-github-actions` |
| App resource group | `rg-cbb-prod` |
| State resource group | `rg-cbb-tfstate` |
| State storage account | `stcbbtfstate` |
| VNet | `vnet-cbb-prod` (`10.0.0.0/16`) |
| Subnet | `snet-cbb-prod` (`10.0.1.0/24`) |
| LAN route table | `rt-cbb-prod-lan` |
| Key Vault (shared/infra) | `kv-cbb-shared-prod` |
| Key Vault (ScopeDOPE app) | `kv-scope-dope-prod` |
| ACR | `crcbbprod` / `crcbbprod.azurecr.io` |
| Storage account | `stcbbprod` |
| SWA (ScopeDOPE) | `swa-scopedope-prod` (eastus2) |
| SWA (CBB) | `swa-cbb-prod` (eastus2) |
| SWA (ballisticcomputer.com) | `swa-ballisticcomputer-prod` (eastus2) |
| SWA (ballisticscomputer.com) | `swa-ballisticscomputer-prod` (eastus2) |
| Container App environment | `cae-cbb-prod` |
| Container App (DocuSeal) | `ca-docuseal-prod` |
| DocuSeal Azure FQDN | `ca-docuseal-prod.ambitiouswater-c352d6df.northcentralus.azurecontainerapps.io` |
| DocuSeal managed identity | `84f4f44c-1334-4dd8-a6fd-09e3e496d0b1` |
| Logic App (signed doc transfer) | `logic-docuseal-onedrive-prod` |
| Logic App managed identity | `2c299f73-5093-46bb-8549-a0096f1333c3` |
| Container App subnet | `snet-ca-prod` (`10.0.4.0/23`) |
| VM | `vm-infra-prod` (IPsec gateway + WireGuard endpoint) |
| VM public IP | `pip-infra-prod` → `52.159.73.194` |
| VM private IP | `10.0.1.4` |
| WireGuard interface | `wg0` — `10.10.0.1/30`, UDP 51820 |
| WireGuard server public key | `3cUWGLPEuZBZxY/S7y69wgDkMrhMe0SRYzOfZj6voig=` |
| Managed PostgreSQL | `psql-cbb-prod` → `psql-cbb-prod.postgres.database.azure.com` (private) |
| PostgreSQL subnet | `snet-pg-prod` (10.0.2.0/24, delegated) |
| PostgreSQL private DNS | `privatelink.postgres.database.azure.com` |

---

## 16. DNS Hostnames

DNS for all CBB domains is managed in **Azure DNS** via Terraform (`cbb-infra/services/dns`). Namecheap is the registrar only — NS delegation points all four domains at Azure DNS. See `Namecheap/Namecheap_Architecture_Reference.md` for registrar details.

| Hostname | Type | Target | Purpose |
|---|---|---|---|
| `infra.coldboreballisticsllc.com` | A | `52.159.73.194` | VM public name / IPsec endpoint |
| `infra-int.coldboreballisticsllc.com` | A | `10.0.1.4` | VM private name — reachable only over IPsec tunnel/LAN |
| `esign.coldboreballisticsllc.com` | CNAME | `ca-docuseal-prod.ambitiouswater-c352d6df.northcentralus.azurecontainerapps.io` | DocuSeal e-signature platform |
| `sign.coldboreballisticsllc.com` | CNAME | `esign.coldboreballisticsllc.com` | DocuSeal convenience alias |
| `scope-dope.com` + `www` | Alias A / CNAME | `swa-scopedope-prod` default hostname | ScopeDOPE marketing site |
| `coldboreballisticsllc.com` + `www` | Alias A / CNAME | `swa-cbb-prod` default hostname | Company site |
| `ballisticcomputer.com` + `www` | Alias A / CNAME | `swa-ballisticcomputer-prod` default hostname | Parked trademark — redirects to `www.scope-dope.com` |
| `ballisticscomputer.com` + `www` | Alias A / CNAME | `swa-ballisticscomputer-prod` default hostname | Parked trademark — redirects to `www.scope-dope.com` |

---

## 17. Secrets and Key Material

> **Confidential.** Secret *values* are NOT stored in this document. All runtime secrets
> live in Azure Key Vault; this section is a pointer.

| Where | Holds |
|---|---|
| `kv-cbb-shared-prod` | Infrastructure secrets: `ipsec-psk`, `vm-db-password`, `vm-ssh-public-key`, `wg-server-private-key`, `wg-peer-ctimchak-public-key`, plus DocuSeal secrets (`docuseal-database-url`, `docuseal-secret-key-base`, `docuseal-storage-key`, `docuseal-smtp-password`, `docuseal-admin-password`) and shared service creds (sendgrid/twilio/namecheap/atlassian). Read by the DocuSeal Container App managed identity and Terraform operators. |
| `kv-scope-dope-prod` | ScopeDOPE app runtime secrets (reserved for feedback service rebuild at v1.1.0); placeholder secrets from prior config. |

> The `acr-registry-password` secret was removed from both vaults — the ACR admin user is
> disabled and the Container App pulls via managed identity (§7.1).

**Network access (both vaults):** `network_acls.default_action = Deny` with an
`AzureServices` bypass. `kv-cbb-shared-prod` allows the operator IP (secret seeding) and the
Container App environment's static outbound IP (DocuSeal runtime); `kv-scope-dope-prod`
allows the operator IP. This is a network-firewall layer on top of RBAC — access requires
both an allowed network path and an RBAC role.

Vault details, the full secret inventory, and rotation procedures are in
`Azure_Key_Vault/Azure_Key_Vault_Architecture_Reference.md` and `…_Operating_Runbook.md`.

**IPsec PSK rotation:** generate a new PSK (`openssl rand -base64 32`), update
`kv-cbb-shared-prod/ipsec-psk` and `services/scope-dope/terraform.tfvars` (`ipsec_psk`),
update the UniFi Site-to-Site Pre-Shared Key to match, and re-apply Terraform (the VM is
recreated; cloud-init writes the new PSK).

---

## Document Control

| Field | Value |
|---|---|
| **Document owner** | `jschwefel@coldboreballisticsllc.com` |
| **Classification** | Confidential (03_-_Confidential/) |
| **Location** | `OneDrive/CBB_Corporate/03_-_Confidential/Infrastructure/Azure/Azure_Architecture_Reference.md` |
| **Cross-references** | `Azure_Operating_Runbook.md`; `Azure_Rebuild_Guide.md`; `Azure_Acquisition_Brief.md`; `Azure_Key_Vault_Architecture_Reference.md` |
| **Created** | 2026-06-07 |
| **Last updated** | 2026-06-10 |
| **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 |
| 1.0 | 2026-06-08 | jschwefel | — | — | Add Logic App (§8.2), managed identity, Drive ID, app reg audit reference, cost row, reference identifiers; retire Power Automate reference |
| DRAFT | 2026-06-10 | jschwefel | — | — | Add §10 WireGuard Remote-User VPN; renumber §10–16 → §11–17; update TOC |
