# 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. The cgit web interface respects the same model — it is configured with `export-ok=git-daemon-export-ok`, so only public repos are browsable. ### 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.