Repository navigation
feat(deploy): O10 worker VM, restricted DB role, locked-down api host, /healthz - #38
Aspirant200715 wants to merge 5 commits into
Conversation
…i host, /healthz - deploy/worker-vm: compose file, runbook, env template and preflight for the agent worker on its own VM - worker: refuses a second process with the same WORKER_ID, can require every agent key, sweeps abandoned workspaces, warns when named after its hostname - deploy/worker-vm/db-role.sql: a mergit_worker role that can touch only jobs, pr_requests, plans and tasks - api: GET /healthz (database check), compose healthcheck, update.sh waits for it - Caddy: api.mergit.site serves browser routes and /healthz; /v0/internal only from WORKER_VM_IP - cp_postgres published on CP_DB_BIND (loopback by default)
…e compose The postgres:16 image's pg_hba ends with `host all all all scram-sha-256`, so once CP_DB_BIND publishes cp_postgres on the private network the worker VM can log in as the superuser with the password committed in compose.controlplane.yaml, which makes the restricted role pointless. cp_postgres now uses deploy/postgres/pg_hba.conf: any role from the compose network (samenet), and from anywhere else only mergit_worker on mergit_cp. In-container socket access (backup.sh, wallet-interest.sh, create-db-role.sh) is unchanged.
OfficialAbhinavSingh
left a comment
There was a problem hiding this comment.
grants line up with every query the worker makes and the role script reruns cleanly. one real problem: the stock postgres image lets any role in over tcp and the postgres password is committed, so once CP_DB_BIND is set the worker VM could log in as superuser and skip mergit_worker. pushed a pg_hba.conf so only mergit_worker gets in from outside the compose network, with a test.
follow-ups for later: the advisory lock isn't retaken if cp_postgres restarts, and a db that's down at start gives a raw traceback.
Viscous106
left a comment
There was a problem hiding this comment.
Good direction on the lockdown. The Caddy routes, the create-db-role.sh input handling and /healthz returning only {"ok": …} all look solid. I read the diff only and didn't run it, so please treat the first two items as things to confirm.
Should fix
- Lock release after a power loss or reboot (
startup.py:247-268). The advisory lock only frees when Postgres notices the dead session. A SIGKILL frees it at once, but a VM power loss or network partition sends no FIN, so the lock can stay held until TCP keepalive fires (about 2h by default). The restarted worker then logs "already running", exits 2, and Docker loops it.test_a_killed_worker…usespg_terminate_backend, which doesn't model this. Please setkeepalives_idle,keepalives_intervalandkeepalives_counton the lock connection, ortcp_keepalives_*on the server. A dropped lock connection also loses the lock silently, so it's worth handling. - Superuser protection relies on
samenet(pg_hba.conf:1017).postgreswith the hardcodedmergitpassword is allowed fromsamenet. The comment says Docker preserves the source IP on published ports. With docker-proxy,userland-proxyoriptables:false, the source is the bridge gateway, which is insidesamenet, so the worker VM would get superuser. Please use the explicit compose subnet, or a non-guessable password. The tests are static text checks and can't catch this. /healthzis public (health.py:69-76) and runs a DB query per request with no rate limit. A flood can exhaust the API pool. Cache the result for about 2s, or probe the DB only from the compose healthcheck.- README order. Step 3 (
create-db-role.sh) runs before step 4 (up -d), but the script needscp_postgresrunning and the tables created. On a fresh server it can leave a half-configured role.
Worth fixing
- The DB link is cleartext by default (
sslmode=prefer, plainhost). Usehostsslandverify-full, or say plainly that cleartext is accepted. ports: ["8000:8000"]onapiis unchanged but still publishes the unproxied API, bypassing the Caddy lockdown. Bind it to127.0.0.1or drop it.update.sh:1055makes a good deploy exit non-zero if the API isn't reachable through Caddy, even forweborappdeploys.UPDATEis granted on every column ofjobsandpr_requests. A compromised worker can rewrite anypr_url, so the README's "least privilege" wording overstates it.- The tests run
DROP ROLE mergit_worker(cluster-global) and terminate any backend matchingpg_try_advisory_lock. That's unsafe against a real Postgres and flaky in parallel runs.
Nits: os.chmod in _remove_workspace follows symlinks. preflight.sh keeps quote characters Compose strips. psycopg.connect has no connect_timeout. Nothing asserts the CP_DB_BIND default of 127.0.0.1.
…ipt, fail-fast db connects Follow-up to Viscous106's review of mergit-io#38. - pg_hba.conf matched the compose network with samenet, which under some Docker network settings (docker-proxy's own bridge gateway, iptables: false) is the address a published port sees a connection arrive from: the superuser, password and all, would then be reachable from outside. The compose network now pins a subnet (networks.default.ipam) and pg_hba.conf matches that exact CIDR instead. - api's 8000:8000 published to every interface, bypassing the Caddy lockdown for a run without compose.server.yaml. Bound to 127.0.0.1. - deploy/worker-vm/README.md ran create-db-role.sh before the stack that creates the control plane's tables, which create-db-role.sh itself says it needs. Swapped the order. - The three places a worker or the api reconnects to Postgres (WorkerIdLock, PgJobStore, PgPrStore) had no connect_timeout, so a database that is down or unreachable hung the call forever instead of letting start-up or a reconnect fail and get retried. Two new tests pin the subnet and the api binding so they cannot drift back to samenet or an open port silently. Checked locally: full control-plane suite (328 passed), ruff and pyright clean, and `docker compose config` resolves correctly for compose.testnet+controlplane, and with compose.server.yaml layered on top (api's port is fully unpublished there, as before).
|
pushed
two new tests pin the subnet and the loopback binding. full suite green (328 passed), ruff and pyright clean. still open from that review, not attempted here: the worker-VM-power-loss case (TCP keepalives so Postgres notices a dead session faster than its ~2h default) and bigger thing found doing this: this branch is now stale against main's Caddyfile in a real, non-mechanical way. main has since merged i don't know whether that unification on main was deliberate for |
Viscous106
left a comment
There was a problem hiding this comment.
Re-checked the current diff (25 files) against my earlier review. Three of the findings are fixed, which is good, but #38 conflicts with main, has no CI checks, and several findings remain. I read the diff only and ran nothing.
Fixed, thanks
pg_hba.confnow uses an exact CIDR with the pinned subnet instead ofsamenet.- The README order is right (stack up, then the role script).
- The API publishes on
127.0.0.1:8000, with a test. connect_timeout=10is inWorkerIdLock.acquire,pr_store.pyandjobs.py.
Still open
- Lock after a power loss or reboot (
startup.py). There are no TCP keepalives anywhere. A VM power loss or network partition leaves a half-open backend holding the advisory lock for about 2 hours until the OS timeout, and the restarted worker exits 2 and loops. Setkeepalives_idle,keepalives_intervalandkeepalives_counton the lock conninfo, ortcp_keepalives_*on the server. /healthzis public, runs a poolSELECT 1per request with no cache or rate limit, and logs a full traceback withexc_info=True. A flood can exhaust the API pool. Cache the result for about 2s, or probe the DB only from the compose healthcheck.- Cleartext DB link:
sslmode=preferincreate-db-role.shandworker.env.example, with nohostssl. update.sh:wait_healthyreturns 1 underset -eifapi.mergit.siteis unreachable, so a good deploy exits non-zero.- Tests run
DROP ROLE mergit_worker(test_worker_role.py), which is cluster-wide. Against a dev or prod cluster it destroys the real role. CONNECTisn't revoked from PUBLIC indb-role.sql, and there are no default privileges, so tables created later aren't covered byREVOKE ALL ON ALL TABLES.
New
- The
networks.default.ipampin incompose.controlplane.yaml: the merged compose files share one default network, soupmay fail on a server whose existing network has a different subnet. The README doesn't say the network must be recreated. I couldn't confirm this.
Process
- It conflicts with main (GitHub shows CONFLICTING), so it needs a rebase.
- #42 is stacked on an older snapshot of this PR and lacks several of the fixes above. Please coordinate: land this first, then rebase #42 to its delta only.
Not verified: anything at runtime, the cause of the conflict (inferred), and Docker's source-IP behaviour.
Part of #26 (O10). Covers the worker VM, the hosting wiring around it, and the edge cases around crashes and redeploys.
What this adds
deploy/worker-vm/: compose file, runbook, env template,preflight.sh,create-db-role.sh+db-role.sql(a role that can touch only jobs, pr_requests, plans, tasks)WORKER_ID(advisory lock), optionalREQUIRE_ALL_AGENT_KEYS, sweep of abandoned workspaces, warning when named after the hostnameGET /healthz, compose healthcheck;update.shwaits for itapi.mergit.siteserves browser routes and /healthz;/v0/internal/*only fromWORKER_VM_IP; the rest is 404. One shared route list for app. and api.cp_postgrespublished on${CP_DB_BIND:-127.0.0.1}:5433What I verified
Not verified
Follow-up
workerservice from compose.controlplane.yaml after cutover (see deploy/worker-vm/README.md)