Skip to content

Commit 5deeff5

Browse files
MimoJanraclaude
andcommitted
fix: security hardening and health check for v2.0.1
- Add GET /health endpoint (no auth) for Docker/K8s probes - Fix audit bypass: notification-style requests now always logged - Fix CORS default: disabled by default, warn on startup if unset - Fix /health method restriction: GET/HEAD only, 405 otherwise - Fix Docker HEALTHCHECK: /sse (401 with auth) -> /health - Update docs to prefer MCP_AUTH_TOKENS=name:token,... format - Update K8s probes in docs to use /health Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ace5a6c commit 5deeff5

11 files changed

Lines changed: 69 additions & 35 deletions

File tree

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [2.0.1] - 2026-06-01 - Security & Health Check Fixes
11+
12+
### Added
13+
- `GET /health` — unauthenticated liveness probe endpoint; returns `200 ok`. Used by Docker `HEALTHCHECK`, Kubernetes liveness/readiness probes, and uptime monitors. Does not expose any server state.
14+
15+
### Fixed
16+
- **Audit log bypass** — notification-style JSON-RPC requests (no `id` field) were silently skipped by the audit middleware. A client could send `tools/call` without an `id` and the tool would execute without being logged. All requests are now audited regardless of notification status; notification entries get `status: "notification"`.
17+
- **Docker `HEALTHCHECK`** — was calling unauthenticated `GET /sse`, which returns `401` when `MCP_AUTH_TOKENS` is configured. Now calls `GET /health`.
18+
- **CORS wildcard default**`CORS_ALLOWED_ORIGIN` now defaults to `""` (disabled) instead of `"*"`. Operators must explicitly set the origin. A startup INFO log explains the setting and suggests `https://claude.ai` for browser-based access. Claude Desktop and CLI tools (mcp-remote) are unaffected — they do not enforce browser CORS policy.
19+
- **`/health` method restriction** — previously accepted any HTTP method and returned `200`. Now only `GET` and `HEAD` are accepted; other methods return `405 Method Not Allowed`.
20+
21+
### Changed
22+
- All documentation updated to prefer `MCP_AUTH_TOKENS=name:token,...` over the legacy `MCP_AUTH_TOKEN` single-token variable in shared server examples.
23+
- Kubernetes liveness/readiness probes in docs updated to use `/health` instead of `/sse` or `/messages`.
24+
1025
## [2.0.0] - 2026-05-31 - MCP Protocol Improvements
1126

1227
### Added

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,8 @@ cat > .env << EOF
185185
ALLURE_BASE_URL=https://your-testops.com
186186
PORT=3000
187187
LOG_LEVEL=INFO
188-
MCP_AUTH_TOKEN=generate-random-string-here
188+
# Preferred for teams: name:token pairs (each user gets their own token)
189+
MCP_AUTH_TOKENS=artem:$(openssl rand -hex 16),ivan:$(openssl rand -hex 16)
189190
EOF
190191
```
191192

@@ -406,7 +407,7 @@ Or deploy with:
406407
- Default mode for Claude Desktop
407408

408409
### Team Shared Server (HTTP Mode)
409-
**Always set `MCP_AUTH_TOKEN` in production:**
410+
**Always set `MCP_AUTH_TOKENS` in production:**
410411
- Use HTTPS (or ngrok) to encrypt credentials
411412
- Set concrete CORS origin (not `*`)
412413
- Place behind reverse proxy (nginx, Caddy)

cmd/server/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ func runHTTP(mcpServer *mcp.Server, cfg *config.Config, logger *core.Logger) {
7878
if len(cfg.Users) == 0 {
7979
logger.Warn("no auth tokens configured (MCP_AUTH_TOKENS / MCP_AUTH_TOKEN) — server accepts unauthenticated requests", nil)
8080
}
81+
if cfg.CORSAllowOrigin == "" {
82+
logger.Info("CORS not configured — fine for Claude Desktop and CLI tools; set CORS_ALLOWED_ORIGIN=https://claude.ai if connecting via a web browser", nil)
83+
}
8184

8285
mux := http.NewServeMux()
8386
mux.HandleFunc("/health", mcpServer.HandleHealth)

docs/API.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,7 @@ data: {"jsonrpc":"2.0","id":1,"result":{"content":[...]}}
501501
```
502502

503503
**Headers:**
504-
- `Authorization: Bearer <MCP_AUTH_TOKEN>` (if `MCP_AUTH_TOKEN` is set)
504+
- `Authorization: Bearer <your-token>` — required when `MCP_AUTH_TOKENS` is configured on the server
505505

506506
---
507507

@@ -539,7 +539,7 @@ Content-Length: 0
539539
The actual response is delivered to the SSE stream.
540540

541541
**Headers:**
542-
- `Authorization: Bearer <MCP_AUTH_TOKEN>` (if required)
542+
- `Authorization: Bearer <your-token>` required when `MCP_AUTH_TOKENS` is configured on the server
543543
- `Content-Type: application/json`
544544

545545
---
@@ -760,7 +760,7 @@ asyncio.run(main())
760760
| `200` | OK (SSE stream opened) | GET /sse |
761761
| `202` | Accepted (request queued) | POST /messages |
762762
| `400` | Bad Request | Missing sessionId, invalid JSON |
763-
| `401` | Unauthorized | Missing/invalid `MCP_AUTH_TOKEN` |
763+
| `401` | Unauthorized | Missing or invalid bearer token |
764764
| `404` | Not Found | Unknown sessionId |
765765
| `500` | Server Error | Crash, internal bug |
766766

docs/INSTALLATION.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ Run the container:
126126
docker run \
127127
-e ALLURE_BASE_URL=https://your-allure.com \
128128
-e ALLURE_TOKEN=your_token_here \
129-
-e MCP_AUTH_TOKEN=your_shared_secret \
129+
-e MCP_AUTH_TOKENS=alice:token-for-alice,bob:token-for-bob \
130130
-e LOG_LEVEL=INFO \
131131
-p 3000:3000 \
132132
allure-mcp
@@ -213,11 +213,11 @@ spec:
213213
secretKeyRef:
214214
name: allure-secrets
215215
key: token
216-
- name: MCP_AUTH_TOKEN
216+
- name: MCP_AUTH_TOKENS
217217
valueFrom:
218218
secretKeyRef:
219219
name: mcp-secrets
220-
key: auth-token
220+
key: auth-tokens
221221
livenessProbe:
222222
httpGet:
223223
path: /health

docs/QUICKSTART.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,12 @@ docker build -t allure-mcp .
5858
docker run -d \
5959
-e ALLURE_BASE_URL=https://your-allure.com \
6060
-e ALLURE_TOKEN=your_token_here \
61-
-e MCP_AUTH_TOKEN=your_shared_secret \
61+
-e MCP_AUTH_TOKENS=alice:token-for-alice,bob:token-for-bob \
6262
-p 3000:3000 \
6363
allure-mcp --http
6464

65-
# 3. Test
66-
curl -H "Authorization: Bearer your_shared_secret" \
65+
# 3. Test (use any configured user's token)
66+
curl -H "Authorization: Bearer token-for-alice" \
6767
http://localhost:3000/sse
6868

6969
```

docs/SECURITY.md

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,27 +23,30 @@ Security considerations and best practices for Allure MCP Server.
2323

2424
### HTTP Mode (Team/Server)
2525

26-
**Always set `MCP_AUTH_TOKEN` in production.**
26+
**Always set `MCP_AUTH_TOKENS` in production** (preferred for teams — each user gets their own named token):
2727

2828
```bash
29-
# Generate a strong random token
30-
openssl rand -base64 32
29+
# Generate a strong random token per user
30+
openssl rand -hex 32
3131

32-
# Export to .env
33-
MCP_AUTH_TOKEN=your_generated_token_here
32+
# Set in .env — format: name:token,name:token,...
33+
MCP_AUTH_TOKENS=artem:token1,ivan:token2,qa:token3
3434
```
3535

36-
Clients must include the token:
36+
Each client authenticates with their own token:
3737

3838
```bash
39-
curl -H "Authorization: Bearer $MCP_AUTH_TOKEN" http://localhost:3000/sse
39+
curl -H "Authorization: Bearer $MY_TOKEN" http://localhost:3000/sse
4040
```
4141

42-
The token is checked on:
43-
- `GET /sse` — SSE stream endpoint
44-
- `POST /messages` — Message submission endpoint
42+
The token is checked on every authenticated endpoint (`/sse`, `/messages`, `/mcp`). `/health` is exempt.
43+
44+
**Legacy single-token mode** (backward-compatible, treated as user `"default"`):
45+
```bash
46+
MCP_AUTH_TOKEN=your_generated_token_here
47+
```
4548

46-
**Note:** Token is case-sensitive and checked with Bearer scheme.
49+
**Note:** Tokens are case-sensitive and checked with the Bearer scheme.
4750

4851
---
4952

@@ -238,7 +241,7 @@ type: Opaque
238241
stringData:
239242
ALLURE_BASE_URL: https://allure.example.com
240243
ALLURE_TOKEN: your_token_here
241-
MCP_AUTH_TOKEN: your_mcp_secret
244+
MCP_AUTH_TOKENS: alice:token1,bob:token2
242245
```
243246

244247
Mount in pod:
@@ -261,13 +264,13 @@ env:
261264
3. Restart server with new token
262265
4. Revoke old token in Allure UI
263266

264-
**MCP Auth Token:**
267+
**MCP Auth Tokens:**
265268

266-
1. Generate new token: `openssl rand -base64 32`
267-
2. Update `MCP_AUTH_TOKEN` in `.env` or secret manager
269+
1. Generate new token for the user: `openssl rand -hex 32`
270+
2. Update `MCP_AUTH_TOKENS` in `.env` or secret manager (replace only that user's entry)
268271
3. Restart server
269-
4. Notify team of new token
270-
5. Clients must update their config
272+
4. Notify the affected user of their new token
273+
5. Other users' tokens remain valid — no team-wide disruption
271274

272275
---
273276

@@ -361,9 +364,9 @@ Send logs to centralized system:
361364
### Before Production Deployment
362365

363366
- [ ] **Authentication**
364-
- [ ] `MCP_AUTH_TOKEN` is set to strong random value
365-
- [ ] Token is stored in secret manager (Vault, AWS Secrets Manager, etc.)
366-
- [ ] Token rotated monthly
367+
- [ ] `MCP_AUTH_TOKENS` is set with strong per-user tokens
368+
- [ ] Tokens are stored in secret manager (Vault, AWS Secrets Manager, etc.)
369+
- [ ] Tokens rotated periodically per user
367370

368371
- [ ] **Network**
369372
- [ ] HTTPS/TLS enabled

internal/config/config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const (
1515
maxTimeoutSec = 600
1616
defaultPort = ":3000"
1717
defaultLogLevel = "INFO"
18-
defaultCORSOrigin = "*"
18+
defaultCORSOrigin = "" // disabled by default; set CORS_ALLOWED_ORIGIN explicitly in production
1919
defaultAuditRetentionDays = 30
2020
defaultAuditLogPath = "audit"
2121
)

internal/config/config_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ func TestLoad_Defaults(t *testing.T) {
5858
if cfg.LogLevel != "INFO" {
5959
t.Errorf("default log level = %q", cfg.LogLevel)
6060
}
61-
if cfg.CORSAllowOrigin != "*" {
61+
if cfg.CORSAllowOrigin != "" {
6262
t.Errorf("default CORS = %q", cfg.CORSAllowOrigin)
6363
}
6464
}

internal/mcp/middleware.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,18 @@ func panicRecoveryMiddleware(logger *core.Logger) middlewareFunc {
4747
func auditMiddleware(auditLog *audit.Logger) middlewareFunc {
4848
return func(next requestHandler) requestHandler {
4949
return func(ctx context.Context, req *JSONRPCRequest) *JSONRPCResponse {
50-
if auditLog == nil || req.IsNotification() {
50+
if auditLog == nil {
5151
return next(ctx, req)
5252
}
53+
// Always audit, even notification-style requests (id=null), so that
54+
// omitting the id field cannot be used to silently skip the audit log.
5355
start := time.Now()
56+
isNotif := req.IsNotification()
5457
resp := next(ctx, req)
5558
status := "ok"
56-
if resp != nil && resp.Error != nil {
59+
if isNotif {
60+
status = "notification"
61+
} else if resp != nil && resp.Error != nil {
5762
status = "error"
5863
}
5964
entry := audit.Entry{
@@ -73,6 +78,9 @@ func auditMiddleware(auditLog *audit.Logger) middlewareFunc {
7378
}
7479
}
7580
auditLog.Write(entry)
81+
if isNotif {
82+
return nil
83+
}
7684
return resp
7785
}
7886
}

0 commit comments

Comments
 (0)