Files
AegisGitea-MCP/docs/deployment.md
T
Latte cf3d2e17d3 fix: fail closed on an unwritable audit log, bound the rate limiter and harden startup
The audit logger is opened at startup (exit 2 on failure) and obtained before
every call, which is refused with audit_unavailable otherwise. Audit records only
take validated path and sudo values and use the request id as correlation id. The
rate limiter evicts idle keys, caps tracked keys, audits the first excess per key
and window and uses the settings passed to the app. FORWARDED_ALLOW_IPS is now a
setting, and startup warns about a missing host allow-list and unavailable OAuth.
2026-10-14 16:24:51 +02:00

135 lines
5.9 KiB
Markdown

# Deployment
This page covers running the HTTP server for remote clients. For a local stdio setup see
[getting-started.md](getting-started.md). Read [hardening.md](hardening.md) as well: this server
hands the power of its Gitea token to every client that can authenticate.
## Choose a mode
- Admin-token only: set `GITEA_TOKEN` and `MCP_BEARER_TOKEN`. Every client with the bearer token
acts as the token owner.
- Per-user OAuth: additionally set `OAUTH_ENABLED=true` with the OAuth settings. Users sign in
with their own Gitea account and act under their own permissions. The static bearer keeps
working next to it as the admin identity; leave `MCP_BEARER_TOKEN` unset to turn that off.
## Docker
Image: `git.hiddenden.cafe/hiddenden/aegisgitea-mcp`. Tag `dev` is built from the dev branch and
`latest` from main. `uv run aegis-gitea-mcp setup` prints this compose service, with your port:
```yaml
services:
aegis-gitea-mcp:
image: git.hiddenden.cafe/hiddenden/aegisgitea-mcp:dev
restart: unless-stopped
env_file: .env
environment:
MCP_TRANSPORT: http
MCP_HOST: 0.0.0.0 # allowed because authentication is configured
DATA_DIR: /var/lib/aegis-mcp
ports:
- "127.0.0.1:8080:8080"
volumes:
- aegis-mcp-data:/var/lib/aegis-mcp
read_only: true
tmpfs:
- /tmp
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
volumes:
aegis-mcp-data:
```
Points that matter:
- Inside the container the server must bind `0.0.0.0` so the published port works. That is
allowed only because `MCP_BEARER_TOKEN` or OAuth is configured; otherwise it exits with code 2.
- The port is published on `127.0.0.1` only. The reverse proxy on the same host is the public
entry point.
- `DATA_DIR` must be a persistent volume writable by the container user. It holds the audit log
and the encrypted OAuth token store (`oauth-tokens.json`, mode 0600). Losing the volume loses
all OAuth logins and the audit history. Back it up, and protect the backup like the secrets it
protects.
- Keep `.env` out of version control and out of the image (mode 0600; `setup` writes it that
way). Compose passes it as environment, so it is visible to `docker inspect` for anyone with
Docker access.
Check it: `curl http://127.0.0.1:8080/health`.
## Reverse proxy and TLS
Terminate TLS at a reverse proxy. `PUBLIC_BASE_URL` must be the public https URL (it is required
for OAuth, and http is only accepted for loopback hosts). Mistral Le Chat needs a valid public
certificate.
Caddy:
```
gitea-mcp.example.com {
reverse_proxy 127.0.0.1:8080 {
flush_interval -1
}
}
```
nginx: inside the proxying `location`, set `proxy_buffering off;` so streamed responses are not
held back. Also forward `Host` (`proxy_set_header Host $host;`) and keep the default read timeout
in mind for long tool calls.
The server rejects requests whose `Host` is not allowed (DNS-rebinding protection). Allowed hosts
are the loopback names with the port, the host of `PUBLIC_BASE_URL`, and `ALLOWED_HOSTS`. If
you reach it under another name, add that name to `ALLOWED_HOSTS`. With a non-loopback
`MCP_HOST` (the container) and neither `PUBLIC_BASE_URL` nor `ALLOWED_HOSTS`, every remote `Host`
is rejected with 421, and the server logs a warning at startup naming the two variables.
The server trusts `X-Forwarded-For` only from `FORWARDED_ALLOW_IPS` (default `127.0.0.1`). When
the proxy reaches the container from another address, for example a Docker network, add that
address or network (`FORWARDED_ALLOW_IPS=172.18.0.0/16`). Without it every request looks like it
comes from the proxy and shares one per-IP rate limit bucket. `*` trusts any peer, so use it only
when the port is reachable solely from the proxy.
## OAuth application in Gitea
`setup --public-base-url https://gitea-mcp.example.com` registers the application for you as the
admin's user application. To do it by hand: in Gitea open Settings, Applications, Manage OAuth2
Applications, create an application named `aegis-gitea-mcp` as a confidential client with the
redirect URI `https://gitea-mcp.example.com/oauth/callback`, and put the client id and secret in
`GITEA_OAUTH_CLIENT_ID` and `GITEA_OAUTH_CLIENT_SECRET`. Then set `OAUTH_ENABLED=true`,
`PUBLIC_BASE_URL` and `TOKEN_ENCRYPTION_KEY` (at least 32 characters, `openssl rand -hex 32`).
Gitea has no instance-wide OAuth application API, so the application belongs to the admin account
that created it. Re-running setup reuses it; if the stored client secret is missing, Gitea has to
regenerate it, and setup asks before doing so.
`OAUTH_SCOPES` is the scope list sent to Gitea. Keep it to granular scopes. Narrow it (for
example to `read:repository,read:issue`) to limit what signed-in users' tokens can do through this
server regardless of their Gitea rights.
## Upgrades
- Application: pull the new image (or package) and restart. The data volume is kept.
- `TOKEN_ENCRYPTION_KEY` must stay the same, or stored OAuth logins can no longer be decrypted
and users have to sign in again.
- Upgrading Gitea: tool names and schemas come from the pinned swagger, so a Gitea upgrade does
not change them. To pick up a newer API, re-pin the swagger and regenerate the catalog:
```bash
curl -fsS https://git.example.com/swagger.v1.json -o spec/gitea-swagger.v1.json
uv run python scripts/generate_catalog.py
uv run python scripts/check_docs.py --write
uv run pytest
```
Review the diff of `tools.json` and `docs/tools.md`: renamed or removed operations change tool
names. Until the new spec is pinned, `gitea_api_request` reaches new endpoints.
## CI
The workflows in `.gitea/workflows/` lint, test and build. The `docs-check` job runs
`uv run python scripts/check_docs.py`, which fails when `docs/tools.md` or the README tool summary
is stale, or when a document names a tool that does not exist. `scripts/generate_catalog.py
--check` fails when `tools.json` does not match the pinned swagger.