106 lines
5.4 KiB
Markdown
106 lines
5.4 KiB
Markdown
# deploy-agent
|
|
|
|
**Optional.** This is a host-level self-update mechanism for a specific kind
|
|
of production deployment (own VPS, git-based deploy, no external
|
|
orchestrator). It is not required to run this project — `docker compose up`
|
|
alone is a complete base install, and the Settings → Обновления tab in the
|
|
CRM (`internal/selfupdate`) simply won't do anything if `deploy-agent` isn't
|
|
installed (`DEPLOY_AGENT_TOKEN` unset → those API routes 503, fail-closed).
|
|
Skip this directory entirely unless you specifically want the in-app
|
|
"check/apply update" button.
|
|
|
|
Standalone host process (not a Docker service, not part of `backend`'s Go
|
|
module) that runs `git fetch`/`git pull`/`docker compose build`/`docker
|
|
compose up` for `/root/production` on request, over a Unix domain socket.
|
|
Exists so the CRM's "Обновления" tab can offer a check/apply-update button
|
|
without giving the `backend` container a `docker.sock` mount or a copy of
|
|
this repo's `.git` — either would turn a compromised backend process into
|
|
full host control. Only the one socket file this agent listens on gets
|
|
bind-mounted into `backend` (see `../docker-compose.yml`'s `backend.volumes`
|
|
and `DEPLOY_AGENT_SOCKET_PATH` in `.env`); no TCP port, nothing else about
|
|
the host reachable through it.
|
|
|
|
## What it does
|
|
|
|
- `GET /check` — `git fetch` + list commits between `HEAD` and
|
|
`origin/<branch>`. Read-only.
|
|
- `POST /apply` — fetch, fast-forward-only pull, `docker compose build`,
|
|
`docker compose up -d` for `backend`+`web` only (never `postgres`/`minio`/
|
|
`telegram-bot-api` — narrower blast radius, faster). Serialized: a second
|
|
`/apply` while one is running gets a 409, not a second concurrent deploy
|
|
stepping on the same working tree.
|
|
|
|
Both require `Authorization: Bearer <DEPLOY_AGENT_TOKEN>`.
|
|
|
|
## Install
|
|
|
|
```bash
|
|
cd /root/production/deploy-agent
|
|
go build -o deploy-agent .
|
|
cp deploy-agent.service /etc/systemd/system/
|
|
systemctl daemon-reload
|
|
systemctl enable --now deploy-agent
|
|
journalctl -u deploy-agent -f # confirm it's listening, agent.sock now exists
|
|
```
|
|
|
|
`agent.env` (this directory) already holds a generated `DEPLOY_AGENT_TOKEN`
|
|
— the *same* value must be set as `DEPLOY_AGENT_TOKEN` in `../.env` so
|
|
`backend` can authenticate to the socket. Neither `agent.env` nor the
|
|
compiled `deploy-agent` binary nor `agent.sock` are committed (see
|
|
`../.gitignore`).
|
|
|
|
**Order matters**: `../docker-compose.yml` bind-mounts the *specific file*
|
|
`deploy-agent/agent.sock` into `backend` (not the whole directory — see that
|
|
file's own comment on why), and Docker can only bind-mount a path that
|
|
already exists at container start. So:
|
|
|
|
1. Install/start `deploy-agent` first (above) — this creates `agent.sock`.
|
|
2. Only then `docker compose up -d` (or restart) `backend`.
|
|
|
|
If `backend` was already running before `agent.sock` existed, its container
|
|
was created without that mount — `docker compose up -d backend` again
|
|
(after the agent is up) to recreate it with the socket attached; a plain
|
|
`restart` of an existing container won't pick up a new bind mount.
|
|
|
|
## Security model, and what it does NOT protect against
|
|
|
|
- Runs as root, same as everything else on this host today (see
|
|
`deploy-agent.service`'s own comment). The actual boundary is: only a
|
|
process that can (a) reach `agent.sock` on the filesystem AND (b) present
|
|
the bearer token can trigger a deploy. On this host that's exactly the
|
|
`backend` container (via the bind mount) — nothing else.
|
|
- `backend` itself gates both endpoints behind `auth.RequireRole("owner")`
|
|
(see `internal/selfupdate`) — a manager/master token can't reach this even
|
|
indirectly, regardless of what's in the granular permissions system core
|
|
issues (deliberately not reusing that system here — this is more
|
|
sensitive than any existing permission, a hardcoded role check is a
|
|
smaller, easier-to-audit surface than "whichever role happens to have this
|
|
checkbox ticked").
|
|
- Does not protect against: a compromised `backend` container's own process
|
|
triggering deploys (it holds the token by design, so it can). If that
|
|
container's trust boundary is ever a concern, the next hardening step is
|
|
moving `agent.env`'s token to something backend fetches per-request from a
|
|
secrets manager instead of a static env var — not done here, out of scope
|
|
for the MVP this ships as.
|
|
- Does not protect against a bad commit on `origin/main` — `/apply` pulls
|
|
and deploys whatever's there, same as a human running `git pull &&
|
|
docker compose up -d --build` would. This button doesn't run tests or
|
|
gate on CI status; it's exactly the manual deploy step, just one click
|
|
instead of an SSH session.
|
|
- `/apply` only rebuilds and restarts `backend`+`web` (see
|
|
`DEPLOY_AGENT_COMPOSE_SERVICES`) — it never rebuilds *this agent itself*.
|
|
A commit that changes `deploy-agent/main.go` still gets pulled onto disk,
|
|
but the already-running agent process keeps executing its old compiled
|
|
behavior until someone manually does `go build -o deploy-agent . &&
|
|
systemctl restart deploy-agent`. Self-restarting mid-request is more
|
|
complexity than this MVP is worth; if deploy-agent's own logic changes,
|
|
that's the one case still needing a manual step.
|
|
|
|
## Follow-up hardening ideas (not implemented)
|
|
|
|
- Dedicated non-root `deploy` user with a narrow sudoers entry for exactly
|
|
`git -C /root/production ...` and `docker compose ...` in this directory,
|
|
instead of running the whole agent as root.
|
|
- Rotate `DEPLOY_AGENT_TOKEN` periodically (systemd timer + restart both
|
|
the agent and `backend`).
|