docs: approve platform operations runbooks

This commit is contained in:
2026-08-20 18:54:59 +03:30
parent e93eca1384
commit 83d2e13d14
2 changed files with 48 additions and 46 deletions

View File

@@ -1,6 +1,6 @@
# First-server bootstrap runbook
- Status: Draft operational procedure; commands/artifacts not yet implemented
- Status: Approved procedure specification; commands/artifacts not yet implemented
- Target: `zarinpal.amiirkhl.ir`
- Owner: Platform operator
- Last updated: 2026-08-20
@@ -12,17 +12,17 @@ This is the safe order for preparing a new production host. It is intentionally
Before connecting to the host, record in the change ticket/release record:
- server provider, instance id, public IPv4/IPv6, region, CPU, RAM, disk type/size, and console/recovery access
- approved OS image and checksum/source; initial target is a supported Ubuntu LTS minimal image
- Ubuntu 24.04 LTS image/source for the new 2-core/2-GB server, with Docker already installed and the `ubuntu` user available
- operator names, maintenance window, rollback/abandonment condition, and emergency contact
- DNS zone owner and current `A`/`AAAA`/CAA records for `zarinpal.amiirkhl.ir`
- SSH source allowlist and at least two tested operator public keys
- dedicated deployment username that will receive Docker-group access
- confirmation that the trusted `ubuntu` user has the required Docker-group access
- approved deployment, backend, and frontend repository URLs and expected nested paths
- object-storage bucket/endpoint, retention capability, backup credentials, and separately escrowed encryption key
- target `main` branches and an empty location for the first three-hash release record
- secret inventory and owner; never put values in the ticket, shell history, or this repository
Do not change DNS before the host passes local and direct-IP validation. Do not reuse Qlockify credentials, certificates, volumes, backup paths, or deploy keys.
Do not change DNS before the host passes local and direct-IP validation. Do not reuse Qlockify credentials, certificates, volumes, backup paths, or Git credentials.
## Phase 1: establish recoverable access
@@ -42,37 +42,38 @@ Evidence: OS release/kernel, update result, time-sync state, two successful key
2. Configure default-deny inbound firewall rules. Initially allow SSH from the operator allowlist and TCP 80/443; verify from both an allowed and disallowed external source.
3. Enable unattended security updates with a documented reboot policy and alert path.
4. Configure disk/inode monitoring, time sync, audit/system logs, log rotation, and OOM visibility before application deployment.
5. Create dedicated paths with explicit ownership:
5. Create the approved Qlockify-style runtime paths with explicit `ubuntu` ownership:
```text
/srv/zarinpal-analytics/ deployment control plane and release records
/etc/zarinpal-analytics/config/ protected non-secret environment configuration
/etc/zarinpal-analytics/secrets/ runtime secret files, mode 0600 or stricter
/var/lib/zarinpal-analytics/ host-managed operational state if required
/var/backups/zarinpal-analytics/ encrypted local backup retention only
/home/ubuntu/zarinpal-analytics-deployment/ deployment repository and root .env
/home/ubuntu/zarinpal-analytics-deployment/backend/ ignored backend checkout and backend .env
/home/ubuntu/zarinpal-analytics-deployment/frontend/ ignored frontend checkout and frontend .env
/home/ubuntu/zarinpal-analytics-deployment/releases/ ignored non-secret release records
/home/ubuntu/zarinpal-analytics-deployment/backups/ ignored encrypted local backup retention
```
6. Verify every resolved absolute path is on the intended filesystem, not a symlink to a broad/unexpected target, before applying ownership or any future recursive operation.
7. Reserve enough free space for PostgreSQL growth, one temporary logical dump, three encrypted local backups, container images, and logs while retaining the platform alert floor of at least 20% and 10 GiB free.
8. Create and enable the approved 2 GB swap file, use restrictive permissions, persist it in `/etc/fstab`, and verify it after reboot.
## Phase 3: install and constrain Docker
1. Install Docker Engine and the Compose plugin only from Docker's approved repository for the selected OS; pin/record installed versions.
2. Add only the dedicated trusted deploy user to the `docker` group. Record that this access is effectively root-equivalent and restrict the account accordingly.
1. Verify the preinstalled Docker Engine and Compose plugin came from Docker's approved Ubuntu repository; update/install only if verification fails, then record installed versions.
2. Confirm only the trusted `ubuntu` user has the required `docker` group access. Record that this access is effectively root-equivalent and restrict the account accordingly.
3. Configure the daemon's bounded logging policy before starting application containers.
4. Ensure Docker starts after reboot and that no unauthenticated TCP daemon socket is enabled.
5. Pull a harmless pinned public test image, verify its tag/digest, run it without published ports, then remove the test container.
6. Confirm the deploy user can run Docker Compose without using a broader shared administrator account.
6. Confirm the `ubuntu` user can run Docker Compose without using a broader shared administrator account.
Evidence: Engine/Compose versions, daemon security configuration, socket/group ownership, deploy-user membership, log driver/options, and test-image result.
## Phase 4: install the deployment control plane
1. Create one server Git identity/key with read-only access to all three repositories and no write/push permission.
2. Clone the deployment repository into the approved deployment root, then clone backend and frontend into its exact ignored nested paths.
1. From the `ubuntu` account, clone the deployment repository into `/home/ubuntu/zarinpal-analytics-deployment`, then clone backend and frontend into its exact ignored nested paths. Enter the source-forge HTTPS username/password interactively; do not configure a credential helper, token, or deploy key.
2. Confirm no Git credential was persisted in repository configuration, shell scripts, environment files, or the operating-system credential store.
3. Validate ownership, remote URL, `main` branch, current SHA, clean status, and independent `.git` directory for every repository.
4. Materialize protected non-secret configuration and secret files outside the checkout. Set owner/mode before writing secret values; verify consumers receive only required files.
5. Fetch each repository and confirm it can fast-forward cleanly to `origin/main`; do not reset, build, or start yet.
4. Create the Qlockify-style Git-ignored environment files: deployment-root `.env` plus nested backend/frontend `.env`, all owned by `ubuntu`, mode 0600, and excluded from backup. Verify consumers receive only required values.
5. Manually run `git pull --ff-only origin main` in each repository, entering HTTPS credentials interactively, and confirm each checkout equals `origin/main`; do not reset, build, or start yet.
6. Record the three initial commit hashes and selected infrastructure image tags in a non-secret draft release record.
7. Render/validate Compose configuration without printing secret values. Confirm only Nginx publishes 80/443.
@@ -81,7 +82,7 @@ Do not run `docker compose up`, migrations, import, credential generation, resto
## Phase 5: DNS and TLS staging
1. Lower DNS TTL in advance if an existing record is moving. Preserve the previous values and rollback time.
2. Point `A`/`AAAA` only after direct-IP Nginx/host validation succeeds. Remove an `AAAA` record if IPv6 is not actually routed and firewalled correctly.
2. Point `A` (and `AAAA` only if IPv6 is actually routed/firewalled) for `zarinpal.amiirkhl.ir` after direct-IP Nginx/host validation. No `www` or alternate hostname is configured.
3. Verify public authoritative DNS from multiple resolvers and confirm the address matches the current server.
4. Start only the minimum edge services required for ACME HTTP-01. Verify the challenge path from an external network.
5. Obtain a staging certificate first and exercise renewal. Then request the production certificate.
@@ -90,36 +91,35 @@ Do not run `docker compose up`, migrations, import, credential generation, resto
If issuance fails, stop repeated production requests, inspect DNS/CAA/firewall/challenge routing, and continue serving no plaintext application content.
## Phase 6: first database and application release
## Phase 6: first database, dataset, and application release
1. Acquire the production deployment lock and record the operator/release id.
2. Start PostgreSQL and Redis only. Verify named-volume targets, authentication, resource limits, and health.
3. Run the one-off migration from the recorded backend checkout/build. Confirm exit zero and the exact expected migration version.
4. Start backend and worker. Verify liveness/readiness, worker heartbeat, Asynq `v0.26.0` compatibility, queue limits, and absence of unintended published ports.
5. Start frontend and verify the static sentinel plus the emitted Iran Yekan font resource. The source font/license remain frontend-owned; no deployment-side injection or copy occurs.
6. Start/reload Nginx and run public generic health, canonical redirect, security-header, login-page, and authenticated smoke checks.
7. Verify MVP operator checks: Compose health/restarts, structured logs, disk, certificate, queue/Redis state, and backup destination access.
8. Record the active three-hash release only after all postflight checks pass.
2. Place the approved `challenge_data.csv.gz` at the exact Git-ignored deployment `data/incoming/` path. Verify regular-file/read-only properties, size, and pinned SHA-256; do not copy it into an image.
3. Invoke tracked `scripts/first-start.sh`. It starts PostgreSQL and Redis, waits with bounded backoff, applies the one-off migration, runs the one-shot importer with a read-only mount, and reconciles the pinned counts before starting public services.
4. Review the JSON/Markdown import report and durable import-run/issues metadata. Fatal validation, resource, migration, or reconciliation failure must exit nonzero before backend/frontend/Nginx activation.
5. Verify backend and worker liveness/readiness, worker heartbeat, Asynq `v0.26.0` compatibility, queue limits, and absence of unintended published ports.
6. Verify the frontend static sentinel plus the emitted Iran Yekan font resource. The source font/license remain frontend-owned; no deployment-side injection or copy occurs.
7. Verify Nginx public generic health, canonical redirect, security headers, login page, SSE behavior, and that only 80/443 are public.
8. Run demo credential generation as a separate explicit administrative job. Store one-time plaintext output outside Git/logs/backups.
9. Authenticate as at least two merchant accounts, verify different dashboards and cross-tenant isolation, then remove or escrow plaintext credential output.
10. Verify MVP operator checks: Compose health/restarts, structured logs, disk, certificate, queue/Redis state, and backup destination access.
11. Record the active three-hash release only after all postflight checks pass.
Failure before the active-release switch restores the prior/empty known state. Do not improvise a database down migration.
## Phase 7: one-time data and demo setup
## Phase 7: bootstrap evidence review
This phase is deliberately separate from server/application bootstrap.
1. Copy or mount the approved challenge archive to an absolute operator-owned path outside Git. Validate path, owner, permissions, size, and SHA-256.
2. Confirm no migration, restore, backup, or import is active. Acquire the import lock.
3. Invoke the explicit one-off import service with a read-only mount of exactly that file.
4. Review import status, checksum, source/accepted/rejected counts, quality report, and reconciliation before declaring the dataset ready.
5. Run demo credential generation as a separate explicit administrative job. Select a secure one-time output destination outside the repository and Compose logs.
6. Test at least two merchant accounts and cross-tenant isolation. Remove or escrow plaintext credential output according to the approved operator procedure.
1. Confirm the sole completed import run matches the pinned checksum, schema, source/session/attempt/merchant/terminal/category counts, and contains no fatal issue.
2. Confirm no raw staging remains and the ordinary `docker compose up -d` path cannot invoke migration/import/admin profiles.
3. Run `first-start.sh` a second time only in the controlled acceptance rehearsal and confirm the identical dataset is a safe no-op rather than another promotion.
4. Archive only safe non-sensitive release/import evidence; exclude the CSV, raw rows, reports containing restricted values, and credentials.
Never add the source archive or credentials to Git, a container image, backup package, or diagnostic bundle.
## Phase 8: backup and handoff gate
1. Run the first encrypted logical PostgreSQL backup.
2. Verify archive listing/checksum, encrypted local artifact, remote upload, remote checksum, manifest, and retention counts.
2. Verify package contents, plain-SQL header/checksum, encrypted local artifact, remote upload, remote checksum, manifest, and retention counts.
3. Restore that backup to a clean non-production destination and complete application/tenant/analytics validation within the two-hour RTO.
4. Perform and record the MVP manual certificate, disk, backup-age, availability, API error, SSE heartbeat, and queue-age checks.
5. Record current release, DNS, certificate, backup id, achieved RPO/RTO, remaining exceptions, and on-call ownership.