Exposing the Caddy API over a public endpoint

Hey, we are currently looking into Caddy to be our new internal reverse Proxy. Right now we either use a nginx or a proprietary system. We mainly want to use caddy for its build in API and fast reloads. Certificates are handled by a azure key vault. Right now we are facing a few issues/Questions which i do not find any clear information online. I will create separate topics for each Problem.

1. The problem I’m having:

I’m trying to expose the caddy api to an public endpoint. So that i can faster iterate my configuration before deploying it . When using the given Caddyfile below I receive the error:

2. Error messages and/or full log output:

{"error":"host not allowed: caddy-api.xx.de"}

3. Caddy version:

2.10

4. How I installed and ran Caddy:

We are installing caddy via a script using the given commands from the Ubuntu install guide. We also install x-caddy and re-build caddy with the modules caddy-brotli and coraza-waf.

a. System environment:

Ubuntu running on a azure virtual machine scale set instance.

uname -a 
Linux PCDY400132V00000A 6.11.0-1015-azure #15~24.04.1-Ubuntu SMP Thu May  1 02:52:08 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux

b. Command:

We are running caddy as a systemd service.

c. Service file:

# caddy.service
#
# For using Caddy with a config file.
#
# Make sure the ExecStart and ExecReload commands are correct
# for your installation.
#
# See https://caddyserver.com/docs/install for instructions.
#
# WARNING: This service does not use the --resume flag, so if you
# use the API to make changes, they will be overwritten by the
# Caddyfile next time the service is restarted. If you intend to
# use Caddy's API to configure it, add the --resume flag to the
# `caddy run` command or use the caddy-api.service file instead.

[Unit]
Description=Caddy
Documentation=https://caddyserver.com/docs/
After=network.target network-online.target
Requires=network-online.target

[Service]
Type=notify
User=caddy
Group=caddy
ExecStart=/usr/bin/caddy run --environ --config /etc/caddy/Caddyfile
ExecReload=/usr/bin/caddy reload --config /etc/caddy/Caddyfile --force
TimeoutStopSec=5s
LimitNOFILE=1048576
PrivateTmp=true
ProtectSystem=full
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target

d. My complete Caddy config:

caddy-api.xx.de {
    tls /etc/ssl/xx-de /etc/ssl/xx-de
    reverse_proxy 127.0.0.1:2019
    basic_auth /admin {
        admin XXX
    }

    log {
        output file /var/log/caddy/caddy-api.log {
            roll_size 10mb
            roll_keep 20
            roll_keep_for 90d
        }
        format json
    }
}

:80, :443 {
    root * /var/www
    file_server
    handle /health {
        rewrite * /health.html
        file_server
    }
    log {
        output file /var/log/caddy/caddy-access.log {
            roll_size 10mb
            roll_keep 20
            roll_keep_for 90d
        }
        format json
    }
}

5. Links to relevant resources:

Due to link restrain (only four links allowed) i cannot format them nicely:

"Host not allowed" when calling the API remotely -> https://caddy.community/t/host-not-allowed-when-calling-the-api-remotely/7670
Global options - admin -> https://caddyserver.com/docs/caddyfile/options#admin

What I’ve tried so far

I’ve tried to do my research and found the corresponding admin settings in the global scope. However when i try to implement the origins array as well as the other settings i get an error when loading/starting caddy.

Error:

Jun 25 06:37:43 PCDY400132V00000A systemd[1]: Starting caddy.service - Caddy...
Jun 25 06:37:43 PCDY400132V00000A caddy[22576]: {"level":"info","ts":1750833463.8042324,"msg":"maxprocs: Leaving GOMAXPROCS=2: CPU quota undefined"}
Jun 25 06:37:43 PCDY400132V00000A caddy[22576]: {"level":"info","ts":1750833463.8044524,"msg":"GOMEMLIMIT is updated","package":"github.com/KimMachineGun/automemlimit/memlimit","GOMEMLIMIT":7496592998,"previous":9223372036854775807}
Jun 25 06:37:43 PCDY400132V00000A caddy[22576]: caddy.HomeDir=/var/lib/caddy
Jun 25 06:37:43 PCDY400132V00000A caddy[22576]: caddy.AppDataDir=/var/lib/caddy/.local/share/caddy
Jun 25 06:37:43 PCDY400132V00000A caddy[22576]: caddy.AppConfigDir=/var/lib/caddy/.config/caddy
Jun 25 06:37:43 PCDY400132V00000A caddy[22576]: caddy.ConfigAutosavePath=/var/lib/caddy/.config/caddy/autosave.json
Jun 25 06:37:43 PCDY400132V00000A caddy[22576]: caddy.Version=v2.10.0 h1:fonubSaQKF1YANl8TXqGcn4IbIRUDdfAkpcsfI/vX5U=
Jun 25 06:37:43 PCDY400132V00000A caddy[22576]: runtime.GOOS=linux
Jun 25 06:37:43 PCDY400132V00000A caddy[22576]: runtime.GOARCH=amd64
Jun 25 06:37:43 PCDY400132V00000A caddy[22576]: runtime.Compiler=gc
Jun 25 06:37:43 PCDY400132V00000A caddy[22576]: runtime.NumCPU=2
Jun 25 06:37:43 PCDY400132V00000A caddy[22576]: runtime.GOMAXPROCS=2
Jun 25 06:37:43 PCDY400132V00000A caddy[22576]: runtime.Version=go1.24.4
Jun 25 06:37:43 PCDY400132V00000A caddy[22576]: os.Getwd=/
Jun 25 06:37:43 PCDY400132V00000A caddy[22576]: LANG=C.UTF-8
Jun 25 06:37:43 PCDY400132V00000A caddy[22576]: PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/snap/bin
Jun 25 06:37:43 PCDY400132V00000A caddy[22576]: SGX_AESM_ADDR=1
Jun 25 06:37:43 PCDY400132V00000A caddy[22576]: NOTIFY_SOCKET=/run/systemd/notify
Jun 25 06:37:43 PCDY400132V00000A caddy[22576]: USER=caddy
Jun 25 06:37:43 PCDY400132V00000A caddy[22576]: LOGNAME=caddy
Jun 25 06:37:43 PCDY400132V00000A caddy[22576]: HOME=/var/lib/caddy
Jun 25 06:37:43 PCDY400132V00000A caddy[22576]: INVOCATION_ID=7dd60881315642adb9008076733bd638
Jun 25 06:37:43 PCDY400132V00000A caddy[22576]: JOURNAL_STREAM=9:206266
Jun 25 06:37:43 PCDY400132V00000A caddy[22576]: SYSTEMD_EXEC_PID=22576
Jun 25 06:37:43 PCDY400132V00000A caddy[22576]: MEMORY_PRESSURE_WATCH=/sys/fs/cgroup/system.slice/caddy.service/memory.pressure
Jun 25 06:37:43 PCDY400132V00000A caddy[22576]: MEMORY_PRESSURE_WRITE=c29tZSAyMDAwMDAgMjAwMDAwMAA=
Jun 25 06:37:43 PCDY400132V00000A caddy[22576]: {"level":"info","ts":1750833463.8046012,"msg":"using config from file","file":"/etc/caddy/Caddyfile"}
Jun 25 06:37:43 PCDY400132V00000A caddy[22576]: {"level":"info","ts":1750833463.8047514,"msg":"maxprocs: No GOMAXPROCS change to reset"}
Jun 25 06:37:43 PCDY400132V00000A caddy[22576]: Error: adapting config using caddyfile: /etc/caddy/Caddyfile:3: unrecognized global option: origins
Jun 25 06:37:43 PCDY400132V00000A systemd[1]: caddy.service: Main process exited, code=exited, status=1/FAILURE
Jun 25 06:37:43 PCDY400132V00000A systemd[1]: caddy.service: Failed with result 'exit-code'.
Jun 25 06:37:43 PCDY400132V00000A systemd[1]: Failed to start caddy.service - Caddy.

Caddy-Config

Here is my modified config including the “origins” and “enforce_origins” settings:

{
        admin localhost:2019
        origins caddy-api.xx.de
        enforce_origins
}

caddy-api.xx.de {
    tls /etc/ssl/xx-de /etc/ssl/xx-de
    reverse_proxy 127.0.0.1:2019
    basic_auth /admin {
        admin xxx
    }

    log {
        output file /var/log/caddy/caddy-api.log {
            roll_size 10mb
            roll_keep 20
            roll_keep_for 90d
        }
        format json
    }
}

:80, :443 {
    root * /var/www
    file_server
    handle /health {
        rewrite * /health.html
        file_server
    }
    log {
        output file /var/log/caddy/caddy-access.log {
            roll_size 10mb
            roll_keep 20
            roll_keep_for 90d
        }
        format json
    }
}

This is not right. First of all, the (lack of) nesting is incorrect.

1 Like

You are right, I’ve changed that to:

}
        admin localhost:2019{
                origins caddy-api.xx.de
                enforce_origins
        }
}

I’ve also tried a variant without the localhost:

}
        admin :2019{
                origins caddy-api.xx.de
                enforce_origins
        }
}

Both variants still results in this error (unrecognized global option: origins):

Jun 25 07:18:46 PCDY400132V00000A systemd[1]: Starting caddy.service - Caddy...
Jun 25 07:18:46 PCDY400132V00000A caddy[22801]: {"level":"info","ts":1750835926.1332195,"msg":"maxprocs: Leaving GOMAXPROCS=2: CPU quota undefined"}
Jun 25 07:18:46 PCDY400132V00000A caddy[22801]: {"level":"info","ts":1750835926.1334367,"msg":"GOMEMLIMIT is updated","package":"github.com/KimMachineGun/automemlimit/memlimit","GOMEMLIMIT":7496592998,"previous":9223372036854775807}
Jun 25 07:18:46 PCDY400132V00000A caddy[22801]: caddy.HomeDir=/var/lib/caddy
Jun 25 07:18:46 PCDY400132V00000A caddy[22801]: caddy.AppDataDir=/var/lib/caddy/.local/share/caddy
Jun 25 07:18:46 PCDY400132V00000A caddy[22801]: caddy.AppConfigDir=/var/lib/caddy/.config/caddy
Jun 25 07:18:46 PCDY400132V00000A caddy[22801]: caddy.ConfigAutosavePath=/var/lib/caddy/.config/caddy/autosave.json
Jun 25 07:18:46 PCDY400132V00000A caddy[22801]: caddy.Version=v2.10.0 h1:fonubSaQKF1YANl8TXqGcn4IbIRUDdfAkpcsfI/vX5U=
Jun 25 07:18:46 PCDY400132V00000A caddy[22801]: runtime.GOOS=linux
Jun 25 07:18:46 PCDY400132V00000A caddy[22801]: runtime.GOARCH=amd64
Jun 25 07:18:46 PCDY400132V00000A caddy[22801]: runtime.Compiler=gc
Jun 25 07:18:46 PCDY400132V00000A caddy[22801]: runtime.NumCPU=2
Jun 25 07:18:46 PCDY400132V00000A caddy[22801]: runtime.GOMAXPROCS=2
Jun 25 07:18:46 PCDY400132V00000A caddy[22801]: runtime.Version=go1.24.4
Jun 25 07:18:46 PCDY400132V00000A caddy[22801]: os.Getwd=/
Jun 25 07:18:46 PCDY400132V00000A caddy[22801]: LANG=C.UTF-8
Jun 25 07:18:46 PCDY400132V00000A caddy[22801]: PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/snap/bin
Jun 25 07:18:46 PCDY400132V00000A caddy[22801]: SGX_AESM_ADDR=1
Jun 25 07:18:46 PCDY400132V00000A caddy[22801]: NOTIFY_SOCKET=/run/systemd/notify
Jun 25 07:18:46 PCDY400132V00000A caddy[22801]: USER=caddy
Jun 25 07:18:46 PCDY400132V00000A caddy[22801]: LOGNAME=caddy
Jun 25 07:18:46 PCDY400132V00000A caddy[22801]: HOME=/var/lib/caddy
Jun 25 07:18:46 PCDY400132V00000A caddy[22801]: INVOCATION_ID=70bd6404d3ec4171a6ce7e6d11219869
Jun 25 07:18:46 PCDY400132V00000A caddy[22801]: JOURNAL_STREAM=9:209031
Jun 25 07:18:46 PCDY400132V00000A caddy[22801]: SYSTEMD_EXEC_PID=22801
Jun 25 07:18:46 PCDY400132V00000A caddy[22801]: MEMORY_PRESSURE_WATCH=/sys/fs/cgroup/system.slice/caddy.service/memory.pressure
Jun 25 07:18:46 PCDY400132V00000A caddy[22801]: MEMORY_PRESSURE_WRITE=c29tZSAyMDAwMDAgMjAwMDAwMAA=
Jun 25 07:18:46 PCDY400132V00000A caddy[22801]: {"level":"info","ts":1750835926.1335785,"msg":"using config from file","file":"/etc/caddy/Caddyfile"}
Jun 25 07:18:46 PCDY400132V00000A caddy[22801]: {"level":"info","ts":1750835926.1336691,"msg":"maxprocs: No GOMAXPROCS change to reset"}
Jun 25 07:18:46 PCDY400132V00000A caddy[22801]: Error: adapting config using caddyfile: /etc/caddy/Caddyfile:3: unrecognized global option: origins
Jun 25 07:18:46 PCDY400132V00000A systemd[1]: caddy.service: Main process exited, code=exited, status=1/FAILURE
Jun 25 07:18:46 PCDY400132V00000A systemd[1]: caddy.service: Failed with result 'exit-code'.
Jun 25 07:18:46 PCDY400132V00000A systemd[1]: Failed to start caddy.service - Caddy.

Can you do me a favour - try adding a space between 2019 and {

Also, the global options start with opening curly brackets not closing ones.

2 Likes

THANK YOU!!! That’s it…:

Changed it to:

{
        admin localhost:2019 {
                origins caddy-api.xx.de
                enforce_origin
        }
}

There was also a typo in the enforce_origin… it was “enforce_origins” in my original config.

Now its starting and i can try curl:

curl -vvvv -u admin:$(echo "xxx" | base64 -d) https://caddy-api.xx.de/admin/config
*   Trying 128.251.115.115:443...
* Connected to caddy-api.xx.de (128.251.115.115) port 443 (#0)
* ALPN: offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
*  CAfile: /etc/ssl/certs/ca-certificates.crt
*  CApath: /etc/ssl/certs
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256
* ALPN: server accepted h2
* Server certificate:
*  subject: CN=*.xx.de
*  start date: Dec 10 00:00:00 2024 GMT
*  expire date: Dec  9 23:59:59 2025 GMT
*  subjectAltName: host "caddy-api.xx.de" matched cert's "*.xx.de"
*  issuer: C=US; O=DigiCert Inc; OU=www.digicert.com; CN=Thawte TLS RSA CA G1
*  SSL certificate verify ok.
* using HTTP/2
* Server auth using Basic with user 'admin'
* h2h3 [:method: GET]
* h2h3 [:path: /admin/config]
* h2h3 [:scheme: https]
* h2h3 [:authority: caddy-api.xx.de]
* h2h3 [authorization: Basic xxx
* h2h3 [user-agent: curl/7.88.1]
* h2h3 [accept: */*]
* Using Stream ID: 1 (easy handle 0x5ca5401a27a0)
> GET /admin/config HTTP/2
> Host: caddy-api.xx.de
> authorization: Basic xxx
> user-agent: curl/7.88.1
> accept: */*
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
< HTTP/2 403 
< alt-svc: h3=":443"; ma=2592000
< content-type: application/json
< date: Wed, 25 Jun 2025 07:43:19 GMT
< via: 1.1 Caddy
< content-length: 59
<
{"error":"client is not allowed to access from origin ''"}
* Connection #0 to host caddy-api.xx.de left intact

I will try to get the origins right now.

1 Like

Since you’re using enforce_origin, you need to send an Origin header in your request now. It needs to match the defined origins

4 Likes

Yes, got that to work also.

Thank you again :)! Great to get help so fast!

Have a nice day.