How We Found These
Between January and February 2026, researchers scanned over 8,000 publicly reachable MCP server endpoints using Shodan and custom fingerprinting tools. Of the 1,000+ that had admin panels or API endpoints exposed, the same 5 configuration mistakes appeared in over 90% of vulnerable instances.
This isn't theoretical. These are the actual patterns found in production deployments — from solo developers to funded startups. Every fix below includes the exact config you can copy into your deployment today.
Mistake #1: Admin Panel Bound to 0.0.0.0
Found in: 94% of exposed instances
The most common mistake by far. The default docker-compose.yml in most MCP frameworks binds the admin panel to 0.0.0.0:8080, making it accessible from any IP address. Developers start the container, it works locally, and they never change the binding.
The result: your admin panel is indexed by Shodan within hours, and attackers can browse agent configurations, view tool lists, and in many cases execute actions directly.
The Fix
Bind the admin panel to localhost only. If you need remote access, use SSH tunneling or a reverse proxy with authentication.
Docker Compose fix:
# ❌ WRONG — exposed to the internet
ports:
- "8080:8080"
# ✅ CORRECT — localhost only
ports:
- "127.0.0.1:8080:8080"
If you need remote access, use SSH tunneling:
# From your local machine:
ssh -L 8080:127.0.0.1:8080 user@your-server
# Then access at http://localhost:8080
Mistake #2: No Authentication on API Endpoints
Found in: 87% of exposed instances
Even when developers are aware of the binding issue, many deploy MCP servers with API endpoints that require no authentication. The /api/execute, /api/tools, and /api/config routes are wide open. Anyone who finds the endpoint can list available tools, modify agent behavior, or trigger tool executions.
The Fix
Add API key authentication at the reverse proxy level (defense in depth). Here's a complete Caddy config:
# Caddyfile — MCP secure reverse proxy
your-mcp.example.com {
# Force HTTPS (automatic with Caddy)
# API key authentication for all routes
@api_auth {
header X-API-Key {env.MCP_API_KEY}
}
# Block requests without valid API key
@no_auth {
not header X-API-Key {env.MCP_API_KEY}
}
respond @no_auth 401 {
body "Unauthorized"
close
}
# Rate limiting
rate_limit {
zone mcp_api {
key {remote_host}
events 30
window 1m
}
}
# Proxy to local MCP server
reverse_proxy @api_auth 127.0.0.1:8080 {
header_up X-Real-IP {remote_host}
header_up X-Forwarded-Proto {scheme}
}
# Security headers
header {
X-Content-Type-Options nosniff
X-Frame-Options DENY
Referrer-Policy strict-origin-when-cross-origin
-Server
}
}
Nginx equivalent:
server {
listen 443 ssl http2;
server_name your-mcp.example.com;
# API key check
set $api_key_valid 0;
if ($http_x_api_key = "YOUR_SECRET_API_KEY") {
set $api_key_valid 1;
}
if ($api_key_valid = 0) {
return 401 "Unauthorized";
}
# Rate limiting
limit_req_zone $binary_remote_addr zone=mcp:10m rate=30r/m;
limit_req zone=mcp burst=5 nodelay;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Security headers
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
}
Mistake #3: Secrets Stored in Config Files
Found in: 76% of exposed instances
API keys for OpenAI, Anthropic, and other services are hardcoded in config.json, .env files committed to Git, or passed as plain-text environment variables in Docker Compose files. When an admin panel is exposed, attackers can read these keys directly.
In the Clawdbot incident, over 200 API keys were confirmed extracted. Some were used to run thousands of dollars in compute through victim accounts.
The Fix
Use Docker secrets or a secret manager. Never commit secrets to version control.
# docker-compose.yml with secrets
version: "3.8"
services:
mcp-server:
image: your-mcp-image:latest
ports:
- "127.0.0.1:8080:8080"
environment:
- OPENAI_API_KEY_FILE=/run/secrets/openai_key
- ANTHROPIC_API_KEY_FILE=/run/secrets/anthropic_key
secrets:
- openai_key
- anthropic_key
secrets:
openai_key:
file: ./secrets/openai_key.txt # NOT committed to Git
anthropic_key:
file: ./secrets/anthropic_key.txt # NOT committed to Git
And in your .gitignore:
# .gitignore
secrets/
.env
*.key
*.pem
For production, use a proper secret manager:
# HashiCorp Vault example
export OPENAI_API_KEY=$(vault kv get -field=key secret/mcp/openai)
# AWS Secrets Manager
export OPENAI_API_KEY=$(aws secretsmanager get-secret-value --secret-id mcp/openai-key --query SecretString --output text)
# Azure Key Vault
export OPENAI_API_KEY=$(az keyvault secret show --vault-name your-vault --name openai-key --query value -o tsv)
Mistake #4: All Tools Enabled by Default
Found in: 71% of exposed instances
Most MCP frameworks ship with all tools enabled: shell_execute, file_write, http_request, browser_navigate. Developers leave the defaults because "I might need them later." In practice, an AI agent with shell access and no allowlist is a remote code execution vulnerability.
The OWASP LLM Top 10 classifies this as LLM06 — Excessive Agency: giving an agent more permissions than its task requires.
The Fix
Explicitly allowlist only the tools your agent needs. Deny everything else.
// mcp-config.json — principle of least privilege
{
"tools": {
"mode": "allowlist",
"allowed": [
"web_search",
"read_file",
"list_directory"
],
"denied": [
"shell_execute",
"file_write",
"file_delete",
"http_request",
"browser_navigate",
"send_email"
],
"confirmation_required": [
"read_file",
"web_search"
]
},
"sandbox": {
"enabled": true,
"network": "restricted",
"filesystem": "read-only",
"max_execution_time_ms": 30000
}
}
Docker network isolation (defense in depth):
# docker-compose.yml — network segmentation
services:
mcp-server:
networks:
- mcp-internal
# No access to host network or internet
mcp-proxy:
networks:
- mcp-internal
- external
# Only the proxy can reach the internet
networks:
mcp-internal:
internal: true # No external access
external:
driver: bridge
Mistake #5: No Rate Limiting or Monitoring
Found in: 68% of exposed instances
Without rate limiting, an attacker who finds an exposed endpoint can send thousands of requests per minute — extracting data, burning API credits, or using your agent as a proxy. Without monitoring, you won't know it's happening until the invoice arrives.
The Fix
Apply rate limiting at the reverse proxy and enable audit logging.
# UFW firewall baseline
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow 443/tcp # HTTPS only
sudo ufw enable
# Verify
sudo ufw status verbose
Audit logging setup:
# docker-compose.yml — audit logging
services:
mcp-server:
environment:
- LOG_LEVEL=info
- AUDIT_LOG=true
- AUDIT_LOG_PATH=/var/log/mcp/audit.json
volumes:
- ./logs:/var/log/mcp
logging:
driver: json-file
options:
max-size: "50m"
max-file: "5"
Monitoring with a simple alert script:
#!/bin/bash
# monitor-mcp.sh — run via cron every 5 minutes
LOG="/var/log/mcp/audit.json"
THRESHOLD=100
ALERT_EMAIL="security@yourteam.com"
# Count requests in last 5 minutes
RECENT=$(jq -r 'select(.timestamp > (now - 300)) | .source_ip' "$LOG" \
| sort | uniq -c | sort -rn | head -1)
COUNT=$(echo "$RECENT" | awk '{print $1}')
IP=$(echo "$RECENT" | awk '{print $2}')
if [ "$COUNT" -gt "$THRESHOLD" ]; then
echo "ALERT: $COUNT requests from $IP in 5 min" | \
mail -s "[MCP] Rate abuse detected" "$ALERT_EMAIL"
# Auto-block
sudo ufw deny from "$IP"
fi
The Secure-by-Default Deployment Template
Here's a complete docker-compose.yml that fixes all 5 mistakes at once. Copy this and customize for your stack:
version: "3.8"
services:
# ── MCP Server ──────────────────────────────────
mcp-server:
image: your-mcp-image:latest
restart: unless-stopped
ports:
- "127.0.0.1:8080:8080" # Fix #1: localhost only
environment:
- OPENAI_API_KEY_FILE=/run/secrets/openai_key # Fix #3: secrets
- TOOL_MODE=allowlist # Fix #4: least privilege
- ALLOWED_TOOLS=web_search,read_file
- LOG_LEVEL=info
- AUDIT_LOG=true # Fix #5: monitoring
secrets:
- openai_key
- anthropic_key
networks:
- mcp-internal
volumes:
- ./logs:/var/log/mcp
logging:
driver: json-file
options:
max-size: "50m"
max-file: "5"
healthcheck:
test: ["CMD", "curl", "-f", "http://127.0.0.1:8080/health"]
interval: 30s
timeout: 5s
retries: 3
# ── Reverse Proxy (Caddy) ──────────────────────
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "443:443"
- "80:80"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
networks:
- mcp-internal
- external
environment:
- MCP_API_KEY_FILE=/run/secrets/mcp_api_key # Fix #2: auth
secrets:
- mcp_api_key
# ── Secrets ─────────────────────────────────────
secrets:
openai_key:
file: ./secrets/openai_key.txt
anthropic_key:
file: ./secrets/anthropic_key.txt
mcp_api_key:
file: ./secrets/mcp_api_key.txt
# ── Networks ────────────────────────────────────
networks:
mcp-internal:
internal: true # No external access
external:
driver: bridge
# ── Volumes ─────────────────────────────────────
volumes:
caddy_data:
Audit Your Stack in 30 Minutes
Run through this checklist right now. It takes 30 minutes and covers the 5 critical mistakes.
- Check your port bindings — run
docker compose psand look for0.0.0.0. Fix any public bindings. - Test authentication —
curl http://YOUR_IP:8080/api/toolsfrom an external machine. If it returns data, you're exposed. - Search for hardcoded secrets — run
grep -r "sk-" . --include="*.json" --include="*.yml" --include="*.env"in your project. - List enabled tools — check your MCP config for
shell_execute,file_write,http_request. Disable any you don't need. - Check your firewall — run
sudo ufw status. Ensure only SSH and HTTPS are allowed. - Verify rate limiting — send 50 rapid requests to your API. If they all succeed, add rate limiting.
- Check Shodan — search for your IP at shodan.io. See what's visible to attackers.
- Review logs — check for unusual request patterns in the last 7 days.
If you found issues, fix them now. If you want the full interactive checklist with progress tracking:
Get the complete 10-point hardening checklist here
Subscribe to the weekly security digest for new threats and hardening guides.
