Infrastructure
The Dash runs on DigitalOcean infrastructure deployed inside a private Virtual Private Cloud (VPC), which keeps all platform traffic on an isolated internal network rather than the public internet. Compute, storage, and database resources are provisioned within this VPC using infrastructure-as-code, so every environment is reproducible, version-controlled, and auditable.
- Introduction & Scope
- Infrastructure Overview
- Network Architecture
- DNS & Domain Architecture
- TLS & Edge Security
- Compute & Application Layer
- Storage & Data Layer
- Secrets Management
- Identity & Access
- Infrastructure as Code
- Supply-Chain Controls
- Observability
- Audit Infrastructure
- Backup & Disaster Recovery
- Capacity & Scaling
- Maintenance & Patching
- Appendices
Introduction & Scope
Document purpose, platform context, referenced documents
Chapter 1 — Introduction & Scope
1.1 Document Purpose
This document (LH-SBS-INF-001) is the authoritative infrastructure specification for The Dash — SBS's Multi-Gated AI Orchestrator Platform, designed by Ledger Hub Networks exclusively for SBS. It defines the complete DigitalOcean hosting environment on which the Platform operates: account and project structure, network topology, DNS and domain architecture, TLS and edge security, compute and storage layers, secrets management, identity integration, infrastructure-as-code practice, observability, audit infrastructure, and backup and disaster recovery.
The specification serves four functions. First, it is the build reference: every infrastructure resource provisioned for the Platform must conform to the configurations stated herein, and any deviation requires a documented change under the governance forms in effect for the engagement (LH-SBS-CO- series). Second, it is the verification baseline: the acceptance gates defined in LH-SBS-INST-001 (V1–V22) test the environment against this document. Third, it carries the Build Register (Appendix, Chapter 18): the tracked checklist of provisioning items, security controls, and open nonconformances between the design target and the as-built state, with per-item completion status. Fourth, it is the operating reference for the IT execution owner (Nelson Santos, IT Director) and any personnel subsequently authorized to administer the environment.
Where the as-built environment deviates from the design target, this document states both: the target configuration (normative) and the current state (informative, dated), with the gap tracked in the Build Register. A statement of current state is not acceptance of it.
This document is 100% technical in content. Commercial terms, fees, and engagement governance are out of scope and are addressed in their respective controlling documents.
1.2 Platform Context
The Dash is a zero-trust, identity-aware platform that runs SBS's routine corporate workflows through isolated, per-function Compartments. Inside each Compartment, three classes of AI agents operate under supervision — Workflow Agents perform the first-pass work, Guardian Agents enforce policy and Risk Tiers, and Supervisor Agents route Medium/High/Critical decisions to named SBS staff. The domain is publicly resolvable but serves a private audience behind SSO and phishing-resistant MFA; every agent decision is written to a tamper-evident, hash-chained audit log.
The Platform is hosted on DigitalOcean under a dedicated SBS account, organized into two DigitalOcean Projects that define the environment topology:
- SBS The Dash Dev — development and staging environment
- SBS The Dash Prod — production environment
All internal service-to-service traffic is required to remain on private VPC networking. Public exposure is restricted to hardened entry points in two classes.
Tenant surfaces (three-subdomain tenant model):
sbsdash.com— primary tenant surfaceadmin.sbsdash.com— administrative consoleclients.sbsdash.com— client-facing surface
Operational surfaces:
monitor.sbsdash.com— observability stack (Grafana dashboards, Prometheus metrics); deployed and live; access to be IdP-enforced (Chapter 9)wiki.sbsdash.com— documentation portal (BookStack); hosts platform runbooks, configuration references, and operational procedures
Operational surfaces carry no tenant data and sit behind the same TLS, reverse-proxy, and firewall controls as the tenant surfaces, with access scoped to administrative roles only under the least-privilege model of the IAM control family.
Each of the ten functional Compartments is isolated across eight layers — frontend, authentication, application, data, storage, secrets, network, and audit — and this specification defines how each isolation layer is realized in infrastructure: firewall rule sets and VPC segmentation (network); per-Compartment separation across the PostgreSQL, MongoDB, and Redis data services (data, storage); Vault namespace separation (secrets); and hash-chained log capture (audit). The infrastructure supporting the audit log — its storage, integrity chain, and retention — is specified in Chapter 13.
The environment follows a zero-trust posture per NIST SP 800-207 and application controls aligned to OWASP ASVS L2, with AI-specific controls addressing the OWASP LLM Top 10 (including LLM01 prompt injection) as carried in LH-SBS-SEC-001.
1.3 Referenced Documents
| Document ID / Source | Title | Relationship to this Specification |
|---|---|---|
| LH-SBS-KO-001 V3.2 | Technical Install and Discovery | Master runbook. This specification elaborates the infrastructure control families imported into KO-001 from the Foundation Install Spec (Vault, IaC, supply-chain, observability, backup/DR, hash-chained audit, identity depth, WordPress isolation). |
| LH-SBS-SEC-001 | Security & Access Control Specification | Controlling security baseline. The control families (DNS, TLS, IAM, APP, AI, INF, FE, MON) constrain every configuration stated in this document. The Build Register in Chapter 18 tracks implementation status per control ID. Control-set reconciliation pending: the Build Register enumerates 41 control IDs against the 36 recorded in SEC-001 V1.0; SEC-001 revision to V1.1 or register correction to follow. |
| LH-SBS-PRE-001 | Pre-Install Prerequisites | Gate conditions. The nine prerequisite gates must be satisfied before any resource defined in this specification is provisioned (Day 0). |
| SERVER_CHANGELOG.md | Server Changelog — SBS Infrastructure | Operational as-built log maintained by platform engineering. Feeds the Build Register with dated change entries and verification evidence. Entries in the changelog do not modify this specification; divergences it records are tracked as nonconformances. The changelog must contain no credentials (INF-02); see Build Register item 15. |
Where this specification and LH-SBS-SEC-001 address the same control, LH-SBS-SEC-001 states the requirement and this document states the implementation. In the event of conflict, LH-SBS-SEC-001 controls and this document shall be corrected.
Infrastructure Overview
DigitalOcean account structure, region selection, environment topology (production / staging)
Chapter 2 — Infrastructure Overview
2.1 DigitalOcean Account Structure
The Platform operates under a single dedicated DigitalOcean account owned by SBS (the "SBS Account"). A dedicated account — rather than a shared or Ledger Hub-owned account — keeps billing, resource ownership, and access control unambiguously with SBS and satisfies the tenant-separation intent of the IAM control family at the infrastructure root.
Account-level configuration (target):
- Account ownership — SBS-controlled email alias (not an individual mailbox), so ownership survives personnel changes. Execution owner: Nelson Santos (IT Director).
- Account MFA — phishing-resistant MFA enforced on the account root and on every team member, consistent with IAM-03. No shared credentials.
- Team access — DigitalOcean Teams with role-scoped membership. Ledger Hub Networks platform engineering joins as team members with the minimum role required for build activities; access is reviewed under IAM-06 and revocable by SBS without Ledger Hub involvement.
- API tokens — issued per-purpose (IaC pipeline, monitoring integrations), scoped read or write as required, stored exclusively in Vault (Chapter 8), rotated per the secrets lifecycle. No tokens in repositories, plaintext CI variables, or local files — per INF-02 and INF-03.
- Billing alerts — configured at account level so anomalous resource creation (a common compromise indicator) is surfaced to the account owner immediately.
Nonconformance — credential handling (CRITICAL). As of 2026-07-17 the operational changelog carried plaintext credentials: a shared root password stated to cover all project droplets, the GitHub account, the wiki admin account, and the Grafana admin account. All four credential sets are treated as compromised. Remediation (Build Register item 15): rotate all four immediately; migrate to Vault upon its deployment (Chapter 8) with interim storage in an approved password manager; strip the credentials section from the changelog and purge it from any version-control history; enforce distinct per-service credentials. No shared passwords are permitted for any host or service.
2.2 Project Topology
Resources are organized into two DigitalOcean Projects:
| Project | Project ID | Purpose |
|---|---|---|
| SBS The Dash Dev (Default) | ac656e28-2196-410e-847c-bf3b60363710 |
Development and staging. Build and integration work occurs here first; no tenant data. |
| SBS The Dash Prod | c49c646f-f268-4b51-a00e-ad9fb5055569 |
Production. Live Platform resources: application services, observability stack, documentation portal, audit-log storage. |
2.2.1 Droplet Inventory (As Built, 2026-07-18)
| Droplet | Project | Region / VPC | Size | Public IP | Private IP | OS | Role |
|---|---|---|---|---|---|---|---|
Dev-SBS-server |
Dev | NYC2 / default-nyc2 | 8 GB / 160 GB | 162.243.252.138 | 10.100.0.2 | Ubuntu 24.04 LTS | Development server. Docker 29.1.3 / Compose 2.40.3. |
sbsdash-server-prod |
Prod | NYC2 / default-nyc2 | 8 GB / 160 GB | 107.170.72.145 | 10.100.0.3 | TBC | Production application server; tenant surfaces. Docker 29.1.3 / Compose 2.40.3. Administered via jump host (§3.3). |
monitor-servers |
Prod | NYC2 / default-nyc2 | 4 GB / 120 GB | 162.243.28.132 | 10.100.0.4 | Ubuntu 26.04 LTS | Observability — Grafana 13.1.0, Prometheus, nginx 1.28.3, TLS live at monitor.sbsdash.com. Docker 29.1.3 / Compose 2.40.3. De facto SSH bastion (§3.3). |
sbs-wiki |
Prod | NYC1 / default-nyc1 ⚠ | 2 GB / 60 GB | 167.172.135.88 | 10.116.0.2 | TBC | Documentation portal — BookStack; wiki.sbsdash.com. |
Dev-Gitlab-server ⚠ |
Unregistered | VPC 10.120.x | TBC | 138.197.222.154 | TBC | TBC | Undocumented droplet currently receiving the sbsdash.com apex A record. Disposition pending (Build Register item 22). |
⚠ Two inventory nonconformances: (a) sbs-wiki region (§2.3); (b) Dev-Gitlab-server exists outside the two-project inventory yet holds production DNS — see §2.2.2.
2.2.2 Nonconformance — Production DNS Target (CRITICAL)
The apex record sbsdash.com → 138.197.222.154 currently resolves to Dev-Gitlab-server, not to sbsdash-server-prod (107.170.72.145). The primary tenant domain therefore points at an uninventoried development host. Required action (Build Register item 16), one of:
- (a) Repoint — move the apex A record to
sbsdash-server-prodonce its proxy/TLS stack is ready (Chapter 5), and either decommissionDev-Gitlab-serveror inventory it properly under the Dev project with its own firewall posture; or - (b) Hold — if the GitLab box is intentionally serving a temporary landing page, record it in this inventory, apply the Dev firewall posture, and schedule the repoint as a dated cutover.
Until resolved, Dev-Gitlab-server is inside the security boundary of the engagement and subject to all INF-family controls.
2.2.3 Project Topology Rules
- Promotion path — configurations are built and verified in Dev, then promoted to Prod exclusively through the infrastructure-as-code workflow (Chapter 10). No manual, console-driven resource creation in Prod; console access to Prod is break-glass only and logged.
- No shared resources (target) — no droplet, database, volume, or network resource is a member of both environments, and Dev and Prod reside on separate VPC networks with no peering. As built, this rule is not met: Dev, Prod, and monitor droplets all share
default-nyc2(10.100.0.0/20). See §3.1 for the remediation decision. - No production data in Dev — Dev operates on synthetic or sanitized data only. Tenant data, Compartment data, and audit-log content never leave the Prod boundary.
- Naming convention — new resources carry environment-prefixed names (
dev-/prod-). Existing droplet names are grandfathered in the Build Register; renaming may occur at a maintenance window (renames are cosmetic and zero-downtime).
2.3 Region Selection
Primary region: NYC2 (New York).
Rationale:
- Latency to operating footprint — SBS headquarters is in Miami with operations concentrated on the U.S. East Coast and Puerto Rico; the New York regions are the lowest-latency full-service option for the Miami–New York–San Juan corridor.
- Data residency — all tenant data, Compartment data, and audit-log content remain within the continental United States.
- Single-region baseline — both environments deploy in NYC2. Backup archives replicate to SFO3 for geographic redundancy (Chapter 14); no live workload runs outside NYC2.
Nonconformance — sbs-wiki (NYC1). DigitalOcean VPC networks are region-scoped: the NYC1 wiki droplet cannot join any NYC2 VPC. As built, the monitor scrapes the wiki's node_exporter over the public internet, unencrypted, admitted by a ufw rule scoped to the monitor's public IP. Metric content is low-sensitivity (CPU/RAM/disk), so this is an accepted interim state — but it is eliminated, not mitigated, by the wiki rebuild into NYC2 (Build Register item, carried from prior revision). Sequencing: rebuild before VPC enrollment and firewall standardization (Chapter 3); rebuilding after DNS/TLS/proxy work would force rework of all three.
Managed-service availability. NYC2 predates some newer DigitalOcean managed-service capacity. Availability of Managed Databases (PostgreSQL, MongoDB, Redis) in NYC2 is to be confirmed before Chapter 7 is finalized; the fallback design is self-hosted data services under Docker Compose on dedicated droplets within the Prod VPC.
2.4 Environment Topology
| Attribute | Dev (Staging) | Prod |
|---|---|---|
| DigitalOcean Project | SBS The Dash Dev | SBS The Dash Prod |
| Compute | Dev-SBS-server |
sbsdash-server-prod, monitor-servers, sbs-wiki (pending NYC2 rebuild) |
| VPC (target) | Dedicated Dev VPC (NYC2) | Dedicated Prod VPC (NYC2), no peering with Dev |
| VPC (as built) | default-nyc2, shared ⚠ | default-nyc2, shared ⚠ (wiki: default-nyc1) |
| OS | Ubuntu 24.04 LTS | Mixed: 26.04 (monitor), others TBC — standardization decision pending (Build Register item 21) |
| Public surfaces | None (target); as built, sbsdash.com apex resolves to a Dev-side host ⚠ (§2.2.2) |
sbsdash.com, admin., clients. → sbsdash-server-prod (target); monitor.sbsdash.com → monitor-servers (live); wiki.sbsdash.com → sbs-wiki |
| Data services | PostgreSQL, MongoDB, Redis — reduced sizing, synthetic data (not yet installed) | PostgreSQL, MongoDB, Redis — production sizing, per-Compartment separation (Chapter 7) (not yet installed) |
| Observability | node_exporter on VPC-private IP (deployed) | Full stack on monitor-servers (deployed and verified 2026-07-17); node_exporter on prod/wiki (deployed) |
| Documentation | — | BookStack on sbs-wiki (deployed) |
| Runtime toolchain | Docker 29.1.3 / Compose 2.40.3 (deployed). Node.js (pnpm), Rust (Cargo), Python pending — versions pinned per Chapter 11 at install | Same |
| Source control | Single GitHub repository set (platform code + IaC), branch-protected per INF-03 — repository setup pending | Same; Prod deploys from protected release branches only |
Toolchain versions are pinned identically across both environments (Chapter 11) so that Dev verification is meaningful for Prod promotion — version drift between environments is treated as a build defect. The current OS mix (24.04 vs 26.04) is a tracked instance of exactly this class of drift.
2.5 Server Users and Host Access
Target (INF-04): named user accounts per individual; no shared accounts; no direct root login (PermitRootLogin no); SSH key authentication only, password authentication disabled at the daemon; privilege elevation via sudo, logged, without blanket NOPASSWD; authoritative user list maintained in the IaC repository (Chapter 10); administrative access paths and IdP enforcement per Chapter 9.
As built (2026-07-17/18) — nonconformant:
- Root SSH login is enabled fleet-wide and in active use.
- A single SSH keypair (
id_ed25519, one operator workstation) is authorized for bothrootandsbsRooton all droplets — one shared identity for the entire fleet. - Shared
sbsRootaccount (uid 1000) exists on prod, dev, and monitor withNOPASSWD:ALLsudo anddockergroup membership (docker group membership is root-equivalent). - Prod is reachable only via jump host through
monitor-servers(§3.3) — the one element of current practice retained in the target design.
Remediation (Build Register item 17): create per-person named users with individually generated keys; disable root SSH; replace blanket NOPASSWD with scoped sudo (or passworded sudo); retire or restrict sbsRoot to break-glass with vaulted credentials; capture the authorized-user list in IaC. Until complete, individual attribution of host actions is impossible, which also degrades MON-01 (unified audit trail).
Network Architecture
VPC design, subnets, internal routing, egress control, firewall rules
Chapter 3 — Network Architecture
3.1 VPC Design
Target: two dedicated VPC networks, one per environment, both in NYC2, no peering:
| VPC | Environment | Members |
|---|---|---|
prod-vpc-nyc2 |
Prod | sbsdash-server-prod, monitor-servers, sbs-wiki (post NYC2 rebuild) |
dev-vpc-nyc2 |
Dev | Dev-SBS-server |
As built: neither dedicated VPC exists. Current membership:
| VPC (actual) | CIDR | Members |
|---|---|---|
default-nyc2 |
10.100.0.0/20 | Dev-SBS-server (.2), sbsdash-server-prod (.3), monitor-servers (.4) — Dev and Prod share one flat private network ⚠ |
default-nyc1 |
10.116.0.0/20 | sbs-wiki (.2) — unreachable from NYC2 private networking |
| 10.120.x | — | Dev-Gitlab-server (§2.2.2) |
Constraint driving the decision: a DigitalOcean droplet's VPC assignment is fixed at creation — moving a droplet to a new VPC requires snapshot-and-rebuild. Two paths (Build Register item 18):
- (a) Rebuild into dedicated VPCs — recommended. At the current fleet size (4–5 droplets, pre-data-services, pre-tenant-data) rebuilds are cheap: snapshot, re-create in target VPC, re-run configuration. Cost grows sharply once databases and Compartments are live. This path restores the "no shared network" rule of §2.2.3 as a structural guarantee. The wiki NYC2 rebuild is already mandatory, so path (a) folds it into one rebuild wave.
- (b) Accept shared default-nyc2 and compensate with strict host firewalls on every droplet. Dev↔Prod isolation then rests entirely on firewall rulesets rather than network non-adjacency — a weaker claim, and a permanent audit caveat against INF-01.
This specification proceeds on path (a); §3.2 onward describes the target VPCs. If path (b) is elected, §3.1–3.2 are amended by Change Order.
DigitalOcean VPC model notes (unchanged): a VPC is a single flat private network per region — no user-defined subnets, route tables, or security groups; intra-VPC segmentation is enforced by Cloud Firewalls (tag-scoped) plus host firewalls (§3.5, §3.6). VPCs are region-scoped, which is the technical basis for the wiki rebuild requirement.
3.2 Address Plan
| Network | CIDR | State |
|---|---|---|
prod-vpc-nyc2 |
10.10.0.0/20 | Target — created during rebuild wave |
dev-vpc-nyc2 |
10.20.0.0/20 | Target — non-overlapping with Prod and with the legacy 10.100/10.116/10.120 ranges, so logs and tooling remain unambiguous during migration |
default-nyc2 (10.100.0.0/20), default-nyc1 (10.116.0.0/20), 10.120.x |
— | Legacy — vacated at end of rebuild wave |
Private IPs are recorded in the Build Register per droplet at VPC enrollment; the authoritative address inventory lives in IaC state (Chapter 10), not in this document, to avoid drift.
3.3 Internal Routing and Service Communication
All service-to-service traffic between Prod droplets transits the VPC private interface (eth1), never public IPs.
- Application → data services (target) — the application stack on
sbsdash-server-prodreaches PostgreSQL, MongoDB, and Redis exclusively on VPC-private addresses; data services bind to the private interface only, with no public listener (verified at LH-SBS-INST-001 network gates). - Prometheus scrape paths (deployed, verified 2026-07-17) —
monitor-serversscrapes node_exporter on prod (10.100.0.3:9100) and dev (10.100.0.2:9100) over private networking; exporters bind private-only and are unreachable publicly. On the monitor itself, Prometheus (9090), Grafana (3000), and local node_exporter (9100) bind 127.0.0.1 only, fronted by nginx TLS — confirmed byss -tlnpverification. This matches the target design and is marked verified for the NYC2 members. - Wiki scrape (interim nonconformance) —
sbs-wiki(NYC1) is scraped on its public IP (167.172.135.88:9100), unencrypted, admitted only from the monitor's public IP by ufw rule. Accepted temporarily (metrics only, low sensitivity); eliminated by the NYC2 rebuild, after which the wiki exporter rebinds to its private address like the rest of the fleet. - Jump-host / bastion pattern (as built, retained in design) —
sbsdash-server-prodaccepts SSH only via ProxyJump throughmonitor-servers(ssh -J). This makes the monitor a de facto bastion. Consequences now normative: the monitor carries the tightest SSH admission (admin allowlist, §3.5); its compromise radius includes prod SSH reachability, so it receives priority hardening, patching, and log scrutiny (MON-01/MON-02); bastion SSH sessions are logged. Whether the bastion role remains on the monitor or moves to a dedicated minimal droplet is revisited at the rebuild wave. - DNS resolution — droplets use systemd-resolved against DigitalOcean resolvers; internal addressing uses IaC-managed
/etc/hostsentries (small fixed fleet). Revisit if the fleet exceeds ~10 hosts.
Inter-Compartment note (unchanged): Compartment network isolation is realized within sbsdash-server-prod at the Docker Compose network level — one dedicated compose network per Compartment, no shared bridges, cross-Compartment traffic denied by non-membership. Docker 29.1.3 / Compose 2.40.3 are now present on prod, dev, and monitor (2026-07-18), so this layer is buildable. Container-level detail in Chapter 6.
3.4 Egress Control
DigitalOcean provides no managed NAT/egress gateway; egress control is enforced at the host firewall layer:
- Restricted-egress allowlist at the host firewall on each Prod droplet, permitting: TCP 443 to Anthropic API endpoints; TCP 443 to GitHub (deploy pulls, pinned to published ranges, refreshed by IaC); TCP 443 to OS package mirrors and container registries enumerated in Chapter 11; TCP/UDP 123 (NTP); TCP 443 to Let's Encrypt ACME endpoints; DNS 53 to DigitalOcean resolvers only.
- All other outbound denied and logged — egress-deny lines ship to the observability pipeline (Chapter 12) as a MON-02 anomaly input. Unexpected egress from a Compartment container is a primary indicator for AI-02 (tool allowlisting) and LLM01 (prompt-injection exfiltration) violations.
- Compartment containers inherit a narrower egress set than the host: Anthropic API plus explicitly allowlisted per-Compartment integration endpoints, enforced via per-compose-network firewall chains, IaC-managed and change-controlled.
As built: no egress control exists on any droplet (all outbound open). Egress enforcement lands with the host-firewall baseline (§3.6, Build Register item 20).
3.5 Cloud Firewall Rules (DigitalOcean Cloud Firewalls, tag-scoped)
As built: no Cloud Firewalls exist. Host-level ufw is active on monitor and wiki only; prod and dev have no active firewall at any layer — their public interfaces are protected solely by service bind addresses. This is the most exposed element of the current state and is remediated first in the firewall workstream (Build Register item 19).
Target — four firewalls, attached by droplet tag:
FW prod-app → tag prod (sbsdash-server-prod)
| Dir | Proto/Port | Source / Destination | Purpose |
|---|---|---|---|
| In | TCP 443 | 0.0.0.0/0, ::/0 | Tenant surfaces via reverse proxy |
| In | TCP 80 | 0.0.0.0/0, ::/0 | ACME HTTP-01 + 301→443 only |
| In | TCP 22 | monitor-servers private IP only |
SSH via bastion (§3.3) — no direct public SSH |
| In | TCP 9100 + app exporters | monitor-servers private IP |
Prometheus scrape |
| Out | Per §3.4 | — | Restricted egress |
FW prod-monitor → tag grafana (monitor-servers)
| Dir | Proto/Port | Source | Purpose |
|---|---|---|---|
| In | TCP 443 | 0.0.0.0/0 | monitor.sbsdash.com (Grafana behind IdP, Chapter 9) |
| In | TCP 80 | 0.0.0.0/0 | ACME + redirect |
| In | TCP 22 | Admin allowlist IPs (Build Register) | Bastion SSH — sole public SSH entry to Prod |
| — | — | — | 3000/9090/9100 bind 127.0.0.1 — no public rule exists (verified as-built) |
FW prod-wiki → tag wiki (sbs-wiki, post-rebuild)
| Dir | Proto/Port | Source | Purpose |
|---|---|---|---|
| In | TCP 443 | 0.0.0.0/0 | wiki.sbsdash.com (BookStack behind SSO) |
| In | TCP 80 | 0.0.0.0/0 | ACME + redirect |
| In | TCP 22 | monitor-servers private IP |
SSH via bastion |
| In | TCP 9100 | monitor-servers private IP |
node_exporter scrape (rebinds private post-rebuild) |
FW dev-app → tag develop — SSH from admin allowlist; web surfaces admit admin allowlist only (no production DNS points at Dev — enforcement of this depends on resolving §2.2.2).
Deviation from prior draft, deliberate: direct public SSH to prod-app is removed in favor of bastion-only SSH, codifying the as-built jump-host pattern — it is the stronger posture. Rule hygiene unchanged: firewalls are IaC-defined; console edits are drift and reverted; the admin SSH allowlist is a named IaC variable reviewed at each IAM-06 access review.
3.6 Host Firewall
Cloud Firewalls filter at the platform edge; a host firewall on each droplet provides defense-in-depth and the per-container egress allowlist (§3.4), which Cloud Firewalls cannot express.
As built: ufw on monitor (22/80/443 in) and wiki (22/80/443 + 9100-from-monitor in); ufw inactive on prod and dev; no egress rules anywhere; no nftables rulesets deployed.
Target: nftables on all droplets — required because per-Compartment egress chains (one chain per compose network) and logged default-deny outbound are beyond ufw's expressible policy. Migration order: (1) activate baseline host firewall on prod and dev immediately (ufw acceptable as a stopgap — inbound mirror of the Cloud Firewall admit set); (2) replace fleet-wide with IaC-rendered nftables rulesets at the rebuild wave; (3) add per-Compartment chains when Compartment compose networks are created (Chapter 6). Base policy: inbound default-deny mirroring the cloud firewall, outbound default-deny with the §3.4 allowlist, established/related admitted. Rulesets are rendered from IaC templates and versioned; the live ruleset hash is a monitored value (MON-01) so tampering surfaces as drift.
3.7 Verification
Network gates (LH-SBS-INST-001): public-listener scan — no data-service or Prometheus ports on public interfaces (passes today on monitor per ss -tlnp evidence; wiki exporter public listener is a tracked exception until rebuild); Dev↔Prod isolation probe — no private path between environments (fails today: shared default-nyc2; passes after rebuild wave); egress-allowlist test — denied-domain attempt logged (not testable today: no egress control); per-Compartment cross-network denial test (pending Chapter 6 build); bastion path test — prod SSH unreachable except via monitor. INF-01 is the controlling SEC-001 requirement throughout.
DNS & Domain Architecture
sbsdash.com zone, three-subdomain tenant model, record inventory, DNSSEC
Chapter 4 — DNS & Domain Architecture
4.1 Domain and Registrar
sbsdash.com is the sole Platform domain. Registrar-level state is a security control surface in its own right (DNS-01):
- Ownership (target) — the domain is registered to SBS (org-level contact, SBS-controlled email alias — same survivability rationale as the DigitalOcean account, §2.1). The Build Register carries the domain transfer item ("Transfer SBSDASH.COM") until registrar ownership sits with SBS; the current registration under the interim account is a tracked transitional state.
- Registrar lock —
clientTransferProhibitedenabled; transfer-out requires an authenticated unlock action. Domain-transfer attempts are a high-severity alert. - Registrar MFA — phishing-resistant MFA on the registrar account. IAM-03 applies to every control plane, registrar included: registrar compromise is functionally equivalent to full platform compromise, since it permits nameserver redelegation.
- Renewal — auto-renew enabled, multi-year registration, expiry monitored (an expired domain is simultaneously an availability failure and a takeover opportunity).
4.2 Authoritative DNS
As built (2026-07-18): the zone is hosted on GoDaddy nameservers. GoDaddy does not provide DNSSEC signing for externally manageable zones in this configuration, records are console-managed rather than IaC-managed, and no API-driven change control is in place. This state satisfies neither DNS-01 (DNSSEC) nor the IaC management rule (Chapter 10) and is a tracked nonconformance (Build Register item 9).
Target: the authoritative zone is hosted at a DNSSEC-capable, API-managed provider. Approved options, in preference order:
- Cloudflare DNS (free tier sufficient) — one-click DNSSEC signing, DS handoff to the registrar, full API for IaC management, native CAA/CT tooling.
- deSEC — DNSSEC-by-default, API-managed, nonprofit.
- Registrar-hosted DNS only if the registrar signs zones and exposes a management API.
Zone cutover sequence (Nelson Santos, execution owner): replicate the current record set at the new provider → verify resolution parity against §4.3 → lower TTLs to 300 → update NS delegation at the registrar → confirm propagation → enable DNSSEC and publish DS (§4.5) → raise TTLs. The zone is thereafter managed as code (Chapter 10): records are defined in the IaC repository and applied via provider API; console edits are drift and reverted.
4.3 Record Inventory
As built (2026-07-18):
| Record | Type | Value (actual) | State |
|---|---|---|---|
sbsdash.com |
A | 138.197.222.154 (Dev-Gitlab-server) ⚠ |
Nonconformant — apex resolves to an uninventoried dev host, not prod (§2.2.2, Build Register item 16) |
monitor.sbsdash.com |
A | 162.243.28.132 (monitor-servers) |
Live and verified — TLS valid, Grafana responding (changelog 2026-07-17) |
wiki.sbsdash.com |
A | 167.172.135.88 (sbs-wiki, NYC1) |
Live; IP changes at NYC2 rebuild |
admin.sbsdash.com, clients.sbsdash.com, www |
— | Not yet published | Pending prod proxy/TLS readiness (Chapter 5) |
Target:
| Record | Type | Value | TTL | Serves |
|---|---|---|---|---|
sbsdash.com |
A | 107.170.72.145 | 300 (build) → 3600 (steady) | Primary tenant surface — repointed from 138.197.222.154 at cutover |
www.sbsdash.com |
CNAME | sbsdash.com | 3600 | Redirect to apex at proxy |
admin.sbsdash.com |
A | 107.170.72.145 | 300 → 3600 | Administrative console |
clients.sbsdash.com |
A | 107.170.72.145 | 300 → 3600 | Client-facing surface |
monitor.sbsdash.com |
A | 162.243.28.132 | 3600 | Grafana/Prometheus (IdP-gated) |
wiki.sbsdash.com |
A | NYC2 rebuild IP | 300 until rebuild, then 3600 | BookStack portal |
Notes:
- Tenant surfaces share one A target; host-header routing at the reverse proxy (Chapter 5) separates them. No wildcard record exists —
*.sbsdash.comis unresolvable by design (FE family: no surface enumeration; a wildcard would also pollute the CT-monitoring signal, §4.6). - The apex repoint (138.197.222.154 → 107.170.72.145) is executed only after the prod proxy and TLS stack pass their Chapter 5 gates — repointing to an unready origin is an availability failure. Until then the current apex target remains inside the engagement security boundary (§2.2.2).
- Dev has no records in the public zone. The as-built apex record violates this rule today; it is cured by the repoint. Dev access is by IP against the admin allowlist.
- IPv6: AAAA records are added only when droplet IPv6 is enabled and firewall rulesets carry mirrored v6 rules — publishing AAAA without v6 firewall parity is a firewall bypass, so the two land together or not at all. (As built there are no v6 firewall rules anywhere; no AAAA records are published.)
4.4 Mail Posture (DNS-03)
sbsdash.com sends and receives no mail. Null-mail posture, published explicitly so the domain cannot be used as a spoofed sender:
| Record | Type | Value |
|---|---|---|
sbsdash.com |
MX | 0 . (null MX, RFC 7505) |
sbsdash.com |
TXT | v=spf1 -all |
_dmarc.sbsdash.com |
TXT | v=DMARC1; p=reject; adkim=s; aspf=s; rua=mailto:<SBS security alias> |
These records are publishable on GoDaddy today — they do not depend on the zone cutover and should not wait for it. DMARC aggregate reports route to an SBS security mailbox on an existing SBS mail domain (not sbsdash.com), making spoof attempts observable — a MON-02 anomaly input, not merely a static record.
One caution: the Grafana admin account and the changelog reference admin@sbsdash.com and a Yahoo mailbox as service identities. No mailbox exists or will exist on sbsdash.com under the null-mail posture; service accounts must use SBS-controlled aliases on SBS's mail domain. Folded into the credential remediation (Build Register item 15).
4.5 DNSSEC (DNS-01)
As built: unsigned — no signing path exists on the current GoDaddy hosting (§4.2). DNS-01 is unmet until zone cutover completes.
Target, unchanged in substance:
- Zone signed at the DNS provider (ECDSA P-256, algorithm 13 — compact signatures, universal resolver support).
- DS record published at the registrar — signing without registrar DS publication provides no validation path. End-to-end verification:
dig +dnssec sbsdash.comreturns the AD flag from a validating resolver, and DNSViz shows an unbroken chain. This verification is a named Build Register gate (item 10). - Key rollovers are provider-automated (CDS/CDNSKEY where the registrar supports it); manual DS changes are change-controlled.
- Failure-mode note: DNSSEC misconfiguration causes hard resolution failure, not degraded security — DS/keyset changes execute inside the 300-TTL window and are verified before TTLs are raised.
4.6 Certificate and Registration Surveillance
- CAA (DNS-02):
sbsdash.com CAA 0 issue "letsencrypt.org"plusCAA 0 iodef "mailto:<SBS security alias>". Only Let's Encrypt may issue for the domain — consistent with the deployed ACME/certbot practice already live onmonitor.sbsdash.com(Let's Encrypt via certbot, auto-renewal by systemd timer, per changelog 2026-07-17). Any other CA refusing on CAA is the control working.issuewildis intentionally absent (inheritsissue; acceptable — no wildcard certificates are used). Publishable on GoDaddy today; does not wait for cutover. - CT monitoring (DNS-04): subscription against Certificate Transparency logs for
sbsdash.com(Cloudflare CT alerts or crt.sh RSS into the alert pipeline). Baseline note: certificates formonitor.sbsdash.comalready appear in CT from the 2026-07-17 issuance — expected and legitimate. Any CT appearance not originating from the platform's ACME automation is a high-severity incident (CAA bypass or issuance compromise). - Typosquat monitoring (DNS-05): dnstwist scheduled weekly from
monitor-serversagainst a generated permutation set (homoglyphs, transpositions, TLD swaps); newly registered lookalikes feed MON-02 alerting. No defensive registrations by default; escalation case-by-case via Alexandra Del Rey (security-compliance routing).
4.7 Change Control and Verification
Until zone cutover, GoDaddy console changes are restricted to: the three mail records (§4.4), the CAA record (§4.6), and the apex repoint (§4.3) — each logged in SERVER_CHANGELOG.md with date and operator. After cutover, zone changes follow the IaC path (Chapter 10) with the same drift rules as firewalls.
Verification gates: NS delegation at registrar points to the DNSSEC-capable provider; DNSSEC chain validates (AD flag, DNSViz clean); null-MX/SPF/DMARC present; CAA present; apex and all subdomain records resolve per the §4.3 target table (apex → 107.170.72.145, not 138.197.222.154); no wildcard resolves; no Dev host referenced anywhere in the zone. Controls covered: DNS-01 through DNS-05.
Build Register deltas from this chapter: item 9 restated as GoDaddy→provider cutover with sequence; item 16 apex repoint gated on Chapter 5 readiness; new item 23 — publish null-mail + CAA records on GoDaddy immediately (pre-cutover); item 15 extended to cover the admin@sbsdash.com / Yahoo service-identity cleanup. Chapter 5 (TLS & Edge Security) next — the monitor's live nginx/certbot stack gives it a verified as-built baseline to write against.
TLS & Edge Security
Certificate management, reverse proxy configuration, hardening headers
Chapter 5 — TLS & Edge Security
5.1 Edge Model
Every public surface is fronted by an nginx reverse proxy terminating TLS on the droplet that serves it. No CDN or external proxy layer is in scope at this fleet size; origin concealment (TLS-04) is addressed in §5.6 within that constraint.
| Surface | Droplet | Proxy → Upstream | State |
|---|---|---|---|
monitor.sbsdash.com |
monitor-servers |
nginx 1.28.3 → Grafana 127.0.0.1:3000 (websockets on /api/live/) |
Live, verified 2026-07-17 |
wiki.sbsdash.com |
sbs-wiki |
nginx → BookStack (localhost) | Live; re-verified at NYC2 rebuild |
sbsdash.com, www |
sbsdash-server-prod |
nginx → Compartment frontend containers (Docker Compose, Chapter 6) | Pending — gates the apex repoint (§4.3) |
admin.sbsdash.com |
sbsdash-server-prod |
nginx → admin console container | Pending |
clients.sbsdash.com |
sbsdash-server-prod |
nginx → client surface container | Pending |
Host-header routing on sbsdash-server-prod separates the three tenant surfaces behind one IP. A default server block returns 444 (connection close, no response) for any request whose Host/SNI matches no configured surface — direct-to-IP scans and unrecognized hostnames get nothing, including no certificate hint (see §5.2 default cert note).
5.2 Certificate Management (TLS-02)
- CA and protocol: Let's Encrypt via ACME. Issuance and renewal by certbot with the nginx plugin, auto-renewal by
certbot.timer(systemd) — the pattern already live and verified on the monitor is the fleet standard. - One certificate per hostname. No wildcard certificates (consistent with §4.6 CAA —
issuewildnot authorized) and no multi-SAN certificates spanning surfaces: a SAN list enumeratingadmin.andclients.on the apex cert would leak the surface inventory to any TLS client, defeating the FE-family enumeration controls. Per-hostname certs keep CT disclosure to exactly the hostname being served. - Challenge type: HTTP-01 on port 80 per surface (matches the §3.5 firewall admits). DNS-01 challenges become available post zone cutover (§4.2) but are not required — HTTP-01 avoids granting the ACME client DNS-write credentials, a smaller blast radius.
- Renewal monitoring: certificate expiry is a Prometheus-alerted metric (blackbox exporter probe per surface, Chapter 12). A cert inside 14 days of expiry with renewals failing is a paged alert —
certbot.timerfailure is otherwise silent. - Default/fallback certificate: the
444default block carries a self-signed placeholder cert (nginx requires one to complete the handshake before it can close); it discloses no real hostname. - Key hygiene: private keys remain on the serving droplet at
/etc/letsencrypt/live/<host>/, root-owned 0600, never copied off-host, never committed. Keys are not vaulted — reissuance via ACME is cheaper and safer than key escrow.
5.3 Protocol Floor (TLS-01)
Fleet-standard nginx TLS parameters, IaC-templated (Chapter 10), identical on every surface:
ssl_protocols TLSv1.2 TLSv1.3;— nothing below 1.2, ever. 1.3 preferred by client negotiation.- TLS 1.2 cipher set restricted to AEAD/ECDHE (
ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-*-AES256-GCM-*,CHACHA20-POLY1305); no CBC, no RSA key exchange, no 3DES/RC4. ssl_prefer_server_ciphers off;(correct posture for a modern AEAD-only set — client picks its fastest).- ECDSA P-256 leaf keys preferred (smaller handshakes); RSA-2048 acceptable where tooling requires.
- OCSP stapling on (
ssl_stapling on; ssl_stapling_verify on;). - Session tickets off (
ssl_session_tickets off;) — avoids ticket-key rotation burden; session cache local, 10 m. - Verification:
testssl.shrun per surface at each gate; target grade A on SSL Labs semantics with no protocol/cipher findings. The monitor's live config is brought to this template at the next maintenance window (certbot's default bundle admits TLS 1.0/1.1 on some distro snippets — to be confirmed and corrected; Build Register).
5.4 HSTS (TLS-03)
- Port 80 exists solely for ACME HTTP-01 (
/.well-known/acme-challenge/) and a 301 to HTTPS; no content is ever served over HTTP. - Rollout in two steps: (1) at surface go-live,
Strict-Transport-Security: max-age=86400— one day, recoverable if anything mis-serves; (2) after seven clean days, raise tomax-age=31536000; includeSubDomains; preloadfleet-wide and submitsbsdash.comto the Chrome preload list. includeSubDomainsis safe only because the null-mail/no-other-services posture (§4.4) guarantees no plaintext-HTTP subdomain will ever exist. Preload submission is one-way in practice (removal takes months); it is executed as a named Build Register gate after Alexandra Del Rey's sign-off, since it permanently commits every future*.sbsdash.comservice to TLS.
5.5 Security Headers (APP-02) and Frontend Exposure Controls (FE-01…FE-06)
Fleet-standard header set, applied at nginx (add_header ... always;), IaC-templated; per-surface CSP tightening happens at the application layer:
| Header | Value (baseline) | Control |
|---|---|---|
Strict-Transport-Security |
per §5.4 | TLS-03 |
X-Frame-Options |
DENY |
APP-02 (clickjacking) |
X-Content-Type-Options |
nosniff |
APP-02 |
Referrer-Policy |
no-referrer |
FE-04 — no sbsdash.com URL ever appears in an outbound Referer |
Content-Security-Policy |
default-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self' — tightened per app |
APP-02 / FE-05 ('self'-only enforces first-party assets: no CDN fonts, no third-party scripts, no analytics beacons) |
X-Robots-Tag |
noindex, nofollow, noarchive, nosnippet |
FE-01 — anti-indexing at the header layer, covers non-HTML responses too |
Permissions-Policy |
camera=(), microphone=(), geolocation=(), interest-cohort=() |
APP-02 |
Cross-Origin-Opener-Policy |
same-origin |
APP-02 |
- robots.txt (FE-02): every surface serves
User-agent: * / Disallow: /. Defense-in-depth with FE-01 — robots.txt alone is advisory; the header is the enforcement. - No social cards (FE-03): no OpenGraph/Twitter meta tags in any served HTML; link unfurlers (Slack, iMessage, crawlers) get no title, description, or preview image. Grafana and BookStack template overrides to strip their defaults are Build Register items.
- Neutral metadata (FE-06):
server_tokens off;(no nginx version); HTML<title>values are neutral ("Sign in" — not "SBS …"); nometa description; error pages are generic nginx defaults with no branding. The platform's public fingerprint is deliberately information-free: resolvable domain, TLS cert, sign-in page, nothing else. - Grafana behind the proxy requires
proxy_set_header Host+ websocket upgrade headers on/api/live/(already configured); BookStack requires correctX-Forwarded-Prototo generate https URLs.
5.6 Origin Concealment (TLS-04)
Scoped honestly for a no-CDN architecture: the A records necessarily disclose droplet IPs, so concealment here means the origin exposes nothing but the intended service:
- Direct-to-IP requests hit the
444default block — no content, no redirect, no certificate naming a real host. - No service banner, version string, or debug endpoint on any public port (§5.5 neutral metadata;
ss -tlnpgate confirms only 80/443/22 public). - Non-public services (Grafana upstream 3000, Prometheus 9090, exporters 9100, future data services) bind 127.0.0.1 or VPC-private only — verified as-built on the monitor.
- If DDoS or scraping pressure later justifies it, fronting with Cloudflare proxy (post zone cutover) upgrades TLS-04 to true IP concealment; that decision is deferred and would arrive as a Change Order touching §4.2, §5.2 (cert model), and §3.5 (firewall admits narrowed to Cloudflare ranges).
5.7 Admin Surface Isolation (APP-03, edge layer)
admin.sbsdash.com receives edge-layer restrictions beyond the tenant surfaces, ahead of the application-layer controls in Chapter 9:
- Separate nginx server block, separate upstream container, separate access log.
- IP allowlist at the edge (
allow/denyfrom the IaC admin-allowlist variable) — the admin console is not globally reachable even pre-authentication, unlikesbsdash.com/clients.. - Stricter rate limit (
limit_reqzone: 5 r/s vs 20 r/s tenant baseline) andclient_max_body_sizescoped to actual admin upload needs. - All four proxies (three tenant + admin) log in a common structured format shipped to the observability pipeline (Chapter 12) — access logs are a MON-01/MON-02 input.
5.8 Verification
Per-surface gate set (LH-SBS-INST-001): testssl.sh clean, protocol floor confirmed (no 1.0/1.1 handshake accepted); cert issued/renewing with expiry probe green; HTTP→HTTPS 301 with ACME path excluded; full header set present (curl -sI diff against template); robots.txt served; direct-IP request returns 444; no OG/Twitter tags in HTML; server_tokens absent; admin surface unreachable from a non-allowlisted IP; upstream ports absent from public ss -tlnp. The monitor surface — already live — is retro-fitted to the full template and re-verified as the reference implementation before the prod surfaces build against it.
Compute & Application Layer
Droplet/container inventory, sizing, per-Compartment isolation mapping to infrastructure resources
Chapter 6 — Compute & Application Layer
6.1 Compute Model
The Platform runs on a small fixed droplet fleet with containerized workloads under Docker Compose — no Kubernetes, no managed PaaS. Rationale: the fleet is ≤5 hosts with a single-application production profile; Compose delivers the required per-Compartment network isolation (§6.4) with a fraction of the operational surface of an orchestrator, and its state is fully expressible in versioned files (IaC, Chapter 10). Revisit only if the Compartment count or droplet fleet grows materially (Chapter 16 thresholds).
As-built baseline: Docker 29.1.3 + Compose v2.40.3 installed from Ubuntu official repos (no third-party apt sources — consistent with the supply-chain posture, Chapter 11) on sbsdash-server-prod, Dev-SBS-server, and monitor-servers (2026-07-18); docker service enabled at boot. The wiki droplet intentionally has no Docker — BookStack runs as a system service and stays that way until the NYC2 rebuild, where containerization is decided (§6.6).
6.2 Droplet Roles and Sizing
| Droplet | Size (as built) | Workload | Sizing assessment |
|---|---|---|---|
sbsdash-server-prod |
8 GB / 4 vCPU / 160 GB | nginx edge (Ch. 5), Compartment application containers, data services (interim, §6.5) | Adequate for build phase and first ~3 Compartments. Memory is the binding constraint once PostgreSQL + MongoDB + Redis colocate; Chapter 16 sets the resize/split trigger. |
Dev-SBS-server |
8 GB / 4 vCPU / 160 GB | Mirror of prod stack, synthetic data | Deliberate 1:1 with prod so Dev verification is load-meaningful. |
monitor-servers |
4 GB / 2 vCPU / 120 GB | Prometheus + Grafana + nginx; bastion (§3.3) | Adequate; Prometheus retention sizing in Ch. 12. No application workloads permitted — bastion role keeps this host minimal. |
sbs-wiki |
2 GB / 1 vCPU / 60 GB | BookStack | Adequate; carries at NYC2 rebuild. |
Rule: the monitor and wiki are single-purpose hosts. Application or data workloads never deploy to them — the monitor because it is the bastion and alerting authority (compromise radius, §3.3), the wiki because it is an operational surface with no tenant data (§1.2).
6.3 Runtime Toolchain
Target toolchain on prod and dev (Build Register items; not yet installed as of 2026-07-18):
| Component | Purpose | Install source | Pinning |
|---|---|---|---|
| Node.js (LTS) + pnpm | Platform application runtime and package manager | NodeSource or nvm per Ch. 11 decision | Exact version in .nvmrc / engines; pnpm via corepack, version pinned |
| Rust + Cargo | Performance-critical platform components | rustup, pinned toolchain file (rust-toolchain.toml) |
Exact toolchain per repo |
| Python 3 | Tooling, ETL, operational scripts | Ubuntu system python + venvs | Per-project requirements.txt hash-pinned |
| Git | Deploy pulls, IaC | Ubuntu repo | — |
Two rules carried from §2.4: versions are identical on prod and dev (drift = build defect), and runtimes exist on the host primarily for build/operational tooling — application code executes inside containers, whose images pin their own runtime versions (Ch. 11). Host-level Node/Rust/Python are not a second execution path for platform services.
6.4 Per-Compartment Isolation at the Compute Layer
This section realizes the application/network/data slices of the eight-layer isolation model (§1.2) in Compose terms. Each of the ten Compartments deploys as an isolated stack:
- One Compose project per Compartment (
compartment-01-hr…compartment-10-pr), each with its own compose file rendered from a common IaC template — no shared "mega-compose." - One dedicated bridge network per Compartment (
net-c01…net-c10),internal: truewhere the Compartment needs no direct egress (egress instead via its declared integration endpoints per §3.4 chains). No container joins two Compartment networks. Cross-Compartment traffic is denied by non-membership — there is no route to filter because there is no shared segment. - Edge attachment: each Compartment's frontend container additionally joins a shared
net-edgenetwork reachable only by nginx. nginx is the sole multi-network process; Compartment-to-Compartment via the edge network is impossible because only frontends and nginx sit on it and frontends accept only proxied requests (verified by header check at the app layer, APP-05). - Per-Compartment resource limits:
mem_limitandcpusset per service so one Compartment's runaway agent load cannot starve the other nine (availability isolation, not just confidentiality). - No privileged containers, no docker.sock mounts,
no-new-privileges: true, read-only root filesystems where the app allows — container escape hardening baseline. - Secrets: injected at runtime from Vault (Ch. 8) via env-file rendered at deploy, never baked into images, never in compose files committed to the repo (INF-02). Per-Compartment Vault namespaces mean Compartment 03's containers can never read Compartment 08's credentials (AI-03 credential isolation at the infra layer).
- AnythingMCP deploys as a per-Compartment sidecar container on that Compartment's network — MCP tool servers are Compartment-scoped, which is the infrastructure half of AI-02 (tool allowlisting): a Compartment can only reach the MCP tools deployed on its own network, and each MCP container's outbound is bound by that Compartment's §3.4 egress chain.
6.5 Data Services Placement (Interim)
Pending the NYC2 Managed Database availability check (§2.3), PostgreSQL, MongoDB, and Redis run as containers on sbsdash-server-prod:
- Each data service on its own compose network (
net-data-pg, etc.); Compartment application containers join only the data networks they are entitled to, and per-Compartment isolation inside each database engine (schemas/databases/ACLs per Compartment) is specified in Chapter 7. - Data services bind container ports to the VPC-private interface or localhost only — never 0.0.0.0 (public-listener gate, §3.7).
- Volumes on the droplet's block storage with the backup regime of Chapter 14.
- If Managed Databases are confirmed in NYC2, migration moves data off-droplet and this section is amended by Change Order; the entitlement model (which Compartment reaches which database) is unchanged either way.
6.6 Wiki and Monitor Stacks
- monitor-servers: Prometheus/Grafana remain apt-installed system services (as built, verified) — containerizing the monitoring stack adds a Docker dependency to the thing that watches Docker; current form is deliberate. Docker exists on this host solely for operational tooling (e.g., blackbox exporter if containerized later).
- sbs-wiki: at NYC2 rebuild, BookStack is redeployed either as system service (snapshot restore, fastest) or containerized (compose template, consistent with fleet). Decision at rebuild; either way it stays a single-purpose host on the Prod VPC with private-bound exporter.
6.7 Deployment Flow
Images are built in CI from the GitHub repository (Ch. 10/11), pushed to the registry enumerated in the supply-chain allowlist, and pulled by prod over the §3.4 egress path. Deploys are docker compose up -d against IaC-rendered files, executed by the deploy user (per-person or CI identity — INF-04 remediation applies, §2.5); manual docker run on prod is prohibited and surfaces as drift (running-container set vs. compose state is a monitored comparison, MON-01). Rollback = redeploy previous pinned image tag; images are immutable and tagged by digest.
6.8 Verification
Gates (LH-SBS-INST-001): Compose project per Compartment present with dedicated network; cross-Compartment connectivity probe fails between any two Compartment networks; frontend reachable only via nginx (direct container port probe fails); no privileged container, no docker.sock mount, no-new-privileges set (docker inspect sweep); data-service ports absent from public interface; resource limits present on every Compartment service; running containers match compose state (drift check); toolchain versions identical prod↔dev (node -v, rustc -V, python3 -V diff).
Storage & Data Layer
Managed databases, object storage, volume encryption, data isolation boundaries
Chapter 7 — Storage & Data Layer
7.1 Data Layer Overview
Three engines serve the Platform, each with a distinct role; nothing else is introduced without a Change Order:
| Engine | Role | Persistence class |
|---|---|---|
| PostgreSQL | System of record — Compartment relational data, platform configuration, decision metadata | Durable, backed up, point-in-time recovery target |
| MongoDB | Document workloads — agent artifacts, semi-structured Compartment content | Durable, backed up |
| Redis | Cache, queues, session and rate-limit state | Ephemeral by default; only explicitly declared keyspaces persisted (AOF), never treated as a system of record |
As of 2026-07-18 none of the three is installed — this chapter is fully normative, with the §6.5 interim placement (containers on sbsdash-server-prod) as the build path and managed services as the preferred end state.
7.2 Managed vs. Self-Hosted Decision
- Preferred: DigitalOcean Managed Databases for PostgreSQL, MongoDB, and Redis (Valkey) — provider-handled failover, automated backups, patch management, and TLS-enforced connections reduce the operational load on a small team.
- Blocking check (Build Register, carried from §2.3): Managed Database availability in NYC2 is unconfirmed — newer DO capacity often lands in NYC1/NYC3 first. The check is: can all three engines be provisioned in NYC2 attached to the target Prod VPC (
prod-vpc-nyc2, §3.1)? Cross-region managed databases are disqualified — data would transit public networking, violating INF-01, the same class of defect as the wiki scrape (§3.3). - Fallback (interim, buildable now): self-hosted containers per §6.5. The remainder of this chapter is written engine-by-engine to be valid under either deployment, with deltas flagged.
- Migration posture: if the build starts self-hosted and Managed becomes available, migration is dump/restore per engine during a maintenance window, amended by Change Order. The isolation model (§7.4) is deployment-invariant, so no application change is required — only connection endpoints (Vault-managed, Ch. 8).
7.3 Object Storage
DigitalOcean Spaces (S3-compatible), region NYC3 (Spaces is not offered in NYC2; object storage over TLS to a neighboring region is acceptable — unlike database traffic, it is not latency- or VPC-bound):
| Bucket | Content | Access |
|---|---|---|
sbs-dash-artifacts |
Compartment file artifacts (agent outputs, uploads) | Per-Compartment prefix + per-Compartment scoped keys (§7.4) |
sbs-dash-audit |
Hash-chained audit log archives (Ch. 13) | Write-once pattern: writer key has PUT-only; no key held by the platform can DELETE (deletion requires the account control plane) |
sbs-dash-backups |
Database dumps + config archives (Ch. 14) | Backup identity only; replicated to SFO3 per §2.3 |
Rules: all buckets private (no public-read ever — FE family applies to storage too); access exclusively via scoped Spaces keys stored in Vault, one keypair per purpose, never a shared account-wide key (INF-02); TLS enforced on every operation; bucket inventory and lifecycle policies defined in IaC (Ch. 10). CDN feature on Spaces stays off — these are private buckets, and enabling the CDN endpoint would create an unauthenticated public URL surface.
7.4 Data Isolation Boundaries (per-Compartment)
The data and storage slices of the eight-layer model (§1.2), stated per engine. The principle throughout: isolation is enforced by the engine's own privilege system, not by application discipline — a compromised Compartment 03 credential must be structurally unable to read Compartment 08 data.
- PostgreSQL — one database per Compartment (
c01_hr…c10_pr) in the cluster; one role per Compartment withCONNECTon its own database only;PUBLICconnect revoked on every database; no cross-database query paths (nopostgres_fdw, nodblinkextensions installed); platform-shared configuration lives in a separateplatformdatabase with its own role. Superuser credentials exist only in Vault, used only by migration tooling. - MongoDB — one database per Compartment; per-Compartment user with
readWritescoped to its own database; authentication mandatory (--auth), authorization enabled from first boot — never an open dev instance that later gets locked down. - Redis — Redis ACLs (v6+) with one ACL user per Compartment, key-pattern-restricted (
~c01:*), dangerous commands (FLUSHALL,CONFIG,KEYS,SCRIPT) denied to all Compartment users;defaultuser disabled. If workload interference ever matters more than memory efficiency, escalation path is numbered logical databases or separate instances per Compose network — Change Order. - Connection path (self-hosted interim): Compartment containers join only the data networks they are entitled to (§6.5), so isolation is double-walled — network non-membership and engine ACL. Under Managed Databases the network wall is the VPC + per-database credentials; the engine ACL layer is identical.
- Object storage: per-Compartment prefix (
c01/…) with per-Compartment Spaces keys whose policy is prefix-scoped. AI-03 (credential isolation) extends here: an agent's runtime credentials reach exactly its Compartment's database, its Redis keyspace, and its Spaces prefix — nothing else.
7.5 Encryption
- At rest, block storage: DigitalOcean encrypts droplet disks and Volumes at rest (AES-256) at the platform layer. This is accepted as the baseline for the interim self-hosted deployment; no LUKS layer is added on top — key management for host-level LUKS on cloud VMs adds operational fragility (unattended reboot problem) for marginal gain against the relevant threat model (DO platform compromise is out of scope; stolen-disk scenarios are covered by DO's layer).
- At rest, engine level: PostgreSQL — no TDE in community builds; compensating controls are the DO layer plus strict dump handling (§7.6). MongoDB Community — same posture. Redis — ephemeral data, AOF file inherits disk encryption.
- At rest, object storage: Spaces server-side encryption; audit archives additionally carry their own integrity chain (hash-chained, Ch. 13) so tampering is detectable independent of encryption.
- In transit: TLS on every database connection including VPC-internal (zero-trust, NIST SP 800-207 — the private network is not a trust boundary): PostgreSQL
ssl=onwithsslmode=verify-fullin clients; MongoDBtls=true; Redis TLS listener. Internal certs from a platform-internal CA generated and rotated by IaC (not Let's Encrypt — these hosts have no public names). Managed Databases enforce TLS natively. - Backups/dumps: encrypted client-side (age or GPG, key in Vault) before upload to
sbs-dash-backups— a leaked bucket key must not equal readable data. Detail in Ch. 14.
7.6 Operational Rules
- No production data leaves the Prod boundary (§2.2.3): Dev databases are seeded synthetically or from sanitized dumps produced by a reviewed script (PII/tenant content stripped), never raw prod dumps.
- Ad-hoc human access to prod data engines is break-glass only, via bastion (§3.3), with individually attributed credentials (post INF-04 remediation, §2.5), logged as a MON-01 event. Routine inspection happens through dashboards and read-only replicas if/when provisioned.
- Retention: Compartment operational data per SBS function-level policy captured at Discovery (LH-SBS-DISC-001/002 outputs); audit log 7-year minimum (Ch. 13); Redis nothing beyond declared AOF keyspaces.
- Volume/disk capacity is a Prometheus-alerted metric (Ch. 12); databases run on dedicated DO Volumes (not the droplet root disk) so storage scales and migrates independently of the droplet — Volume creation is part of the data-services Build Register item.
7.7 Verification
Gates (LH-SBS-INST-001): cross-Compartment access probe per engine fails (c03 role → c08 database denied in PostgreSQL, MongoDB, Redis ACL, and Spaces prefix); no public listener on any engine port (ss -tlnp + external scan); TLS handshake required on every engine (plaintext connection refused); PUBLIC/default access revoked (engine ACL dump review); dumps encrypted before leaving host (backup pipeline inspection); Dev contains no production data (sampled content check); Spaces buckets private (unauthenticated GET fails); audit bucket writer key cannot DELETE (negative test).
Secrets Management
Vault deployment, secret lifecycle, rotation policy, application integration
Chapter 8 — Secrets Management
8.1 Current State and Scope
As of 2026-07-18 no secrets manager is deployed. Credential state is the Chapter 2 CRITICAL nonconformance: plaintext passwords in the operational changelog, a shared fleet-wide root password, and service identities on non-SBS mailboxes (Build Register item 15). This chapter defines the target system that item 15 migrates into, covering: platform secrets (API tokens, DB credentials, Spaces keys, internal-CA material, webhook URLs), per-Compartment agent credentials (AI-03), and deploy-time injection into containers (§6.4). Out of scope: end-user passwords (IdP domain, Chapter 9) and TLS private keys for public surfaces (deliberately host-local, §5.2).
8.2 Vault Deployment
HashiCorp Vault (OSS) on a dedicated minimal droplet:
| Attribute | Value |
|---|---|
| Droplet | prod-vault — 1 GB / 1 vCPU, NYC2, member of prod-vpc-nyc2 (created in the §3.1 rebuild wave) |
| Storage backend | Integrated Raft, single node, on a dedicated DO Volume |
| Listener | VPC-private interface only, TLS from the internal CA (§7.5) — no public interface listener at all; Vault has no DNS name in the public zone |
| Access paths | API from prod/dev droplets over VPC; human access via bastion (§3.3) with individually attributed tokens |
| Firewall | Cloud Firewall prod-vault: TCP 8200 from sbsdash-server-prod, Dev-SBS-server (for dev-namespace reads), and bastion private IPs only; SSH via bastion only |
Placement rationale: not on sbsdash-server-prod (the secrets store must survive and be trustworthy independent of the host running the workloads that consume it — a prod-droplet compromise should not equal immediate Vault-storage compromise), and not on monitor-servers (bastion + alerting authority already concentrates enough; adding secrets makes it a single point of total compromise). A 1 GB droplet is sufficient — Vault's footprint at this scale is trivial.
Unseal model: DigitalOcean offers no KMS, so auto-unseal via cloud KMS is unavailable. Shamir key shares: 5 shares, threshold 3, held by named individuals (Ledger Hub platform engineering ×2, Nelson Santos, Alexandra Del Rey, one SBS-designated alternate) — no individual can unseal alone; any 3 can recover. Shares are stored in each holder's personal password manager, never together, never in the changelog or repo. Unseal is required at Vault restart only; restarts are rare, scheduled events. Root token is revoked after initial setup (vault operator generate-root recreates one under quorum if ever needed) — routine administration uses named admin tokens under policy.
Availability posture, stated honestly: single-node Vault means Vault downtime blocks deploys and credential rotation, not running workloads (containers hold their injected credentials until restart, §8.5). Accepted at this fleet size; mitigation is Raft snapshot every 6 h to sbs-dash-backups (encrypted client-side per §7.5), restore-tested quarterly (Ch. 14). HA (3-node Raft) is a Chapter 16 growth trigger, not a Day-1 requirement.
8.3 Namespace and Policy Layout
Vault OSS lacks Enterprise namespaces; the equivalent is mount-path + policy separation, which is sufficient because policies are default-deny:
secret/platform/ # DO API tokens, GitHub deploy keys, Spaces keys,
# registrar/DNS API tokens, alerting webhooks
secret/infra/ca/ # internal CA key material (§7.5)
secret/db/platform/ # migration-tool superuser credentials (§7.4)
secret/c01/ … secret/c10/ # per-Compartment: DB role, Redis ACL user,
# Spaces prefix keys, Anthropic API key,
# per-Compartment integration credentials
secret/dev/… # dev-environment mirror, synthetic/dev-scoped values only
Policies, one per consumer class: policy-c01 grants read on secret/c01/* and nothing else — the AI-03 guarantee at the secrets layer, matching the network (§6.4) and engine-ACL (§7.4) walls: Compartment 03's runtime identity cannot read secret/c08/* even if every other control fails. policy-deploy reads what the deploy pipeline renders; policy-admin is held by named humans; no policy grants secret/* wildcard. Dev identities can read secret/dev/* only — prod paths are unreachable from Dev (the promotion path carries templates, not values, §2.2.3).
8.4 Secret Lifecycle and Rotation Policy
| Class | Examples | Rotation | Mechanism |
|---|---|---|---|
| Static, high-blast-radius | DO API token, GitHub deploy key, registrar/DNS API token, Spaces keys | 90 days, and immediately on personnel change or suspected exposure | Manual rotation runbook (wiki), tracked as recurring Build Register/ops-calendar item; each rotation logged |
| Database credentials | Per-Compartment PostgreSQL / MongoDB / Redis users | 30 days target | Vault database secrets engine (dynamic short-TTL credentials) once data services are up — Vault creates per-lease users, expiry is automatic, rotation ceases to be a human task. Until then: static creds at 90 days |
| Anthropic API keys | Per-Compartment keys | 90 days; immediately on anomalous-usage alert (MON-02 / §3.4 egress signals) | Manual via Anthropic console, stored per-Compartment path |
| Internal CA leaf certs | DB TLS (§7.5) | 90-day leaf TTL, auto-renewed by IaC | Vault PKI engine issues leaves; CA root offline in secret/infra/ca/ |
| Unseal shares | — | On any holder change | Rekey ceremony (vault operator rekey, quorum) |
Rules: every secret has a named owner and a recorded rotation date (KV metadata); KV v2 versioning on all mounts so rotation keeps the prior version recoverable for rollback; no secret is ever emailed, chat-pasted, or written to the changelog — the changelog records that a rotation happened, never the value. Audit device (file → shipped to Ch. 12 pipeline) logs every read/write with the requesting identity: secret access is itself a MON-01 event stream, and reads outside deploy windows or from unexpected identities are MON-02 anomaly inputs.
8.5 Application Integration
Injection model (carried from §6.4), chosen for minimal moving parts:
- AppRole per consumer — one AppRole per Compartment (
role-c01… bound topolicy-c01) and one for the deploy pipeline. RoleID is IaC-managed config; SecretID is delivered response-wrapped at deploy time with short TTL and use-limit 1 — a leaked wrapped token is worthless after first use or expiry. - Deploy-time render — the deploy step authenticates via AppRole, reads the Compartment's paths, renders the env-file consumed by
docker compose(§6.7), sets mode 0600, and deletes the env-file after container start (compose has already captured the environment). Values never enter images, compose files, the repo, or CI logs (INF-02, INF-03). - Refresh — containers hold credentials until redeploy. With dynamic DB credentials (§8.4), lease TTLs are set ≥ the deploy cadence so normal deploys renew credentials as a side effect; an emergency revocation is: revoke lease in Vault → redeploy Compartment (minutes, and only the affected Compartment restarts — per-Compartment stacks pay off here).
- Prohibited patterns — no Vault agent sidecars for now (added complexity before it's needed), no secrets in
docker inspect-visible plainenvironment:keys in committed files, no shared "platform god token" consumed by all Compartments.
Bootstrap note (the classic first-secret problem): the deploy host must hold one credential to start — the RoleID (low sensitivity, useless without SecretID) on disk, and SecretID issuance gated by the CI/operator identity. This is the accepted trust anchor and is documented in the wiki runbook rather than hidden.
8.6 Migration of Existing Credentials (Item 15 Execution Order)
- Deploy
prod-vault, initialize, distribute shares, revoke root token. - Rotate the four exposed credential sets (GitHub, droplet root password → abolished entirely in favor of keys per §2.5, wiki admin, Grafana admin) — rotate then store; never vault a burned value.
- Write rotated values + all new secrets created since (DO tokens, Spaces keys as provisioned) into their §8.3 paths.
- Strip the changelog credentials section; purge from any git history (
git filter-repo) if the file was ever committed; add pre-commit secret-scanning (gitleaks) to the repo (Ch. 10/11) so re-introduction is blocked mechanically, not procedurally. - Re-point service identities to SBS-controlled aliases (§4.4 note) as each is touched.
8.7 Verification
Gates (LH-SBS-INST-001): Vault reachable from prod over VPC TLS, unreachable from any public interface (external scan); policy probe — role-c03 token reads secret/c03/*, denied on secret/c08/* and secret/platform/*; response-wrapped SecretID single-use confirmed (second unwrap fails); env-file absent post-deploy (filesystem check); no secret string present in repo, images, compose files, or CI logs (gitleaks sweep); audit device shipping to observability pipeline; Raft snapshot present in sbs-dash-backups, encrypted, restore-tested; changelog contains no credential values (item 15 closure evidence); rekey ceremony documented with current holder list.
Identity & Access
IdP integration, SSO, SCIM provisioning, admin access paths, least-privilege model
Chapter 9 — Identity & Access
9.1 Current State and Scope
As of 2026-07-18 no IdP integration exists: Grafana and BookStack run on local admin accounts (both credentials exposed in the changelog — item 15 applies), the platform application is pre-deployment, and host access is the shared-key nonconformance of §2.5. This chapter defines the target identity architecture covering the IAM control family: IAM-01 (identity-aware proxy), IAM-02 (SSO federation), IAM-03 (phishing-resistant MFA), IAM-04 (tenant separation), IAM-05 (SCIM lifecycle), IAM-06 (access reviews). Human identity only — machine/agent credentials are Chapter 8; per-Compartment application RBAC internals (APP-05) are application-layer and specified in the platform application documentation, but their identity source is defined here.
9.2 Identity Provider
Single source of truth: SBS's corporate IdP. The Platform maintains no local user store — no local passwords on any surface (the two as-built local admins are migrated then reduced to break-glass, §9.6). Users exist on the Platform only as federated identities projected from the SBS directory.
Discovery dependency (blocking): SBS's incumbent IdP product (Entra ID, Okta, Google Workspace, or other) is unconfirmed — it is a named LH-SBS-DISC-002 IT-function question and a gate for this chapter's build items. The architecture below is written IdP-agnostic against capabilities every mainstream enterprise IdP provides: OIDC, SAML 2.0, SCIM 2.0, group claims, WebAuthn/FIDO2 MFA enforcement. Nelson Santos is execution owner for the IdP-side configuration (app registrations, group creation, SCIM token issuance).
Federation protocol: OIDC (Authorization Code + PKCE) everywhere it is supported — Grafana (native OIDC), the platform application (OIDC middleware), the identity-aware proxy (§9.3). SAML only where a component supports nothing better. Token hygiene: short-lived access tokens (≤1 h), refresh handled server-side, email_verified + group claims required in the ID token.
MFA (IAM-03): enforced at the IdP, not per-application — phishing-resistant methods only (WebAuthn/FIDO2 hardware or platform authenticators) for all Platform-mapped groups; TOTP acceptable only as a documented exception with expiry; SMS/voice prohibited. Conditional-access (where the IdP supports it): Platform app registrations require MFA on every authentication, no "remembered device" exceeding 12 h for admin roles.
9.3 Identity-Aware Proxy (IAM-01)
Enforcement point for surfaces that lack robust native OIDC or need pre-application gating. Implementation: oauth2-proxy deployed alongside nginx on the serving droplet, wired via auth_request:
| Surface | Enforcement |
|---|---|
monitor.sbsdash.com |
Grafana native OIDC (preferred — it maps groups→roles internally, §9.5). oauth2-proxy not required |
wiki.sbsdash.com |
BookStack SAML/OIDC if licensed features suffice; else oauth2-proxy in front — no unauthenticated byte beyond the IdP redirect is served |
admin.sbsdash.com |
oauth2-proxy in addition to application auth — admin surface requires valid IdP session before the application sees the request, layered on the §5.7 edge IP allowlist |
sbsdash.com, clients. |
Application-native OIDC; anonymous access limited to the sign-in redirect itself (FE posture: the public fingerprint is a sign-in page, §5.5) |
oauth2-proxy session cookies: __Host- prefix, Secure, HttpOnly, SameSite=Lax, secret from Vault (secret/platform/), session lifetime ≤ 8 h with IdP re-auth. The proxy passes identity to upstreams via signed headers; upstreams reject unsigned/absent identity headers so the proxy cannot be bypassed by direct container access (pairs with the net-edge design, §6.4).
9.4 SCIM Provisioning (IAM-05)
Lifecycle automation — the control that makes offboarding real:
- Direction: IdP → Platform. SBS directory group membership drives Platform access; the Platform never invites users directly.
- Targets: the platform application (SCIM 2.0 endpoint, Compartment-role mapping per §9.5) and Grafana (SCIM or team-sync per licensing; fallback is OIDC group-claim mapping at login — acceptable because login-time mapping plus short sessions bounds staleness to the session length).
- Deprovisioning SLA: IdP deactivation propagates to Platform deactivation within 15 minutes (SCIM push) and in the worst case at next token expiry (≤ 1 h). A user disabled in the SBS directory holds no live Platform session beyond that hour — this is the tested metric, not the config's existence.
- SCIM tokens live in Vault (
secret/platform/scim/), rotate on the 90-day static schedule (§8.4), and their use is logged (MON-01). - Joiner/mover/leaver flows are exercised end-to-end as a named gate: create test user → group add → verify access; group move → verify role change; deactivate → verify lockout inside SLA.
9.5 Least-Privilege Model and Group Mapping (IAM-04 / APP-05 boundary)
Access is group-derived, never user-granted. SBS directory groups map to Platform roles; the group list is the single audit surface for IAM-06:
| Directory group (naming) | Grants |
|---|---|
dash-platform-admin |
Master Admin console; oauth2-proxy admission to admin.sbsdash.com. Named individuals only, target ≤ 4 |
dash-c01-user … dash-c10-user |
That Compartment's surfaces and workflows — the human half of IAM-04 tenant separation, mirroring the network/data/secrets walls (§6.4, §7.4, §8.3) |
dash-c01-supervisor … |
Receives that Compartment's Medium/High/Critical routings from Supervisor Agents; resolution authority |
dash-monitor-view / dash-monitor-admin |
Grafana Viewer / Admin via group-claim mapping |
dash-wiki-edit / dash-wiki-view |
BookStack roles |
dash-exec-dashboard |
COO/CFO read-only reporting views |
Rules: no wildcard "all-Compartments" user group exists — cross-Compartment humans (e.g., the COO) hold the exec-dashboard role, which reads reporting outputs, not Compartment interiors; platform-admin grants administration, and its members' actions are fully logged (MON-01) — admin ≠ unobserved. Group membership changes are IdP-audited events; the quarterly IAM-06 access review walks every dash-* group against current staffing (Alexandra Del Rey coordinates sign-off; Nelson Santos executes directory changes), and the review artifact is filed in the wiki with date and reviewer — access certification (MON-04) consumes the same artifact.
9.6 Admin Access Paths
Consolidated statement of every privileged path and its chain, replacing ad-hoc practice:
| Path | Chain | Controls |
|---|---|---|
| Web admin | IdP (MFA) → oauth2-proxy → edge IP allowlist (§5.7) → admin.sbsdash.com |
IAM-01/02/03; all actions app-logged |
| Host SSH | Operator (personal key) → bastion monitor-servers → target droplet |
Per-person users post item 17; bastion sessions logged; admin allowlist IaC-managed (§3.5) |
| Vault admin | Bastion → named admin token under policy-admin |
§8.2; every operation in Vault audit device |
| DO console / registrar / DNS | Provider MFA (phishing-resistant), SBS-owned accounts | §2.1, §4.1; console use in Prod is break-glass + logged (§2.2.3) |
| Break-glass | Sealed local credentials for Grafana/BookStack/app, stored in Vault + one printed copy in SBS-controlled physical custody | Used only on IdP outage; use triggers immediate rotation + incident note (MON-05); tested annually |
The break-glass row is the honest residual of the "no local accounts" rule: total IdP outage must not equal total platform lockout, so exactly one dormant local admin per critical surface survives — vaulted, alarmed on use, rotated after.
9.7 Verification
Gates (LH-SBS-INST-001): unauthenticated request to every gated surface yields IdP redirect, zero application bytes (curl sweep); direct-to-container bypass attempt rejected (unsigned identity header); MFA challenge presented on fresh session for admin group (manual gate); SCIM lifecycle test — provision/move/deactivate with deactivation lockout ≤ 15 min (timed); group-mapping probe — dash-c03-user reaches c03 surfaces, denied on c08 (IAM-04 human-layer test mirroring §7.7); local password login disabled on Grafana/BookStack (negative login test); break-glass procedure executed in drill, rotation confirmed; IAM-06 review artifact exists with current date.
Infrastructure as Code
IaC tooling, repository structure, change workflow, drift detection
Chapter 10 — Infrastructure as Code
10.1 Current State and Scope
As of 2026-07-18 no IaC exists: all resources to date (droplets, DNS records, ufw rules, Grafana/Prometheus configs) were created by console or shell, documented after the fact in SERVER_CHANGELOG.md. That changelog discipline is the right instinct executed at the wrong layer — this chapter moves the source of truth from prose describing what was done to code that does it, with the changelog demoted to narrative context. Scope: DigitalOcean resources, DNS zone (post-cutover, §4.2), host configuration, firewall rulesets, Vault policy/mount layout, compose templates, and monitoring config. Out of scope: application source code (its CI/CD is platform-repo concern; supply-chain controls shared with Chapter 11).
10.2 Tooling
Two tools, sharply divided by responsibility — resisting the single-tool temptation because each is poor at the other's job:
| Layer | Tool | Manages |
|---|---|---|
| Provisioning (things that exist) | OpenTofu (Terraform-compatible, MPL-licensed — no BSL exposure) with the digitalocean provider |
Droplets, VPCs, Cloud Firewalls, Volumes, Spaces buckets + keys, DO project membership, DNS records via the post-cutover provider API (§4.2), reserved IPs |
| Configuration (state of things that exist) | Ansible | OS baseline (users/keys post-item-17, sshd config, unattended-upgrades), nftables rulesets rendered from templates (§3.6), nginx server blocks + TLS params (§5.3), node_exporter binds, Vault server config, compose file rendering + deploy (§6.7), internal-CA leaf distribution (§7.5) |
Supporting: gitleaks (pre-commit + CI, §8.6), promtool/nginx -t/vault policy fmt as validation steps in CI, Infracost not adopted (fleet too small to justify).
State backend: OpenTofu state in the sbs-dash-backups Spaces bucket (S3-compatible backend) with state locking via DO Spaces conditional writes; state contains secrets-adjacent values (IPs, IDs — not credentials, which stay in Vault per §8.5) and the bucket is private + encrypted (§7.3). Ansible is stateless by design; its "state" is the repo.
10.3 Repository Structure
One infrastructure repository (sbs-dash-infra) in the GitHub organization — separate from application code so infra reviewers and app reviewers are distinct CODEOWNERS populations:
sbs-dash-infra/
├── tofu/
│ ├── environments/
│ │ ├── prod/ # prod VPC, droplets, firewalls, volumes
│ │ └── dev/ # dev VPC, droplet
│ ├── modules/
│ │ ├── droplet/ # standard droplet: VPC member, tags, monitoring
│ │ ├── firewall/ # tag-scoped Cloud Firewall pattern (§3.5)
│ │ └── spaces/ # bucket + scoped key pattern (§7.3)
│ ├── dns/ # zone records post-cutover (§4.3 target table)
│ └── globals/ # admin SSH allowlist, region, naming — the named
│ # variables referenced throughout this spec
├── ansible/
│ ├── inventory/ # generated FROM tofu output — single source for IPs
│ ├── roles/
│ │ ├── base/ # users, sshd, patching (§2.5 target, INF-05)
│ │ ├── nftables/ # §3.6 templates incl. per-Compartment chains
│ │ ├── nginx-edge/ # §5.3 TLS params + §5.5 header template
│ │ ├── monitoring/ # prometheus jobs, exporter binds, dashboards
│ │ ├── vault/ # §8.2 server config
│ │ └── compose/ # §6.4 Compartment template renderer
│ └── playbooks/
├── vault/policies/ # §8.3 policy HCL, applied via CI
├── .gitleaks.toml
└── CODEOWNERS
Repository controls (INF-03, shared baseline with Ch. 11): default branch protected — PR required, ≥1 review from CODEOWNERS, no force-push, no direct commit, signed commits required, CI green as merge condition; admin bypass disabled; the GitHub org and repo live under SBS ownership with Ledger Hub engineers as members (same revocability principle as §2.1).
10.4 Change Workflow
Every infrastructure change follows one path — the §2.2.3 promotion rule made concrete:
- Branch + PR against
sbs-dash-infra. PR description states intent and links the Build Register item or Change Order where applicable (LH-SBS-CO- series for scope-level changes; routine parameter changes need only the PR). - CI validation on PR:
tofu validate+tofu plan(both environments, plan output posted to the PR),ansible-lint, template syntax checks (nginx -tin container,promtool check config), gitleaks sweep. A plan showing destruction of stateful resources (Volumes, databases, Spaces buckets) requires an explicitdestructive-changelabel + second reviewer. - Review by CODEOWNERS — for firewall, Vault-policy, or IAM-adjacent paths, the reviewer set includes the security owner (Alexandra Del Rey routing).
- Apply to Dev on merge: CI runs
tofu apply(dev workspace) + Ansible against dev inventory automatically. - Apply to Prod as a manually approved CI job (GitHub environment protection, named approvers) — never from an operator laptop. The CI identity authenticates to Vault via its own AppRole (§8.5) for any secret-touching render; the DO token it uses is the pipeline-scoped token of §2.1.
- Emergency path: break-glass console/shell changes are permitted to restore service (MON-05 incident context), and must be back-ported to the repo within 24 h — the next drift run (§10.5) enforces this mechanically by flagging anything that wasn't.
10.5 Drift Detection
Drift — divergence between repo-declared state and live state — is the failure mode this whole chapter exists to prevent, and every preceding chapter's "console edits are drift and reverted" clauses land here as mechanism:
- Provisioning drift: scheduled CI job runs
tofu plan -detailed-exitcodenightly per environment. Exit code 2 (changes present) → alert to the ops Slack channel (Ch. 12 routing) with the plan diff. Triage outcomes: revert the live change (apply), or adopt it (PR that codifies it) — silent coexistence is not an outcome. - Configuration drift: nightly
ansible-playbook --check --diffagainst full inventory; any changed-task output alerts identically. Ansible runs are also idempotent enforcement — the weekly scheduled real run re-asserts declared state, so unrecorded manual edits have a maximum lifetime of seven days. - Ruleset integrity: live nftables ruleset hash and running-container-vs-compose comparison (§3.6, §6.7) are exported as Prometheus metrics; mismatch alerts in minutes rather than nightly — these two are the fastest-moving tamper indicators (MON-01 inputs).
- DNS drift (post-cutover): nightly diff of provider-API zone dump vs.
tofu/dns/— catches registrar-console edits (§4.7). - Drift alerts are not auto-remediated on Prod (auto-apply of a malicious or mistaken repo state is its own attack path); human triage stands between detection and correction, with the weekly enforcement run as the backstop.
10.6 Bootstrap and Migration of As-Built Resources
The existing fleet was built by hand; it is imported, not rebuilt, except where the §3.1 rebuild wave already mandates recreation:
- Stand up repo, branch protection, CI skeleton, gitleaks (already Build Register item 47).
tofu importcurrent resources: 4–5 droplets, existing ufw-era firewall facts recorded as-is, Spaces buckets when created. Import produces an honest baseline whose firstplanoutput is the formal nonconformance list — the delta between as-built and this specification, machine-generated.- Author target state per this spec (dedicated VPCs, Cloud Firewalls, prod-vault droplet); the §3.1 rebuild wave then executes as reviewed PRs — the rebuild becomes the first real exercise of the §10.4 workflow rather than another manual event.
- Ansible baseline role applied fleet-wide, which is the execution vehicle for item 17 (per-person users, sshd hardening) and item 20 (host firewalls).
- SERVER_CHANGELOG.md thereafter records narrative/context only; "what exists" questions are answered by the repo and its state, and the changelog's §1.3 role is updated accordingly.
10.7 Verification
Gates (LH-SBS-INST-001): repo exists under SBS org with branch protection verified (settings API dump); direct push to default branch rejected (negative test); PR without CI green cannot merge; tofu plan clean on both environments (zero drift at gate time); nightly drift jobs scheduled and alerting path tested (induced drift: manual test-tag change → alert received → reverted); Prod apply requires named approval (job history evidence); emergency-change backport rule exercised in drill; gitleaks blocks a seeded dummy secret in pre-commit and CI; inventory generated from tofu output matches live fleet.
Supply-Chain Controls
Image provenance, dependency pinning, artifact signing, registry policy
Chapter 11 — Supply-Chain Controls
11.1 Current State and Scope
As-built supply-chain posture is minimal but deliberately clean so far: everything installed to date (Docker, Compose, Prometheus, nginx, certbot) came from Ubuntu official repos or the official Grafana apt repo — no curl-pipe-bash installs, no third-party PPAs, no unpinned container images yet, because no application containers exist yet. This chapter sets the controls before the application build starts pulling npm/crates/PyPI dependencies and publishing images — retrofitting provenance onto an already-polluted dependency tree is an order of magnitude harder than starting gated. Scope: container images, OS packages, language dependencies (Node/pnpm, Rust/Cargo, Python), the artifact registry, and the build pipeline that connects them. Controlling SEC-001 requirement: INF-03 (repository controls) plus the supply-chain family imported into KO-001; AI-02 intersects where MCP tool containers are concerned (§6.4).
11.2 Registry Policy
GitHub Container Registry (GHCR) under the SBS GitHub organization is the sole artifact registry:
- One registry, private visibility, org-owned — same ownership/revocability principle as §2.1 and §10.3. No Docker Hub publishing; no images pulled from Docker Hub except the official library base images enumerated in the allowlist (§11.3).
- Pull access from droplets via a registry-scoped read-only token stored in Vault (
secret/platform/ghcr/), rotated on the 90-day static schedule (§8.4). Push access belongs to CI only — no human pushes images; an image that wasn't built by the pipeline doesn't exist as far as deployment is concerned. - The §3.4 egress allowlist admits exactly: GHCR endpoints, Ubuntu archive/security mirrors, the Grafana apt repo, and the language registries (registry.npmjs.org, crates.io/static.crates.io, pypi.org/files.pythonhosted.org) from build contexts only — production droplets pull finished images, never raw dependencies; the language registries are unreachable from prod runtime (a compromised container cannot
pnpm addits way to a payload).
11.3 Image Provenance
- Base image allowlist, pinned by digest, reviewed quarterly: official
node:<LTS>-slim,rust:<pinned>-slim(build stage only),python:3.x-slim,nginx:stable,postgres/mongo/redisofficial images for the §6.5 interim,hashicorp/vault. Nothing outside the list without a PR touching the allowlist file (CODEOWNERS: security reviewer per §10.4). - Digest pinning everywhere:
FROM node:22-slim@sha256:…in Dockerfiles; compose files reference deployed images by digest, not floating tags (:latestis prohibited and CI-linted). Tag-based human readability is preserved by also tagging, but the digest is what deploys — a registry-side tag repoint cannot swap what prod runs. - Multi-stage builds — build stages (compilers, package managers) never ship; runtime stages carry the application and its production dependencies only. Runtime images run as non-root UID, no shell where the base permits (
distroless-style trimming is a later optimization, not a Day-1 gate). - Rebuild cadence: weekly CI rebuild of all images against updated base digests +
apt upgradelayer, so CVE patching is a pipeline event, not an emergency (§INF-05 patching applies to images as much as hosts). Trivy scan (§11.6) gates the result.
11.4 Dependency Pinning
Per-ecosystem, all enforced in CI (a lockfile that isn't verified is documentation, not control):
| Ecosystem | Pinning mechanism | CI enforcement |
|---|---|---|
| Node / pnpm | pnpm-lock.yaml committed; packageManager field + corepack pins pnpm itself |
pnpm install --frozen-lockfile — any drift fails the build; pnpm audit report attached |
| Rust / Cargo | Cargo.lock committed (binaries); rust-toolchain.toml pins the compiler |
cargo build --locked; cargo audit (RustSec) |
| Python | requirements.txt with --hash=sha256:… per package (pip-compile generated) |
pip install --require-hashes; pip-audit |
| OS packages | Versions asserted in the Ansible base role for security-relevant packages | Drift surfaces in the nightly --check run (§10.5) |
Dependency updates are PRs like any other change (Renovate bot, grouped weekly, auto-PR but never auto-merge) — the update path goes through the same review + CI gauntlet, so a poisoned upstream release sits in a diff a human looks at, not in a silent nightly pull. New top-level dependencies require a one-line justification in the PR body; transitive bloat is reviewed via the lockfile diff.
11.5 Artifact Signing and Verification
- Commits: signed commits required on both repos (already a §10.3 branch-protection condition) — SSH-key signing acceptable; the key inventory rides the per-person identity work of item 17.
- Images: CI signs every pushed image with cosign keyless (Sigstore, GitHub OIDC identity) — the signature attests "built by this workflow, from this commit, in this repo," which is the provenance claim that matters. No long-lived signing key to protect or rotate (the §8 lifecycle problem cosign keyless exists to delete).
- Verification at deploy: the deploy step (§6.7 / §10.4) runs
cosign verifyagainst the expected workflow identity beforecompose up; an unsigned or wrongly-attested image fails the deploy. This closes the loop: registry compromise alone cannot inject a runnable image, because the attacker must also produce a valid Sigstore attestation from the protected CI workflow. - SBOM: CI generates SPDX SBOMs (syft) per image, attached as a cosign attestation and archived to
sbs-dash-backups— when the next log4shell-class event lands, "are we exposed" is a query against stored SBOMs, not an archaeology project.
11.6 Pipeline Integrity and Scanning
The build pipeline is itself supply chain:
- GitHub Actions pinned by action digest (not
@v4floating tags) — the same rule applied to the tooling as to the images; third-party actions from a reviewed allowlist only. - Workflow permissions default
contents: read; the push job alone getspackages: write+id-token: write(for cosign OIDC). No self-hosted runners at this fleet size — GitHub-hosted runners keep runner compromise off the threat sheet at the cost of trusting GitHub, which the design already does (repo, registry). - Trivy scans every built image (CI gate: fail on CRITICAL with fix available; HIGH warns and files an issue) and re-scans the deployed digest set nightly against fresh CVE data — the nightly catches vulnerabilities published after the image shipped, feeding MON-06 (vulnerability scanning) with the §11.3 weekly rebuild as the remediation vehicle.
- gitleaks (already items 47/§10.4) covers the secrets half of pipeline hygiene.
11.7 Verification
Gates (LH-SBS-INST-001): base-image allowlist file exists, all Dockerfiles resolve FROM digests within it (lint); :latest absent from every compose/Dockerfile (grep gate); frozen-lockfile builds pass and a deliberately drifted lockfile fails (negative test); cosign verify passes on a pipeline image and fails on a manually-pushed unsigned test image (negative test); deploy refuses the unsigned image end-to-end; SBOM attestation present for every deployed digest; Trivy CI gate demonstrated on a seeded-CVE test image; language registries unreachable from prod runtime (egress probe, §3.4); Actions digests pinned (workflow lint); prod running-set digests ⊆ registry signed-set (drift cross-check with §10.5).
Observability
Monitoring stack, logging pipeline, alert routing, dashboards, health checks
Chapter 12 — Observability
12.1 Current State and Scope
Prometheus + Grafana 13.1.0 + nginx/TLS on monitor-servers, serving monitor.sbsdash.com; node_exporter on all four droplets (prod/dev private-bind at 10.100.0.3/.2:9100, wiki public-bind with ufw scope — the §3.3 interim, monitor local at 127.0.0.1); scrape job droplets with per-server labels; dashboards "Node Exporter Full" (ID 1860) and custom "SBS Servers Overview" (uid sbs-overview, provisioned as home dashboard); 30-second kiosk playlist. Verified 2026-07-17: 5 targets up, promtool clean, Grafana healthy.
What exists is metrics + visualization. This chapter completes the design: logging pipeline (absent), alerting (absent — dashboards without alerts is observability that only works when someone is looking), blackbox/health probes (absent), and the MON-family feeds every prior chapter has been promising (MON-01 unified audit trail inputs, MON-02 anomaly detection). Grafana local-admin → OIDC migration is already item 52 (Ch. 9); Prometheus job additions below fold into the Ansible monitoring role (§10.3) rather than remaining hand-edits.
12.2 Metrics (Prometheus)
Retained as deployed — apt-installed system service on monitor-servers, 127.0.0.1-bound behind nginx (§6.6 rationale: don't containerize the watcher). Extensions, all IaC-managed:
| Job | Targets | Purpose |
|---|---|---|
droplets (live) |
node_exporter ×4 (+prod-vault, +rebuilt wiki private bind at rebuild wave) |
Host CPU/RAM/disk/net |
blackbox-external (new) |
Probes of all five public URLs (§4.3) from the monitor via blackbox_exporter | Surface up/down, TLS validity, cert expiry (§5.2 feed), HTTP status, redirect correctness |
nginx (new) |
stub_status per edge droplet, private bind | Request rates, connections — MON-02 baseline input |
containers (new, at Compartment build) |
cAdvisor on prod/dev, private bind | Per-Compartment container CPU/RAM — §6.4 resource-limit pressure visibility |
integrity (new) |
textfile-collector metrics: nftables ruleset hash, compose-vs-running match, backup age, drift-job status | §10.5 fast-tamper indicators; §14 backup freshness |
vault (new, at Ch. 8 build) |
Vault telemetry endpoint, VPC-private | Seal status, token/lease counts, audit-device health |
postgres/mongo/redis exporters (new, at Ch. 7 build) |
Private binds per engine | Engine health, connections, replication/persistence state |
Retention: 30 days local TSDB (sized fine on 120 GB); longer-horizon capacity trending is served by the 30-day window plus quarterly-review screenshots — remote-write long-term storage is a Chapter 16 growth item, not Day-1. Scrape interval stays 15 s. Every new exporter binds VPC-private or localhost — the §3.7 public-listener gate applies to observability itself.
12.3 Logging Pipeline
Grafana Loki + promtail/alloy agents — chosen over an ELK stack deliberately: one vendor surface with Grafana (logs and metrics correlate in the same UI), index-light architecture that fits a 4 GB monitor droplet, and LogQL's label model matches the Prometheus labels already in place.
- Loki on
monitor-servers, 127.0.0.1-bound, filesystem storage on a dedicated DO Volume; retention 90 days hot. Audit-relevant streams are additionally shipped to the Ch. 13 hash-chained archive — Loki is the operational search tier, not the evidentiary store (different integrity guarantees, different retention: 90 d vs 7 y). - Agents on every droplet (Ansible role), shipping over the VPC (wiki joins post-rebuild; until then its logs ship over TLS to the monitor's public interface — same interim-acceptance logic as its scrape path, §3.3):
| Stream | Source | Notes |
|---|---|---|
| System | journald | sshd (bastion session evidence, §3.3/§9.6), sudo, unattended-upgrades |
| Edge | nginx access/error, structured format (§5.7) | All five surfaces; access logs are the MON-02 baseline for request anomalies |
| Auth | oauth2-proxy, Grafana, BookStack auth events | IAM-01/02 evidence; failed-auth series feeds §12.4 alerts |
| Vault | audit device (§8.4) | Secret-access event stream — MON-01's highest-value feed |
| Containers | Docker json-file via agent, per-Compartment labels | Compartment-scoped log isolation: Grafana folder permissions (§9.5 group mapping) keep c03 logs invisible to c08 personnel — the log layer of IAM-04 |
| Egress denials | nftables log target (§3.4) | AI-02/LLM01 exfiltration-attempt indicator |
| Drift/CI | §10.5 job outputs | Codified-change audit trail |
12.4 Alert Routing
Alertmanager on monitor-servers (the changelog's own "pendiente" item, now normative), 127.0.0.1-bound:
- Channels: ops Slack channel (webhook from Vault
secret/platform/alerting/) for everything; email to named on-call forseverity=criticalas the Slack-independent second path. The per-Compartment Slack channel fabric (platform-level alerting, §5.2 of the SOW-side design) is application-layer and separate — this section is infrastructure alerting; the two fabrics share the Slack workspace but not routes. - Severity model:
critical= platform-down/data-risk (surface down ≥ 2 min, cert ≤ 7 d with renewal failing, disk ≥ 90 %, Vault sealed, backup age > 26 h, integrity-hash mismatch, target down ≥ 5 min);warning= degradation (disk ≥ 80 %, cert ≤ 14 d, CPU/RAM sustained ≥ 85 % 15 min, drift detected, Trivy nightly CRITICAL finding, egress-denial burst, failed-auth burst ≥ 10/5 min/IP);info= notable (deploys, rotation confirmations, weekly Ansible enforcement diff). - Routing: group by alertname+instance, 4 h repeat for unresolved critical, inhibition (droplet-down inhibits its per-service alerts). Dead-man's switch: a permanently-firing
Watchdogalert routes to a heartbeat receiver — if the watchdog goes silent, the monitoring stack itself is down; this is the alert that covers the monitor (§12.6 covers it structurally). - Alert rules live in the repo (
monitoringrole), promtool-validated in CI (§10.4) — alert changes are PRs, not console edits.
12.5 Dashboards
Provisioned-by-file discipline (as already practiced — sbs-overview and 1860 are file-provisioned) becomes the rule: no console-authored production dashboards; Grafana UI is for exploration, dashboards graduate to the repo. Dashboard set:
| Dashboard | Audience (via §9.5 groups) | Content |
|---|---|---|
| SBS Servers Overview (live) | dash-monitor-view |
Fleet UP/DOWN, CPU/RAM/disk bars, history, uptime — home dashboard, kiosk playlist retained |
| Node Exporter Full (live) | dash-monitor-view |
Per-host deep dive |
| Edge & Surfaces (new) | monitor-view | Blackbox probe status, cert days-remaining, request rates, 4xx/5xx, auth failures |
| Compartments (new, at build) | monitor-view; per-Compartment panels visible per group | cAdvisor per-Compartment resources, container restarts, egress denials by Compartment |
| Data Services (new, at Ch. 7 build) | monitor-admin | Engine health, connections, volume capacity, backup age |
| Security & Integrity (new) | monitor-admin + security routing | Vault seal/audit status, drift jobs, ruleset hashes, gitleaks/Trivy findings, bastion session count |
12.6 Monitoring the Monitor
The stack's structural blind spot, addressed three ways: the dead-man's switch (§12.4); an external uptime check on https://monitor.sbsdash.com from outside DO (UptimeRobot free tier or equivalent — the one approved external service, alerting to the same email path); and the monitor droplet's own metrics riding the same droplets job with its alerts routed identically. Loki/Alertmanager/blackbox process health via systemd unit-state metrics on the integrity job. The bastion role (§3.3) already makes this host priority-patched; observability adds priority-watched.
12.7 Verification
Gates (LH-SBS-INST-001): all §12.2 jobs green with private binds confirmed (ss -tlnp sweep per droplet); blackbox probes cover all five URLs incl. cert-expiry metric present; induced-failure drill — stop a service, alert received on both channels inside SLA, inhibition correct; dead-man silence drill — stop Alertmanager, heartbeat receiver fires; Loki streams present per table incl. Vault audit and egress denials (LogQL sample per stream); Compartment log visibility probe — c03 group cannot read c08 stream (mirrors §7.7/§9.7); dashboard set file-provisioned, console-save on prod folders denied; alert rules pass promtool in CI, console-edit reverted by drift run; external uptime check firing path tested; retention conf 30 d TSDB / 90 d Loki confirmed; wiki agent path migrates to VPC at rebuild (carried gate).
Audit Infrastructure
Hash-chained audit log architecture, retention, integrity verification
Chapter 13 — Audit Infrastructure
13.1 Purpose and Distinction from Operational Logging
This chapter specifies the evidentiary tier: the tamper-evident, hash-chained audit record backing the Platform's compliance claims — every AI Agent decision tagged by Risk Tier, admissible for FCC and SOX review, retained seven years minimum (per the engagement's audit-log commitments). It is deliberately separate from Loki (§12.3), and the separation is the design:
| Property | Loki (operational) | Audit tier (this chapter) |
|---|---|---|
| Purpose | Search, debugging, alerting | Evidence, attestation, regulatory review |
| Retention | 90 days | 7 years minimum |
| Integrity | Best-effort (filesystem) | Cryptographic hash chain, externally anchored |
| Mutability | Deletable by admins | Append-only; no platform-held credential can delete (§7.3) |
| Query | LogQL, fast | Sequential verification + indexed retrieval, slow is acceptable |
As of 2026-07-18 nothing is built; nothing blocks it either — the storage substrate (sbs-dash-audit bucket, §7.3) and the event sources (§13.3) are already specified. This is the audit layer of the eight-layer Compartment isolation model (§1.2) and the infrastructure realization of MON-01 (unified audit trail), consumed by Guardian Agents as writers and by SBS/regulators as readers.
13.2 Hash-Chain Architecture
Design: per-Compartment append-only event chains, batched into signed, anchored segments.
- Event record. Every auditable event is a canonical JSON document:
{seq, ts (RFC 3339 UTC), compartment, actor (agent id | human identity per §9), event_type, risk_tier, payload_hash, prev_hash, entry_hash}whereentry_hash = SHA-256(canonical_serialization(record minus entry_hash))andprev_hashis the previous event'sentry_hash. Canonicalization is fixed (RFC 8785 JCS) — hash stability across implementations is what makes verification portable to an auditor's independent tooling. - Payload separation. The chain entry carries the hash of the full decision payload, not the payload itself; full payloads (prompts, tool calls, outputs — potentially large, potentially sensitive) are stored adjacent in the same segment object, addressed by
payload_hash. The chain stays compact and verifiable in isolation; payload disclosure decisions (e.g., what an auditor sees) are separable from integrity proof. - Ten chains, not one. One chain per Compartment (
c01…c10) plus aplatformchain (identity events, break-glass use, Vault audit summaries, infra changes from §10.4). Per-Compartment chains preserve the isolation model in the evidence itself — Compartment 03's audit history is disclosable to an auditor without touching Compartment 08's — and keep verification parallelizable. Cross-chain ordering, where ever needed, is provided by the anchor layer, not by merging chains. - Segments. The writer service (§13.3) closes a segment every hour or 10 000 events, whichever first:
segment = {chain_id, seq_range, first_hash, last_hash, event_file, payload_file}, uploaded tosbs-dash-auditunder{chain}/{yyyy}/{mm}/{dd}/{segment_id}.tar.zstwith a manifest. Segment manifests are cosign-signed (keyless, the CI/service identity — same machinery as §11.5, no new key lifecycle). - Anchoring (the external trust root). Each segment's
last_hashis anchored outside the platform's own control so that even a full-platform compromise cannot silently rewrite history: (a) appended to a daily anchor digest — one line per chain per day — which is (b) cosign-signed and (c) submitted to the Sigstore Rekor public transparency log, whose inclusion proof is stored alongside. Rekor gives an independent, publicly verifiable timestamp for free, using tooling already in the stack. The daily anchor digest is additionally emailed to the SBS security alias (§4.4) — a low-tech second anchor in SBS-controlled custody that no platform credential can retract.
13.3 Write Path and Event Sources
- Audit writer service — a small dedicated service (one per environment, prod's on
sbsdash-server-prod, its own compose networknet-audit): receives events on a VPC/localhost-only endpoint, assignsseq/prev_hash/entry_hash, appends to the active segment on a dedicated DO Volume (write buffer), closes/uploads/anchors segments. It holds the only credential with PUT access tosbs-dash-audit(§7.3 write-once posture: PUT-only, no DELETE, no overwrite — bucket versioning on as belt-and-braces). - Sources (deduplicated against §12.3 — same events, second delivery, different guarantee): Guardian Agent decision records (the primary stream — every agent action with Risk Tier, per SOW-side commitments); Supervisor routing/resolution events (Medium/High/Critical human decisions, §9.5 supervisor groups); identity events (§9.4 SCIM lifecycle, break-glass use §9.6); Vault audit device (§8.4); infrastructure changes (merged PRs, Prod applies, emergency backports §10.4); egress-denial summaries (§3.4).
- Delivery semantics, stated honestly: at-least-once from sources to writer with idempotent dedup on
(source, source_seq); the writer's local buffer means a writer crash loses at most the unflushed tail (fsync per event for Guardian decisions — the stream where loss is least acceptable; batched fsync elsewhere). The chain structure means loss is always detectable (sequence gap) even where not preventable — a gap is an incident (MON-05), never silently renumbered. - Backpressure rule: if the writer is unavailable, Guardian Agents queue locally and continue enforcing (availability of the platform does not hinge on the audit path), but a queue older than 15 minutes is a
criticalalert (§12.4) — extended audit outage is treated with the severity of a control failure, because it is one.
13.4 Retention and Lifecycle
- Seven years minimum, rolling, per the engagement's FCC/SOX-grade retention commitment; segments carry their close-date and become deletion-eligible only after
close + 7y, and deletion is a manual, dual-controlled annual ceremony (SBS sign-off via Alexandra Del Rey routing + Ledger Hub execution), logged — there is no automated deletion path at all, which is the safest failure mode for an evidentiary store. - Storage layout: hot segments in
sbs-dash-audit(NYC3), replicated to the SFO3 backup bucket on the §14 schedule — two regions, plus the Rekor/email anchors which survive even dual-bucket loss (anchors prove what the data was; replication ensures the data survives — both are required, neither substitutes). - Capacity honesty: at plausible agent volumes (10 Compartments × thousands of decisions/day), zstd-compressed JSON runs low single-digit GB/year for chains and tens of GB/year with payloads — Spaces-trivial; the 7-year cost concern is nil. The Volume write buffer is sized for 72 h of disconnected accumulation.
- Legal hold: a hold flag on any chain/date-range suspends deletion eligibility indefinitely; holds are themselves platform-chain events.
13.5 Integrity Verification
Verification is continuous and layered — an integrity system that is only checked when someone subpoenas it is not an integrity system:
- Continuous (automated): a verifier job on
monitor-servers(deliberately a different host than the writer — the checker must not share the writer's compromise) re-downloads a rolling sample daily: yesterday's segments fully, plus a random historical segment per chain per week. It recomputes everyentry_hash, walksprev_hashcontinuity across segment boundaries, checks cosign signatures, and verifies Rekor inclusion proofs. Results export asintegrityjob metrics (§12.2); any mismatch iscritical(§12.4) and a MON-05 incident. - Full-chain verification: quarterly, complete re-verification of all chains end-to-end (compute-cheap at these volumes), timed with the quarterly executive review cadence — the review receives a signed verification report, generated evidence rather than asserted health.
- Independent verification path (the point of the whole design): a standalone, dependency-light verifier CLI (single static binary, in the repo, itself cosign-signed) that an SBS auditor or regulator can run against exported segments without any Ledger Hub system access: input segments + the public Rekor log + the emailed anchor digests → PASS/FAIL per chain. Admissibility rests on this property — the evidence verifies against public roots, not against the operator's word.
- Tamper response: verification failure freezes the affected chain's deletion eligibility, triggers incident response (MON-05), and the last-good anchor bounds the affected window precisely — the chain structure turns "was anything altered?" into "these specific sequence ranges after this timestamp are suspect."
13.6 Verification (Build Gates)
Gates (LH-SBS-INST-001): writer reachable only on net-audit/VPC (public + cross-Compartment probe fails); Guardian test event → chained entry with correct prev_hash linkage (recompute check); segment close → bucket object + cosign signature + Rekor inclusion proof all present; PUT-only posture proven (writer credential DELETE attempt fails, §7.7 carried gate); induced-tamper drill — modify one byte in a staged segment copy, continuous verifier alerts within its cycle; sequence-gap drill — drop an event in staging, gap detected and alerted; independent verifier CLI produces PASS on clean export and FAIL on the tampered copy, run from a machine with zero platform access; queue-age alert fires in writer-outage drill; anchor email arriving at SBS alias confirmed; 7-year lifecycle rules + hold flag present in IaC with no automated deletion path (code review gate).
Backup & Disaster Recovery
Backup schedule, RPO/RTO targets, restore procedures, DR test cadence
Chapter 14 — Backup & Disaster Recovery
14.1 Current State and Scope
As of 2026-07-18, tier 1 (DigitalOcean Backups) is enabled on the droplet fleet — the first backup layer in production. Tiers 2 and 3 remain unbuilt: no database dumps (no databases yet), no repo mirrors, no Vault snapshots (no Vault yet), no offline custody artifacts. Single-droplet loss is now recoverable; application-consistent recovery, region loss, and account compromise are not yet covered. Scope of this chapter: everything required to reconstruct the Platform (data, configuration, state, secrets, evidence); the recovery procedures per failure class; and the test cadence that keeps "we have backups" meaning "we can restore." Chapter 13's audit anchors are the integrity counterpart; this chapter is survivability. Controlling family: the backup/DR control set imported into KO-001.
14.2 Backup Architecture — Three Tiers
The design principle: IaC-first recovery — most of the platform is rebuilt from code, not restored from images. Three complementary tiers, each covering a failure class the others cannot:
| Tier | Mechanism | State | Covers | Cannot cover |
|---|---|---|---|---|
| 1 — Disk images | DigitalOcean Backups (native) | Live | Fast single-droplet restore | Application consistency (crash-consistent only); account compromise (backups are not downloadable and live inside the DO control plane) |
| 2 — Application artifacts | Encrypted dumps + state archives to Spaces, replicated NYC3→SFO3 | Unbuilt | Application-consistent data recovery; region loss | Hostile DO account — see tier 3 |
| 3 — Off-account survivability | Repo mirrors, offline age identity (SBS custody), Vault Shamir shares, audit anchors (Rekor + SBS email) | Unbuilt | Total account/custody compromise (§14.6) | — (this is the floor) |
14.2.1 Tier 1 — DigitalOcean Backups (Live)
Enabled on the fleet as of 2026-07-18. Target configuration:
| Droplet | Frequency (target) | Retention (target) |
|---|---|---|
sbsdash-server-prod |
Daily | 7 days |
prod-vault (at creation) |
Daily | 7 days |
monitor-servers |
Daily | 7 days |
sbs-wiki (carries through NYC2 rebuild) |
Daily | 7 days |
Dev-SBS-server |
Weekly | 4 weeks |
Open verification items against the live enablement (Build Register item 96 residual):
- Plan and settings audit — confirm which plan is active (usage-based, GA since Nov 2025 and priced by restorable file size with configurable frequency/retention, vs. legacy flat-percentage) and the actual per-droplet frequency selected (usage-based plans allow 4 h / 6 h / 12 h / daily / weekly). Align to the target table or amend it.
- Codify in tofu at the §10.6 import — enablement was performed via console; once imported, a droplet created without backups surfaces as a plan diff rather than a discovered gap. Console-enabled today is acceptable as bootstrap; console-managed permanently is drift.
Dev-Gitlab-server— uninventoried (§2.2.2) but currently serving the apex record; take one backup/snapshot of it before the DNS repoint as cheap insurance, regardless of its final disposition.
Ongoing tier-1 mechanics: monthly, one Prod backup per droplet is converted to a snapshot and retained on a 12-month rolling basis (backups convert to snapshots for indefinite storage — the medium-term image tier). Restore paths are in-place restore or new-droplet-from-backup; creation runs roughly 2 min/GB used, keeping backup windows in minutes at our footprint.
Position in the design, stated plainly: tier 1 is the speed layer for single-droplet loss only. Disk images are crash-consistent — a mid-write PostgreSQL image can restore dirty, so databases recover from tier 2; and an image restores whatever existed at image time, drift and compromise included, so the IaC rebuild (§10) remains the clean-room path.
14.2.2 Tier 2 — Application Artifacts (What Code Cannot Regenerate)
| Asset | Reconstruction source | Backup artifact |
|---|---|---|
| Droplets (OS + services) | Rebuilt — tofu + Ansible (§10); tier-1 image as fast path | — |
| Infra + app repos, IaC state | GitHub (off-platform) + tofu state in Spaces (§10.2) | Nightly git bundle mirror of both repos to sbs-dash-backups — GitHub org loss is inside the threat model (§2.1 revocability cuts both ways) |
| PostgreSQL | — | pg_dump per-database (per-Compartment, §7.4) nightly + WAL archiving continuous once the PITR trigger fires (§14.4) |
| MongoDB | — | mongodump per-database nightly |
| Redis | Declared AOF keyspaces only (§7.1) | AOF-covered; nightly RDB copy off-host. Cache loss accepted by design |
| Vault | — | Raft snapshot every 6 h (§8.2) — the single most critical artifact; losing Vault + its snapshots = re-keying the platform |
| Audit segments | Already in sbs-dash-audit |
Cross-region replication NYC3→SFO3 (§13.4); anchors survive dual-bucket loss |
| Grafana dashboards, alert rules, nginx, compose, nftables | Repo (file-provisioned, §12.5/§10.3) | Covered by repo mirror — this is why §12.5 bans console-authored dashboards |
| TLS keys | Not backed up — reissued via ACME (§5.2) | Deliberate: reissuance ≤ minutes; escrow risk > loss cost |
| BookStack (wiki) | — | Nightly DB dump + uploads archive (runbooks are operational capital) |
All dumps are encrypted client-side before upload (age; key in Vault, with the age identity also held offline in SBS custody per the §9.6 break-glass pattern — an encrypted backup whose only key lives in the Vault it must survive is a circular dependency, broken here) and land in sbs-dash-backups (NYC3) with lifecycle replication to SFO3.
14.3 Schedule and Retention
| Artifact | Frequency | Retention |
|---|---|---|
| DO Backups (disk images) — live | Daily Prod / weekly Dev (target; audit pending), DO-scheduled | 7 d / 4 w native; 12 monthly via snapshot conversion |
| DB dumps (PG, Mongo, BookStack) | Nightly 03:00 ET | 14 nightly, 8 weekly, 12 monthly (GFS) |
| WAL archive (when enabled) | Continuous | 14 days |
| Vault Raft snapshot | 6-hourly | 28 snapshots (7 d) + 12 monthly |
| Repo mirrors | Nightly | 30 days |
| Audit replication | On segment close (§13.2) | 7 y (§13.4) |
| Redis RDB | Nightly | 7 days |
Every tier-2 job emits completion + size + age metrics to the integrity job (§12.2); tier-1 backup age is polled from the DO API per droplet and exported to the same job — now actionable immediately and prioritized, since it is the only automated proof the live backups are actually completing. Any backup age > 26 h (tier-1 daily set or tier-2 nightly set) is critical (§12.4) — a silently failing backup is the canonical DR failure and is alerted, not discovered.
14.4 RPO / RTO Targets
Per failure class — one blended number would be fiction:
| Failure class | RPO | RTO | Recovery path |
|---|---|---|---|
| Single service/container failure | 0 | ≤ 15 min | Compose restart / redeploy pinned digest (§6.7) |
| Single droplet loss (non-data) | 0 (stateless) | ≤ 30 min (tier-1 image restore) / ≤ 2 h (IaC clean-room rebuild) | Image restore when the failure is mechanical; IaC rebuild when state is suspect |
| Prod app droplet loss (interim data services, §6.5) | ≤ 24 h (nightly dumps); ≤ 5 min once WAL PITR enabled | ≤ 4 h | Image-restore or rebuild droplet, restore tier-2 dumps, re-assert ACLs (§7.4), redeploy |
| Vault droplet loss | ≤ 6 h | ≤ 2 h | New droplet (IaC or image) + Raft snapshot restore + quorum unseal (§8.2) — RTO includes assembling 3 shareholders; that human dependency is the honest bottleneck |
| Monitor/bastion loss | 0 config / 30 d metrics history accepted lost | ≤ 4 h | Image restore or IaC rebuild; interim SSH path: temporary direct-SSH firewall rule to prod via emergency PR (§10.4), removed at bastion restoration |
| Wiki loss | ≤ 24 h | ≤ 4 h | Image restore or IaC rebuild + dump restore |
| Region loss (NYC2) | ≤ 24 h (SFO3 replicas; tier-1 images are region-bound and do not participate) | ≤ 3 business days | Full IaC re-deploy to alternate region (region is a tofu variable, §10.3 globals); DNS repoint (§4.3); restore from SFO3. Not hot-standby — stated cost decision at this scale, revisited at Ch. 16 triggers |
| Account compromise (DO or GitHub org) | Bounded by tier-3 artifacts | Days, incident-governed | §14.6 — tier 1 and in-account tier 2 presumed lost |
Until tier 2 exists, the honest fleet-wide posture is: RPO for any future data = undefined; recoverability = single-droplet mechanical failure only. This line is deleted from the specification when items 86–92 close — it is written here so the gap is a stated fact, not an implication. Database RPO ≤ 24 h is the accepted Day-1 posture once dumps run; the WAL-archiving upgrade to ≤ 5 min is triggered when the first Compartment carries production tenant data — not before (cost honesty), mandatorily not after.
14.5 Restore Procedures
Each is a wiki runbook (§1.4 publication target) with exact commands and a verification tail; summarized:
- Single-droplet fast path (tier 1 — available today): restore image or create-from-backup → verify drift (
tofu plan+ Ansible--checkclean against repo state once §10 exists; until then, manual diff against SERVER_CHANGELOG.md — the interim reality, stated) → rejoin monitoring → smoke test. - Database restore (tier 2): provision clean engine (IaC) → decrypt dump (age key from Vault, or offline custody copy if Vault is the casualty) → restore per-database → re-assert per-Compartment roles/ACLs from IaC before any application start (a restored database with default grants is an isolation breach, §7.4) → row-count comparison vs. manifest → smoke test.
- Vault restore: IaC/image droplet →
raft snapshot restore→ quorum unseal → audit-device continuity check → rotate any credential issued after snapshot timestamp (derivable from the Vault audit stream). - Full-region DR: run-order: VPCs → Vault (secrets first — everything authenticates through it) → data services + restores → app droplets + deploys → monitor → DNS cutover (TTLs drop to 300 at DR declaration) → §12.7-style verification sweep before declaring recovery.
- Restore-time isolation rule: Compartment agent workloads stopped during recovery — agents never run against partially restored data (Guardian policy state could lag data state); restart is gated on the verification tail and is itself an audit-chain event (§13.3).
14.6 Worst-Case: Account/Custody Compromise
The survivability floor. If the DO account and GitHub org are both lost or hostile: tier 1 is gone by definition (backups are not downloadable and live inside the account); in-account tier 2 is presumed gone. Reconstruction requires exactly the tier-3 set — repo mirrors (SFO3 replicas), encrypted dumps + offline age identity (SBS custody), Vault snapshot + Shamir shares (5 named holders, §8.2), and audit anchors (Rekor + SBS email, §13.2). Each lives outside the compromised trust domain by design. The runbook is the incident-response DR annex (MON-05 linkage); the §14.7 annual tabletop rehearses this scenario specifically.
14.7 DR Test Cadence
Untested backups are hypotheses. Each test produces a dated wiki report (MON-04/quarterly-review evidence):
| Test | Cadence | Pass criterion |
|---|---|---|
| Tier-1 restore drill — first execution scheduled now (the layer is live; prove it): restore latest backup of a non-critical droplet (wiki or dev) to a new droplet, boot, inspect, destroy | First: immediate. Then quarterly within the rotation below | Boots, services start, data present as of backup time |
| Automated restore-verify: latest PG/Mongo dump into throwaway Dev container, row counts + ACL assertions | Weekly, automated (CI), from tier-2 build | Green integrity metric; failure = critical |
| Single-droplet recovery drill, alternating quarterly between tier-1 image restore and IaC clean-room rebuild | Quarterly | RTO ≤ target; post-restore drift check clean |
| Vault snapshot restore into isolated instance (test shares, no prod unseal) | Quarterly | Unseal + read probe passes |
| Full-region tabletop + partial technical (Vault+DB restore into SFO3) | Annually | Runbook gaps filed as Build Register items |
| Account-compromise tabletop (§14.6) | Annually | Custody chain confirmed current (share holders, offline key, alias access) |
14.8 Verification (Build Gates)
Gates (LH-SBS-INST-001): DO Backups confirmed active on all five droplets with plan/frequency audit closed and settings codified in tofu (item 96 residual); tier-1 backup-age visible via DO API and exported to integrity per droplet; first tier-1 restore drill passed; every §14.2.2 artifact present in sbs-dash-backups with age metric green; SFO3 replication object-count parity; dump decrypts with the offline-custody key copy (circular-dependency break proven, not just the Vault copy); weekly restore-verify green ≥ 2 consecutive cycles pre-Cutover; Vault snapshot restore drill passed; backup-age alert fires in induced-failure drill; monthly snapshot-conversion automation present in IaC; restore runbooks on wiki with last-tested dates; WAL-PITR trigger condition documented; region variable proven by clean tofu plan targeting SFO3; Dev-Gitlab-server pre-repoint backup taken.
Capacity & Scaling
Baseline sizing, growth thresholds, scaling procedures
Chapter 15 — Capacity & Scaling
15.1 Philosophy and Scope
Capacity planning at this fleet size is not autoscaling — it is knowing the numbers, naming the thresholds, and pre-writing the procedures so scaling is a scheduled maintenance action rather than an incident response. The Platform's load profile helps: Compartment agent workloads are queue-driven and tolerate minutes of latency (a traffic-log reconciliation that waits 5 minutes is not an incident), so the design scales for throughput over time, not burst absorption. The only latency-sensitive paths are the human web surfaces and the audit write path. Scope: droplet sizing, per-Compartment resource budgets, storage growth, the thresholds that trigger action, and the procedures per scaling event. Growth triggers named in earlier chapters (Vault HA §8.2, remote-write metrics §12.2, hot-standby DR §14.4, private DNS §3.3) consolidate here.
15.2 Baseline Sizing and Budgets
As-built fleet totals: 22 GB RAM / 11 vCPU / 500 GB disk across five droplets (§2.2.1, incl. prod-vault at creation). Per-droplet budgets:
sbsdash-server-prod (8 GB / 4 vCPU / 160 GB) — the contended host. Budget under the §6.5 interim (data services colocated):
| Allocation | RAM | Notes |
|---|---|---|
| OS + Docker + nginx + agents | 1.0 GB | |
| PostgreSQL | 1.5 GB | shared_buffers 512 MB, capped connections via per-Compartment pool |
| MongoDB | 1.0 GB | WiredTiger cache capped 512 MB |
| Redis | 0.5 GB | maxmemory 384 MB, allkeys-lru on cache keyspaces |
| Audit writer + buffer | 0.5 GB | |
| Compartment containers | 3.5 GB | ≈ 1.0–1.2 GB per Compartment stack (app + MCP sidecar) at §6.4 limits |
| Headroom | ~0.5 GB | Below 1 GB free is itself a warning signal |
Consequence, stated plainly: the 8 GB prod droplet supports ~3 concurrent Compartment stacks in the interim topology. That matches the engagement's minimum-Compartment starting footprint, but full ten-Compartment operation on this host requires either the data-service offload (Managed DBs, §7.2) or the resize/split ladder (§15.4) — this is a designed checkpoint, not a surprise.
Other hosts: monitor-servers (4 GB) — Prometheus 30 d TSDB ≈ 2–6 GB disk at current cardinality, Loki 90 d on its Volume; RAM adequate until Loki ingest grows with Compartment count (threshold below). prod-vault (1 GB) — trivial load, no growth coupling. sbs-wiki (2 GB) — static. Dev-SBS-server — mirrors prod budgets by design (§2.4).
15.3 Growth Thresholds (Named Triggers → Named Actions)
Thresholds are Prometheus alert rules (§12.4 warning tier unless noted), each mapped to a §15.4 procedure — a threshold without a pre-decided action is just anxiety:
| Signal | Threshold | Action |
|---|---|---|
| Prod RAM sustained (15 min) | ≥ 85 % | P1: resize droplet |
| Prod free RAM | < 1 GB at Compartment deploy time | Deploy blocked; P1 or P2 first |
| Compartment count | 4th Compartment Workorder received | P2: data-service offload decision forced (Managed DB check, item 36, must be closed by now) |
| Per-Compartment container at its §6.4 limit | throttling/OOM-kill events > 0 | Raise that Compartment's budget via PR (visible, reviewed) — never silently unlimit |
| Any disk | ≥ 80 % / ≥ 90 % | warning: P3 volume grow / critical: immediate P3 |
| PG connections | ≥ 80 % of max | Pool tuning, then P2 |
| Loki ingest | > 5 GB/day sustained | P4: monitor resize or Loki retention drop to 60 d (decision, not drift) |
| Prometheus TSDB | > 40 GB or scrape latency alerts | P4 + revisit remote-write (§12.2 deferred item) |
| Audit writer queue age | > 5 min sustained under normal ops (distinct from §13.3 outage alert) | P5: writer I/O tuning / dedicated Volume IOPS |
| Fleet size | > 10 droplets | Private DNS zone (§3.3 trigger); Vault HA evaluation (§8.2) |
| Any single droplet | > 16 GB resize on the table | P6: split evaluation instead — vertical scaling stops being the answer |
15.4 Scaling Procedures
All procedures are IaC changes (§10.4 workflow) executed in maintenance windows; none is novel at execution time because each is pre-written as a wiki runbook:
- P1 — Droplet resize (vertical). tofu size change → PR → apply = powered-off resize (~1–3 min downtime, disk+CPU+RAM resize; reserve the disk-inclusive variant so storage grows too). Order of preference: resize prod before adding hosts — vertical is operationally free until §15.3's 16 GB ceiling. Tier-1 backup taken pre-resize (fast rollback = restore + revert PR).
- P2 — Data-service offload. The designed inflection at Compartment #4: Managed DBs available in NYC2 (item 36) → migrate per §7.2 (dump/restore per engine, maintenance window, Change Order) → frees ~3 GB on prod → Compartment ceiling rises to ~7–8 on the same droplet. If Managed DBs unavailable → dedicated
prod-datadroplet (8 GB) in the Prod VPC running the §6.5 compose stacks — same isolation model, same entitlement networks, one hop away. - P3 — Storage grow. DO Volumes resize online (grow-only); filesystem expand follows in the same window. Root-disk pressure on droplets without Volumes → P1 disk-inclusive resize. Spaces is elastic — no procedure, only the cost line.
- P4 — Observability scaling. Monitor resize (P1 pattern) or retention reduction — an explicit reviewed trade, never silent data loss.
- P5 — Audit path tuning. Volume IOPS/size bump, fsync batching review (§13.3 semantics preserved — Guardian stream stays per-event), segment-close interval tuning.
- P6 — Horizontal split. When prod outgrows vertical: split along the already-drawn lines — data tier out first (P2's droplet variant), then Compartments 6–10 to a second app droplet (
net-edgespans hosts via VPC; nginx upstreams update by IaC variable). The compose-per-Compartment design (§6.4) makes Compartment placement a scheduling decision, not a re-architecture — this is the payoff for refusing the mega-compose. - Scale-down is the same procedures reversed, gated on 30 days below 50 % of the triggering threshold — flap-damping for capacity decisions.
15.5 Capacity Review Cadence
Quarterly (aligned with the §12.5/§13.5 quarterly review artifacts): dashboard-sourced trend review of RAM/CPU/disk/ingest slopes per host, Compartment-count forecast against the P2 checkpoint, cost line (droplets + Volumes + Spaces + DO Backups usage-based charges + Anthropic consumption trend from egress/billing data), and a written one-page capacity position filed to the wiki. The review's only mandatory output: confirmation that the next scaling event is known, named, and scheduled — or explicitly "none within horizon."
15.6 Verification (Build Gates)
Gates (LH-SBS-INST-001): §15.3 thresholds present as alert rules in the repo (promtool-validated, mapped action in annotation); §6.4 resource limits present on every Compartment service and OOM/throttle events exported as metrics; P1 resize drill executed once on Dev (timed, rollback path proven); Compartment-deploy RAM precondition check wired into the deploy pipeline (blocked-deploy negative test); P2 decision documented and item 36 closed before 4th-Compartment Workorder acceptance; capacity runbooks (P1–P6) published to wiki; first quarterly capacity position filed.
Maintenance & Patching
Patch windows, upgrade procedure, rollback
Chapter 17 — Maintenance & Patching
17.1 Current State and Scope
As-built patching is unmanaged: no unattended-upgrades confirmation, no defined windows, no reboot policy, and one already-tracked drift instance (OS mix 24.04/26.04, item 21). This chapter defines the patch regime for every layer — OS packages, kernel/reboots, container images, system services (nginx, Prometheus, Grafana, Vault, BookStack), data engines, and the DO platform events outside our control — plus the rollback path per layer. Controlling requirement: INF-05 (patching); MON-06 (vulnerability scanning, §11.6) is the detection feed this chapter remediates. The governing principle: patching is a pipeline event with a rollback, never an SSH session with apt upgrade and hope.
17.2 Patch Windows and Cadence
| Window | When (ET) | Scope |
|---|---|---|
| Standing weekly | Tuesday 05:00–07:00 | Dev: everything. Prod: security-only OS patches (unattended-upgrades applies these continuously; the window is for anything needing coordination) |
| Standing monthly | Second Tuesday 05:00–07:00 | Prod: full OS package upgrade, pending reboots, service minor versions, base-image rebuild rollout (§11.3 weekly rebuilds accumulate; deploys land here unless CVE-driven earlier) |
| Emergency | Any time, MON-05 incident context | Actively exploited / CRITICAL-with-fix (Trivy nightly or vendor advisory). Target: patch in Dev + deploy to Prod ≤ 48 h from advisory; §10.4 emergency path applies with 24 h backport rule |
| Freeze | Cutover ± 5 business days; declared SBS blackout dates | Security-only; everything else queues |
Ordering rule, always: Dev first, soak ≥ 24 h (weekly cycle) with monitoring green, then Prod. The 1:1 Dev sizing (§2.4) exists precisely so this soak is meaningful. Within Prod, order is: wiki → monitor → vault → prod-app — least-critical first, and the bastion/alerting host (monitor) is patched before the app host so a monitor regression is discovered while the app host is still stable, never both at once.
17.3 Patching by Layer
- OS security patches:
unattended-upgradesenabled fleet-wide (Ansible base role, item 60) — security pocket only, no automatic reboots, mail/log output shipped to Loki. This makes security patching continuous and the windows about coordination, not application. - Kernel/reboot:
needrestart/reboot-required flag exported as anintegritymetric (§12.2); pending-reboot age > 14 days =warning. Reboots execute in the monthly window, one host at a time, in the §17.2 order, with the bastion-loss interim SSH note (§14.4) pre-staged when monitor reboots. DO Backup taken pre-reboot on each host (tier-1 as rollback, §14.2.1). - Container images: already fully specified — weekly CI rebuild against updated bases + Trivy gate (§11.3/§11.6); this chapter adds only the rollout rule: rebuilt images deploy to Dev on build, Prod in the monthly window or the emergency path. Rollback is redeploy-previous-digest (§6.7) — the strongest rollback in the whole stack, which is why the design pushes as much as possible into images.
- System services (nginx, Prometheus, Grafana, Loki, certbot): apt-managed on their hosts; minor versions ride the monthly window. Grafana major versions (13 → 14 class) get a Dev-first upgrade with dashboard/provisioning verification before Prod — Grafana majors have history of provisioning-format changes, and the §12.5 file-provisioning rule is what makes this testable.
- Vault: upgrade = new binary via Ansible, service restart, unseal quorum required (§8.2) — so Vault upgrades are scheduled with shareholders, monthly window only, never emergency-path unless the CVE is Vault-critical. Raft snapshot taken immediately pre-upgrade; rollback = binary revert + snapshot restore (§14.5.3).
- Data engines (interim containers, §6.5): minor/patch versions ride the image rebuild cycle. Major versions (PG 16→17 class) are Change-Order events with dump/restore rehearsal in Dev, never a window item. Under Managed DBs (§7.2), DO handles patch/minor; majors remain scheduled decisions.
- BookStack: monthly window, dump first (tier-2), standard upstream upgrade path.
- DO platform events (hypervisor migrations, mandatory maintenance): outside our scheduling — mitigations are the tier-1 backups, the alert stack (droplet-down alerts already routed), and reading DO's status/email notices, which route to the SBS-controlled account alias (§2.1).
17.4 Upgrade Procedure (Standard Template)
Every window execution follows one runbook shape (wiki-published, per-layer specifics as annexes):
- Pre: confirm monitoring green fleet-wide; confirm last backup age green (tier-1 and applicable tier-2, §14.3 metrics); announce in ops channel (start/scope/expected end); freeze deploys for the window.
- Execute: Dev-proven changes only (except emergency path); one host at a time in §17.2 order; between hosts, verify: services active, blackbox probes green (§12.2), no new alerts for 10 min before proceeding.
- Verify: per-layer smoke (nginx
-t+ probe sweep; Grafana health + one dashboard render; Vault unsealed + read probe; app surfaces via blackbox; audit writer queue age nominal). - Close: announce complete; changelog entry (narrative layer, §10.6) with versions before/after; any deviation → Build Register item within 24 h.
17.5 Rollback
Rollback authority: the window executor rolls back without seeking approval when any post-step verification fails — the approval was for the change; reverting to known-good needs none. Paths per layer, worst-case first:
| Layer | Rollback | Bound |
|---|---|---|
| Container/app | Redeploy previous digest (§6.7) | Minutes |
| OS package set | apt downgrade where pinnable; else tier-1 image restore pre-window backup |
≤ 30 min (§14.4) |
| Kernel/reboot regression | Boot previous kernel (GRUB retains) or image restore | ≤ 30 min |
| Grafana/Prometheus/Loki | apt downgrade or image restore; dashboards/rules are repo-provisioned so config rollback = git revert | Minutes–30 min |
| Vault | Binary revert + pre-upgrade Raft snapshot; quorum re-unseal | ≤ 2 h (§14.4 — shareholder-bound) |
| Data engine minor | Previous image digest + AOF/WAL intact | Minutes |
| Data engine major | Restore rehearsed dump (the rehearsal is the Change-Order gate) | Per §14.4 data-loss class |
| DNS/zone changes | Not a window item (§4.7 change control); rollback = revert record, 300-TTL bound | ≤ 5 min propagation |
Rollback is itself verified (same §17.4.3 smoke set) and logged; a rolled-back change re-enters through Dev with a written cause, never retried directly on Prod — the failure taught something or it didn't; the PR discussion says which.
17.6 Verification (Build Gates)
Gates (LH-SBS-INST-001): unattended-upgrades active fleet-wide with security-pocket-only config (Ansible-asserted, drift-checked); reboot-required metric exported and alerting at 14 d; window calendar published to wiki with §17.2 order and freeze rules; standard runbook + per-layer annexes published; one full monthly-window execution completed end-to-end on the fleet with changelog evidence; one deliberate rollback drill on Dev (failed-verification simulation → digest revert → smoke green); Vault pre-upgrade snapshot step present in its annex and rehearsed with test shares (§14.7 linkage); OS-mix remediation (item 21) closed or scheduled through these windows — the mechanism that fixes the existing drift is the mechanism that prevents the next.
Appendices
Resource inventory, firewall rule table, acronym table, references (NIST SP 800-207, OWASP ASVS L2)
Chapter 18 — Appendices
Appendix A — Resource Inventory
A.1 DigitalOcean Account & Projects
| Resource | Identifier | Notes |
|---|---|---|
| DO Account | SBS-owned (alias ownership pending, §2.1) | MFA/team-audit pending item 15/17 chain |
| Project: SBS The Dash Dev | ac656e28-2196-410e-847c-bf3b60363710 |
Default project |
| Project: SBS The Dash Prod | c49c646f-f268-4b51-a00e-ad9fb5055569 |
A.2 Droplets (As Built)
| Droplet | Project | Region / VPC | Size | Public IP | Private IP | OS | Role | DO Backups |
|---|---|---|---|---|---|---|---|---|
Dev-SBS-server |
Dev | NYC2 / default-nyc2 | 8 GB / 4 vCPU / 160 GB | 162.243.252.138 | 10.100.0.2 | Ubuntu 24.04 | Dev server; Docker 29.1.3 | ✅ (target: weekly) |
sbsdash-server-prod |
Prod | NYC2 / default-nyc2 | 8 GB / 4 vCPU / 160 GB | 107.170.72.145 | 10.100.0.3 | TBC | Prod app; tenant surfaces; Docker 29.1.3; bastion-only SSH | ✅ (target: daily) |
monitor-servers |
Prod | NYC2 / default-nyc2 | 4 GB / 2 vCPU / 120 GB | 162.243.28.132 | 10.100.0.4 | Ubuntu 26.04 | Grafana 13.1.0 / Prometheus / nginx 1.28.3 / Loki (planned); bastion | ✅ (target: daily) |
sbs-wiki |
Prod | NYC1 / default-nyc1 ⚠ | 2 GB / 1 vCPU / 60 GB | 167.172.135.88 | 10.116.0.2 | TBC | BookStack; NYC2 rebuild pending | ✅ (target: daily) |
Dev-Gitlab-server ⚠ |
Unregistered | VPC 10.120.x | TBC | 138.197.222.154 | TBC | TBC | Holds apex DNS; disposition item 22 | Pre-repoint backup: item 101 |
prod-vault |
Prod | NYC2 / prod-vpc-nyc2 | 1 GB / 1 vCPU | — | — | — | Planned — Vault (§8.2), rebuild wave | Daily at creation |
A.3 Network (Target vs. As Built)
| Network | CIDR | State |
|---|---|---|
prod-vpc-nyc2 |
10.10.0.0/20 | Target — rebuild wave (item 18/59) |
dev-vpc-nyc2 |
10.20.0.0/20 | Target — rebuild wave |
default-nyc2 |
10.100.0.0/20 | As built — Dev+Prod shared ⚠, vacated at rebuild |
default-nyc1 |
10.116.0.0/20 | As built — wiki only, vacated at rebuild |
A.4 DNS Zone (sbsdash.com — GoDaddy NS, cutover pending item 9)
| Record | Target (as built) | Target (spec) |
|---|---|---|
| apex A | 138.197.222.154 ⚠ | 107.170.72.145 (item 16, gated on Ch. 5) |
| www CNAME | — | sbsdash.com |
| admin A / clients A | — | 107.170.72.145 |
| monitor A | 162.243.28.132 ✅ live+TLS | unchanged |
| wiki A | 167.172.135.88 | NYC2 rebuild IP |
| MX / SPF / DMARC / CAA | absent | null-mail + LE-only CAA (item 23 — publishable now) |
A.5 Storage & Registry (Planned)
| Resource | Location | Chapter |
|---|---|---|
Spaces: sbs-dash-artifacts, sbs-dash-audit, sbs-dash-backups |
NYC3 (+SFO3 replication) | §7.3, §13, §14 |
| DO Volumes: data engines, Loki, audit writer, Vault Raft | NYC2 | §7.6, §12.3, §13.3, §8.2 |
| GHCR (SBS org) | — | §11.2 |
Repos: sbs-dash-infra, platform app repo |
GitHub SBS org | §10.3 |
A.6 Software Baseline (As Built)
Docker 29.1.3 / Compose 2.40.3 (prod, dev, monitor) · Grafana 13.1.0 (grafana apt repo) · Prometheus + node_exporter ×4 (Ubuntu repo) · nginx 1.28.3 · certbot + LE cert live on monitor · BookStack (wiki). Pending install: Node LTS/pnpm, Rust/Cargo, Python venvs, PostgreSQL, MongoDB, Redis, Vault, Loki, Alertmanager, blackbox/cAdvisor/engine exporters, oauth2-proxy, AnythingMCP.
Appendix B — Firewall Rule Tables (Target, §3.5–3.6)
B.1 Cloud Firewalls (tag-scoped) — as built: none exist ⚠ (item 19)
| FW / tag | Inbound | Outbound |
|---|---|---|
prod-app / prod |
443, 80 ← any · 22 ← monitor private IP only (bastion) · 9100+exporters ← monitor private IP | §3.4 allowlist |
prod-monitor / grafana |
443, 80 ← any · 22 ← admin allowlist (sole public SSH into Prod) · [3000/9090/9100 localhost-bound — no rule] | §3.4 |
prod-wiki / wiki |
443, 80 ← any · 22 ← monitor private IP · 9100 ← monitor private IP | §3.4 |
prod-vault / vault |
8200 ← prod-app, dev, bastion private IPs · 22 ← monitor private IP | §3.4 (narrow) |
dev-app / develop |
22 + web ← admin allowlist only | §3.4 + language registries (build context) |
B.2 Host Firewall (nftables target; as built: ufw on monitor/wiki only, none on prod/dev ⚠ item 20)
Baseline: inbound default-deny mirroring Cloud FW; outbound default-deny + allowlist — 443→Anthropic API, GitHub/GHCR, apt mirrors, LE ACME; 123→NTP; 53→DO resolvers. Per-Compartment chains: egress = Anthropic + that Compartment's declared integrations only (§3.4). Ruleset hash exported to integrity job (§12.2).
B.3 Egress Allowlist Summary
| Destination | Ports | Scope |
|---|---|---|
| Anthropic API | 443 | All prod + per-Compartment chains |
| GitHub / GHCR | 443 | Deploy/CI paths |
| Ubuntu archive/security, grafana apt | 443/80 | Hosts |
| registry.npmjs.org, crates.io, pypi.org | 443 | Build contexts only — unreachable from prod runtime (§11.2) |
| Let's Encrypt ACME | 443 | Edge hosts |
| DO resolvers / NTP pool | 53 / 123 | All |
Appendix C — Build Register (Consolidated, items 1–115)
Status: ✅ done · ◐ partial · ○ open · ⛔ blocked/decision · numbering per chapter of origin.
| # | Item | Ch. | Status |
|---|---|---|---|
| 1–3 | DO account; Dev + Prod projects | 2 | ✅ |
| 4 | Dedicated VPCs ×2 (10.10/10.20) | 3 | ○ (rebuild wave) |
| 5 | Cloud Firewalls ×4(+vault) | 3 | ○ |
| 6 | Admin SSH allowlist captured in IaC | 3 | ○ |
| 7 | nftables baseline + Compartment chains | 3 | ○ |
| 8 | Domain transfer to SBS + lock + registrar MFA | 4 | ◐ (registered; transfer pending) |
| 9 | Zone cutover GoDaddy → DNSSEC provider + NS | 4 | ○ |
| 10 | DS publication + chain verification (AD/DNSViz) | 4 | ○ (after 9) |
| 11–12 | Null-mail records; CAA | 4 | ○ — publishable now (item 23) |
| 13–14 | CT alert subscription; dnstwist weekly | 4 | ○ |
| 15 | Credential rotation + Vault migration + changelog purge + SBS-alias identities | 2/8 | ⛔ CRITICAL — execute §8.6 order |
| 16 | Apex repoint 138.197.222.154 → prod | 2/4 | ⛔ gated on Ch. 5 prod edge |
| 17 | Per-person users, root-SSH disable, sudo scoping | 2 | ○ (via item 60) |
| 18 | VPC rebuild decision (a)/(b) | 3 | ⛔ decision — spec assumes (a) |
| 19 | Cloud FW creation | 3 | ○ (=5) |
| 20 | Host firewall activation prod/dev (stopgap ufw → nftables) | 3 | ○ urgent |
| 21 | OS standardization (24.04 vs 26.04) | 2 | ⛔ decision; executes via item 115 |
| 22 | Dev-Gitlab-server disposition |
2 | ⛔ decision |
| 23 | Publish null-mail + CAA on GoDaddy pre-cutover | 4 | ○ immediate |
| 24–29 | Monitor nginx to fleet TLS/header template; social-card stripping; HSTS staged+preload sign-off; expiry probes; prod nginx+certs; admin edge allowlist | 5 | ○ |
| 30–35 | Toolchain install+pinning; Compartment compose template+net-edge; container hardening; interim data stacks; MCP sidecar pattern; compose-vs-running drift metric | 6 | ○ |
| 36 | NYC2 Managed DB availability check | 7 | ⛔ blocks §7.2 path + item 106 |
| 37–41 | Data Volumes; per-Compartment DB/role/ACL scripts; internal CA; Spaces buckets+keys; sanitized-dump script | 7 | ○ |
| 42–48 | Vault droplet+Volume+FW; Shamir ceremony; mounts/policies/AppRoles; DB+PKI engines; deploy-render integration; gitleaks; rotation runbook | 8 | ○ |
| 49 | IdP product confirmation (DISC-002) | 9 | ⛔ blocks 50–54 |
| 50–56 | IdP registrations+groups; oauth2-proxy; Grafana OIDC; BookStack SSO; SCIM+timed test; break-glass ceremony; first IAM-06 review | 9 | ○ |
| 57–62 | Infra repo+protection+CI; tofu import (nonconformance baseline); rebuild-wave authoring; Ansible base role fleet-wide; drift jobs+induced test; Prod approval gate | 10 | ○ — 58 executes 96-residual; 59 executes 4/18/42; 60 executes 17/20/109 |
| 63–70 | GHCR+pull token; base-image allowlist+digest lint; frozen-lockfile gates; cosign sign+verify; SBOM; Trivy gate+nightly; Renovate; Actions digest pinning | 11 | ○ |
| 71–78 | Alertmanager+routes; blackbox ×5; Loki+agents; cAdvisor+engine exporters; integrity metrics; dead-man+external check; dashboard repo graduation; monitoring-role absorption | 12 | ○ (core stack ✅ live) |
| 79–85 | Audit writer+net-audit+Volume; segment format+signing; Rekor+email anchors; continuous verifier; independent CLI; deletion ceremony+legal hold; drill set | 13 | ○ |
| 86–95 | Tier-2 dump pipeline; offline age custody; repo mirrors; Vault snapshots; SFO3 replication; age metrics+alert; weekly restore-verify; runbooks; WAL-PITR trigger; drill calendar | 14 | ○ |
| 96 | DO Backups enablement | 14 | ◐ ✅ enabled; residual: plan/frequency audit + tofu codification |
| 97 | DO API backup-age → integrity metric | 14 | ○ prioritized |
| 98 | Monthly backup→snapshot conversion | 14 | ○ |
| 99 | Plan-type/pricing verification | 14 | ○ |
| 100 | First tier-1 restore drill | 14 | ○ immediate |
| 101 | Gitlab-server pre-repoint backup | 14 | ○ before item 16 |
| 102–108 | Threshold rules+annotations; OOM metrics; P1 drill; deploy RAM gate; P2 decision before Compartment #4; P1–P6 runbooks; quarterly capacity filing | 15 | ○ (106 ⛔ on 36) |
| 109–115 | unattended-upgrades; reboot metric; window calendar; window runbook+annexes; first monthly window; rollback drill; OS-mix via windows | 17 | ○ |
Critical path: 15 → 20 → 57/58 → 18(a) rebuild wave (4, 19, 42, wiki NYC2) → 60 (17) → Ch. 5 prod edge (28) → 16 apex repoint → 9/10 DNSSEC cutover. Blocking decisions: 18, 21, 22, 36, 49.
Appendix D — Acronym Table
| Acronym | Expansion |
|---|---|
| ACL | Access Control List |
| ACME | Automatic Certificate Management Environment |
| AD (flag) | Authenticated Data (DNSSEC) |
| AOF | Append-Only File (Redis) |
| API | Application Programming Interface |
| CA / CAA | Certificate Authority / CA Authorization (DNS record) |
| CD / CI | Continuous Delivery / Continuous Integration |
| CIDR | Classless Inter-Domain Routing |
| CLI | Command-Line Interface |
| CSP | Content Security Policy |
| CT | Certificate Transparency |
| CVE | Common Vulnerabilities and Exposures |
| DMARC / SPF | Domain-based Message Authentication, Reporting & Conformance / Sender Policy Framework |
| DNS / DNSSEC | Domain Name System / DNS Security Extensions |
| DO | DigitalOcean |
| DR | Disaster Recovery |
| DS | Delegation Signer (DNSSEC record) |
| ECDSA | Elliptic Curve Digital Signature Algorithm |
| FCC | Federal Communications Commission |
| FW | Firewall |
| GFS | Grandfather-Father-Son (retention) |
| GHCR | GitHub Container Registry |
| HSTS | HTTP Strict Transport Security |
| IaC | Infrastructure as Code |
| IAM | Identity and Access Management |
| IdP | Identity Provider |
| IOPS | Input/Output Operations Per Second |
| JCS | JSON Canonicalization Scheme (RFC 8785) |
| KMS | Key Management Service |
| KV | Key-Value (Vault store) |
| LE | Let's Encrypt |
| LTS | Long-Term Support |
| MCP | Model Context Protocol |
| MFA | Multi-Factor Authentication |
| MX | Mail Exchanger (DNS record) |
| NS | Nameserver |
| NTP | Network Time Protocol |
| OCSP | Online Certificate Status Protocol |
| OIDC | OpenID Connect |
| OOM | Out Of Memory |
| OS | Operating System |
| OWASP ASVS | Open Worldwide Application Security Project — Application Security Verification Standard |
| PII | Personally Identifiable Information |
| PITR | Point-In-Time Recovery |
| PKCE | Proof Key for Code Exchange |
| PKI | Public Key Infrastructure |
| PR | Pull Request |
| RBAC | Role-Based Access Control |
| RDB | Redis Database (snapshot format) |
| RPO / RTO | Recovery Point Objective / Recovery Time Objective |
| SAML | Security Assertion Markup Language |
| SAN | Subject Alternative Name |
| SBOM | Software Bill of Materials |
| SCIM | System for Cross-domain Identity Management |
| SLA | Service Level Agreement |
| SNI | Server Name Indication |
| SOX | Sarbanes-Oxley Act |
| SPDX | Software Package Data Exchange |
| SSH | Secure Shell |
| SSO | Single Sign-On |
| TDE | Transparent Data Encryption |
| TLS | Transport Layer Security |
| TSDB | Time-Series Database |
| TTL | Time To Live |
| UID | User Identifier |
| VPC | Virtual Private Cloud |
| WAL | Write-Ahead Log (PostgreSQL) |
Appendix E — References
| Ref | Document | Applied in |
|---|---|---|
| [1] | NIST SP 800-207, Zero Trust Architecture | §1.2, §7.5 (TLS on internal links — private network ≠ trust boundary), §9 (identity-centric access) |
| [2] | OWASP ASVS v4.x, Level 2 | §1.2, APP-01 baseline, §5.5 headers, §9 session/auth |
| [3] | OWASP LLM Top 10 (incl. LLM01 Prompt Injection) | §3.4 egress-as-exfiltration control, §6.4 MCP scoping (AI-02), §8.3 credential isolation (AI-03) |
| [4] | LH-SBS-SEC-001 — Security & Access Control Specification | Controlling baseline, all chapters; reconciliation 41-vs-36 pending |
| [5] | LH-SBS-KO-001 V3.2 — Technical Install and Discovery | Master runbook; control-family import |
| [6] | LH-SBS-PRE-001 — Pre-Install Prerequisites | Day-0 gates |
| [7] | LH-SBS-INST-001 — Install Runbook, gates V1–V22 | Verification sections §§2–17 |
| [8] | RFC 7505 (Null MX) · RFC 8785 (JCS) · RFC 8659 (CAA) | §4.4, §13.2, §4.6 |
| [9] | DigitalOcean product documentation — Backups, VPC, Spaces, Managed Databases | §14.2.1, §3.1, §7.2–7.3 |
| [10] | Sigstore (cosign, Rekor) documentation | §11.5, §13.2 |
| [11] | SERVER_CHANGELOG.md | As-built evidence layer (§1.3) |