Admin Endpoint #

The ability to update configuration instantly without disturbing active connections is one of the most revolutionary features of modern web servers. Caddy leads this innovation by providing a built-in REST API by default that lets you read, manipulate, and reload the entire server configuration structure in real time. Unlike traditional web servers like Nginx or Apache, which rely on re-reading physical config files from disk and replacing worker processes, Caddy treats configuration as a dynamic in-memory data document. We’ll dive deep into Caddy’s Admin API architecture, its default behavior limits, advanced security options using Mutual TLS (mTLS), JSON Path mapping, and the use of special object markers (@id) for advanced server management automation.


Caddy REST API Architecture vs Traditional #

Before diving into technical commands, we need to understand the philosophy behind Caddy’s Admin API architecture and how it differs from traditional web server approaches:

+--------------------------------------------------------------------------------+
||                            Server Control Architecture                       ||
+--------------------------------------------------------------------------------+
||  Traditional Model (Nginx / Apache):                                          ||
||  [Config File on Disk] --(Manual Edit)--> [reload Command] --(Restart Workers)-->|
||                                                                                ||
||  Modern Caddy Model:                                                           ||
||  [Application / CLI] --(HTTP REST API)--> [Caddy Admin API] --(Atomic RAM Swap)-->|
+--------------------------------------------------------------------------------+

On traditional web servers like Nginx, the configuration update workflow always involves physical file interaction:

  1. The system administrator edits the configuration file (e.g., nginx.conf) on local disk.
  2. The nginx -s reload command is run to trigger a system signal.
  3. Nginx’s master process validates the file, then spawns new worker processes with the new configuration, while old worker processes are slowly shut down after finishing active requests (graceful draining).

Although this model has proven stable for years, it has major limitations in the dynamic modern cloud ecosystem. Writing files to disk limits programmatic automation from outside the server, triggers file permission issues, and risks race conditions if several scripts try to change the configuration simultaneously.

Caddy solves this problem by eliminating the need to write to physical files for configuration updates. Inside Caddy, there’s a runtime engine controlled by a unified JSON document in RAM. When you make an HTTP API call to Caddy, the changes are validated directly in memory, and if valid, immediately applied using an atomic swap operation. This makes Caddy the top choice for multi-tenant SaaS platforms, dynamic scaling infrastructure (auto-scaling), and high-level automation integration.


Admin API Default Behavior #

By default, as soon as you run the Caddy process, the Admin API automatically activates and listens for connections at the following address:

  • Listen Address: http://localhost:2019
  • Protocol: HTTP (without TLS encryption, because it only serves internal traffic).
  • Interface Binding: Strictly bound to the loopback interface (127.0.0.1 for IPv4 and [::1] for IPv6).
  • Authentication: No password or authentication token required by default. The security of this endpoint relies entirely on local network isolation (only processes running on the same machine can send data packets to port 2019).

For most common deployment scenarios, this default behavior is already very safe. Because it’s only bound to the loopback interface, access attempts from outside the server using a public IP address are automatically rejected at the OS kernel level, even without extra firewall rules.


Securing the Admin API #

Although the default configuration is safe for a single machine, production scenarios often demand changing this behavior — whether to tighten security by disabling the API entirely, moving the port, or opening remote access for distributed cluster needs.

1. Changing the Listen Address #

You can move the Admin API location by defining it in the global options block at the very top of the Caddyfile. You can move it to a different local TCP port, or use a Unix domain socket, which provides stricter permission control on Linux-based operating systems:

# Change the Admin API port to local 2020
{
    admin localhost:2020
}

example.com {
    reverse_proxy localhost:3000
}

If you want to use a Unix Domain Socket for extra filesystem-permission-based security:

# Using a Unix Socket for file-system-based access control
{
    admin unix//run/caddy-admin.sock
}

example.com {
    file_server
}

With a Unix socket, you can set the ownership of the /run/caddy-admin.sock file and its access permissions (permission 0600) using Linux OS utilities so only specific system users (like root or the caddy user) can control the server.

2. Disabling the Admin API #

On static production servers — where Caddy’s configuration is only updated manually by operations teams through Caddyfile file updates and there’s no external API integration — you’re strongly recommended to disable the Admin API entirely to minimize the attack surface:

# CORRECT: Disabling the Admin API entirely in static production
{
    admin off
}

example.com {
    file_server {
        root /var/www/html
    }
}

[!WARNING] When you set admin off, Caddy completely shuts down its internal API engine. The consequence is that Caddy’s built-in CLI commands like caddy reload, caddy stop, or caddy adapt will no longer work from the server’s local terminal. This happens because Caddy’s command-line utilities internally communicate with the main Caddy process through the local Admin API port. If the API is off, configuration updates can only be done by killing the Caddy process (kill / systemctl stop) and starting it again from scratch.

3. Remote Admin with Two-Way TLS (mTLS) #

In cluster architectures where you have several Caddy server nodes controlled by one central server (control plane), you need to open the Admin API for access from the external network.

Never enable API binding to a public interface (like admin 0.0.0.0:2019) without a security mechanism. Because Caddy’s Admin API has no username/password authentication system, anyone on the internet who finds that port open immediately gets full control of your server.

To absolutely secure remote access, you must use HTTPS encryption combined with client certificate authentication (Mutual TLS / mTLS). With this method, Caddy only accepts requests if the client sends a valid digital certificate signed by your trusted internal Certificate Authority (CA):

# CORRECT: Securing the Remote Admin API with custom mTLS
{
    admin {
        # Listen on all interfaces on port 2019
        listen :2019
        
        # Validate the HTTP Origin header to prevent CSRF attacks
        enforce_origin
        origins control-plane.internal.net
        
        # TLS security configuration
        tls {
            # HTTPS server certificate for this admin port
            cert_file /etc/caddy/certs/admin-server.crt
            key_file  /etc/caddy/certs/admin-server.key
            
            # Require client certificate authentication (mTLS)
            client_auth {
                mode require_and_verify
                trusted_ca_certs_pem_files /etc/caddy/certs/internal-ca.crt
            }
        }
    }
}

example.com {
    reverse_proxy localhost:8080
}

In the configuration above:

  • listen :2019 makes the API accessible from the external network.
  • enforce_origin ensures requests sent through a browser have an origin header matching control-plane.internal.net to prevent Cross-Origin Resource Sharing (CORS) / CSRF attacks.
  • client_auth with require_and_verify mode guarantees that every client calling the API must present a client certificate issued by the internal CA at /etc/caddy/certs/internal-ca.crt. Clients without that certificate are rejected right at the TLS handshake phase before the HTTP request is even processed.

All Admin API Endpoints #

Caddy provides a set of structured RESTful endpoints for interacting with various server subsystems. Here’s the complete list with usage scenarios:

1. /config/ — Active Configuration Manipulation #

This endpoint is the main gateway for reading and changing the active configuration document. The document is returned in structured JSON format.

Reading the Active Configuration (GET) #

You can call this endpoint with the GET method to get a dump of your server’s active configuration. You can use the jq utility to format the JSON output for human readability:

# Read the entire active configuration
curl -s http://localhost:2019/config/ | jq .

Replacing the Entire Configuration (POST) #

The POST method to the /config/ root endpoint instantly replaces the entire active configuration document with the new JSON payload you send in the request body:

# Replace the entire configuration with a local JSON file
curl -s -X POST http://localhost:2019/config/ \
  -H "Content-Type: application/json" \
  --data-binary @/etc/caddy/new_config.json

Updating a Configuration Subset (PATCH) #

If you only want to update a small part of the configuration without re-uploading the entire JSON file, you can use the PATCH method. This method merges the object you send into the active configuration structure:

# Do a configuration merge
curl -s -X PATCH http://localhost:2019/config/apps/http/servers/srv0/ \
  -H "Content-Type: application/json" \
  -d '{"read_timeout": "10s"}'

2. /reverse_proxy/upstreams/ — Real-Time Upstream Status #

This endpoint is crucial for DevOps teams to monitor the health status of backend clusters behind Caddy’s reverse proxy. Caddy returns the list of all active upstream servers with their healthy status, the number of running requests, and accumulated recorded passive failures:

# Read the health status of backend upstreams
curl -s http://localhost:2019/reverse_proxy/upstreams/ | jq .

Example output payload from Caddy:

[
  {
    "address": "10.0.1.15:8080",
    "healthy": true,
    "num_requests": 142,
    "fails": 0
  },
  {
    "address": "10.0.1.16:8080",
    "healthy": false,
    "num_requests": 0,
    "fails": 3
  }
]

3. /pki/ca/ — Monitoring the Internal CA Infrastructure #

Caddy has integrated Public Key Infrastructure (PKI) management that automatically manages local certificates. Through this endpoint, you can check the status of Caddy’s local Certificate Authority (CA), and export the Root CA certificate for installation on client devices:

# Check Caddy's local CA list
curl -s http://localhost:2019/pki/ca/ | jq .

# Export Caddy's local Root CA certificate to a PEM file
curl -s http://localhost:2019/pki/ca/local | jq -r '.root.pem' > caddy-root.crt

4. /load — Reloading Configuration from a File #

The /load endpoint is used to reload a new configuration. This endpoint accepts raw file input. The main advantage of this endpoint is that Caddy automatically detects the input format based on the Content-Type header you send. You can send raw JSON documents or Caddyfile files directly:

# Load a new configuration using a Caddyfile file directly
curl -s -X POST http://localhost:2019/load \
  -H "Content-Type: text/caddyfile" \
  --data-binary @/etc/caddy/Caddyfile

5. /stop — Stopping the Server #

This endpoint sends a safe server shutdown signal (graceful shutdown). Caddy stops accepting new connections, gradually finishes all currently processing requests, then completely stops the Caddy runtime process:

# Gracefully stop the Caddy server
curl -s -X POST http://localhost:2019/stop

Path Navigation in the Admin API #

Caddy’s configuration document is modeled as a single JSON object tree. The URL paths on the Admin API are designed to precisely mirror that tree structure. This provides incredible flexibility because you can manipulate even the smallest elements in the configuration directly through URL path modifications.

Let’s look at the representation of Caddy’s JSON tree structure:

          [Config Root] (/config/)
                 |
               [apps] (/config/apps/)
                 |
               [http] (/config/apps/http/)
                 |
              [servers] (/config/apps/http/servers/)
                 |
               [srv0] (/config/apps/http/servers/srv0/)
                 |
        +--------+--------+
        |                 |
    [routes]          [listen]
(/srv0/routes/)    (/srv0/listen/)

For example, if you want to interact with the routing table on Caddy’s first HTTP server, the path is /config/apps/http/servers/srv0/routes/.

If your route structure is a JSON array, you can target specific elements in the array using numeric indices (starting from 0 for the first element):

# Read the first route (index 0) on server srv0
curl -s http://localhost:2019/config/apps/http/servers/srv0/routes/0 | jq .

# Instantly delete the second route (index 1) from memory
curl -s -X DELETE http://localhost:2019/config/apps/http/servers/srv0/routes/1

Using @id for Stable References #

Although numeric index navigation (like /routes/0) is very easy to use, this method has a critical weakness in dynamic automation environments. If you have an external application that frequently adds or removes routes dynamically, other route indices automatically shift.

For example, if you delete the route at index 0, the route previously at index 1 shifts to index 0. If your automation script sends a PUT request to /routes/1 without realizing this shift, you’ll accidentally overwrite the wrong route configuration (race condition).

To solve this problem, Caddy introduces the Unique Identifier (@id) feature. You can insert a custom @id property with a unique string value on any object in your JSON configuration. Once an object has an @id, you can access, change, or delete it directly through the /id/{id_name} endpoint, regardless of the object’s index position in the JSON document.

Example of Inserting @id into a JSON Configuration #

{
  "apps": {
    "http": {
      "servers": {
        "srv0": {
          "routes": [
            {
              "@id": "main-app-route",
              "match": [{"host": ["example.com"]}],
              "handle": [
                {
                  "handler": "file_server",
                  "root": "/var/www/main"
                }
              ]
            },
            {
              "@id": "api-backend-route",
              "match": [{"host": ["api.example.com"]}],
              "handle": [
                {
                  "handler": "reverse_proxy",
                  "upstreams": [{"dial": "localhost:8080"}]
                }
              ]
            }
          ]
        }
      }
    }
  }
}

Accessing and Modifying Using @id #

With the configuration above, you don’t need to care whether the API route is at index 0, 1, or 10. You can manipulate it directly through its unique path:

# ANTI-PATTERN: Accessing objects using numeric indices in automation scripts
curl -s http://localhost:2019/config/apps/http/servers/srv0/routes/1 | jq .

# CORRECT: Accessing objects consistently using a unique ID
curl -s http://localhost:2019/id/api-backend-route | jq .

You can also safely update that object’s configuration using the PUT method to that ID endpoint:

# Safely update the target upstream for the API
curl -s -X PUT http://localhost:2019/id/api-backend-route \
  -H "Content-Type: application/json" \
  -d '{
    "@id": "api-backend-route",
    "match": [{"host": ["api.example.com"]}],
    "handle": [
      {
        "handler": "reverse_proxy",
        "upstreams": [{"dial": "10.0.1.50:8080"}]
      }
    ]
  }'

If your application needs to remove that API route from rotation instantly:

# Instantly delete the route using its unique ID
curl -s -X DELETE http://localhost:2019/id/api-backend-route

Practical Scripting and Monitoring #

You can leverage Caddy’s Admin API to create automated monitoring scripts to keep your production cluster healthy. Here’s a complete Bash script example that can be installed on the server as a cron job or integrated into an external monitoring system:

#!/bin/bash
# Caddy runtime health monitoring script via the Admin API

CADDY_API="http://localhost:2019"
CHECK_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$CADDY_API/")

if [ "$CHECK_STATUS" -ne 200 ]; then
    echo "✗ ERROR: Caddy Admin API is not responding (HTTP $CHECK_STATUS)!"
    exit 1
fi

echo "=== CADDY RUNTIME STATUS REPORT ==="
echo "Check Date: $(date)"
echo "-------------------------------------"

# 1. Check Upstream Server Stability
echo "[1] Backend Server Health Status:"
UPSTREAMS_JSON=$(curl -s "$CADDY_API/reverse_proxy/upstreams/")

if [ "$UPSTREAMS_JSON" = "[]" ] || [ -z "$UPSTREAMS_JSON" ]; then
    echo "  - No reverse proxy backends are currently configured."
else
    # Parse the JSON log to see health status details
    echo "$UPSTREAMS_JSON" | jq -r '.[] | "  -> Upstream: \(.address) | Healthy: \(.healthy) | Active Requests: \(.num_requests) | Recorded Errors: \(.fails)"'
fi

# 2. Check Active Domains
echo ""
echo "[2] List of Currently Served Domains (Active Routes):"
# Get the host matcher list from the srv0 server configuration
curl -s "$CADDY_API/config/apps/http/servers/srv0/routes" 2>/dev/null | \
jq -r '.[]? | select(.match != null) | "  -> Host: \(.match[0].host[]?)"' 2>/dev/null || echo "  - No host routes registered."

# 3. Validate the Local CA Certificate
echo ""
echo "[3] Caddy Local CA Details:"
curl -s "$CADDY_API/pki/ca/local" 2>/dev/null | \
jq -r '"  -> CA Name: \(.name)\n  -> Valid From: \(.root.not_before)\n  -> Expires On: \(.root.not_after)"' 2>/dev/null || echo "  - Local CA information is not available."

echo "-------------------------------------"
echo "Check Complete."

The script above makes HTTP calls to the local Admin API, validates responses, parses proxy backend health status data using jq, filters active domain routes, and displays the local CA certificate status. You can route this script’s output to a monitoring system like Grafana Loki or send Slack notifications if a "healthy": false property is detected on any backend server.


Summary #

  • Built-in REST API: Caddy provides a built-in REST API running at localhost:2019 by default for instant configuration modification without restarts.
  • Loopback Security: By default, the API is bound to the loopback interface so it can only be accessed locally from within the machine itself.
  • Disabling the API: In static production, you’re advised to use admin off in the global options to minimize the attack surface.
  • Secure Remote Access: If you must open remote API access, use HTTPS with mTLS (client_auth require_and_verify) for strong encryption and authentication.
  • JSON Reflective URLs: The Admin API URL paths mirror the JSON tree structure of Caddy’s configuration (e.g., /config/apps/http/servers/).
  • The @id Advantage: Use unique @id markers on configuration objects to avoid shifting numeric index errors during dynamic manipulation.
  • Real-Time Metrics: The /reverse_proxy/upstreams/ endpoint provides real-time backend health and active connection metrics for observability needs.

← Previous: Admin API   Next: Config API →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact