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.
135 lines
5.9 KiB
Markdown
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.
|