docs: Add documentation site and API reference
This commit is contained in:
155
docs/security.md
Normal file
155
docs/security.md
Normal file
@@ -0,0 +1,155 @@
|
||||
# Security
|
||||
|
||||
## Authentication
|
||||
|
||||
AegisGitea MCP uses bearer token authentication. Clients must include a valid API key with every tool call.
|
||||
|
||||
### How It Works
|
||||
|
||||
1. The client sends `Authorization: Bearer <key>` with its request.
|
||||
2. The server extracts the token and validates it against the configured `MCP_API_KEYS`.
|
||||
3. Comparison is done in **constant time** to prevent timing attacks.
|
||||
4. If validation fails, the failure is counted against the client's IP address.
|
||||
|
||||
### Generating API Keys
|
||||
|
||||
Use the provided script to generate cryptographically secure 64-character hex keys:
|
||||
|
||||
```bash
|
||||
make generate-key
|
||||
# or: python scripts/generate_api_key.py
|
||||
```
|
||||
|
||||
Keys must be at least 32 characters long. The script also saves metadata (creation date, expiration) to a `keys/` directory.
|
||||
|
||||
### Multiple Keys (Grace Period During Rotation)
|
||||
|
||||
You can configure multiple keys separated by commas. This allows you to add a new key and remove the old one without downtime:
|
||||
|
||||
```env
|
||||
MCP_API_KEYS=newkey...,oldkey...
|
||||
```
|
||||
|
||||
Remove the old key from the list after all clients have been updated.
|
||||
|
||||
---
|
||||
|
||||
## Key Rotation
|
||||
|
||||
Rotate keys regularly (recommended: every 90 days).
|
||||
|
||||
```bash
|
||||
make rotate-key
|
||||
# or: python scripts/rotate_api_key.py
|
||||
```
|
||||
|
||||
The rotation script:
|
||||
1. Reads the current key from `.env`
|
||||
2. Generates a new key
|
||||
3. Offers to replace the key immediately or add it alongside the old key (grace period)
|
||||
4. Creates a backup of your `.env` before modifying it
|
||||
|
||||
### Checking Key Age
|
||||
|
||||
```bash
|
||||
make check-key-age
|
||||
# or: python scripts/check_key_age.py
|
||||
```
|
||||
|
||||
Exit codes: `0` = OK, `1` = expiring within 7 days (warning), `2` = already expired (critical).
|
||||
|
||||
---
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
Failed authentication attempts are tracked per client IP address.
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---|---|---|
|
||||
| `MAX_AUTH_FAILURES` | `5` | Maximum failures before the IP is blocked |
|
||||
| `AUTH_FAILURE_WINDOW` | `300` | Rolling window in seconds |
|
||||
|
||||
Once an IP exceeds the threshold, all further requests from that IP return HTTP 429 until the window resets. This is enforced entirely in memory — a server restart resets the counters.
|
||||
|
||||
---
|
||||
|
||||
## Audit Logging
|
||||
|
||||
All security-relevant events are written to a structured JSON log file.
|
||||
|
||||
### Log Location
|
||||
|
||||
Default: `/var/log/aegis-mcp/audit.log`
|
||||
Configurable via `AUDIT_LOG_PATH`.
|
||||
|
||||
The directory is created automatically on startup.
|
||||
|
||||
### What Is Logged
|
||||
|
||||
| Event | Description |
|
||||
|---|---|
|
||||
| Tool invocation | Every call to a tool: tool name, arguments, result status, correlation ID |
|
||||
| Access denied | Failed authentication attempts: IP address, reason |
|
||||
| Security event | Rate limit triggers, invalid key formats, startup authentication status |
|
||||
|
||||
### Log Format
|
||||
|
||||
Each entry is a JSON object on a single line:
|
||||
|
||||
```json
|
||||
{
|
||||
"timestamp": "2026-02-13T10:00:00Z",
|
||||
"event": "tool_invocation",
|
||||
"correlation_id": "a1b2c3d4-...",
|
||||
"tool": "get_file_contents",
|
||||
"owner": "myorg",
|
||||
"repo": "backend",
|
||||
"path": "src/main.py",
|
||||
"result": "success",
|
||||
"client_ip": "10.0.0.1"
|
||||
}
|
||||
```
|
||||
|
||||
### Using Logs for Monitoring
|
||||
|
||||
Because entries are newline-delimited JSON, they are easy to parse:
|
||||
|
||||
```bash
|
||||
# Show all failed tool calls
|
||||
grep '"result": "error"' /var/log/aegis-mcp/audit.log | jq .
|
||||
|
||||
# Show all access-denied events
|
||||
grep '"event": "access_denied"' /var/log/aegis-mcp/audit.log | jq .
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Access Control Model
|
||||
|
||||
AegisGitea MCP does **not** implement its own repository access control. Access to repositories is determined entirely by the Gitea bot user's permissions:
|
||||
|
||||
- If the bot user has no access to a repository, it will not appear in `list_repositories` and `get_repository_info` will return an error.
|
||||
- Grant the bot user the minimum set of repository permissions needed.
|
||||
|
||||
**Principle of least privilege:** create a dedicated bot user and grant it read-only access only to the repositories that the AI needs to see.
|
||||
|
||||
---
|
||||
|
||||
## Network Security Recommendations
|
||||
|
||||
- Run the MCP server behind a reverse proxy (e.g. Traefik or nginx) with TLS.
|
||||
- Do not expose the server directly on a public port without TLS.
|
||||
- Restrict inbound connections to known AI client IP ranges where possible.
|
||||
- The `/mcp/tools` endpoint is intentionally public (required for ChatGPT plugin discovery). If this is undesirable, restrict it at the network/proxy level.
|
||||
|
||||
---
|
||||
|
||||
## Container Security
|
||||
|
||||
The provided Docker image runs with:
|
||||
|
||||
- A non-root user (`aegis`, UID 1000)
|
||||
- `no-new-privileges` security option
|
||||
- CPU and memory resource limits (1 CPU, 512 MB)
|
||||
|
||||
See [Deployment](deployment.md) for details.
|
||||
Reference in New Issue
Block a user