Version: 1.0 Last Updated: 2024-11-08
Note: All endpoints in this API are protected and require a valid JWT access token. See the Authentication API documentation for details on how to obtain a token.
The Proxy Management API provides endpoints to create, read, update, and delete proxy configurations. It supports three proxy types:
- Reverse Proxy - Forward requests to backend servers (with optional load balancing)
- Redirect - HTTP redirects (301, 302, 307, 308)
- Static - Serve static HTML/files
Forward incoming requests to one or more backend servers.
Features:
- Single upstream (simple reverse proxy)
- Multiple upstreams (automatic load balancing)
- Custom headers
- Health checks
- WebSocket support (automatic - no configuration needed)
Redirect requests to another URL.
Features:
- Permanent redirects (301, 308)
- Temporary redirects (302, 307)
- Path preservation
- Query string handling
Serve static files or HTML templates.
Features:
- File server
- Template rendering
- Custom index files
- Directory browsing
/api/proxies
All endpoints require JWT token:
Authorization: Bearer <jwt_token>
CREATE TABLE proxies (
id SERIAL PRIMARY KEY,
-- Common fields
type VARCHAR(50) NOT NULL, -- 'reverse_proxy', 'redirect', 'static'
name VARCHAR(255) NOT NULL,
hostname VARCHAR(255) UNIQUE NOT NULL,
description TEXT,
-- SSL/TLS
ssl_enabled BOOLEAN DEFAULT true,
ssl_forced BOOLEAN DEFAULT true,
-- Type-specific configurations (stored as JSON)
-- For reverse_proxy type
upstreams TEXT, -- JSON: [{"host": "...", "port": 8080, "scheme": "http"}]
load_balancing TEXT, -- JSON: {"strategy": "round_robin", "health_checks": {...}}
block_exploits BOOLEAN DEFAULT true,
custom_headers TEXT, -- JSON: {"X-Header": "value"}
-- For redirect type
redirect_config TEXT, -- JSON: {"target": "...", "status_code": 301, ...}
-- For static type
static_config TEXT, -- JSON: {"root_path": "...", "index_file": "index.html", ...}
-- Status
is_active BOOLEAN DEFAULT true,
-- Metadata
created_by INTEGER NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE CASCADE
);
-- Indexes for better query performance
CREATE INDEX idx_proxies_hostname ON proxies(hostname);
CREATE INDEX idx_proxies_type ON proxies(type);
CREATE INDEX idx_proxies_is_active ON proxies(is_active);
CREATE INDEX idx_proxies_created_by ON proxies(created_by);
CREATE INDEX idx_proxies_created_at ON proxies(created_at);| Field | Type | Nullable | Description |
|---|---|---|---|
id |
INTEGER | No | Primary key, auto-increment |
type |
VARCHAR(50) | No | Proxy type: reverse_proxy, redirect, static |
name |
VARCHAR(255) | No | Display name for the proxy |
hostname |
VARCHAR(255) | No | Domain/subdomain (unique) |
description |
TEXT | Yes | Optional description |
ssl_enabled |
BOOLEAN | No | Enable SSL/TLS (default: true) |
ssl_forced |
BOOLEAN | No | Force HTTPS redirect (default: true) |
upstreams |
TEXT (JSON) | Yes | Array of upstream servers (reverse_proxy only) |
load_balancing |
TEXT (JSON) | Yes | Load balancing config (reverse_proxy with multiple upstreams) |
block_exploits |
BOOLEAN | No | Block common exploits (default: true) |
custom_headers |
TEXT (JSON) | Yes | Custom HTTP headers (reverse_proxy only) |
redirect_config |
TEXT (JSON) | Yes | Redirect configuration (redirect type only) |
static_config |
TEXT (JSON) | Yes | Static file server config (static type only) |
is_active |
BOOLEAN | No | Proxy enabled/disabled status (default: true) |
created_by |
INTEGER | No | User ID who created the proxy |
created_at |
TIMESTAMP | No | Creation timestamp |
updated_at |
TIMESTAMP | No | Last update timestamp |
upstreams (TEXT - JSON Array):
[
{
"host": "192.168.100.5",
"port": 8080,
"scheme": "http"
}
]load_balancing (TEXT - JSON Object):
{
"strategy": "round_robin",
"health_checks": {
"enabled": true,
"path": "/health",
"interval": "30s",
"timeout": "5s",
"unhealthy_threshold": 2,
"healthy_threshold": 2
}
}custom_headers (TEXT - JSON Object):
{
"X-Custom-Header": "value",
"X-API-Version": "v1"
}redirect_config (TEXT - JSON Object):
{
"target": "https://new-domain.com",
"status_code": 301,
"preserve_path": true,
"preserve_query": true
}static_config (TEXT - JSON Object):
{
"root_path": "/var/www/html",
"index_file": "index.html",
"browse": false,
"try_files": ["index.html"],
"template_rendering": false
}{
"id": 1,
"type": "reverse_proxy", // "reverse_proxy", "redirect", "static"
"name": "My Proxy",
"hostname": "app.example.com",
"description": "Optional description",
"ssl_enabled": true,
"ssl_forced": true,
"is_active": true,
"created_by": {
"id": 1,
"name": "Admin User",
"email": "admin@example.com"
},
"created_at": "2024-11-08T10:30:00Z",
"updated_at": "2024-11-08T10:30:00Z"
}{
"type": "reverse_proxy",
// Single upstream
"upstreams": [
{
"host": "192.168.100.5",
"port": 8080,
"scheme": "http"
}
],
// OR Multiple upstreams (triggers load balancing)
"upstreams": [
{
"host": "192.168.100.5",
"port": 8080,
"scheme": "http"
},
{
"host": "192.168.100.6",
"port": 8080,
"scheme": "http"
}
],
// Load balancing (only when multiple upstreams)
"load_balancing": {
"strategy": "round_robin", // "round_robin", "least_conn", "ip_hash", "random"
"health_checks": {
"enabled": true,
"path": "/health",
"interval": "30s",
"timeout": "5s",
"unhealthy_threshold": 2,
"healthy_threshold": 2
}
},
// Features
"block_exploits": true,
"custom_headers": {
"X-Custom-Header": "value"
}
}{
"type": "redirect",
"redirect": {
"target": "https://new-domain.com",
"status_code": 301, // 301, 302, 307, 308
"preserve_path": true,
"preserve_query": true
}
}{
"type": "static",
"static": {
"root_path": "/var/www/html", // Path to static files
"index_file": "index.html", // Default file
"browse": false, // Enable directory browsing
"try_files": ["index.html"], // Fallback files for SPA
"template_rendering": false // Enable Caddy templates
}
}GET /api/proxies
Lists all proxy configurations with pagination.
...
...
- 401 Unauthorized: If the JWT token is missing or invalid.
- 500 Internal Server Error: If there is a problem fetching the data from the database.
GET /api/proxies/:id
Retrieves the full details of a single proxy configuration.
Returns the full proxy object.
{
"success": true,
"data": {
"id": 1,
"type": "reverse_proxy",
"name": "My Application",
"hostname": "app.example.com",
...
}
}- 401 Unauthorized: If the JWT token is missing or invalid.
- 404 Not Found: If no proxy with the specified ID exists.
{ "success": false, "error": { "code": "NOT_FOUND", "message": "Proxy not found" } } - 500 Internal Server Error: For any other server-side errors.
POST /api/proxies
Creates a new proxy configuration.
See the "Data Models" section for request body examples.
Returns the newly created proxy object.
{
"success": true,
"message": "Proxy created successfully",
"data": {
"id": 6,
...
}
}- 400 Bad Request: If the request body is invalid or fails validation.
- 401 Unauthorized: If the JWT token is missing or invalid.
- 409 Conflict: If a proxy with the same hostname already exists.
- 500 Internal Server Error: For database errors.
- 502 Bad Gateway: If the configuration could not be applied to the Caddy server.
{ "success": false, "error": { "code": "EXTERNAL_SERVICE_ERROR", "message": "caddy API error (status 400): ..." } }
PUT /api/proxies/:id
Updates an existing proxy configuration.
Returns the updated proxy object.
{
"success": true,
"message": "Proxy updated successfully",
"data": {
"id": 1,
...
}
}- 400 Bad Request: If the request body is invalid.
- 401 Unauthorized: If the JWT token is missing or invalid.
- 404 Not Found: If the proxy does not exist.
- 409 Conflict: If the new hostname conflicts with an existing proxy.
- 500 Internal Server Error: For database errors.
- 502 Bad Gateway: If the configuration could not be applied to the Caddy server.
DELETE /api/proxies/:id
Deletes a proxy configuration from the database and the Caddy server.
{
"success": true,
"message": "Proxy deleted successfully",
"data": null
}- 401 Unauthorized: If the JWT token is missing or invalid.
- 404 Not Found: If the proxy does not exist.
- 500 Internal Server Error: For any server-side errors.
POST /api/proxies/:id/enable
Enables a disabled proxy, applying its configuration to the Caddy server.
{
"success": true,
"message": "Proxy enabled successfully",
"data": null
}- 400 Bad Request: If the proxy is already enabled.
- 401 Unauthorized: If the JWT token is missing or invalid.
- 404 Not Found: If the proxy does not exist.
- 500 Internal Server Error: For database errors.
- 502 Bad Gateway: If the configuration could not be applied to the Caddy server.
POST /api/proxies/:id/disable
Disables an active proxy, removing its configuration from the Caddy server.
{
"success": true,
"message": "Proxy disabled successfully",
"data": null
}- 400 Bad Request: If the proxy is already disabled.
- 401 Unauthorized: If the JWT token is missing or invalid.
- 404 Not Found: If the proxy does not exist.
- 500 Internal Server Error: For any server-side errors.
| Strategy | Description | Use Case |
|---|---|---|
round_robin |
Distribute requests evenly in rotation | Default, works well for most cases |
least_conn |
Send to server with fewest active connections | Varying request durations |
ip_hash |
Hash client IP to determine server (sticky sessions) | Session persistence needed |
random |
Random selection | Simple, stateless |
Health checks automatically monitor upstream server availability.
Configuration:
{
"health_checks": {
"enabled": true,
"path": "/health", // Endpoint to check
"interval": "30s", // Check frequency
"timeout": "5s", // Request timeout
"unhealthy_threshold": 2, // Failures before marking unhealthy
"healthy_threshold": 2 // Successes before marking healthy
}
}Behavior:
- Caddy sends GET requests to
{upstream}/{path}everyinterval - Expects HTTP 2xx/3xx response within
timeout - After
unhealthy_thresholdconsecutive failures, marks upstream as down - After
healthy_thresholdconsecutive successes, marks upstream as up - Unhealthy upstreams are excluded from load balancing
| Code | Type | Caching | Use Case |
|---|---|---|---|
| 301 | Permanent | Yes | Domain migration, permanent URL change |
| 302 | Temporary | No | Temporary maintenance, A/B testing |
| 307 | Temporary (preserves method) | No | POST/PUT redirects |
| 308 | Permanent (preserves method) | Yes | POST/PUT permanent redirects |
preserve_path: true (default)
https://old.example.com/page/123
↓
https://new.example.com/page/123
preserve_path: false
https://old.example.com/page/123
↓
https://new.example.com
preserve_query: true (default)
https://old.example.com/page?foo=bar
↓
https://new.example.com/page?foo=bar
preserve_query: false
https://old.example.com/page?foo=bar
↓
https://new.example.com/page
Before using the static proxy type, you need to mount your static files into the Waygates container so they're accessible at root_path.
1. Update docker-compose.yml:
waygates:
build:
context: .
dockerfile: Dockerfile
volumes:
- caddy-data:/data
- caddy-config:/config
- caddy-etc:/etc/caddy
# Mount your static files
- ./sites/my-app:/srv/my-app:ro
- ./sites/docs:/srv/docs:ro2. Directory structure:
waygates/
├── docker-compose.yml
└── sites/
├── my-app/ # React/Vue/Angular SPA build output
│ ├── index.html
│ ├── assets/
│ └── ...
└── docs/ # Static documentation
├── index.html
└── ...
3. Create proxy via API:
curl -X POST http://localhost:8080/api/proxies \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "static",
"name": "My SPA",
"hostname": "app.example.com",
"static": {
"root_path": "/srv/my-app",
"index_file": "index.html",
"try_files": ["index.html"]
}
}'4. Restart container to pick up new volume mounts:
docker compose restart waygatesUpdating files: After copying new files to ./sites/my-app/, no restart is needed - Caddy serves files directly from disk.
Caddy serves files from root_path directory.
Basic configuration:
{
"static": {
"root_path": "/var/www/html",
"index_file": "index.html"
}
}Request flow:
GET /about
↓
Looks for:
1. /var/www/html/about
2. /var/www/html/about/index.html (if about is a directory)
3. Returns 404 if not found
For Single Page Applications, use try_files to fallback to index.html:
{
"static": {
"root_path": "/var/www/spa",
"index_file": "index.html",
"try_files": ["index.html"]
}
}Request flow:
GET /dashboard/settings
↓
1. Try: /var/www/spa/dashboard/settings
2. Not found → Try: /var/www/spa/index.html
3. Return index.html (let SPA router handle routing)
Enable Caddy's template engine for dynamic content:
{
"static": {
"root_path": "/etc/caddy/templates",
"index_file": "maintenance.html",
"template_rendering": true
}
}Template example (maintenance.html):
<!DOCTYPE html>
<html>
<head>
<title>Maintenance</title>
</head>
<body>
<h1>Scheduled Maintenance</h1>
<p>Current time: {{now | date "2006-01-02 15:04:05"}}</p>
<p>Server: {{.Host}}</p>
</body>
</html>Available template variables:
{{.Host}}- Request hostname{{.OriginalReq.URL.Path}}- Request path{{now}}- Current timestamp- See Caddy template docs for full list
When block_exploits: true is set on a reverse proxy, Caddy will import security rules from conf/snippets/security.caddy to block common web attacks.
The backend generates Caddy configuration with an import directive:
api.example.com {
import snippets/security # Imported when block_exploits = true
reverse_proxy 192.168.1.100:8080
}The security snippet blocks the following attack patterns:
1. SQL Injection Attempts
- Query strings containing:
union select,union all select,concat() - Returns:
403 Forbidden - SQL injection detected
2. File Injection/Traversal
- Path traversal:
../,../../,../../../ - Remote file inclusion:
http://,https://in query parameters - Returns:
403 Forbidden - File injection detected
3. Common Exploits
- XSS attacks:
<script>,%3Cscript - PHP global exploits:
GLOBALS=,_REQUEST= - System access:
/proc/self/environ - Base64 encoding attempts:
base64_encode(),base64_decode() - Returns:
403 Forbidden - Common exploit detected
4. Spam Keywords
- Pharmaceutical spam:
viagra,cialis,levitra,xanax,valium - Other spam terms:
tramadol,phentermin,ambien - Returns:
403 Forbidden - Spam detected
5. Malicious User Agents
- Download managers:
GetRight,GetWeb!,Go!Zilla,Download Demon - Bots:
TurnitinBot,GrabNet,libwww-perl - Known malicious agents:
Indy Library - Returns:
403 Forbidden - Blocked user agent
Request:
{
"type": "reverse_proxy",
"name": "Protected API",
"hostname": "api.example.com",
"upstreams": [{"host": "192.168.1.100", "port": 8080, "scheme": "http"}],
"block_exploits": true
}Generated Caddyfile:
api.example.com {
# Security rules applied first
import snippets/security
# Then reverse proxy
reverse_proxy 192.168.1.100:8080 {
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
}
}Test that security rules are working:
# SQL injection - should return 403
curl "https://api.example.com/?id=1 union select password from users"
# XSS attempt - should return 403
curl "https://api.example.com/?search=<script>alert(1)</script>"
# Path traversal - should return 403
curl "https://api.example.com/?file=../../../etc/passwd"
# Bad user agent - should return 403
curl -A "libwww-perl/6.0" "https://api.example.com/"
# Normal request - should work
curl "https://api.example.com/?search=hello"To modify security rules:
- Edit
/conf/snippets/security.caddy - Add/remove patterns as needed
- Validate:
make validate - Apply changes:
make restart
Changes apply to all proxies with block_exploits: true enabled.
- Minimal overhead - Pattern matching happens at Caddy level (compiled code)
- Applied before proxying - Malicious requests never reach your backend
- No extra dependencies - Uses native Caddy matchers
- Not a WAF - This is basic pattern matching, not a full Web Application Firewall
- False positives possible - Legitimate requests might be blocked if they contain flagged patterns
- Query string only - Most checks apply to URL query strings and headers, not request bodies
For advanced protection, consider:
- Cloudflare WAF (if using Cloudflare)
- Dedicated WAF solution (ModSecurity, etc.)
caddy-securityplugin for advanced features
{
"success": true,
"message": "Optional success message",
"data": {
// Response data
}
}{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "Human-readable error message",
"details": {
// Optional additional error details
}
}
}| Status Code | Description | Usage |
|---|---|---|
| 200 OK | Success | GET, PUT, POST (enable/disable) |
| 201 Created | Resource created | POST (create) |
| 400 Bad Request | Invalid request | Validation errors |
| 401 Unauthorized | Not authenticated | Missing/invalid token |
| 403 Forbidden | Not authorized | Insufficient permissions |
| 404 Not Found | Resource not found | Invalid ID |
| 409 Conflict | Resource conflict | Duplicate hostname |
| 422 Unprocessable Entity | Business logic error | Cannot process valid request |
| 500 Internal Server Error | Server error | Unexpected errors |
| 502 Bad Gateway | External service error | Caddy API errors |
| Code | HTTP Status | Description |
|---|---|---|
VALIDATION_ERROR |
400 | Request validation failed |
UNAUTHORIZED |
401 | Authentication required |
FORBIDDEN |
403 | Insufficient permissions |
PROXY_NOT_FOUND |
404 | Proxy doesn't exist |
HOSTNAME_CONFLICT |
409 | Hostname already in use |
PROXY_ALREADY_ENABLED |
409 | Proxy is already enabled |
PROXY_ALREADY_DISABLED |
409 | Proxy is already disabled |
CADDY_API_ERROR |
502 | Caddy Admin API error |
INTERNAL_ERROR |
500 | Unexpected server error |
- Load Balancing: Automatically enabled when
upstreams.length > 1 - Health Checks: Recommended for production load-balanced setups
- Security/Exploit Blocking: Uses reusable Caddy snippet (
conf/snippets/security.caddy) imported whenblock_exploits: true - WebSocket Support: Automatic in Caddy's reverse_proxy - no configuration needed
- Redirects: Use 301 for SEO-friendly permanent redirects
- Static Sites: Perfect for documentation, landing pages, SPAs
- Templates: Useful for maintenance pages, error pages
- Timestamps: All in ISO 8601 format (UTC)
- Hostname Uniqueness: Must be unique across all proxies
- SSL/TLS: Automatic certificates via configured ACME provider (see CADDY_ACME_PROVIDER)
Document Version: 1.0 Status: Complete