From f5b667c80e49117c94481d49c5b0c77dbcf2804a Mon Sep 17 00:00:00 2001 From: bndw Date: Tue, 10 Feb 2026 21:29:32 -0800 Subject: 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. --- SECURITY.md | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 SECURITY.md (limited to 'SECURITY.md') diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..ad04094 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,55 @@ +# Security Model & Known Gaps + +Ship is a single-user VPS deployment tool. The threat model assumes: +- You control the VPS and have root SSH access +- You trust everyone who has SSH push access (their keys are copied to the `git` user) +- The VPS runs only your own apps + +## Mitigations in place + +### App name validation +All 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. + +### Scoped sudoers +The `git` user's sudo rules are restricted to specific paths: +- `systemctl restart/enable` only for services matching `[a-z]*` +- `cp` only from `.ship/` subdirectories to `/etc/systemd/system/` and `/etc/caddy/sites-enabled/` +- `mkdir` only under `/var/lib/` and `/var/www/` +- `chown` only for `git:git` under `/var/lib/` and `/var/www/` + +### Scoped safe.directory +Git's `safe.directory` is set only for the `www-data` user (not system-wide), preserving CVE-2022-24765 protection for other users. + +## Accepted risks (by design) + +### SSH key access = root access +The `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. + +### Git repo visibility +Repos 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. + +### User-controlled systemd units +The `.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. + +## Known gaps (not yet addressed) + +### SSH host key verification disabled +`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. + +### Env files may have loose permissions +Environment 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. + +### host init is not idempotent +Running `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. + +### No rollback on failed docker build +The 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. + +### ship deploy vs git push ownership mismatch +`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. + +### No concurrent push protection +Simultaneous pushes can race on the checkout directory and docker build. For single-user usage this is unlikely but not impossible. + +### Port allocation is monotonic +Ports are never reclaimed when apps are removed. After ~57,000 create/remove cycles, ports would be exhausted. Not a practical concern. -- cgit v1.2.3