Files
haproxy-mcp/CLAUDE.md
kaffa dbacb86d60 feat: Add certificate management tools (6 new MCP tools)
New tools for SSL/TLS certificate management via acme.sh:
- haproxy_list_certs: List all certificates with expiry info
- haproxy_cert_info: Get detailed certificate info (expiry, issuer, SANs)
- haproxy_issue_cert: Issue new certificate via Cloudflare DNS validation
- haproxy_renew_cert: Renew specific certificate (with force option)
- haproxy_renew_all_certs: Renew all certificates due for renewal
- haproxy_delete_cert: Delete certificate from acme.sh and HAProxy

Features:
- Automatic PEM deployment to HAProxy certs directory
- HAProxy hot-reload after certificate changes (USR2 signal)
- Cloudflare DNS validation with CF_Token support
- Wildcard certificate support

Total MCP tools: 22 → 28

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 03:55:26 +00:00

428 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
HAProxy Dynamic Load Balancer Management Suite - a system for runtime management of HAProxy without restarts. Provides MCP interface for dynamic domain/server management. Frontend supports HTTP/HTTPS/HTTP3, backends use HTTP only (SSL termination at HAProxy).
**Key Feature:** Zero-reload domain management using HAProxy map files and pool backends.
## Architecture
```
┌──────────────────────────────────────────────────┐
│ HAProxy (host network) │
│ TCP 80/443 (HTTP/1.1, HTTP/2) + UDP 443 (HTTP/3) │
│ Runtime API: localhost:9999 │
│ MCP Proxy: mcp.inouter.com → localhost:8000 │
└──────────────┬───────────────────────────────────┘
┌─────▼─────┐
│ MCP │
│ Server │
│ :8000 │
└───────────┘
```
### Domain Routing Flow
```
Request → HAProxy Frontend
domains.map lookup (Host header)
pool_N backend (N = 1-100)
Server (from servers.json, auto-restored on startup)
```
## Services
### HAProxy (Podman Quadlet)
```bash
systemctl start haproxy # Start
systemctl stop haproxy # Stop
systemctl restart haproxy # Restart
systemctl status haproxy # Status
```
- Quadlet config: `/etc/containers/systemd/haproxy.container`
- Network mode: `host` (direct access to host services)
### MCP Server (systemd)
```bash
systemctl start haproxy-mcp # Start
systemctl stop haproxy-mcp # Stop
systemctl restart haproxy-mcp # Restart
journalctl -u haproxy-mcp -f # Logs
```
- Transport: `streamable-http`
- Internal: `http://localhost:8000/mcp`
- External: `https://mcp.inouter.com/mcp` (via HAProxy)
- **Auto-restore:** Servers restored from `servers.json` on startup
### Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `MCP_HOST` | `0.0.0.0` | MCP server bind host |
| `MCP_PORT` | `8000` | MCP server port |
| `HAPROXY_HOST` | `localhost` | HAProxy Runtime API host |
| `HAPROXY_PORT` | `9999` | HAProxy Runtime API port |
| `HAPROXY_STATE_FILE` | `/opt/haproxy/data/servers.state` | State file path |
| `HAPROXY_MAP_FILE` | `/opt/haproxy/conf/domains.map` | Map file path |
| `HAPROXY_SERVERS_FILE` | `/opt/haproxy/conf/servers.json` | Servers config path |
| `HAPROXY_POOL_COUNT` | `100` | Number of pool backends |
| `HAPROXY_MAX_SLOTS` | `10` | Max servers per pool |
| `HAPROXY_CONTAINER` | `haproxy` | Podman container name |
| `LOG_LEVEL` | `INFO` | Logging level (DEBUG/INFO/WARNING/ERROR) |
## Zero-Reload Domain Management
### How It Works
1. **domains.map**: Maps domain → pool backend (e.g., `example.com pool_1`)
2. **Pool backends**: 100 pre-configured backends (`pool_1` to `pool_100`)
3. **Runtime API**: Add/remove map entries and configure servers without reload
4. **servers.json**: Persistent server configuration, auto-restored on MCP startup
### Adding a Domain
```bash
haproxy_add_domain("example.com", "10.0.0.1")
# Creates: pool_N_1 → 10.0.0.1:80
haproxy_add_domain("api.example.com", "10.0.0.1", 8080)
# Creates: pool_N_1 → 10.0.0.1:8080
```
This will:
1. Find available pool (e.g., `pool_5`)
2. Add to `domains.map`: `example.com pool_5`
3. Update HAProxy runtime map: `add map ... example.com pool_5`
4. Configure server in pool via Runtime API (HTTP only)
5. Save to `servers.json` for persistence
**No HAProxy reload required!**
### Backend Configuration
- Backends always use HTTP (port 80 or custom)
- SSL/TLS termination happens at HAProxy frontend
- Each pool has 10 server slots (pool_N_1 to pool_N_10)
- **IPv6 supported**: Both IPv4 and IPv6 addresses accepted
### Bulk Server Operations
```bash
# Add multiple servers at once
haproxy_add_servers("api.example.com", '[
{"slot": 1, "ip": "10.0.0.1", "http_port": 80},
{"slot": 2, "ip": "10.0.0.2", "http_port": 80},
{"slot": 3, "ip": "2001:db8::1", "http_port": 80}
]')
```
### Files
| File | Purpose |
|------|---------|
| `domains.map` | Domain → Pool mapping |
| `servers.json` | Server IP/port persistence |
| `haproxy.cfg` | Pool backend definitions (static) |
## SSL/TLS Certificates
### Current Setup
| Item | Value |
|------|-------|
| ACME Client | acme.sh (Google Trust Services CA) |
| Cert Directory | `/opt/haproxy/certs/` (auto-loaded by HAProxy) |
| acme.sh Home | `~/.acme.sh/` |
### How It Works
1. HAProxy binds with `crt /etc/haproxy/certs/` (directory, not file)
2. All `.pem` files in directory are auto-loaded
3. SNI selects correct certificate based on domain
4. acme.sh deploy hook auto-creates combined PEM on issue/renewal
### Adding New Certificate
```bash
# Issue certificate (Google Trust Services)
~/.acme.sh/acme.sh --issue \
--dns dns_cf \
-d "newdomain.com" -d "*.newdomain.com" \
--reloadcmd "cat ~/.acme.sh/newdomain.com_ecc/fullchain.cer ~/.acme.sh/newdomain.com_ecc/newdomain.com.key > /opt/haproxy/certs/newdomain.com.pem && podman exec haproxy kill -USR2 1"
```
### Certificate Commands
```bash
# List certificates
~/.acme.sh/acme.sh --list
# Renew all
~/.acme.sh/acme.sh --cron
# Force renew specific domain
~/.acme.sh/acme.sh --renew -d "domain.com" --force
# Check loaded certs in HAProxy
ls -la /opt/haproxy/certs/
```
### Cloudflare Credentials
- File: `~/.secrets/cloudflare.ini`
- Format: `dns_cloudflare_api_token = TOKEN`
- Also export: `export CF_Token="your_token"`
## HTTP/3 (QUIC)
Frontend supports all protocols:
```
TCP 443: HTTP/1.1, HTTP/2
UDP 443: HTTP/3 (QUIC)
```
Clients receive `alt-svc: h3=":443"; ma=86400` header for HTTP/3 upgrade.
## MCP Security
HAProxy proxies MCP with Bearer Token authentication (configured in frontend ACL).
### Access Rules
| Source | Token Required |
|--------|----------------|
| Tailscale (100.64.0.0/10) | No |
| External (CF Worker, etc.) | Yes |
### Token Location
- File: `/opt/haproxy/conf/mcp-token.env`
### Connection Examples
**Tailscale (no auth):**
```bash
claude mcp add --transport http haproxy http://100.108.39.107:8000/mcp
```
**External (with auth):**
```bash
curl -X POST https://mcp.inouter.com/mcp \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}},"id":0}'
```
## Health Check
### System Health (`haproxy_health`)
Returns overall system status:
```json
{
"status": "healthy",
"components": {
"mcp": {"status": "ok"},
"haproxy": {"status": "ok", "version": "3.3.2", "uptime_sec": 3600},
"config_files": {"status": "ok", "files": {"map_file": "ok", "servers_file": "ok"}},
"container": {"status": "ok", "state": "running"}
}
}
```
### Domain Health (`haproxy_domain_health`)
Returns backend server status for a specific domain:
```json
{
"domain": "api.example.com",
"backend": "pool_3",
"status": "healthy",
"servers": [
{"name": "pool_3_1", "addr": "10.0.0.1:80", "status": "UP", "check_status": "L4OK"}
],
"healthy_count": 1,
"total_count": 1
}
```
**Status values:** `healthy` (all UP), `degraded` (partial UP), `down` (all DOWN), `no_servers`
## MCP Tools (28 total)
### Domain Management
| Tool | Description |
|------|-------------|
| `haproxy_list_domains` | List domains (use `include_wildcards=true` for wildcards) |
| `haproxy_add_domain` | Add domain to available pool (no reload), supports IPv6 |
| `haproxy_remove_domain` | Remove domain from pool (no reload) |
### Server Management
| Tool | Description |
|------|-------------|
| `haproxy_list_servers` | List servers for a domain |
| `haproxy_add_server` | Add server to slot (1-10 or 0=auto), auto-saved |
| `haproxy_add_servers` | Bulk add servers (JSON array), auto-saved |
| `haproxy_remove_server` | Remove server from slot, auto-saved |
| `haproxy_set_server_state` | Set state: ready/drain/maint |
| `haproxy_set_server_weight` | Set weight (0-256) |
| `haproxy_set_domain_state` | Set all servers of domain to ready/drain/maint |
| `haproxy_wait_drain` | Wait for connections to drain (timeout configurable) |
### Health Check
| Tool | Description |
|------|-------------|
| `haproxy_health` | System health (MCP, HAProxy, config files) |
| `haproxy_domain_health` | Domain-specific health with server status |
| `haproxy_get_server_health` | Get UP/DOWN/MAINT status for all servers |
### Monitoring
| Tool | Description |
|------|-------------|
| `haproxy_stats` | HAProxy statistics |
| `haproxy_backends` | List backends |
| `haproxy_list_frontends` | List frontends with status |
| `haproxy_get_connections` | Active connections per server |
### Configuration
| Tool | Description |
|------|-------------|
| `haproxy_reload` | Validate, reload config, auto-restore servers |
| `haproxy_check_config` | Validate config syntax |
| `haproxy_save_state` | Save server state to disk (legacy) |
| `haproxy_restore_state` | Restore state from disk (legacy) |
### Certificate Management
| Tool | Description |
|------|-------------|
| `haproxy_list_certs` | List all certificates with expiry info |
| `haproxy_cert_info` | Get detailed certificate info (expiry, issuer, SANs) |
| `haproxy_issue_cert` | Issue new certificate via acme.sh + Cloudflare DNS |
| `haproxy_renew_cert` | Renew specific certificate (force option available) |
| `haproxy_renew_all_certs` | Renew all certificates due for renewal |
| `haproxy_delete_cert` | Delete certificate from acme.sh and HAProxy |
## Key Conventions
### Pool-Based Routing
- Domains map to pools: `example.com``pool_N`
- 100 pools available: `pool_1` to `pool_100`
- Each pool has 10 server slots
### Server Naming
Pool backends use simple naming: `pool_N_1` to `pool_N_10`
**Examples:**
```
example.com → pool_5
└─ pool_5_1 (slot 1)
└─ pool_5_2 (slot 2)
└─ ... up to pool_5_10
api.example.com → pool_6
└─ pool_6_1 (slot 1)
```
### Persistence
- **domains.map**: Survives HAProxy restart
- **servers.json**: Auto-restored by MCP on startup
- No manual save required - `haproxy_add_server` auto-saves
### Safety Features
- **Atomic file writes**: Temp file + rename prevents corruption
- **File locking**: Prevents race conditions on concurrent operations
- **Disk-first pattern**: Config saved before HAProxy update, rollback on failure
- **Command validation**: HAProxy responses checked for errors
- **Input validation**: Domain format, IP (v4/v6), port range, slot limits
- **Bulk limits**: Max 10 servers per bulk add, 10KB JSON size limit
### Performance Optimization
- **Command batching**: Multiple HAProxy commands sent in single TCP connection
- Server config (addr + state): 1 connection instead of 2
- Startup restore: All servers restored in 1 connection (was 2×N for N servers)
- Example: 7 servers restored = 1 connection (was 14 connections)
## HAProxy Runtime API
```bash
# Show domain mappings
echo "show map /usr/local/etc/haproxy/domains.map" | nc localhost 9999
# Add domain mapping (runtime only)
echo "add map /usr/local/etc/haproxy/domains.map example.com pool_5" | nc localhost 9999
# Show server state
echo "show servers state" | nc localhost 9999
# Show backends
echo "show backend" | nc localhost 9999
# Set server address
echo "set server pool_1/pool_1_1 addr 10.0.0.1 port 80" | nc localhost 9999
# Enable server
echo "set server pool_1/pool_1_1 state ready" | nc localhost 9999
```
## Directory Structure
```
/opt/haproxy/
├── haproxy_mcp/ # MCP server package (streamable-http)
│ ├── server.py # Main entry point
│ ├── config.py # Configuration and constants
│ ├── exceptions.py # Exception classes
│ ├── validation.py # Input validation
│ ├── haproxy_client.py # HAProxy Runtime API client
│ ├── file_ops.py # File I/O operations
│ ├── utils.py # Parsing utilities
│ └── tools/ # MCP tools (28 total)
│ ├── domains.py # Domain management (3 tools)
│ ├── servers.py # Server management (7 tools)
│ ├── health.py # Health checks (3 tools)
│ ├── monitoring.py # Monitoring (4 tools)
│ ├── configuration.py # Config management (4 tools)
│ └── certificates.py # Certificate management (6 tools)
├── conf/
│ ├── haproxy.cfg # Main HAProxy config (100 pool backends)
│ ├── domains.map # Domain → Pool mapping
│ ├── servers.json # Server persistence (auto-managed)
│ └── mcp-token.env # Bearer token for MCP auth
├── certs/ # SSL/TLS certificates (HAProxy PEM format)
├── data/ # Legacy state files
├── scripts/ # Utility scripts
└── run/ # Runtime socket files
~/.acme.sh/
├── *_ecc/ # Certificate directories (one per domain)
└── acme.sh # ACME client script
/etc/containers/systemd/
└── haproxy.container # Podman Quadlet config
```
## Configuration Files
| File | Purpose |
|------|---------|
| `/opt/haproxy/conf/haproxy.cfg` | HAProxy main config |
| `/opt/haproxy/conf/domains.map` | Domain routing map |
| `/opt/haproxy/conf/servers.json` | Server persistence |
| `/opt/haproxy/certs/*.pem` | SSL certificates (auto-loaded) |
| `/etc/containers/systemd/haproxy.container` | Podman Quadlet |
| `/etc/systemd/system/haproxy-mcp.service` | MCP service |
| `/opt/haproxy/conf/mcp-token.env` | MCP auth token |
| `~/.acme.sh/` | acme.sh certificates and config |
| `~/.secrets/cloudflare.ini` | Cloudflare API token |
## Startup Sequence
```
1. systemd starts haproxy.service
2. HAProxy loads haproxy.cfg (100 pool backends)
3. HAProxy loads domains.map (domain → pool mapping)
4. systemd starts haproxy-mcp.service
5. MCP server reads servers.json
6. MCP server restores servers via Runtime API
7. Ready to serve traffic
```