Stratify
Legal and compliance

Operations & Change Management Policy

docs/compliance/isms/policies/POL-07-operations-change-management.md

Source updated 03. Aug. 2026

Operations & Change Management Policy

Satisfies the Annex A operational-security controls covering network security, system configuration, change management, capacity, malware protection, software-installation discipline, network segregation, and web filtering, as applied to stratify's single production host. All factual claims are grounded in `register/facts.md` and cited to docs/runbooks/; control statuses match `register/controls.yaml` and are not restated more favorably here.

1. Policy statement

Stratify's production estate is a single Hetzner host running Docker containers behind Traefik. That is a deliberate, small-team architecture, not a scaled-down version of a larger design — and this policy states plainly what operational security looks like at that scale today, rather than describing controls the team hasn't built.

Three things are real and already operating, independent of this document:

  • Network access to the host is Tailscale-only. The public IP (128.140.8.187) is

firewalled so that no service is reachable from the open internet except through the Tailscale mesh (docs/runbooks/deploy.md:3-19). This is the baseline network-security requirement for the whole stack, not an aspiration — every admin action, SSH session, and manual database operation described in docs/runbooks/db-recovery.md assumes this firewalling is in place.

  • Traefik is the sole TLS-terminating ingress. Nothing else on the host accepts direct

inbound connections; Postgres, the app containers, and Supabase's Auth/Realtime/Storage services are reached through Traefik or not reached from outside the host at all.

  • Production changes flow through a required CI gate before deploy. Every change to

main must pass dos-ci.yml's verify check (lint, typecheck, tests, gitleaks secret scan, build) before the pull-based stratify-deploy.timer — polling the Hetzner box every two minutes — will ever pick it up (docs/runbooks/deploy.md:1-24, facts.md).

Where this policy states a commitment beyond what's built today, it says so explicitly rather than implying the gap is closed.

2. Network security (8.20) — status: partial

What exists: Tailscale-only firewalling of the host's public IP, as described in §1. This is a real, current, meaningfully strong baseline — it means the attack surface for the production database and application containers is not "the internet," it's "the Tailscale network plus whatever Traefik intentionally exposes."

Gap: the firewall rule exists as infrastructure configuration, not as a documented, reviewable policy. There is no periodic check confirming the Tailscale-only posture hasn't drifted (e.g., a debugging session that opens a port and doesn't close it). This document is the first time the requirement is stated as policy rather than left implicit in a runbook.

Commitment: Tailscale-only access to the production host is the standing requirement. Any temporary exception (e.g., opening a port for a one-off diagnostic) must be closed the same session it was opened, and — once PRO-05 (change & release management) exists — logged as a change.

3. Security of network services (8.21) — status: partial

What exists: Traefik is the only service on the host that terminates TLS and accepts inbound traffic; internal services (Postgres, GoTrue, Realtime, Storage) are reached only through the Docker network or through Traefik-routed paths. This is a real architectural discipline, not a written one.

Gap: there is no documented inventory of exactly which routes Traefik exposes versus which services are internal-only. Today that inventory would have to be reconstructed from infra/hetzner/ compose files by hand — workable for one engineer, not auditable by anyone else without doing the same reconstruction.

Commitment: the exposed-service inventory should be written down (even as a short table in infra/hetzner/README.md, once that document's other stale references are corrected — see RISK-001) rather than left as tribal knowledge.

4. Segregation of networks (8.22) — status: partial

Stratify runs one shared Postgres instance for all subscribers, pilots, and B2B partners — there is no per-tenant network segregation, and none is planned. That is a deliberate architectural choice, not an oversight: Row-Level Security is the correct tenant-isolation control for this architecture, not network segmentation. RLS is real and enforced on 23+ tables, with auth.uid() = user_id as the core self-access pattern and explicit deny-by-default (no SELECT policy at all) on audit_log and signal_deliveries (facts.md, supabase/migrations/0002_rls.sql) — Annex A 8.3 is scored implemented in controls.yaml for exactly this reason.

This control (8.22) is scored partial rather than not_applicable because the reasoning above — "RLS substitutes for network segregation here" — has not, until this document, been written down as an explicit policy position. It is now: any future reviewer asking "why isn't there a segregated network per tenant" should be pointed at this section and at 8.3's implemented status, not left to guess.

5. Web filtering (8.23) — status: not_applicable

Stratify does not operate a network that end users browse the web through — it is not an ISP, a corporate network operator, or a managed-device fleet operator for anyone but the founder's own laptop. Web filtering, as an Annex A control, does not apply to this architecture. controls.yaml records this as not_applicable and this policy agrees.

6. Change management (8.32) — status: partial

What exists: a real, working change path — pull request → required verify CI check → merge to main → the pull-based stratify-deploy.timer picks up the change on its next 2-minute poll and deploys it (docs/runbooks/deploy.md). This replaced a push-based GitHub Actions deploy workflow that, per facts.md, "never once succeeded" — the current mechanism is a genuine improvement, not a downgrade dressed up as one.

Gap: this is an informally documented change path (a runbook), not a formal change-approval process. CONTRIBUTING.md states that human approval is optional at "Tier 1" — the mandated review is an AI-agent /code-review run in a fresh context, not a second human reviewer, because there is no second human reviewer (facts.md, zero human committers besides Toby across all 181 commits; see RISK-002). CODEOWNERS exists in the repo but is fully commented out and routes nothing.

Commitment: this is a proportionate posture for a single-engineer team, not a permanent one. When a second engineer joins, this policy's change-management section must be revised to require human review before merge, and CODEOWNERS activated rather than left inert.

7. Configuration management (8.9) — status: partial

What exists: two real, if partial, configuration-management practices:

  • Database schema changes are sequentially-ledgered migrations (0001_init.sql through

0021+, facts.md) — every schema change is versioned, ordered, and reviewable in git history.

  • The Hetzner Docker Compose and Traefik configuration exists as versioned files in

infra/hetzner/, tracked in the same repo as the application code.

Gap: neither is managed via broader infrastructure-as-code tooling (no Terraform, no Pulumi, no drift detection). There is no automated check confirming the configuration actually running on the Hetzner host matches what's committed to infra/hetzner/ — a manual edit made directly on the host during an incident, for example, would not be caught.

8. Capacity management (8.6) — status: not_started

No capacity-management or resource-monitoring process exists for the single Hetzner host — no documented disk, CPU, or memory thresholds, and no alerting tied to them (see POL-08 §3 for the related monitoring gap). This matters concretely: the nightly pg_dump backups and the live database share the same host and, presumably, the same disk (docs/runbooks/db-recovery.md) — a disk-capacity failure could degrade both the running product and the backup mechanism at once, with no early warning today.

9. Protection against malware (8.7) — status: not_started

No anti-malware control is documented. This is primarily relevant to the founder's own laptop endpoint — the production host is a Linux container environment, where the more relevant control is 8.8 (technical vulnerability management, itself not_started; see RISK-003) and 8.19 below, not signature-based anti-malware. No dedicated endpoint-protection tooling is deployed on the founder's laptop today; OS-level baseline protections are assumed but not verified or documented.

10. Installation of software on operational systems (8.19) — status: partial

What exists: dependency versions are pinned via package.json/pnpm lockfiles across the codebase — a real, structural discipline that limits accidental drift in what code actually runs in production.

Gap: there is no documented policy restricting ad-hoc software installation directly on the operational Hetzner host outside the deploy pipeline — nothing technically prevents a manual apt install or similar during an SSH/Tailscale session, and no log of when that has or hasn't happened.

11. Forward path

The concrete next actions this policy points to, rather than commits to a fabricated timeline for:

  • Write the exposed-service inventory referenced in §3 into infra/hetzner/README.md once

its other stale hosting references are corrected (RISK-001).

  • Define the security objectives and any target metrics for capacity monitoring and

dependency scanning in GOV-08 (pending), rather than inventing a number here.

  • PRO-05 (Change & Release Management, pending per PLAN.md §4) should formalize the change

path described in §6 into a documented procedure, including what "informal" review looks like today and what triggers escalation to formal review.

  • PRO-06 (Vulnerability & Patch Management, pending) is where 8.19's software-installation

discipline and 8.7/8.8's scanning gaps get closed together, since they're the same underlying problem (nothing watches what's installed or outdated on the host).

12. Related documents

  • GOV-01 — ISMS Scope & Context
  • GOV-02 — Information Security Policy
  • POL-08 — Logging & Monitoring Policy (capacity/alerting overlap, §8 above)
  • docs/runbooks/deploy.md — the operative deploy mechanism this policy describes
  • docs/runbooks/db-recovery.md — the operative backup/recovery runbook, relevant to §8's

capacity concern

  • register/risks.yaml — RISK-001 (documentation drift), RISK-002 (segregation of duties),

RISK-003 (no SCA scanning)