summaryrefslogtreecommitdiffstats
path: root/SECURITY.md
diff options
context:
space:
mode:
authorbndw <ben@bdw.to>2026-02-10 21:29:32 -0800
committerbndw <ben@bdw.to>2026-02-10 21:29:32 -0800
commitf5b667c80e49117c94481d49c5b0c77dbcf2804a (patch)
treeb9b6d8f4cca99639b47fecacebefcd769ba48ed7 /SECURITY.md
parentc49a067ac84ac5c1691ecf4db6a9bf791246899f (diff)
Rewrite README and add SECURITY.md
Document both deployment modes (git push and direct), all commands, architecture, VPS file layout, and vanity imports. Add SECURITY.md covering threat model, mitigations, and known gaps.
Diffstat (limited to 'SECURITY.md')
-rw-r--r--SECURITY.md55
1 files changed, 55 insertions, 0 deletions
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..ad04094
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,55 @@
1# Security Model & Known Gaps
2
3Ship is a single-user VPS deployment tool. The threat model assumes:
4- You control the VPS and have root SSH access
5- You trust everyone who has SSH push access (their keys are copied to the `git` user)
6- The VPS runs only your own apps
7
8## Mitigations in place
9
10### App name validation
11All app/project names are validated against `^[a-z][a-z0-9-]{0,62}$` before being used in shell commands, file paths, systemd units, or DNS labels. This prevents command injection via crafted names.
12
13### Scoped sudoers
14The `git` user's sudo rules are restricted to specific paths:
15- `systemctl restart/enable` only for services matching `[a-z]*`
16- `cp` only from `.ship/` subdirectories to `/etc/systemd/system/` and `/etc/caddy/sites-enabled/`
17- `mkdir` only under `/var/lib/` and `/var/www/`
18- `chown` only for `git:git` under `/var/lib/` and `/var/www/`
19
20### Scoped safe.directory
21Git's `safe.directory` is set only for the `www-data` user (not system-wide), preserving CVE-2022-24765 protection for other users.
22
23## Accepted risks (by design)
24
25### SSH key access = root access
26The `git` user is in the `docker` group, which is root-equivalent (can mount the host filesystem). Additionally, `.ship/service` files pushed via git are installed as systemd units. Anyone with SSH push access effectively has root. This is intentional for a single-user tool.
27
28### Git repo visibility
29Repos are private by default (not cloneable over HTTPS). Use `ship init --public` to make a repo publicly cloneable. This is controlled by the `git-daemon-export-ok` marker file in each bare repo. Only public repos are accessible via `go get` or `git clone` over HTTPS.
30
31### User-controlled systemd units
32The `.ship/service` file in each repo is copied to `/etc/systemd/system/` on push. A malicious service file could run arbitrary commands as root. This is equivalent to the Docker access risk above.
33
34## Known gaps (not yet addressed)
35
36### SSH host key verification disabled
37`ssh.InsecureIgnoreHostKey()` is used for all SSH connections, and `StrictHostKeyChecking=no` for scp/rsync. This makes connections vulnerable to MITM attacks on untrusted networks. A future improvement would use `known_hosts` verification.
38
39### Env files may have loose permissions
40Environment files at `/etc/ship/env/{name}.env` are created via `sudo tee` and may be world-readable depending on umask. These files can contain secrets. The `deploy` flow does `chmod 600` but `ship init` does not. A future improvement would ensure consistent restrictive permissions.
41
42### host init is not idempotent
43Running `ship host init` twice will overwrite `/etc/caddy/Caddyfile` and the base domain Caddy config, destroying any manual edits. No guard checks whether setup has already been completed.
44
45### No rollback on failed docker build
46The post-receive hook installs `.ship/service` and `.ship/Caddyfile` before running `docker build`. If the build fails, the configs are updated but the old image is still running, creating a mismatch. The old container keeps running (due to `set -e`), but a manual restart would use the new (mismatched) unit file.
47
48### ship deploy vs git push ownership mismatch
49`ship deploy` runs commands as root (the SSH user), while `git push` triggers the hook as the `git` user. Files checked out by `ship deploy` become root-owned, which can prevent subsequent `git push` deploys from overwriting them.
50
51### No concurrent push protection
52Simultaneous pushes can race on the checkout directory and docker build. For single-user usage this is unlikely but not impossible.
53
54### Port allocation is monotonic
55Ports are never reclaimed when apps are removed. After ~57,000 create/remove cycles, ports would be exhausted. Not a practical concern.