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 @@
# PostgreSQL backup and restore runbook # PostgreSQL backup and restore runbook
- Status: Draft operational contract; scripts not yet implemented - Status: Approved procedure specification; scripts not yet implemented
- RPO: 24 hours - RPO: 24 hours
- RTO: two hours - RTO: two hours
- Last updated: 2026-08-20 - Last updated: 2026-08-20
@@ -51,15 +51,17 @@ The manifest stores no password, connection URL, secret value, license identifie
## Scheduled backup procedure ## Scheduled backup procedure
Run daily at `02:00 UTC` from the `ubuntu` user's reviewed host cron entry.
1. Acquire a backup lock that conflicts with restore and destructive migration but need not block ordinary application reads/writes. 1. Acquire a backup lock that conflicts with restore and destructive migration but need not block ordinary application reads/writes.
2. Resolve and verify the exact production host/database identity. Validate free disk for temporary output plus the encrypted artifact. 2. Resolve and verify the exact production host/database identity. Validate free disk for temporary output plus the encrypted artifact.
3. Confirm PostgreSQL health, client/server major compatibility, active three-hash release id, and migration version. 3. Confirm PostgreSQL health, client/server major compatibility, active three-hash release id, and migration version.
4. Create a consistent logical dump using PostgreSQL 18 `pg_dump` custom or directory format. Capture stderr and fail on a non-zero exit. 4. Create a consistent PostgreSQL 18 plain-SQL dump using Qlockify's reviewed `pg_dump --format=plain --clean --if-exists --no-owner --no-privileges` pattern. Capture stderr and fail on a non-zero exit.
5. Inspect the archive table of contents with `pg_restore --list`; reject an empty/truncated/unreadable archive. 5. Reject an empty output, verify the expected PostgreSQL dump header/format, and calculate its SHA-256.
6. Build the non-secret manifest and calculate the plaintext archive SHA-256. 6. Build the non-secret manifest and calculate the plaintext archive SHA-256.
7. Encrypt the package before moving it into durable local retention. The approved implementation uses authenticated encryption/KDF parameters recorded by `encryption_scheme_version`. 7. Encrypt the package before durable local retention with Qlockify's `openssl enc -aes-256-cbc -salt -pbkdf2 -iter 200000` flow. The manifest records the scheme/version and SHA-256 integrity evidence because CBC is not authenticated encryption.
8. Upload over TLS to the private S3-compatible destination. Upload to a temporary object key, verify remote size/checksum/metadata, then finalize the immutable timestamped key. 8. Upload over TLS to the private S3-compatible destination. Upload to a temporary object key, verify remote size/checksum/metadata, then finalize the immutable timestamped key.
9. Mark the backup successful only after remote verification. Export success timestamp/duration/size metrics. 9. Mark the backup successful only after remote verification. Record success timestamp, duration, and size in structured MVP evidence; export the same signals as metrics in the required post-MVP observability phase.
10. Retain the newest three verified encrypted local artifacts and seven verified encrypted remote artifacts. Delete stale artifacts only after the new remote object is verified; never delete the last known-good local or remote backup. 10. Retain the newest three verified encrypted local artifacts and seven verified encrypted remote artifacts. Delete stale artifacts only after the new remote object is verified; never delete the last known-good local or remote backup.
11. Release the lock and ensure temporary plaintext files are removed from the exact temporary directory. 11. Release the lock and ensure temporary plaintext files are removed from the exact temporary directory.
@@ -71,7 +73,7 @@ A cleanup failure is a ticket, not a reason to mark the backup failed if the ver
- archive/manifest/encrypted-object checksums and sizes are non-zero and agree - archive/manifest/encrypted-object checksums and sizes are non-zero and agree
- retention counts are at least one and no greater than approved limits after cleanup - retention counts are at least one and no greater than approved limits after cleanup
- backup duration/size are within expected bounds or investigated - backup duration/size are within expected bounds or investigated
- no excluded filenames/classes appear in the archive listing - no excluded filenames/classes appear in the package-content inspection
- local backup directory and remote bucket are not publicly readable - local backup directory and remote bucket are not publicly readable
- decryption key is retrievable from separate escrow by authorized operators without exposing it in the check record - decryption key is retrievable from separate escrow by authorized operators without exposing it in the check record
@@ -82,10 +84,10 @@ Run monthly and before any approved destructive migration.
1. Select an explicit backup id, not an unexamined implicit `latest`, and record why it was chosen. 1. Select an explicit backup id, not an unexamined implicit `latest`, and record why it was chosen.
2. Provision an empty non-production destination with compatible PostgreSQL and enough free space. Confirm its environment/hostname/database do not match production. 2. Provision an empty non-production destination with compatible PostgreSQL and enough free space. Confirm its environment/hostname/database do not match production.
3. Download the exact encrypted object to a new restrictive temporary directory. 3. Download the exact encrypted object to a new restrictive temporary directory.
4. Verify remote-object checksum/size, decrypt, validate package and archive checksum, parse the manifest, and inspect `pg_restore --list`. 4. Verify remote-object checksum/size, decrypt, validate the package and plain-SQL checksum/header, and parse the manifest.
5. Treat the dump as trusted code only if it came from the approved bucket/path and passes provenance checks. PostgreSQL warns that restore executes statements controlled by the dump source. 5. Treat the dump as trusted code only if it came from the approved bucket/path and passes provenance checks. PostgreSQL warns that restore executes statements controlled by the dump source.
6. Create a new empty database from `template0` or the equivalent approved empty-cluster procedure. Do not reuse a partially populated database. 6. Create a new empty database from `template0` or the equivalent approved empty-cluster procedure. Do not reuse a partially populated database.
7. Restore with error-stop semantics and reviewed owner/privilege behavior. Preserve complete stdout/stderr in a restricted evidence path without secrets. 7. Restore with `psql -X --set ON_ERROR_STOP=on` and the reviewed transaction/owner/privilege behavior. Preserve complete stdout/stderr in a restricted evidence path without secrets.
8. Check out/build the selected release record if needed, then run its validation suite: 8. Check out/build the selected release record if needed, then run its validation suite:
- expected migration/import manifests - expected migration/import manifests
- representative table/session/attempt counts and invariants - representative table/session/attempt counts and invariants
@@ -109,7 +111,7 @@ Production restore is destructive/exclusive and always requires explicit human a
- approved incident/change record and named operator - approved incident/change record and named operator
- current-state backup when PostgreSQL is readable and doing so will not worsen the incident - current-state backup when PostgreSQL is readable and doing so will not worsen the incident
If any input is absent or mismatched, exit before stopping writers, dropping/renaming a database, extracting over a target, or running `pg_restore`. If any input is absent or mismatched, exit before stopping writers, dropping/renaming a database, extracting over a target, or running `psql`.
## Production restore sequence ## Production restore sequence
@@ -131,7 +133,7 @@ Do not automatically restore `.env`/secrets, TLS, Redis, or frontend assets from
| Failure | Action | | Failure | Action |
|---|---| |---|---|
| Backup lock already held | Exit without starting a second backup; alert if stale | | Backup lock already held | Exit without starting a second backup; alert if stale |
| `pg_dump` non-zero or unreadable listing | Delete only the exact failed temporary artifact; keep prior backups; page on age threshold | | `pg_dump` non-zero, empty, or invalid plain-SQL output | Delete only the exact failed temporary artifact; keep prior backups; page on age threshold |
| Encryption failure | Do not persist/upload plaintext; secure exact temp path and investigate | | Encryption failure | Do not persist/upload plaintext; secure exact temp path and investigate |
| Remote upload/checksum failure | Keep encrypted local artifact and all prior remote copies; bounded retry; alert | | Remote upload/checksum failure | Keep encrypted local artifact and all prior remote copies; bounded retry; alert |
| Retention cleanup failure | Keep extra copies; ticket; never broaden deletion globs/paths | | Retention cleanup failure | Keep extra copies; ticket; never broaden deletion globs/paths |

View File

@@ -1,6 +1,6 @@
# First-server bootstrap runbook # 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` - Target: `zarinpal.amiirkhl.ir`
- Owner: Platform operator - Owner: Platform operator
- Last updated: 2026-08-20 - 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: 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 - 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 - operator names, maintenance window, rollback/abandonment condition, and emergency contact
- DNS zone owner and current `A`/`AAAA`/CAA records for `zarinpal.amiirkhl.ir` - DNS zone owner and current `A`/`AAAA`/CAA records for `zarinpal.amiirkhl.ir`
- SSH source allowlist and at least two tested operator public keys - 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 - approved deployment, backend, and frontend repository URLs and expected nested paths
- object-storage bucket/endpoint, retention capability, backup credentials, and separately escrowed encryption key - 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 - 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 - 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 ## 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. 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. 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. 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 ```text
/srv/zarinpal-analytics/ deployment control plane and release records /home/ubuntu/zarinpal-analytics-deployment/ deployment repository and root .env
/etc/zarinpal-analytics/config/ protected non-secret environment configuration /home/ubuntu/zarinpal-analytics-deployment/backend/ ignored backend checkout and backend .env
/etc/zarinpal-analytics/secrets/ runtime secret files, mode 0600 or stricter /home/ubuntu/zarinpal-analytics-deployment/frontend/ ignored frontend checkout and frontend .env
/var/lib/zarinpal-analytics/ host-managed operational state if required /home/ubuntu/zarinpal-analytics-deployment/releases/ ignored non-secret release records
/var/backups/zarinpal-analytics/ encrypted local backup retention only /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. 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. 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 ## 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. 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. Add only the dedicated trusted deploy user to the `docker` group. Record that this access is effectively root-equivalent and restrict the account accordingly. 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. 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. 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. 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. 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 ## 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. 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. Clone the deployment repository into the approved deployment root, then clone backend and frontend into its exact ignored nested paths. 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. 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. 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. Fetch each repository and confirm it can fast-forward cleanly to `origin/main`; do not reset, build, or start yet. 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. 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. 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 ## 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. 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. 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. 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. 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. 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. 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. 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. Run the one-off migration from the recorded backend checkout/build. Confirm exit zero and the exact expected migration version. 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. Start backend and worker. Verify liveness/readiness, worker heartbeat, Asynq `v0.26.0` compatibility, queue limits, and absence of unintended published ports. 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. 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. 5. Verify backend and worker liveness/readiness, worker heartbeat, Asynq `v0.26.0` compatibility, queue limits, and absence of unintended published ports.
6. Start/reload Nginx and run public generic health, canonical redirect, security-header, login-page, and authenticated smoke checks. 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 MVP operator checks: Compose health/restarts, structured logs, disk, certificate, queue/Redis state, and backup destination access. 7. Verify Nginx public generic health, canonical redirect, security headers, login page, SSE behavior, and that only 80/443 are public.
8. Record the active three-hash release only after all postflight checks pass. 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. 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. 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.
1. Copy or mount the approved challenge archive to an absolute operator-owned path outside Git. Validate path, owner, permissions, size, and SHA-256. 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.
2. Confirm no migration, restore, backup, or import is active. Acquire the import lock. 4. Archive only safe non-sensitive release/import evidence; exclude the CSV, raw rows, reports containing restricted values, and credentials.
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.
Never add the source archive or credentials to Git, a container image, backup package, or diagnostic bundle. Never add the source archive or credentials to Git, a container image, backup package, or diagnostic bundle.
## Phase 8: backup and handoff gate ## Phase 8: backup and handoff gate
1. Run the first encrypted logical PostgreSQL backup. 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. 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. 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. 5. Record current release, DNS, certificate, backup id, achieved RPO/RTO, remaining exceptions, and on-call ownership.