Add OAuth2/OIDC per-user Gitea authentication
Introduce a GiteaOAuthValidator for JWT and userinfo validation and fallbacks, add /oauth/token proxy, and thread per-user tokens through the request context and automation paths. Update config and .env.example for OAuth-first mode, add OpenAPI, extensive unit/integration tests, GitHub/Gitea CI workflows, docs, and lint/test enforcement (>=80% cov).
This commit is contained in:
182
README.md
182
README.md
@@ -1,85 +1,149 @@
|
||||
# AegisGitea-MCP
|
||||
|
||||
Security-first, policy-driven MCP gateway for Gitea.
|
||||
Security-first MCP server for self-hosted Gitea with per-user OAuth2/OIDC authentication.
|
||||
|
||||
AegisGitea-MCP exposes controlled read and optional write capabilities to AI agents through MCP-compatible endpoints, with strict validation, policy enforcement, tamper-evident audit logging, and secure-by-default runtime controls.
|
||||
AegisGitea-MCP exposes MCP tools over HTTP/SSE and validates each user token against Gitea so tool access follows each user's actual repository permissions.
|
||||
|
||||
## Highlights
|
||||
## Securing MCP with Gitea OAuth
|
||||
|
||||
- Security-first defaults (localhost bind, write mode disabled, no stack traces in production errors).
|
||||
- YAML policy engine with global/per-repository tool allow/deny and optional path restrictions.
|
||||
- Expanded read tools for repositories, commits, diffs, issues, PRs, labels, tags, and releases.
|
||||
- Strict write mode (opt-in + policy enforcement, with whitelist by default).
|
||||
- Tamper-evident audit logging with hash-chain integrity validation.
|
||||
- Secret detection/sanitization for outbound payloads.
|
||||
- Structured JSON logging + Prometheus metrics.
|
||||
- Hardened Docker runtime (non-root, no-new-privileges, capability drop, read-only where practical).
|
||||
### 1) Create a Gitea OAuth2 application
|
||||
|
||||
## Quick Start
|
||||
1. Open `https://git.hiddenden.cafe/user/settings/applications` (or admin application settings).
|
||||
2. Create an OAuth2 app.
|
||||
3. Set redirect URI to the ChatGPT callback URL shown after creating a New App.
|
||||
4. Save the app and keep:
|
||||
- `Client ID`
|
||||
- `Client Secret`
|
||||
|
||||
### 1. Install dependencies
|
||||
Required scopes:
|
||||
- `read:repository`
|
||||
- `write:repository` (only needed when using write tools)
|
||||
|
||||
```bash
|
||||
make install-dev
|
||||
```
|
||||
|
||||
### 2. Configure environment
|
||||
### 2) Configure this MCP server
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Set at minimum:
|
||||
- `GITEA_URL`
|
||||
- `GITEA_TOKEN`
|
||||
- `MCP_API_KEYS`
|
||||
Set OAuth-first values:
|
||||
|
||||
### 3. Run locally
|
||||
|
||||
```bash
|
||||
make run
|
||||
```env
|
||||
GITEA_URL=https://git.hiddenden.cafe
|
||||
OAUTH_MODE=true
|
||||
GITEA_OAUTH_CLIENT_ID=<your-client-id>
|
||||
GITEA_OAUTH_CLIENT_SECRET=<your-client-secret>
|
||||
OAUTH_EXPECTED_AUDIENCE=<optional; defaults to client id>
|
||||
```
|
||||
|
||||
Server defaults to `127.0.0.1:8080`.
|
||||
### 3) Configure ChatGPT New App
|
||||
|
||||
## Core Commands
|
||||
In ChatGPT New App:
|
||||
|
||||
- `make test`: run pytest with coverage.
|
||||
- `make lint`: run Ruff + mypy.
|
||||
- `make format`: run Black + Ruff autofix.
|
||||
- `make docker-up`: start hardened prod-profile container.
|
||||
- `make docker-down`: stop containers.
|
||||
- `make validate-audit`: validate audit hash chain integrity.
|
||||
- MCP server URL: `https://<your-mcp-domain>/mcp/sse`
|
||||
- Authentication: OAuth
|
||||
- OAuth client ID: Gitea OAuth app client ID
|
||||
- OAuth client secret: Gitea OAuth app client secret
|
||||
|
||||
## Security Model
|
||||
After creation, copy the ChatGPT callback URL and add it to the Gitea OAuth app redirect URIs.
|
||||
|
||||
- Authentication: API keys (`Authorization: Bearer <key>`).
|
||||
- Authorization: policy engine (`policy.yaml`) evaluated before tool execution.
|
||||
- Rate limiting: per-IP and per-token.
|
||||
- Output controls: bounded response size and optional secret masking/blocking.
|
||||
- Write controls: `WRITE_MODE=false` by default; when enabled, use whitelist or opt into `WRITE_ALLOW_ALL_TOKEN_REPOS=true`.
|
||||
### 4) OAuth-protected MCP behavior
|
||||
|
||||
The server publishes protected-resource metadata:
|
||||
|
||||
- `GET /.well-known/oauth-protected-resource`
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"resource": "https://git.hiddenden.cafe",
|
||||
"authorization_servers": ["https://git.hiddenden.cafe"],
|
||||
"bearer_methods_supported": ["header"],
|
||||
"scopes_supported": ["read:repository", "write:repository"],
|
||||
"resource_documentation": "https://hiddenden.cafe/docs/mcp-gitea"
|
||||
}
|
||||
```
|
||||
|
||||
If a tool call is missing/invalid auth, MCP endpoints return `401` with:
|
||||
|
||||
```http
|
||||
WWW-Authenticate: Bearer resource_metadata="https://<mcp-host>/.well-known/oauth-protected-resource", scope="read:repository"
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```text
|
||||
ChatGPT App
|
||||
-> Authorization Code Flow
|
||||
-> Gitea OAuth2/OIDC (issuer: https://git.hiddenden.cafe)
|
||||
-> Access token
|
||||
-> MCP Server (/mcp/sse, /mcp/tool/call)
|
||||
-> OIDC discovery + JWKS cache
|
||||
-> Scope enforcement (read:repository / write:repository)
|
||||
-> Per-request Gitea API calls with Authorization: Bearer <user token>
|
||||
```
|
||||
|
||||
## Example curl
|
||||
|
||||
Protected resource metadata:
|
||||
|
||||
```bash
|
||||
curl -s https://<mcp-host>/.well-known/oauth-protected-resource | jq
|
||||
```
|
||||
|
||||
Expected 401 challenge when missing token:
|
||||
|
||||
```bash
|
||||
curl -i https://<mcp-host>/mcp/tool/call \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"tool":"list_repositories","arguments":{}}'
|
||||
```
|
||||
|
||||
Authenticated tool call:
|
||||
|
||||
```bash
|
||||
curl -s https://<mcp-host>/mcp/tool/call \
|
||||
-H "Authorization: Bearer <user_access_token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"tool":"list_repositories","arguments":{}}'
|
||||
```
|
||||
|
||||
## Threat model
|
||||
|
||||
- Shared bot tokens are dangerous:
|
||||
- one leaked token can expose all repositories reachable by that bot account.
|
||||
- blast radius is repository-wide and cross-user.
|
||||
- Token-in-URL is insecure:
|
||||
- URLs leak via logs, proxies, browser history, and referers.
|
||||
- bearer tokens must be sent in `Authorization` headers only.
|
||||
- Per-user OAuth reduces lateral access:
|
||||
- each call runs as the signed-in user.
|
||||
- users only see repositories they already have permission for in Gitea.
|
||||
|
||||
## CI/CD
|
||||
|
||||
Gitea workflows were added under `.gitea/workflows/`:
|
||||
|
||||
- `lint.yml`: Ruff + formatting + mypy.
|
||||
- `test.yml`: lint + pytest + enforced coverage (`>=80%`).
|
||||
- `docker.yml`: lint+test gated Docker build, SHA tag, `latest` tag on `main`.
|
||||
|
||||
## Docker hardening
|
||||
|
||||
`docker/Dockerfile` uses a multi-stage build, non-root runtime user, production env flags, minimal runtime dependencies, and a healthcheck.
|
||||
|
||||
## Commands
|
||||
|
||||
- `make test`
|
||||
- `make lint`
|
||||
- `make format`
|
||||
- `make docker-build`
|
||||
- `make docker-up`
|
||||
|
||||
## Documentation
|
||||
|
||||
All detailed docs are under `docs/`:
|
||||
|
||||
- `docs/api-reference.md`
|
||||
- `docs/policy.md`
|
||||
- `docs/security.md`
|
||||
- `docs/audit.md`
|
||||
- `docs/write-mode.md`
|
||||
- `docs/configuration.md`
|
||||
- `docs/deployment.md`
|
||||
- `docs/observability.md`
|
||||
- `docs/automation.md`
|
||||
- `docs/governance.md`
|
||||
- `docs/roadmap.md`
|
||||
- `docs/todo.md`
|
||||
|
||||
## Conduct and Governance
|
||||
|
||||
- Contributor/maintainer conduct: `CODE_OF_CONDUCT.md`
|
||||
- AI agent behavioral contract: `AGENTS.md`
|
||||
|
||||
## License
|
||||
|
||||
MIT (see `LICENSE`).
|
||||
- `docs/write-mode.md`
|
||||
|
||||
Reference in New Issue
Block a user