You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
An accessory is reachable from every host that can route to its server
until harden runs, because Docker's FORWARD rules are consulted before
ufw's. That is a security claim users will rely on, so the README, ADR-0007
and CHANGELOG now say it plainly rather than implying ufw alone suffices.
Copy file name to clipboardExpand all lines: CHANGELOG.md
+2-1Lines changed: 2 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -17,7 +17,7 @@ All notable changes to `@devalade/shipnode` will be documented here.
17
17
-**`placement: 'primary'` on a pm2 app** pins a process to the first server the app is declared on — crons and schedulers, where N replicas means every job fires N times. Primary is a property of the *server*, not the roll, so `deploy --on web-b` cannot promote a secondary and start a second scheduler. Rejected on a pm2 app with a port: the LB would keep routing to replicas serving nothing.
18
18
-**`SHIPNODE_<ACCESSORY>_HOST` for cross-server accessories.** Docker networks are host-local, so no connection string was ever generated and the shipnode default of `localhost` silently pointed at nothing once the database moved to its own box. Every app is now handed the address of each accessory it `dependsOn` — `127.0.0.1` when co-located, the host's `privateHost` when not — so moving an accessory to its own server is a config change, not a code change. Declared env wins.
19
19
-**`shipnode drain` / `shipnode undrain`** for manual rotation control: take a replica out without deploying it, or return one that a failed roll left behind.
20
-
-**`--on <server>`** on `deploy`, `rollback`, `harden`, `status`, `logs`, and `env`.
20
+
-**`--on <server>`** on `deploy`, `rollback`, `setup`, `harden`, `status`, `logs`, and `env`.
21
21
-**`shipnode status` reports fleet convergence** — each replica's release and rotation state, and an explicit warning naming which replicas are on which release when they disagree. A replica with no release, or one left drained, counts as not converged. Skipped under `--on`, where one observation proves nothing about the others.
22
22
-**`shipnode deploy --watch` — the development loop.** Runs one normal deploy to establish a baseline, then watches the working tree: every save is rsynced into the live release, rebuilt, reloaded, and health-probed. Each cycle is incremental — rsync receives an explicit `--files-from` list of changed paths instead of scanning the whole tree, dependencies are reinstalled only when a manifest or lockfile changes, and the health probe uses exponential backoff (100ms → 1s) rather than the deploy path's fixed 3s delay plus 2s retry gaps. Falls back to a full-tree sync when a change set is large or contains a delete, which `--files-from` cannot express. Requires `--app <name>` in a multi-app workspace; mutually exclusive with `--dry-run`.
23
23
- This is deliberately not a release: it patches the release that is already serving, so there is no rollback target, reload can drop in-flight requests, and local deletes do not propagate until the next full deploy. Watch mode says so on startup.
@@ -31,6 +31,7 @@ All notable changes to `@devalade/shipnode` will be documented here.
31
31
- The debounce has a max-wait ceiling (2s). A plain debounce resets on every event, so in a repo with a background writer — a turbo daemon, a framework's own watcher — changes would sit unemitted for as long as the writing continued, and the loop would appear hung.
32
32
33
33
### Fixed
34
+
-**Accessory firewall rules restricted nothing.** Two faults, both found only by running against a real firewall. The generated ufw comment contained an apostrophe, which makes ufw reject the entire rule with `ERROR: Invalid syntax` while `ufw --force enable` still succeeds — so the firewall came up with the hole never opened. And Docker publishes container ports past ufw entirely: its own FORWARD rules are consulted first, so `ufw allow from <replica> to any port 5432` was accepted, appeared in `ufw status`, and left the database reachable from every host that could route to the server. `harden` now writes matching `DOCKER-USER` rules — the chain Docker guarantees to consult first — and sanitises comments. Note that **an accessory is exposed until `harden` runs**.
34
35
-**`shipnode setup` failed at PM2 startup on the documented first run.** Deploy-user commands ran from root's home: `sudo` inherits the caller's working directory, setup runs over SSH as root, and `/root` is mode 700 — so Node's `spawn()` returned EACCES and the error named the node binary rather than the cause. Commands now run with `sudo -H` from the target user's home.
35
36
-**`shipnode setup --no-deploy-user` was silently ignored.** Commander turns `--no-deploy-user` into `deployUser: false`, not `noDeployUser: true`, so the flag read as undefined and the user was created anyway.
36
37
-**Caddy was never installed on fleet replicas.** Both install gates tested `app.domain`, and a fleet replica deliberately has none — but it still needs Caddy for the readiness endpoint. The first deploy died writing its site file into a directory that was never created.
Bind the accessory somewhere reachable (`'0.0.0.0:5432:5432'`, not `'127.0.0.1:5432:5432'`) and run `shipnode harden` to open that port to the replicas that need it — and only to them.
284
+
Bind the accessory somewhere reachable (`'0.0.0.0:5432:5432'`, not `'127.0.0.1:5432:5432'` — shipnode rejects the latter when a replica on another host depends on it) and run `shipnode harden` to open that port to the replicas that need it, and only to them.
285
+
286
+
> **`harden` is what closes the port.** Docker publishes container ports straight past UFW — a `ufw allow from …` rule is accepted, shows in `ufw status`, and restricts nothing. `shipnode harden` writes matching `DOCKER-USER` rules, which is the chain Docker actually consults. Until you run it, an accessory bound to `0.0.0.0` is reachable from every host that can route to that server.
Copy file name to clipboardExpand all lines: docs/adr/0007-fleet-replication.md
+10Lines changed: 10 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -89,6 +89,16 @@ Injection happens during config assembly, not at deploy time: scoping the config
89
89
90
90
Accessories stay single-node. Shipnode replicating Postgres is out of scope, and the schema says so rather than letting someone try.
91
91
92
+
### Docker publishes past the firewall
93
+
94
+
An accessory reachable across the network needs its port closed to everything except the replicas that consume it, and `ufw allow from <replica> to any port 5432` looks like it does that. It does not.
95
+
96
+
Docker writes its own ACCEPT rules into the FORWARD path when it publishes a port, and those are consulted before ufw's chain. The ufw rule is accepted, appears in `ufw status`, and restricts nothing — the database stays reachable from every host that can route to the server. This was found by connecting to Postgres from an unrelated machine with the rules in place, and it is invisible to any test that only inspects generated commands.
97
+
98
+
`harden` therefore writes `DOCKER-USER` entries alongside the ufw rules. That is the one chain Docker guarantees to evaluate first and never rewrite: allowed sources `RETURN` and fall through to Docker's own ACCEPT, everything else for that port is `DROP`ped. The `DROP` is inserted *before* the `RETURN`s, because each `-I … 1` pushes the previous entry down — appending it would place it after the `RETURN` Docker keeps at the end of the chain, where it is never reached.
99
+
100
+
The consequence worth stating plainly: **an accessory is exposed until `harden` runs.** Binding it to the server's private address rather than `0.0.0.0` narrows that window to the private network, and is worth doing regardless.
101
+
92
102
## What is deliberately not here
93
103
94
104
-**A shipnode-managed load balancer** — a Cloudflare tunnel with N connectors, or an edge Caddy node. The drain contract is designed so either could be added later as an alternative drain implementation without disturbing the roller.
0 commit comments