Files
AegisGitea-MCP/docs/hardening.md
T

4.0 KiB

Hardening checklist

This server is full access by design (ADR 0001): whoever can call it has the power of the configured Gitea token, which for an admin token means everything. The server keeps a few invariants (audit, secret redaction, bounded output, validated input, fail-closed HTTP authentication), but it does not decide what is safe. This checklist is how you do that.

Decide what to expose

  • Create the Gitea token with the narrowest scopes that do the job. Full access needs a site-admin token with all; a read-only assistant needs only read scopes. The token is the hard limit, because Gitea enforces it.
  • Prefer per-user OAuth to a shared admin token whenever more than one person connects: each user then acts under their own Gitea permissions and admin tools fail for non-admins.
  • Narrow what is listed: READ_ONLY=true, TOOLSETS=..., DISABLED_TOOLS=.... Hide what the assistant never needs: admin, actions (secrets), hook, user (tokens, keys) and the delete tools are the sensitive ones. gitea_api_request can reach any endpoint, so on a restricted profile drop misc or disable it.
  • Keep OAUTH_SCOPES as narrow as the use case allows.
  • Do not use Sudo-capable tokens for clients that do not need to impersonate users.

Treat the credentials as keys to the instance

  • GITEA_TOKEN and MCP_BEARER_TOKEN are equivalent in power on HTTP. Generate the bearer token with openssl rand -hex 32, store it in a secret manager or a mode 0600 env file, rotate it when someone leaves, and never put it in a URL (it only goes in the Authorization header).
  • TOKEN_ENCRYPTION_KEY protects the stored per-user tokens. Keep it separate from the data volume backups. GITEA_OAUTH_CLIENT_SECRET likewise.
  • Use setup, which writes the env file with mode 0600 and never prints secrets.
  • Do not paste tokens into chat with the assistant, and do not commit .env.

Network and transport

  • Keep the default bind 127.0.0.1. Publish the container port on 127.0.0.1 and put a reverse proxy with TLS in front. A non-loopback bind refuses to start without authentication; do not work around it.
  • Terminate TLS with a valid certificate. PUBLIC_BASE_URL must be https.
  • Set ALLOWED_HOSTS to exactly the host names you serve. Requests with other Host values are rejected.
  • Restrict who can reach the endpoint at the proxy or firewall too (VPN, allow list), as a second layer in front of bearer authentication.
  • Leave RATE_LIMIT_PER_MINUTE on. Put request-size and connection limits at the proxy.
  • Use https for GITEA_URL. Plain http sends the token unencrypted (setup warns about it).

Container and host

  • Run the image as the non-root user, read_only: true with a tmpfs for /tmp, cap_drop: ALL and no-new-privileges, as the compose snippet in deployment.md does.
  • Mount only the data volume. Give it to the service account alone (mode 0700).
  • Pin the image by digest in production, and update regularly.
  • Do not run the stdio server with a token that is broader than the user's own needs on a shared machine: any process of that user can read the client configuration.

Audit and monitoring

  • Ship the audit log off the host and validate the chain regularly (audit-logging.md).
  • Alert on repeated forbidden or unauthorized outcomes and on calls to admin, secret and delete tools.
  • Keep LOG_LEVEL=INFO and EXPOSE_ERROR_DETAILS=false in production.
  • Keep SECRET_DETECTION_MODE=mask (or block). Secret detection is a safety net with regular expressions, not a guarantee.

Untrusted content

  • Keep tool-call confirmation enabled in the client for destructive tools. Prompt injection in issues, pull requests, comments and files is the realistic way a model is tricked into using the access it has.
  • Do not connect a write-capable, broad-scope profile to an assistant that also reads untrusted content from other sources.
  • Prefer separate profiles: a read-only one for browsing and triage, a write-capable one that you use deliberately.