Security model¶
A deploy receiver is, by design, an HTTP endpoint that makes the server run code. This page describes what nimdeploy protects, how, and what stays your responsibility.
The webhook endpoint¶
| Threat | Protection |
|---|---|
| Someone forges a push | Every request must carry a valid HMAC SHA-256 signature (GitLab: the secret token) made with that deploy's secret, compared in constant time. Otherwise 401 and nothing runs. |
| A leaked secret affects every app | One secret per deploy, each in its own environment variable. |
| A push from another repository or branch | repository and branch must match the config; anything else is ignored. |
| Replaying a captured delivery | Duplicate delivery IDs are ignored; deploys use the commit from the signed payload, so a replay can at most redeploy that same commit. |
| Command injection through the payload | The payload is never turned into a command or arguments. Only the configured command runs; push data reaches it as plain environment variables (DEPLOY_*). |
| Huge or slow requests | Body capped at 25 MB (max_body_bytes); deploys run in the background so requests return immediately. |
| Flooding with valid pushes | One run at a time per deploy; queued pushes collapse into the latest one. |
Network exposure¶
- nimdeploy listens on
127.0.0.1:9000(or a unix socket) and is reached only through your reverse proxy, which terminates TLS. - Publish only
/hooks/. The API (/status,/history,/deploy) needs a Bearer token (api_token_env) and normally stays on localhost. nimdeploy warns at startup if it listens on a non-local address without one. - The client IP in the logs comes from
X-Forwarded-Foronly when the request came from atrusted_proxiesaddress, read right to left.
The deploy process¶
- Runs as the unprivileged user nimdeploy runs as, never as root (unless you install it that way on purpose).
- Every secret named in the config is removed from the script's environment: a compromised build step cannot read the webhook secrets or the API token from its environment.
- Runs in its own process group: on timeout the whole group gets
SIGTERMand thenSIGKILL, so no orphan build keeps running. - Started directly with
exec, no shell, unless you configure one.
Privilege separation on shared servers¶
With setup-root.sh:
root (admins) installs once; owns nginx, PHP-FPM, certificates
│
├── operator (named) SSH key, own account; sudo only to:
│ · act as the service account
│ · start/stop/restart/reload nimdeploy and pm2-deploy
│ reads logs through the systemd-journal group
│
└── deploy (service) owns code, releases, config, logs
runs nimdeploy, deploy scripts, pm2 (User=deploy units)
no password, no SSH login of its own
- Every action is traceable: sudo logs the operator's name; the journal logs each webhook and deploy; each deploy has its own log file.
- No
sudo systemctl status,journalctlorsystemctl edit: they open a pager or editor as root, from which a root shell is one keystroke away. - No root service ever executes a file owned by the service account. This
is what makes "acting as
deploy" not equivalent to root. Keep it that way when you add cron jobs, timers or units.
File permissions¶
| Path | Owner | Mode |
|---|---|---|
config.toml |
service account | 640 |
secrets.env |
service account | 600 |
~/.ssh/ deploy keys |
service account | 600, read-only keys, one per repository |
~/.config/composer/auth.json |
service account | 600 |
| app directories | service account, team group | 2775 (setgid) |
Laravel .env |
service account, PHP-FPM group | 640 |
storage/, bootstrap/cache/ |
shared through the PHP-FPM group | group-writable, PHP-FPM with UMask=0002 |
What stays your responsibility¶
Your deploy script runs your repository's code
npm install, composer install and build steps execute code from your
dependencies with the service account's permissions. Use lockfiles
(--frozen-lockfile, npm ci), approve install scripts explicitly
(pnpm's allowBuilds), and review changes to them like any other change
to the server.
- Who can push to the deploy branch can deploy. Protect that branch (reviews, required checks) and consider wait for CI.
- Secrets in git:
.envfiles,secrets.envandauth.jsonnever belong in a repository or in documentation. Rotate any secret that was ever shared in a chat or a document. - Separate environments: production gets its own webhook secrets, deploy
keys,
APP_KEYand database credentials, never reused from staging. - Migrations are not rolled back automatically; keep database snapshots.
Reporting a vulnerability¶
Please open a private security advisory on GitHub instead of a public issue.