Stage1: безопасный импорт VK, PostgreSQL и production deployment #4

Open
ochenstarik wants to merge 7 commits from codex/stage1-completion into main
16 changed files with 620 additions and 116 deletions
Showing only changes of commit ca28be6ae6 - Show all commits

View file

@ -0,0 +1,62 @@
# Independent rereview — cce70c0
Base SHA: `17faecd962c2cf40e0a3f6ec4434811cfffdd312`
Reviewed SHA: `cce70c0b8feb263cc9209332c8c50a44bc97428a`
Result SHA: `cce70c0b8feb263cc9209332c8c50a44bc97428a` (review only; no source changes)
## Prior findings and status
### auditor1 MEDIUM — live smoke could accept mock-provider data: CLOSED at this SHA
The previous reproduction set `USE_VK_MOCK=true` in the production environment while a real service token existed, then omitted the optional restricted-post case. This no longer yields live-smoke evidence:
- [`ProviderFactory`](/srv/projects/Agent%20projects/randomayzer/src/providers/factory.ts:10) now throws in `NODE_ENV=production` when the mock flag is true, before returning the mock provider. The production Compose service also sets `USE_VK_MOCK: "false"` explicitly at [compose.production.yml](/srv/projects/Agent%20projects/randomayzer/compose.production.yml:33), overriding the env-file value.
- The preview and import responses report `providerMode` from the actual `VkProvider` instance at [preview route](/srv/projects/Agent%20projects/randomayzer/src/app/api/posts/preview/route.ts:32) and [participants route](/srv/projects/Agent%20projects/randomayzer/src/app/api/giveaways/%5Bid%5D/participants/route.ts:99). The token is not included in that field or response.
- The smoke script requires `success` and `providerMode === LIVE` for preview; G/H additionally require a safe integer `totalCount > 0` at [vk-smoke.sh](/srv/projects/Agent%20projects/randomayzer/tools/vk-smoke.sh:94). Thus MOCK and empty imports are failures.
- The regression suite passed: production mock refusal even with a service token; mock instance reports MOCK despite SERVICE auth metadata; real provider reports LIVE without exposing a marker token; and actual shell smoke subprocess fixtures reject MOCK and zero-count imports while accepting LIVE nonempty imports. The tests also assert the cookie marker is absent from output.
Closure is limited to the false-positive code path and regression evidence. No live VK run was performed.
### auditor2 HIGH — malformed likes/comments could be silently omitted or collapsed: CLOSED at this SHA
The `VkProvider` validates both payload envelopes and each returned participant record before returning any list. Likes require complete count/items, object items, positive safe-integer IDs, valid optional profile field types, and unique user IDs across pages. Comments require complete count/items, valid profile lists and IDs, and nonzero safe-integer author IDs; negative community author IDs are explicitly excluded as nonparticipants. Malformed responses throw `VkIncompleteDataError`.
The import route does not call `GiveawayStore.updateParticipants` until provider fetch, enrichment, and filtering all complete. The focused route regression loops through 25 malformed likes/comments payloads and checks HTTP 502, `VK_INCOMPLETE_DATA`, and preservation of the prior participant list for each. A separate test confirms valid negative community comment authors are ignored. Focused suites passed 13 R3 tests including these cases.
Closure is limited to malformed-item validation and no-partial-save evidence. External VK response schemas were not verified against current official documentation or a live response.
## Remaining LOW finding
### LOW — COMMUNITY metadata still advertises admin detection without a manager-list implementation: OPEN / unchanged
[`resolveEffectiveCapabilities`](/srv/projects/Agent%20projects/randomayzer/src/providers/vk/vk-capabilities.ts:34) still sets `adminDetection: true` for COMMUNITY auth and removes the explanatory note. This commit does not change that code. The prior auditor noted that the participant import route validates against the static capability object, which keeps the current server-side exclusion guard false; the inaccurate dynamic metadata remains a preview/API contract issue and should stay tracked separately.
## New findings
CRITICAL: none.
HIGH: none in the rereviewed fixes.
MEDIUM: none in the rereviewed fixes.
LOW: the unchanged COMMUNITY capability-metadata finding above.
## Commands actually run
- Initial `npx vitest run tests/vk-smoke-provider-mode.test.ts tests/vk-r3-safety.test.ts` overlapped the coordinator's `npm ci`; Vitest failed to load PostCSS because `dlv` was temporarily unavailable. A second attempt during the same install window reported `vitest: not found`. Per coordinator instruction, these transient install-state errors were not treated as source findings.
- After the coordinator confirmed `npm ci`/Prisma generation had completed: `npx vitest run tests/vk-smoke-provider-mode.test.ts tests/vk-r3-safety.test.ts` — exit 0; 2 files, 19 tests passed (13 R3 safety, 6 provider-mode/smoke tests).
- `bash -n tools/vk-smoke.sh` — exit 0.
- `node --check tools/production-transport.cjs` — exit 0.
- `node --check tools/serve-production.cjs` — exit 0.
- `git diff --check 17faecd962c2cf40e0a3f6ec4434811cfffdd312..cce70c0b8feb263cc9209332c8c50a44bc97428a` — exit 0.
The coordinator separately reported `npm ci`, Prisma generation, and full `npm test` exit 0 with 380/380 tests at this SHA; I did not run those commands. Lint/build/audit, real PostgreSQL, and Docker remain coordinator-owned.
## UNVERIFIED and blockers
- Actual live VK behavior, consent, token scopes, and current official payload contracts remain UNVERIFIED.
- R4 production Docker startup and backup/restore drill remain unverified by this rereview.
- R5 DNS, TLS/basic-auth, live domain login, Docker-preserved peer address, firewall persistence, and internet/LAN/WireGuard isolation remain operator acceptance blockers.
- No phase closure or release readiness is claimed.
Files changed by this rereview: `agents/codex/inbox/2026-10-19-auditor1-rereview-cce70c0.md`, `agents/codex/done/2026-10-19-auditor1-rereview-cce70c0.md`, and individual memory worklog `/srv/projects/AI-Memory/01_PROJECTS/randomayzer/worklog/2026-10-19_auditor1_cce70c0-rereview.md`. No production code or tests changed; no commit/push was made. Pre-existing `AGENTS.md` modification was preserved.

View file

@ -0,0 +1,61 @@
# Auditor2 independent re-review — malformed VK data and live smoke
Reviewed SHA: `cce70c0b8feb263cc9209332c8c50a44bc97428a`
Comparison SHA: `17faecd962c2cf40e0a3f6ec4434811cfffdd312`
Base SHA: `6382cb8e0473ba9ea5ba7a3f94f34e8ab0d4ec60`
Result SHA: `cce70c0b8feb263cc9209332c8c50a44bc97428a` (review only; no code changes).
## Conclusion
The previous **HIGH** finding for malformed VK likes/comments participant data is **CLOSED** at the reviewed SHA. The implementation now rejects malformed records before the import route persists any result, and focused route regressions confirm that prior stored participants survive all included malformed cases.
The follow-up live-smoke false-success issue is also addressed: smoke checks require the actual provider marker `LIVE`; G/H additionally require a positive safe-integer participant count. Production refuses `USE_VK_MOCK=true` and the production Compose profile explicitly sets it false. Focused adversarial fixtures cover mock provider, empty live imports, and nonempty live imports.
The COMMUNITY admin capability discrepancy remains a **pre-existing, nonblocking debt**, not a finding introduced by this stage. I independently confirm that classification for current server routes: preview passes `fetchPost` only `organizerId` (no explicit COMMUNITY auth), and the resolver's automatic/service/user selection returns only SERVICE or USER. Giveaway detail derives only SERVICE/USER. Participant/snapshot enforcement reads the provider's static capability, which remains `adminDetection: false`.
No CRITICAL, HIGH, or MEDIUM findings remain from these reviewed issues. This report does not declare the phase closed.
## Re-reviewed issues and evidence
### Prior HIGH — CLOSED: malformed likes/comments responses
At [vk-provider.ts](/srv/projects/Agent%20projects/randomayzer/src/providers/vk/vk-provider.ts:34), profile records now require a plain object with a positive safe-integer user ID and correctly typed returned fields. [Response validation](/srv/projects/Agent%20projects/randomayzer/src/providers/vk/vk-provider.ts:49) requires a response object, an items array, a nonnegative safe-integer count, and object-shaped items. Likes also reject duplicate user IDs at lines 297–303. Comments reject malformed/zero/non-integer/unsafe author IDs at lines 355–359 and validate returned profile arrays and IDs.
The earlier concrete failures are blocked: a missing `id` cannot become the participant ID `"undefined"`; a string or unsafe ID is rejected; and malformed comment author IDs are no longer silently omitted while the reported count is accepted. Negative integer comment author IDs remain deliberately excluded as community authors, matching prior behavior and now covered by a focused test.
At [participants route](/srv/projects/Agent%20projects/randomayzer/src/app/api/giveaways/%5Bid%5D/participants/route.ts:86), fetch/enrichment must finish before `updateParticipants` at line 97. The route-level regression at [vk-r3-safety.test.ts](/srv/projects/Agent%20projects/randomayzer/tests/vk-r3-safety.test.ts:200) iterates 25 malformed likes/comments cases, expects `502 VK_INCOMPLETE_DATA`, and checks the previous participant list remains unchanged after each case.
### Smoke false-success — CLOSED
[ProviderFactory](/srv/projects/Agent%20projects/randomayzer/src/providers/factory.ts:10) throws in production if mock mode is explicitly enabled, even when a service token exists. Production Compose sets `USE_VK_MOCK: "false"` at [compose.production.yml](/srv/projects/Agent%20projects/randomayzer/compose.production.yml:36).
Preview and participant-import responses identify the actual provider instance as `LIVE` or `MOCK`. [vk-smoke.sh](/srv/projects/Agent%20projects/randomayzer/tools/vk-smoke.sh:94) requires `providerMode === "LIVE"`; G/H also require `totalCount` to be a safe integer greater than zero. Its test fixture proves MOCK with reported participants fails before creating/importing a giveaway, LIVE with zero participants fails G/H, and LIVE with nonempty participants passes D/G/H: [vk-smoke-provider-mode.test.ts](/srv/projects/Agent%20projects/randomayzer/tests/vk-smoke-provider-mode.test.ts:66).
### Pre-existing LOW metadata discrepancy — nonblocking, not stage1-introduced
`resolveEffectiveCapabilities` still sets `adminDetection: true` for COMMUNITY auth at [vk-capabilities.ts](/srv/projects/Agent%20projects/randomayzer/src/providers/vk/vk-capabilities.ts:34), though no manager-list lookup exists. The exact behavior is present in base SHA `6382cb8e0473ba9ea5ba7a3f94f34e8ab0d4ec60`. Current routes do not supply or resolve COMMUNITY auth: preview calls `fetchPost(url, { organizerId })`; its automatic resolver chooses SERVICE or USER; giveaway details likewise derive only SERVICE/USER. Import and snapshot checks use `provider.capabilities`, whose static `adminDetection` remains false, as enforced by [giveaway-schemas.ts](/srv/projects/Agent%20projects/randomayzer/src/core/validation/giveaway-schemas.ts:89). Thus this metadata branch is unreachable through current routes and cannot enable admin exclusion. I agree it is pre-existing nonblocking debt for this phase, not an open stage1 finding.
## Commands run by this review
- `git show -s --format='%H%n%P%n%s' cce70c0b8feb263cc9209332c8c50a44bc97428a` — confirmed fixed commit parent is `17faecd962c2cf40e0a3f6ec4434811cfffdd312`.
- `git diff 17faecd962c2cf40e0a3f6ec4434811cfffdd312 cce70c0b8feb263cc9209332c8c50a44bc97428a -- <reviewed files>` — reviewed only the immutable commit delta.
- `USE_VK_MOCK=true npx vitest run tests/vk-r3-safety.test.ts tests/vk-smoke-provider-mode.test.ts` — exit 0; 2 files, 19 tests passed (13 R3 safety, 6 smoke provider-mode).
- `bash -n tools/vk-smoke.sh` — exit 0.
- `git diff --check 17faecd962c2cf40e0a3f6ec4434811cfffdd312 cce70c0b8feb263cc9209332c8c50a44bc97428a` — exit 0; no whitespace errors.
Coordinator separately reported raw `npm test` at this SHA passed 380/380 across 63 files; I did not independently rerun the full suite, install, Prisma generation, lint, build, audit, or Docker gates. Live VK and target-host acceptance remain operator-dependent and unverified.
## UNVERIFIED / remaining blockers
- Actual live VK payload contracts and stable offset-pagination behavior remain **UNVERIFIED**; this review does not assert external VK method contracts. The existing documentation discloses that changing VK data during offset pagination cannot be proven point-in-time consistent.
- Live VK consent, real credentials, and production A–H smoke remain unverified.
- Actual VPS/WireGuard socket peer preservation, firewall persistence, TLS/basic-auth, and LAN/internet isolation require operator checks on target hosts.
- Coordinator's full lint/build/audit/Docker acceptance is outside this rereview; no phase-closure claim is made.
## Files changed by this rereview session
- `agents/codex/inbox/2026-10-19-auditor2-high-rereview.md` — task record.
- `agents/codex/done/2026-10-19-auditor2-high-rereview.md` — this report.
- `/srv/projects/AI-Memory/01_PROJECTS/randomayzer/worklog/2026-10-19_auditor2_high-rereview.md` — individual memory worklog.
No production source/tests were changed, and no commit, push, container mutation, or communication with auditor1 was made.

View file

@ -0,0 +1,48 @@
# Result — coder2 review of amended R5 proxy deployment
Base SHA: `8284eb5d006943d42133eb665c99dcbc3133d6a6`.
Reviewed/result SHA: `8284eb5d006943d42133eb665c99dcbc3133d6a6` (no commit; shared branch remains uncommitted).
## Scope
Reviewed the amended R5 assignment, Caddy 2.8 site fragment, HAProxy 3.0 additive snippet, operator runbook, and `agents/codex/notes/validate-r5-templates.sh`. I did not execute remote VPS/home-server operations, start/stop containers, modify app source/tests, or alter deployment templates.
The runbook now captures before/after responses for every listed current hostname on both HTTP and HTTPS, explicitly requires a concrete local listener address, and stops if a baseline request cannot connect. It requires a root shell for the VPS backup/Docker steps, compares active-container copies with host sources before edits, records and checks single-file mount device/inode, compares the active files with host source after edits, and uses HAProxy `-c -V` with an accurate success expectation. Existing instructions keep HTTP and TLS backend routes distinct, preserve the HTTP-01 token path, require SNI routing before catch-all, gate reload on actual process/config mechanism, and restore only the two changed configurations before rechecking all sites.
## Files changed by this review
- `agents/codex/inbox/TASK-2026-10-19-coder2-r5-review.md`
- `docs/STAGE1_DEPLOYMENT_RUNBOOK.md`
- `agents/codex/done/2026-10-19-r5-template-coder2-review.md`
The Caddy and HAProxy templates were reviewed but not edited.
## Verification
Commands I ran:
- `python3` extraction of all fenced `bash` blocks from `docs/STAGE1_DEPLOYMENT_RUNBOOK.md`, piping each block to `bash -n` — exit 0; 21 blocks parsed.
- `git diff --check -- docs/STAGE1_DEPLOYMENT_RUNBOOK.md deploy/randomayzer.Caddyfile deploy/randomayzer.haproxy.cfg deploy/randomayzer.nginx.conf` — exit 0.
- `git rev-parse HEAD` — `8284eb5d006943d42133eb665c99dcbc3133d6a6`.
Coordinator-provided validation evidence (not rerun by me because this assignment forbids container changes): `agents/codex/notes/r5-template-validation.log` records Caddy `Valid configuration`, HAProxy `Configuration file is valid`, and the local syntax validator PASS for Caddy 2.8 / HAProxy 3.0 fake fixtures. The Caddy warnings note formatting and an explicit `X-Forwarded-For` overwrite; the overwrite is intentional for this trust boundary. These fixture checks do not establish live VPS configuration compatibility.
## Findings
- CRITICAL: none.
- HIGH: none.
- MEDIUM: none.
- LOW: none open in my reviewed scope.
This is a scoped runbook review, not the independent R5 security review or live operator acceptance.
## UNVERIFIED
- Whether the actual VPS has the expected container names, exact Caddy/HAProxy patch versions, mounts, includes, listener addresses, routing modes, Caddy admin endpoint, and HAProxy graceful reload mechanism.
- Whether all active hostnames and their expected baseline results are correctly enumerated by the operator before change.
- Whether `10.77.0.1` is the confirmed VPS peer, the DNS record points to the VPS, HTTP-01 reaches Caddy unchanged, and the home firewall rules persist safely.
- Certificate issuance, Basic Auth response, domain VK login, and other-site behavior after a real reload.
## Remaining blockers / non-blocking tech debt
R5 cannot be accepted until the operator verifies the unknown topology and reload mechanisms, executes the documented backups/validation, and records successful before/after checks for every existing hostname. The parent-run local fixture validator assembles routes before its fixture catch-all to syntax-check them; it does not test ordering in the actual HAProxy config, which must be confirmed during operator inspection and insertion.

View file

@ -0,0 +1,33 @@
# Result — R5 runbook for existing proxy containers
Base SHA: `8284eb5d006943d42133eb665c99dcbc3133d6a6`
Result SHA: `8284eb5d006943d42133eb665c99dcbc3133d6a6` (working-tree edits only; no commit)
## Files changed
- `deploy/randomayzer.Caddyfile` — Caddy 2.8 site snippet, Basic Auth fallback, 1 MB request body limit, no access log, safe header overrides, no second proxy/listener.
- `deploy/randomayzer.haproxy.cfg` — additive SNI/HTTP routing examples with separate TLS and cleartext Caddy backends and explicit preservation of existing routes/default behavior.
- `deploy/randomayzer.nginx.conf` — removed obsolete nginx alternative, which does not match the confirmed Docker Caddy/HAProxy setup.
- `docs/STAGE1_DEPLOYMENT_RUNBOOK.md` — R5 operator procedure for actual container topology, redacted inspection, private backups, in-place source updates and inode/hash checks, pre/post validation, no-restart reload gates, other-site baselines, external checks, and rollback. Historical R4 loopback steps remain in place.
- `agents/codex/inbox/2026-10-19-r5-vps-container-runbook.md`
- `agents/codex/notes/2026-10-19-r5-vps-container-runbook.md`
- `agents/codex/done/2026-10-19-r5-vps-container-runbook.md`
## Verification actually run
- `docker run --rm --entrypoint caddy caddy:2.8-alpine version` — exit 0, local image reports `v2.8.4`.
- `docker run --rm --entrypoint haproxy haproxy:3.0-alpine -vv` — exit 0, local image reports `3.0.30-47d806b`.
- `docker run --rm -i --entrypoint caddy caddy:2.8-alpine validate --config /dev/stdin --adapter caddyfile` with the proposed site block and an artificial bcrypt hash — exit 0, `Valid configuration`; formatting and explicit X-Forwarded-For warnings only.
- `python3` temporary-fixture harness running `docker run --rm -v <tempdir>:/tmp/config:ro --entrypoint haproxy haproxy:3.0-alpine -c -f /tmp/config/haproxy.cfg` — exit 0. The fixture includes TCP SNI routing plus a distinct HTTP/cleartext backend; it is not the unknown active VPS config.
- `git diff --check -- docs/STAGE1_DEPLOYMENT_RUNBOOK.md deploy/randomayzer.Caddyfile deploy/randomayzer.nginx.conf` — exit 0.
- Python trailing-whitespace scan on the new HAProxy example and task inbox — no trailing whitespace.
Parent separately reported target-image validation of these templates; evidence files are under `agents/codex/notes/r5-*`.
No npm, Prisma or application test/lint/build gates were run for this documentation/config-template task. No remote machine was accessed or changed.
Findings: no new CRITICAL/HIGH/MEDIUM/LOW finding from these local template checks. The runbook intentionally leaves live R5 blocked until actual inspection confirms the host source/config paths, active SNI/HTTP routing, safe HAProxy reload mechanism, Caddy admin endpoint, correct DNS and WG peer. No HAProxy signal/restart is guessed.
UNVERIFIED: exact deployed patch versions; actual HAProxy topology/backend names/master-worker/control socket/reload procedure; active mounted config source paths and inode semantics; port-80 path to Caddy; whether Authelia policy is usable for this vhost; DNS target and peer identity; live issuance/renewal, Basic Auth/VK callback, other-site health and network isolation. No production claim is made.
Remaining blockers: operator/owner must perform the gated inspection and live acceptance. Non-blocking tech debt: none identified in this scoped task.

View file

@ -0,0 +1,7 @@
# Task
Base SHA: 17faecd962c2cf40e0a3f6ec4434811cfffdd312
Reviewed SHA: cce70c0b8feb263cc9209332c8c50a44bc97428a
Scope: Independent rereview of the fixes for the auditor1 smoke-provider finding and auditor2 malformed VK items finding, against the immutable fix commit.
Do not change: Production code/tests or prior review reports; no commit/push, no containers, no delegation. Preserve pre-existing AGENTS.md modification.
Definition of Done: Independently trace the fixes and regression tests; classify prior findings CLOSED or OPEN only with source/test evidence; record any new findings, blockers, UNVERIFIED claims, exact SHA and commands.
Required verification: Focused tests only; root owns final full gates.

View file

@ -0,0 +1,3 @@
# Auditor2 rereview — malformed VK participant data fix
Review immutable commit `cce70c0b8feb263cc9209332c8c50a44bc97428a` against prior reviewed commit `17faecd962c2cf40e0a3f6ec4434811cfffdd312`. Reassess auditor2 HIGH finding about malformed likes/comments records and partial participant persistence; inspect/reproduce focused regression tests. Independently assess whether COMMUNITY admin capability metadata discrepancy is pre-existing and nonblocking under current routes/server guards. Read-only: no production/test edits, commit/push, or container mutation. Do not communicate conclusions with auditor1 before report freeze. Write separate report under agents/codex/done and individual memory worklog; exact reviewed SHA, evidence, commands, severity and CLOSED/OPEN status. Do not declare phase closed; root owns final gates and Docker verification.

View file

@ -0,0 +1,7 @@
# Assignment — R5 runbook for the actual VPS proxy containers
Base SHA: `8284eb5d006943d42133eb665c99dcbc3133d6a6`
Scope: only deploy templates and `docs/STAGE1_DEPLOYMENT_RUNBOOK.md`. Update the R5 procedure for the reported target containers `krona-proxy-mux` (`haproxy:3.0-alpine`) and `krona-reverse-proxy` (`caddy:2.8-alpine`). Add safe in-place additive SNI and site examples, preserve unrelated sites/default routes and the HTTP-01 path, include private backups, version-appropriate validation, non-restart reload only after inspection confirms the actual mechanism, before/after other-site baselines, and a concrete rollback. Use Basic Auth as the supported fallback; Authelia remains optional only if the existing policy is verified. Unknown mount/config paths, routing mode, reload mechanism, DNS/peer identity and live acceptance remain `UNVERIFIED` and must stop the operator until observed. No real remote changes.
Do not modify application source/tests, existing review reports, compose/auth/deploy app settings, unrelated services or existing user changes. No commit/delegation. Record result and evidence in this agent's `agents/codex/{notes,done}`.

View file

@ -0,0 +1,6 @@
# Task
Base SHA: 8284eb5d006943d42133eb665c99dcbc3133d6a6
Scope: Review the amended R5 Caddy 2.8 / HAProxy 3.0 templates, operator runbook, and reproducible local syntax validator. Fix concrete defects only in the R5 runbook/templates and write an individual review report.
Do not change: application source/tests, compose/auth settings, prior review reports, AGENTS.md, existing unrelated services, remote VPS/home-server state, containers, commits, or delegation.
Definition of Done: Templates/runbook provide safe backups, source and active mount checks, correct separate TLS 443 and HTTP 80 routes, HTTP-01 preservation, safe no-restart validation/reload gates, before/after checks for every existing website, and rollback; unknown facts remain operator blockers.
Required verification: review validator evidence, run read-only syntax/diff checks, record exact base/result SHA, changes, findings, UNVERIFIED items, and blockers.

View file

@ -6,3 +6,5 @@
stage1-baseline-schema.prisma stage1-baseline-schema.prisma
backups/ backups/
r5-fixtures/

View file

@ -0,0 +1,15 @@
# Notes — R5 Docker proxy runbook
Base SHA: `8284eb5d006943d42133eb665c99dcbc3133d6a6`. No commit made; current HEAD remains unchanged.
Scope stayed limited to proxy templates and the deployment runbook. Updated R5 to the reported proxy containers (`krona-proxy-mux`, HAProxy 3.0; `krona-reverse-proxy`, Caddy 2.8), with inspections of command/mounts that omit environment, config backup, same-inode edits/checks, actual config validation, reload gates, host/passthrough and cleartext backends kept separate, other-site before/after baselines, Basic Auth only, and rollback. Exact VPS paths, HAProxy routing/process model/reload, Caddy admin endpoint, DNS and peer identity remain gated UNVERIFIED facts.
Local image checks run:
- `docker run --rm --entrypoint caddy caddy:2.8-alpine version` — `v2.8.4`.
- `docker run --rm --entrypoint haproxy haproxy:3.0-alpine -vv` — `3.0.30-47d806b`.
- Caddy 2.8.4 `validate --config /dev/stdin --adapter caddyfile` against the proposed site block with an artificial hash — exit 0 / `Valid configuration`; two warnings were formatting and the explicit X-Forwarded-For override.
- HAProxy 3.0.30 `-c -f` against a temporary assembled mode-tcp SNI frontend plus separate mode-http/cleartext frontend fixture — exit 0. This fixture is not the VPS active config.
- `git diff --check -- docs/STAGE1_DEPLOYMENT_RUNBOOK.md deploy/randomayzer.Caddyfile deploy/randomayzer.nginx.conf` — exit 0.
- Python trailing-whitespace scan on the new HAProxy snippet and inbox assignment — no trailing whitespace.
No remote VPS/home-server changes, npm/app gates, Docker production stack changes, or actual TLS/VK acceptance were performed. Parent independently reported that it validated the submitted Caddy snippet and assembled HAProxy snippets with the cached target images; see `agents/codex/notes/r5-*`.

View file

@ -37,3 +37,17 @@ Independent review cycle 1 (immutable 17faecd962c2cf40e0a3f6ec4434811cfffdd312):
- auditor2 HIGH: malformed likes/comments accepted or silently omitted before complete save; coder1 assigned strict element validation + route preservation PoC regressions. - auditor2 HIGH: malformed likes/comments accepted or silently omitted before complete save; coder1 assigned strict element validation + route preservation PoC regressions.
- auditor2 LOW: COMMUNITY effective adminDetection metadata remains true although no manager implementation. This predates our patch; actual static server validation disables manager exclusion, route currently uses SERVICE/USER. Classify as non-blocking pre-existing debt, do not change old tests/expand scope silently. - auditor2 LOW: COMMUNITY effective adminDetection metadata remains true although no manager implementation. This predates our patch; actual static server validation disables manager exclusion, route currently uses SERVICE/USER. Classify as non-blocking pre-existing debt, do not change old tests/expand scope silently.
- Final gates at 17faecd: npm ci/generate/test372/372/tsc/lint(0errors6warnings)/build/audit0 all exit0. Fresh DB randomayzer_stage1_final migration3 applied, integration12/12, schema diff0. Follow-up fixes require new gates/rereview; prior passes do not close findings. - Final gates at 17faecd: npm ci/generate/test372/372/tsc/lint(0errors6warnings)/build/audit0 all exit0. Fresh DB randomayzer_stage1_final migration3 applied, integration12/12, schema diff0. Follow-up fixes require new gates/rereview; prior passes do not close findings.
2026-10-19 owner amendment fetched during final verification:
- Forgejo main advanced to52fb7587d7dda5ae3d1c5e9ecef96b7f48d147a3; GitHub main still6382cb8 at fetch.
- Only change: R5 task now confirms containerized Caddy2.8 krona-reverse-proxy and HAProxy3.0 krona-proxy-mux, requires backups/validation/reload without restart and other-site checks.
- Merged updated manifest into our branch, HEAD8284eb5d006943d42133eb665c99dcbc3133d6a6; production code unchanged from independently re-reviewed cce70c0.
- Coder1 preparing updated R5 templates/runbook; coder2 and independent review to follow. No VPS changes.
- Final cce70c0 production image sha256:8d6a8b2250cf813d078e24dfc79c743be43ec86533a3d24af87ac9d1ab4ed274 up healthy; actual loopback3100 binding, DBports{}, users/limits confirmed. Repeated HTTP PKCE/canonical callback and nonempty backup clean-restore passed. Deleted only our final fake giveaway/user marker after drill.
Final verification cleanup:
- Final production and integration test stacks stopped with their respective compose down commands.
- Before teardown, production test DB counts were User0/Giveaway0/UserCredential0/Session0; volume creation timestamp and project label proved it was created in this session with FAKE credentials.
- Removed only those two owned throwaway test volumes (randomayzer-database, randomayzer-integration-pgdata) after checking their Compose project labels. This prevents a future real deployment inheriting the fake bootstrap database password. No global Docker prune.
- getent ahostsv4 random.ochenstarik.ru returned185.26.122.80 via local resolver; DNS-to178.212.13.102 remains an external blocker. Local krona-wg address10.77.0.3/24 and route to10.77.0.1 were observed; peer's VPS identity still requires owner confirmation.
- Reproducible R5 validator agents/codex/notes/validate-r5-templates.sh returned exit0 for Caddy2.8.4 and HAProxy3.0.30 assembled configs. Initial nonroot Caddy fixture setup failed with dropped file capabilities/empty fake hash; validator now adds only NET_BIND_SERVICE to satisfy the binary filecap and asserts fake hash generation. No ports or network are exposed by validators.

View file

@ -0,0 +1,56 @@
#!/usr/bin/env bash
# Reproducible local syntax check. Fake credentials only; no published ports or VPS access.
set -euo pipefail
root=$(cd "$(dirname "$0")/../../.." && pwd)
cd "$root"
fixtures="$root/agents/codex/notes/r5-fixtures"
mkdir -p "$fixtures"
chmod 755 "$fixtures"
docker run --rm --name randomayzer-caddy-hash-fixture --network none --user 65534:65534 --cap-drop ALL --cap-add NET_BIND_SERVICE --security-opt no-new-privileges:true caddy:2.8-alpine caddy hash-password --plaintext FAKE_R5_CONFIG_TEST_ONLY > "$fixtures/fake-bcrypt.txt"
python3 - <<'PY'
from pathlib import Path
root=Path('agents/codex/notes/r5-fixtures')
h=(root/'fake-bcrypt.txt').read_text().strip()
assert h.startswith('$2'), 'Fake hash generation failed'
s=Path('deploy/randomayzer.Caddyfile').read_text()
assert s.count('owner BASIC_AUTH_HASH') == 1
p=root/'Caddyfile';p.write_text(s.replace('owner BASIC_AUTH_HASH','owner '+h));p.chmod(0o644)
s=Path('deploy/randomayzer.haproxy.cfg').read_text()
tls='\n'.join(x for x in s.splitlines() if x.startswith(('acl ','use_backend '))).replace('CADDY_TLS_BACKEND_FROM_ACTIVE_CONFIG','fixture_caddy_tls')
http='\n'.join(x[2:] for x in s.splitlines() if x.startswith(('# acl randomayzer_http','# use_backend CADDY_HTTP'))).replace('CADDY_HTTP_BACKEND_FROM_ACTIVE_CONFIG','fixture_caddy_http')
assert 'use_backend fixture_caddy_tls' in tls
assert 'use_backend fixture_caddy_http' in http
config='''global
master-worker
defaults
mode tcp
timeout connect 5s
timeout client 10s
timeout server 10s
frontend fixture_tls
bind :8443
tcp-request inspect-delay 5s
tcp-request content accept if { req.ssl_hello_type 1 }
'''+''.join(' '+x+'\n' for x in tls.splitlines())+''' default_backend fixture_other_tls
frontend fixture_http
mode http
bind :8080
'''+''.join(' '+x+'\n' for x in http.splitlines())+''' default_backend fixture_other_http
backend fixture_caddy_tls
mode tcp
server caddy 127.0.0.1:9443
backend fixture_other_tls
mode tcp
server other 127.0.0.1:9444
backend fixture_caddy_http
mode http
server caddy 127.0.0.1:9080
backend fixture_other_http
mode http
server other 127.0.0.1:9081
'''
p=root/'haproxy.cfg';p.write_text(config);p.chmod(0o644)
PY
docker run --rm --name randomayzer-caddy28-validate --network none --user 65534:65534 --cap-drop ALL --cap-add NET_BIND_SERVICE --security-opt no-new-privileges:true -e XDG_DATA_HOME=/tmp/caddy-data -e XDG_CONFIG_HOME=/tmp/caddy-config -v "$fixtures/Caddyfile:/etc/caddy/Caddyfile:ro" caddy:2.8-alpine caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
docker run --rm --name randomayzer-haproxy30-validate --network none --cap-drop ALL --security-opt no-new-privileges:true -v "$fixtures/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro" haproxy:3.0-alpine haproxy -c -V -f /usr/local/etc/haproxy/haproxy.cfg
printf 'PASS local Caddy 2.8 / HAProxy 3.0 template syntax (fake fixture, not live VPS acceptance)\n'

View file

@ -1,4 +1,8 @@
# Add this site only to an existing Caddy. Replace BASIC_AUTH_HASH with `caddy hash-password` output. # Add this site block to the existing Caddy 2.8 Caddyfile (or an already-imported
# site-fragment directory). This does not define a second listener or proxy.
# Replace BASIC_AUTH_HASH with interactive `caddy hash-password` output.
# Basic Auth is the supported access gate; do not add Authelia without confirming
# the existing proxy's auth policy and trusted identity headers.
random.ochenstarik.ru { random.ochenstarik.ru {
tls { tls {
issuer acme { issuer acme {
@ -8,6 +12,7 @@ random.ochenstarik.ru {
basic_auth { basic_auth {
owner BASIC_AUTH_HASH owner BASIC_AUTH_HASH
} }
# Caddy 2.8 supports this body limit. Keep it ahead of the upstream.
request_body { request_body {
max_size 1MB max_size 1MB
} }
@ -16,6 +21,8 @@ random.ochenstarik.ru {
header_up Host random.ochenstarik.ru header_up Host random.ochenstarik.ru
header_up X-Forwarded-Host random.ochenstarik.ru header_up X-Forwarded-Host random.ochenstarik.ru
header_up X-Forwarded-Proto https header_up X-Forwarded-Proto https
# Replace caller-supplied forwarding metadata; Caddy's peer identity
# depends on the confirmed HAProxy-to-Caddy path.
header_up X-Forwarded-For {remote_host} header_up X-Forwarded-For {remote_host}
header_up -X-Real-IP header_up -X-Real-IP
header_up -Forwarded header_up -Forwarded

View file

@ -0,0 +1,26 @@
# Additive examples only; this is NOT a standalone HAProxy configuration.
# Do not paste this whole file over the live config.
#
# 443 TLS passthrough example: insert in the existing mode tcp frontend bound
# to :443 after its existing TLS inspection rules and after explicit routes for
# other domains, but before any catch-all use_backend/default_backend. Replace
# CADDY_TLS_BACKEND_FROM_ACTIVE_CONFIG with the exact backend already used for
# krona-reverse-proxy. Do not create backend addresses based on guesses about
# Docker networks.
# If this frontend has no TLS ClientHello inspection, stop and reconcile its
# active routing design before adding these lines.
#
# tcp-request inspect-delay 5s # add only if the frontend has no inspect delay
acl randomayzer_sni req.ssl_sni -i random.ochenstarik.ru
use_backend CADDY_TLS_BACKEND_FROM_ACTIVE_CONFIG if randomayzer_sni
# Keep existing explicit routes and the original default_backend unchanged.
# HTTP-01 example: only if inspection shows an existing mode http :80 frontend
# does not already route this host to Caddy. Put the host ACL and use_backend
# after existing explicit host routes and before any catch-all rule. This must
# use a distinct cleartext Caddy :80 backend, never the TLS :443 backend above.
# Caddy must receive the full, unchanged /.well-known/acme-challenge/<token>
# path on port 80; do not redirect, rewrite, or route it elsewhere in HAProxy.
#
# acl randomayzer_http_host hdr(host) -i random.ochenstarik.ru
# use_backend CADDY_HTTP_BACKEND_FROM_ACTIVE_CONFIG if randomayzer_http_host

View file

@ -1,37 +0,0 @@
# Include in the existing nginx HTTP context. Install ONLY if nginx already owns 80/443.
server {
listen 80;
server_name random.ochenstarik.ru;
location ^~ /.well-known/acme-challenge/ { root /var/www/randomayzer-acme; }
location / { return 301 https://random.ochenstarik.ru$request_uri; }
}
server {
listen 443 ssl;
server_name random.ochenstarik.ru;
ssl_certificate /etc/letsencrypt/live/random.ochenstarik.ru/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/random.ochenstarik.ru/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
auth_basic "Randomayzer owner";
auth_basic_user_file /etc/nginx/randomayzer.htpasswd;
client_max_body_size 1m;
client_body_timeout 20s;
# OAuth callback has query credentials: never log request paths/query strings.
access_log off;
error_log /var/log/nginx/randomayzer-error.log crit;
location / {
proxy_pass http://10.77.0.3:3100;
proxy_bind 10.77.0.1;
proxy_set_header Host random.ochenstarik.ru;
proxy_set_header X-Forwarded-Host random.ochenstarik.ru;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP "";
proxy_set_header Forwarded "";
proxy_set_header CF-Connecting-IP "";
# Basic credentials are for nginx only, never forward them to the application.
proxy_set_header Authorization "";
proxy_connect_timeout 10s;
proxy_read_timeout 120s;
proxy_send_timeout 120s;
}
}

View file

@ -139,28 +139,49 @@ docker compose -f docker-compose.yml down
Integration tests очищают все данные: **никогда не подставлять production URL**. Имена и volume отделены от production. Integration tests очищают все данные: **никогда не подставлять production URL**. Имена и volume отделены от production.
## 6. R5: предварительно на VPS и домашнем сервере ## 6. R5: подготовка и безопасное изменение действующих proxy-контейнеров
**BLOCKED до подтверждения владельца:** A-record `random.ochenstarik.ru → 178.212.13.102`, R5 остаётся **BLOCKED** до подтверждения владельцем DNS `random.ochenstarik.ru → 178.212.13.102`
peer `10.77.0.1` — именно этот VPS, какой существующий веб-сервер владеет 80/443. и того, что домашний `10.77.0.1` — этот VPS. Не менять WireGuard, firewall manager, Compose-проекты,
Здесь не предполагается, что DNS/сертификат/вход уже проверены. контейнеры или существующие виртуальные хосты других приложений.
На VPS (без изменений): VPS-команды ниже запускай из root shell с доступом к Docker; если SSH-сеанс не root, выполни `sudo -i`
и перейди в checkout. Это нужно для приватного backup под `/root` и управления действующими контейнерами.
Не выводи `.Config.Env`. На домашнем сервере оставайся под своим обычным пользователем, используя `sudo`
только для firewall-команд.
### 6.1. Установить фактическую топологию без просмотра environment
На VPS выполняет оператор. Команды намеренно не выводят `.Config.Env`, содержимое конфигов, логи или секреты:
```bash ```bash
docker ps --filter name=krona-proxy-mux --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'
docker ps --filter name=krona-reverse-proxy --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'
docker inspect --format 'image={{.Config.Image}} path={{.Path}} args={{json .Args}} entrypoint={{json .Config.Entrypoint}} cmd={{json .Config.Cmd}}' krona-proxy-mux
docker inspect --format '{{range .Mounts}}{{printf "%s\t%s\t%s\tRW=%t\n" .Type .Source .Destination .RW}}{{end}}' krona-proxy-mux
docker inspect --format 'image={{.Config.Image}} path={{.Path}} args={{json .Args}} entrypoint={{json .Config.Entrypoint}} cmd={{json .Config.Cmd}}' krona-reverse-proxy
docker inspect --format '{{range .Mounts}}{{printf "%s\t%s\t%s\tRW=%t\n" .Type .Source .Destination .RW}}{{end}}' krona-reverse-proxy
docker exec krona-proxy-mux haproxy -vv | head -n 3
docker exec krona-reverse-proxy caddy version
ss -ltn '( sport = :80 or sport = :443 )' ss -ltn '( sport = :80 or sport = :443 )'
systemctl is-active nginx caddy apache2
ip route get 10.77.0.3
getent ahostsv4 random.ochenstarik.ru getent ahostsv4 random.ochenstarik.ru
curl --connect-timeout 5 http://10.77.0.3:3100/api/auth/me ip route get 10.77.0.3
``` ```
До включения domain overlay последний curl должен быть недоступен. Маршрут к `10.77.0.3` должен иметь Ожидается: контейнеры с указанными именами и образами семейств `haproxy:3.0-alpine` / `caddy:2.8-alpine`,
`src 10.77.0.1`; иначе остановиться, текущий WireGuard не менять. HAProxy 3.0.x и Caddy 2.8.x. Точные patch-версии на VPS зафиксировать из вывода; локальный образ/тег не
Если 443 принадлежит Apache или контейнеру неизвестной конфигурации, эти nginx/Caddy snippets не применять: доказывает версию удалённого контейнера. `getent` должен вернуть подтверждённый A-record, маршрут к дому —
оператор сначала определяет точку подключения отдельного virtual host. Чужой vhost не заменять. `src 10.77.0.1`. Если имя/образ/режим процесса отличается, не применять примеры ниже.
### Домашний сервер: firewall раньше публикации WG-порта По `Entrypoint`, `Cmd` и mounts оператор определяет фактические config path(ы), все `include`-файлы и source
файлы, которыми контейнер управляется. Не печатать/публиковать raw-конфиги или полные inspect-объекты.
Для HAProxy подтвердить, что порт 443 — `mode tcp` TLS passthrough и где уже выполняются SNI inspection,
`use_backend` и catch-all. Для HTTP-01 установить фактическую цепочку порта 80 до Caddy. Если HAProxy
терминирует TLS, существующий SNI-маршрут не ведёт к Caddy, для Caddy не существует достижимого HTTP-01,
или для редактирования/reload не найден штатный механизм — **остановиться**. Не угадывать backend, адрес
Docker-сети, config path или команду reload.
На домашнем сервере сначала проверить владельца WG peer и firewall:
```bash ```bash
ip -brief address show krona-wg ip -brief address show krona-wg
@ -169,11 +190,11 @@ sudo iptables -S DOCKER-USER
sudo iptables -S INPUT sudo iptables -S INPUT
``` ```
Ожидается 10.77.0.3 на существующем интерфейсе и Docker iptables backend. Если используется nftables native, Ожидается `10.77.0.3` на существующем `krona-wg`, маршрут через него и подтверждённый владельцем peer
firewalld/ufw или другая система управления — остановиться и поручить владельцу эквивалентные правила в его `10.77.0.1`. Если используется nftables, firewalld, ufw или иной manager — не добавлять параллельные
существующем manager; не смешивать managers. Для iptables Docker backend следующий отдельный chain iptables rules; остановиться и согласовать правила в существующем manager. Только после подтверждения
ограничивает только original destination 10.77.0.3:3100. Docker DNAT может обходить INPUT, поэтому нужен iptables Docker backend, отсутствия chain с таким именем и проверки текущих rules установить ограничение
DOCKER-USER. Дополнительный INPUT rule покрывает host/userland-proxy путь. до публикации порта:
```bash ```bash
sudo iptables -N RANDOMAYZER-WG sudo iptables -N RANDOMAYZER-WG
@ -181,13 +202,21 @@ sudo iptables -A RANDOMAYZER-WG -s 10.77.0.1 -j RETURN
sudo iptables -A RANDOMAYZER-WG -j DROP sudo iptables -A RANDOMAYZER-WG -j DROP
sudo iptables -I DOCKER-USER 1 -p tcp -m conntrack --ctorigdst 10.77.0.3 --ctorigdstport 3100 -j RANDOMAYZER-WG sudo iptables -I DOCKER-USER 1 -p tcp -m conntrack --ctorigdst 10.77.0.3 --ctorigdstport 3100 -j RANDOMAYZER-WG
sudo iptables -I INPUT 1 -p tcp -d 10.77.0.3 --dport 3100 -j RANDOMAYZER-WG sudo iptables -I INPUT 1 -p tcp -d 10.77.0.3 --dport 3100 -j RANDOMAYZER-WG
sudo iptables -S RANDOMAYZER-WG
sudo iptables -S DOCKER-USER
sudo iptables -S INPUT
``` ```
Выполнять один раз, не дублировать chain/rules. После тестов оператор сохраняет **только добавленные правила** Ожидается разрешение только source `10.77.0.1` для host destination `10.77.0.3:3100`; остальным source
через существующий менеджер firewall. При отсутствии подтверждённой persistence R5 не завершён. соединения к этому destination drop. Если chain уже существует или вывод не соответствует, не повторять
Не открывать 3100 на 0.0.0.0, LAN или роутере. IPv6 WG-порт не публикуется. команды/не вставлять дубли; разобрать текущие rules с владельцем. После проверки сохранить только новые
rules через уже работающий firewall manager. Без подтверждённой persistence R5 не принят. Не публиковать
порт на LAN, wildcard или роутере.
В той же безопасной последовательности, из checkout на домашнем сервере, включить только domain overlay:
```bash ```bash
export RANDOMAYZER_ENV_FILE=/etc/randomayzer/production.env
ryd() { docker compose --env-file "$RANDOMAYZER_ENV_FILE" -f compose.production.yml -f compose.domain.yml "$@"; } ryd() { docker compose --env-file "$RANDOMAYZER_ENV_FILE" -f compose.production.yml -f compose.domain.yml "$@"; }
ryd config --quiet ryd config --quiet
ryd up -d --wait app ryd up -d --wait app
@ -195,79 +224,244 @@ ryd ps
ss -ltn '( sport = :3100 )' ss -ltn '( sport = :3100 )'
``` ```
Ожидаются host bindings только 127.0.0.1:3100 и 10.77.0.3:3100. Domain profile использует HTTPS canonical origin, Ожидается, что app опубликовал только `127.0.0.1:3100` и `10.77.0.3:3100`; DB по-прежнему без host binding.
Secure cookie и VK redirect `https://random.ochenstarik.ru/api/auth/vk/callback`. Loopback URL более не подходит Если Docker показывает `0.0.0.0`, другой host address или DB port, немедленно вернуть loopback-only профиль и
для пользовательского login в строгом профиле. В VK ID оператор меняет зарегистрированный redirect на HTTPS. не продолжать. На VPS до reload прокси проверить новый WG upstream:
На VPS повторить curl к `10.77.0.3:3100/api/auth/me`: ожидается JSON. Подтвердить сохранение socket source
10.77.0.1 при Docker DNAT, например tcpdump метаданными `-q`, **без `-A`/payload/logging OAuth**.
При source NAT на gateway приложение игнорирует proxy headers; менять trust на всю Docker subnet запрещено.
### VPS: добавить ровно один virtual host
**nginx (только если он уже обслуживает 443).** Сохранить текущий конфиг владельцем. Сначала добавить только
HTTP server block из `deploy/randomayzer.nginx.conf` в подключаемый sites directory, создать
`/var/www/randomayzer-acme`, проверить `sudo nginx -t` и выполнить `sudo systemctl reload nginx`.
```bash ```bash
sudo certbot certonly --webroot -w /var/www/randomayzer-acme -d random.ochenstarik.ru curl --connect-timeout 5 --max-time 15 --fail --silent --show-error http://10.77.0.3:3100/api/auth/me
sudo htpasswd -c /etc/nginx/randomayzer.htpasswd owner
sudo chmod 640 /etc/nginx/randomayzer.htpasswd
# Владелец/группа файла должны позволять только nginx worker читать hash.
``` ```
Пароль вводится интерактивно, не через аргументы/логи. Certbot должен сообщить успешную выдачу. Ожидается JSON `authenticated: false`, `user: null`; это проверяет путь до приложения, не внешний TLS/login.
Теперь добавить HTTPS server block из snippet. `sudo nginx -t`, затем `sudo systemctl reload nginx`.
Проверить существующий certbot renewal timer и `sudo certbot renew --dry-run` (не перезапускать Forgejo).
Basic auth применяется также к callback; браузер владельца уже авторизован на том же origin.
**Caddy (только если он уже обслуживает 443).** Выполнить `caddy hash-password` интерактивно, вставить hash ### 6.2. Базовый снимок и резервная копия обоих конфигов
в новый отдельный site block `deploy/randomayzer.Caddyfile`, хранить конфиг с доступом только оператора/Caddy.
Добавить block в существующий Caddyfile, сохранить старую копию; выполнить
`sudo caddy validate --config /etc/caddy/Caddyfile`, затем `sudo systemctl reload caddy`.
TLS-ALPN отключён в этом site; ACME использует HTTP-01. Проверить автоматическое renewal в существующем Caddy.
Фактическая версия Caddy и конфиг валидируются на VPS; если validate отвергает директиву — не reload.
Оба snippets переписывают Host/X-Forwarded-* (не дописывают к client-supplied chain), убирают Basic Authorization До редактирования сохранить ответы всех уже обслуживаемых сайтов по обоим публичным входам. Адреса listener
перед upstream, задают 1MB body limit и таймауты. Не включать access log с callback query/cookie. 80 и 443 оператор находит в `ss`; вводить конкретный локальный адрес listener, например `127.0.0.1`, никогда
Приложение запускается **только** через `tools/serve-production.cjs`: он проверяет socket.remoteAddress, `0.0.0.0`. Перечислить в `BASELINE_HOSTS` все имена из текущих HAProxy/Caddy routes, включая
доверяет forwarded IP/proto только `10.77.0.1`, удаляет все входящие proxy headers остальных клиентов. `git.ochenstarik.ru`. Не включать URL, query, заголовки авторизации или bodies:
Обычный `next start` не является production entry point этих compose-профилей.
### Приёмка и откат R5
С внешней машины:
```bash ```bash
curl -s -o /dev/null -w '%{http_code}\n' https://random.ochenstarik.ru/ read -r -p 'Local HTTP listener IP for port 80 from ss: ' HTTP_LISTENER_IP
# ожидается 401 без basic auth read -r -p 'Local TLS listener IP for port 443 from ss: ' TLS_LISTENER_IP
curl -u owner --fail --silent https://random.ochenstarik.ru/api/auth/me BASELINE_HOSTS=(git.ochenstarik.ru) # дописать все остальные текущие домены из proxy-конфигов
# пароль вводится curl интерактивно; ожидается JSON user:null for host in "${BASELINE_HOSTS[@]}"; do
curl --connect-timeout 5 http://178.212.13.102:3100/ curl --noproxy '*' --connect-timeout 5 --max-time 15 --silent --show-error \
# timeout/refused; порт НЕ должен отвечать --output /dev/null --write-out "$host http %{http_code} %{remote_ip}\n" \
--resolve "$host:80:$HTTP_LISTENER_IP" "http://$host/" || { echo "STOP: HTTP baseline failed for $host" >&2; exit 1; }
curl --noproxy '*' --connect-timeout 5 --max-time 15 --silent --show-error \
--output /dev/null --write-out "$host https %{http_code} %{remote_ip}\n" \
--resolve "$host:443:$TLS_LISTENER_IP" "https://$host/" || { echo "STOP: HTTPS baseline failed for $host" >&2; exit 1; }
done
``` ```
С LAN другой машины `curl --connect-timeout 5 http://HOME_LAN_IP:3100/` — refused/timeout. Записать именно выведенные HTTP и HTTPS status codes/адреса в operator change record. Для случайного сайта до
С другого WG peer запрос к 10.77.0.3:3100 — timeout. С VPS запрос к нему — успешен. добавления route допустим существующий default response; не угадывать ожидаемый статус. Baseline Forgejo и
Браузером проверить сертификат, basic auth, VK login, `/api/auth/me`, Secure/HttpOnly cookie, A–H smoke. каждого другого хоста по обоим протоколам после reload должен совпасть с его собственным доизменительным
Записать фактические результаты/дату/SHA в `agents/claude/done/`, без секретных ответов. результатом.
До этого сертификат, 401 на публичном домене, live VK и изоляция на стенде — **UNVERIFIED**.
Откат: удалить только добавленный vhost, проверить существующий конфиг и reload соответствующего сервера. По mounts определить host source и container path каждого активного Caddy/HAProxy config-файла и всех
На домашнем сервере `ry up -d --wait app` вернёт loopback-only профиль. Сначала проверить исчезновение WG binding, подключаемых конфигов. Если конфиг генерируется entrypoint/Compose, работать через его подтверждённый
затем удалить только наши firewall entries: source/deployment mechanism, не править сгенерированный файл. Создать root-only каталог резервной копии и
снять копии как активных файлов из контейнеров, так и source деревьев конфигурации:
```bash ```bash
sudo iptables -D DOCKER-USER -p tcp -m conntrack --ctorigdst 10.77.0.3 --ctorigdstport 3100 -j RANDOMAYZER-WG umask 077
sudo iptables -D INPUT -p tcp -d 10.77.0.3 --dport 3100 -j RANDOMAYZER-WG BACKUP_DIR="/root/randomayzer-proxy-backup-$(date -u +%Y%m%dT%H%M%SZ)"
sudo iptables -F RANDOMAYZER-WG install -d -m 0700 "$BACKUP_DIR"
sudo iptables -X RANDOMAYZER-WG # Задать пути только после inspection. Если mount содержит include-файлы, копировать всё config-дерево.
CADDY_ACTIVE_CONFIG=/path/confirmed/in/container/Caddyfile
CADDY_HOST_SOURCE=/path/confirmed/on/host/Caddyfile
HAPROXY_ACTIVE_CONFIG=/path/confirmed/in/container/haproxy.cfg
HAPROXY_HOST_SOURCE=/path/confirmed/on/host/haproxy.cfg
docker cp "krona-reverse-proxy:$CADDY_ACTIVE_CONFIG" "$BACKUP_DIR/caddy-active"
docker cp "krona-proxy-mux:$HAPROXY_ACTIVE_CONFIG" "$BACKUP_DIR/haproxy-active"
cp -a -- "$CADDY_HOST_SOURCE" "$BACKUP_DIR/caddy-source"
cp -a -- "$HAPROXY_HOST_SOURCE" "$BACKUP_DIR/haproxy-source"
chmod -R go-rwx "$BACKUP_DIR"
CADDY_INODE_BEFORE=$(stat -Lc '%d:%i' "$CADDY_HOST_SOURCE")
HAPROXY_INODE_BEFORE=$(stat -Lc '%d:%i' "$HAPROXY_HOST_SOURCE")
cmp -s -- "$CADDY_HOST_SOURCE" "$BACKUP_DIR/caddy-active" || { echo 'STOP: Caddy active/source differ'; exit 1; }
cmp -s -- "$HAPROXY_HOST_SOURCE" "$BACKUP_DIR/haproxy-active" || { echo 'STOP: HAProxy active/source differ'; exit 1; }
sha256sum "$CADDY_HOST_SOURCE" "$HAPROXY_HOST_SOURCE" "$BACKUP_DIR/caddy-active" "$BACKUP_DIR/haproxy-active"
``` ```
Владелец также убирает эти entries из persistence своего firewall manager. DB volume, чужие vhosts и tunnel сохраняются. Для directory mounts сделать отдельные backups всего mounted config tree, включая imports (задать подтверждённые
пути directory mounts из `docker inspect`):
```bash
CADDY_CONFIG_DIR=/confirmed/container/config-dir
CADDY_HOST_CONFIG_DIR=/confirmed/host/config-dir
HAPROXY_CONFIG_DIR=/confirmed/container/config-dir
HAPROXY_HOST_CONFIG_DIR=/confirmed/host/config-dir
install -d -m 0700 "$BACKUP_DIR/caddy-tree-active" "$BACKUP_DIR/caddy-tree-source"
install -d -m 0700 "$BACKUP_DIR/haproxy-tree-active" "$BACKUP_DIR/haproxy-tree-source"
docker cp "krona-reverse-proxy:$CADDY_CONFIG_DIR/." "$BACKUP_DIR/caddy-tree-active"
docker cp "krona-proxy-mux:$HAPROXY_CONFIG_DIR/." "$BACKUP_DIR/haproxy-tree-active"
cp -a -- "$CADDY_HOST_CONFIG_DIR/." "$BACKUP_DIR/caddy-tree-source/"
cp -a -- "$HAPROXY_HOST_CONFIG_DIR/." "$BACKUP_DIR/haproxy-tree-source/"
chmod -R go-rwx "$BACKUP_DIR"
```
Сверить хеши активных файлов с source до изменения. Если active path или source не доказан, остановиться. Резервная копия может
содержать proxy credentials и Caddy basic-auth hash: оставить её только в каталоге `0700`, не коммитить,
не отправлять в чат/логи и удалить штатной процедурой владельца после принятия изменения.
### 6.3. Валидация до и после изменения
Сначала текущие конфиги должны проходить те же checks до редактирования:
```bash
docker exec krona-reverse-proxy caddy validate \
--config "$CADDY_ACTIVE_CONFIG" --adapter caddyfile
docker exec krona-proxy-mux haproxy -c -V -f "$HAPROXY_ACTIVE_CONFIG"
```
Если HAProxy стартует с несколькими `-f`, передать в проверку все активные конфиги в том же порядке.
Ожидается `Valid configuration` от Caddy и exit code 0 от HAProxy (`-V` печатает версию; HAProxy может не
печатать отдельную строку успеха). Любая ошибка до изменения —
стоп, не пытаться «починить» baseline конфигурацию в рамках Randomayzer.
Смонтированный **одиночный файл** может оставаться старым inode при atomic rename/`mv`/некоторых редакторах;
Docker продолжит показывать старое содержимое. Смонтированный каталог имеет другую семантику. Поэтому сначала
определить type mount и текущие file inode/hash. Изменять только подтверждённый host source существующим
способом деплоя. Для single-file bind mount сохранять inode: не использовать `sed -i`, `mv`, `sudoedit` или
full-file copy-over. После правки повторить stat/hash checks, убедиться что device/inode остались прежними,
а host source и active container content совпали. Для directory mount также сверить итоговые хеши из контейнера.
При несовпадении ничего не reload;
сначала вернуть подтверждённый source и добиться совпадения.
Не заменять active Caddyfile/HAProxy config целиком шаблонами из репозитория. Вставить только новый Caddy site
block из `deploy/randomayzer.Caddyfile` в активный Caddyfile или уже подключённый fragments directory. Если на
VPS установлен Vim и подтверждённый source — обычный файл, редактировать так, чтобы сохранить inode:
```bash
sudo vim -c 'set backupcopy=yes' "$CADDY_HOST_SOURCE"
sudo vim -c 'set backupcopy=yes' "$HAPROXY_HOST_SOURCE"
```
В редакторе добавить только доменный block/правила из snippet в правильные места, проверить diff локально,
сохранить, затем выполнить host/container inode/hash checks ниже. Если Vim отсутствует, файл — symlink,
конфигурация генерируется или source read-only, остановиться и применить подтверждённый deployment mechanism;
не переключаться на `sed -i`/`mv`/atomic rename. Интерактивно
создать hash: `docker exec -it krona-reverse-proxy caddy hash-password`; значение hash поместить только в
новый локальный конфиг. Не писать пароль/hash в CLI args, shell history или Git. Выбран Basic Auth; Authelia
не подключать без подтверждения действующих access-control policy и identity headers. Для Caddy body limit —
1 MB; access log для этого сайта не добавлять, так как OAuth callback может нести чувствительные query values.
HAProxy snippet `deploy/randomayzer.haproxy.cfg` содержит **только примеры вставок**, а не полный конфиг.
Если и только если активный frontend на 443 — TCP TLS passthrough, адаптировать SNI ACL и `use_backend` к
уже существующему Caddy TLS backend. Вставить новое точное правило после других существующих явных доменных
маршрутов и перед catch-all; существующие rules/default backend не менять. Не добавлять второй 443 listener.
Если активный конфиг подтверждает отдельный `mode http` frontend на 80, который пока не маршрутизирует этот
Host к Caddy, добавить host ACL к существующему Caddy cleartext HTTP backend. Этот HTTP backend должен вести
именно на Caddy HTTP port; не переиспользовать backend TLS passthrough/443. HAProxy должен передать Caddy
`/.well-known/acme-challenge/<token>` неизменённым, не basic-auth/redirect/rewrite на HAProxy и не менять
порт 80. Если текущий route уже передаёт этот Host к Caddy — оставить его без изменений. Не добавлять
Authelia forward-auth backend/rules без подтверждённого существующего policy.
После вносимых дополнений ещё раз проверить хеши и оба конфига:
```bash
sha256sum "$CADDY_HOST_SOURCE" "$HAPROXY_HOST_SOURCE"
docker exec krona-reverse-proxy sha256sum "$CADDY_ACTIVE_CONFIG"
docker exec krona-proxy-mux sha256sum "$HAPROXY_ACTIVE_CONFIG"
test "$CADDY_INODE_BEFORE" = "$(stat -Lc '%d:%i' "$CADDY_HOST_SOURCE")" || { echo 'STOP: Caddy source inode changed'; exit 1; }
test "$HAPROXY_INODE_BEFORE" = "$(stat -Lc '%d:%i' "$HAPROXY_HOST_SOURCE")" || { echo 'STOP: HAProxy source inode changed'; exit 1; }
docker cp "krona-reverse-proxy:$CADDY_ACTIVE_CONFIG" "$BACKUP_DIR/caddy-active-after"
docker cp "krona-proxy-mux:$HAPROXY_ACTIVE_CONFIG" "$BACKUP_DIR/haproxy-active-after"
cmp -s -- "$CADDY_HOST_SOURCE" "$BACKUP_DIR/caddy-active-after" || { echo 'STOP: Caddy host/active differ'; exit 1; }
cmp -s -- "$HAPROXY_HOST_SOURCE" "$BACKUP_DIR/haproxy-active-after" || { echo 'STOP: HAProxy host/active differ'; exit 1; }
docker exec krona-reverse-proxy caddy validate \
--config "$CADDY_ACTIVE_CONFIG" --adapter caddyfile
docker exec krona-proxy-mux haproxy -c -V -f "$HAPROXY_ACTIVE_CONFIG"
```
Ожидается одинаковый source/active hash для каждого файла плюс успешные Caddy/HAProxy checks. Если include
files edited, сверить и провалидировать весь config set. Не reload при любом mismatch/error.
### 6.4. Применение без container restart
Caddy поддерживает штатную загрузку валидированного конфига через admin API, без restart процесса:
```bash
docker exec krona-reverse-proxy caddy reload \
--config "$CADDY_ACTIVE_CONFIG" --adapter caddyfile
```
Ожидается подтверждение reload; если admin API выключен, слушает не стандартный адрес или команда отвергла
конфиг — не использовать сигнал/restart как обход. Сначала определить действующий Caddy admin endpoint и
согласованный reload способ; старый config остаётся baseline до успешного reload.
Для HAProxy **reload command намеренно не задан**: реальный `master-worker` mode, PID/master CLI socket,
entrypoint и compose deployment не подтверждены из локального checkout. После inspection использовать только
штатный безостановочный reload mechanism этого контейнера/его deployment, если он подтверждён его активными
параметрами и документацией. Для обычной master-worker установки это может быть командой master CLI или
управляемым запуском нового процесса с graceful handoff, но конкретный вариант зависит от обнаруженного
process model/config. Не выполнять `docker restart`, `docker compose up --force-recreate`, `kill -HUP` или
`kill -USR2` по догадке. Если безопасный reload не доказан, остановиться после validation; это operational
blocker, а не повод перезапустить общий proxy.
Перед HAProxy reload убедиться, что Caddy config уже validated и source/active hash совпадают. Использовать
подтверждённый reload. Затем выполнить Caddy reload. Ни один контейнер не должен пересоздаваться/перезапускаться;
сравнить `docker ps` IDs/uptime с доизменительным снимком. Сохранить только команды и коды/нечувствительный
вывод, не сохранять конфиги или OAuth access logs.
### 6.5. После reload и приёмка
Повторить baseline loop из 6.2 с теми же `BASELINE_HOSTS` и адресами listener 80/443. Каждый существующий сайт
должен вернуть прежний status code по обоим протоколам и ответить за 15 секунд; минимум Forgejo
`git.ochenstarik.ru` обязателен. Иные
результаты — немедленный rollback.
После подтверждения DNS и внешнего маршрута:
```bash
curl --connect-timeout 5 --max-time 15 --silent --show-error \
--output /dev/null --write-out '%{http_code}\n' https://random.ochenstarik.ru/
```
Ожидается `401` без Basic Auth. Для защищённой проверки `/api/auth/me` использовать интерактивный prompt:
```bash
curl --connect-timeout 5 --max-time 15 --user owner --fail --silent --show-error \
https://random.ochenstarik.ru/api/auth/me
```
Не указывать пароль в команде. Ожидается безопасный JSON с `authenticated: false` и `user: null` до входа.
Проверить TLS-сертификат извне, ручное VK consent/login и callback в браузере. Путь HTTP-01 удостоверяется
фактической выдачей/renewal сертификата через Caddy; `caddy validate` сам по себе выдачу не подтверждает.
Проверить с домашнего сервера, что порт приложения доступен с VPS через WG, но недоступен с LAN; с внешней
машины и неразрешённого WG peer он также недоступен. Не тестировать порт через интернет, если публичный
bind/firewall ещё не подтверждены. Порт БД не должен появиться. Записать наблюдения/даты/SHA в операторский
отчёт, без secret/config/body/log dumps. Сертификат, 401, VK login, source identity и firewall isolation до
фактического стенда остаются `UNVERIFIED`.
### 6.6. Откат без воздействия на остальные сервисы
Если reload не прошёл, TLS/HTTP-01 не работает, Randomayzer не отвечает или любой предыдущий сайт изменил
результат: прекратить дальнейшие проверки. На host source восстановить только изменённые config-файлы из
private backup, записывая в существующий inode (`cat backup-file > existing-mounted-file` после проверки
пути/hash), либо применить подтверждённый штатный config deployment. Не `mv`/atomic replace single-file bind
mount. Не восстанавливать/перезаписывать весь proxy config tree поверх чужих параллельных изменений. Проверить
source и active hashes, затем Caddy `validate` и HAProxy `-c` теми же путями, что выше.
Применить обе подтверждённые reload-команды без рестарта и проверить Caddy/HAProxy container ID/uptime,
затем повторить исходные other-site baselines. Ожидается прежний baseline для Forgejo и каждого перечисленного
домена; Randomayzer снова не должен попадать на новый route. Если безопасный HAProxy reload не найден или
baseline чужого сайта не восстановился, не перезапускать общий proxy: оставить его работающим в доступном
состоянии и немедленно передать владельцу backup, фактический config path, validation output и точную ошибку
без секретов. На домашнем сервере переключить только Randomayzer в loopback-only профиль и удалить только
его добавленные firewall rules через тот manager, которым они были установлены. Существующие containers,
volumes, sites, Authelia, Forgejo, fleet-hub и WireGuard сохраняются.
## Источники конфигурации ## Источники конфигурации
- [Next.js custom server](https://nextjs.org/docs/app/guides/custom-server) — socket-aware ingress перед Next. - [Next.js custom server](https://nextjs.org/docs/app/guides/custom-server) — socket-aware ingress перед Next.
- [Docker Compose services](https://docs.docker.com/reference/compose-file/services/) — exact host port binding и resource limits. - [Docker Compose services](https://docs.docker.com/reference/compose-file/services/) — host bindings и resource limits.
- [nginx proxy module](https://nginx.org/en/docs/http/ngx_http_proxy_module.html) — proxy_bind и overwrite headers. - [HAProxy 3.0 Configuration Manual](https://docs.haproxy.org/3.0/configuration.html) — TCP inspection, SNI sample fetches, ACL/use_backend and validation options. Использовать семантику для фактической версии 3.0.x; локальный template остаётся примером.
- [Caddy reverse_proxy](https://caddyserver.com/docs/caddyfile/directives/reverse_proxy), [TLS](https://caddyserver.com/docs/caddyfile/directives/tls). - [Caddy `basic_auth`](https://caddyserver.com/docs/caddyfile/directives/basic_auth) — хеширование пароля; директива переименована в `basic_auth` до версии 2.8.
- [Caddy `reverse_proxy`](https://caddyserver.com/docs/caddyfile/directives/reverse_proxy) — upstream и HTTP transport timeouts.
- [Caddy automatic HTTPS](https://caddyserver.com/docs/automatic-https) — HTTP-01/redirect/certificate behavior. Доступность пути подтверждается реальной конфигурацией/сертификатом, не статическим предположением.
- [Caddy command line](https://caddyserver.com/docs/command-line) — `validate` и `reload`; admin endpoint и успешный reload должны быть подтверждены на существующем контейнере.
- [Docker inspect](https://docs.docker.com/reference/cli/docker/container/inspect/) and [Docker cp](https://docs.docker.com/reference/cli/docker/container/cp/) — узкий вывод конфигурации контейнера, mounts и резервное копирование. Никогда не выводить `.Config.Env`.