Skip to content

Security: X4Applegate/caddyui

SECURITY.md

Security Policy

Supported Versions

Version Supported
Latest 2.x release ✅ Active — fixes ship on top of the newest release
Older 2.x ⚠️ Please upgrade to the latest before reporting
1.x and earlier ❌ No longer supported — upgrade to the latest 2.x

Always run the most recent release; CaddyUI ships frequently and security fixes are not backported to older versions.


Reporting a Vulnerability

Please do not open a public GitHub Issue for security vulnerabilities.

Report security issues privately by emailing:

admin@richardapplegate.io

Include in your report:

  • A description of the vulnerability and its potential impact
  • Steps to reproduce or a proof-of-concept (PoC)
  • Affected version(s)
  • Any suggested remediation if you have one

You will receive an acknowledgement within 72 hours. If the vulnerability is confirmed, a fix will be prioritised and a patched release issued. You will be credited in the release notes (unless you prefer to remain anonymous).


Security Design

Authentication

  • Passwords hashed with bcrypt (cost factor 12)
  • Sessions use cryptographically random tokens stored in HTTP-only, SameSite=Lax cookies
  • Optional TOTP 2FA per user (RFC 6238)
  • First-run setup creates the admin account; no default credentials

Transport

  • CaddyUI itself is a plain HTTP server — it is designed to sit behind Caddy (which handles TLS termination)
  • All state-changing routes use POST and require a per-session CSRF token (since v2.29.0), on top of the browser's SameSite=Lax cookie policy
  • CaddyUI serves its own pages under a Content-Security-Policy. The sign-in, setup and password-reset pages use a vendored stylesheet; the authenticated pages still load the Tailwind CSS runtime from cdn.tailwindcss.com, which is why the policy allows inline script and eval. Self-hosting it is on the roadmap — until then, treat that CDN as part of your trust base.
  • The Caddy admin API URL is configured server-side and never exposed to the browser

Roles and the admin boundary

CaddyUI has three roles: admin, user (a customer or teammate who manages the resources they own) and view (read-only). Everything that exposes or replaces the live Caddy configuration, the database or stored credentials is admin-only: the live config page, database backup, snapshots (list, download, restore, upload), Import from Caddy, the Porkbun certificate import, notifier status, settings, users and the fleet list. Certificate export to a directory, pushing a certificate to other servers and copying certificates between servers are admin-only too.

A user account's proxy hosts, redirections and Advanced routes become Caddy routes that run with Caddy's full power, so CaddyUI inspects the generated route for every non-admin-owned resource, both when it is saved and again whenever the config is built (so a row stored by any path — UI, REST API, import, AI tool, clone — is covered):

  • upstreams may not be loopback, link-local, cloud-metadata or any registered Caddy node's admin API, and may not use unix sockets, placeholders or dial-syntax tricks; the Host override and forward-proxy / forward-auth URLs are held to the same rule
  • file_server, templates, acme_server and dynamic upstreams are refused
  • {env.…}, {file.…} and Caddyfile {$…} placeholders are refused (Caddy expands them at request time, which would let a user read the Caddy process's environment — where the DNS API token usually lives — and any file the Caddy container can read); Caddyfile text containing them, or importing anything but a snippet by name, is refused before it is sent to Caddy
  • proxy host domains must be plain hostnames and custom health checks are limited to GET/HEAD, because CaddyUI's own probes build URLs from them

Admins are never restricted by these rules.

Sign-in protection: set max_login_attempts (Settings → Security) to lock an address out after repeated failures; second-factor guesses are limited per login attempt and per account regardless of that setting, and a password change signs the account out everywhere.

Set CADDYUI_PUBLIC_URL (for example https://caddyui.example.com) so password-reset and invitation emails link to your real address; without it (or an OIDC redirect URL) the link falls back to the request's Host header.

Data

  • Data is stored in an embedded SQLite file by default, or an optional MariaDB backend, on infrastructure you control
  • Credentials are not logged: the request log records the path only (never the query string, where one-time tokens travel), and webhook / ntfy URLs and CADDY_ADMIN_URL passwords are redacted from other log lines. Nothing is transmitted to a third party except what you configure (webhooks, SMTP, AI backends, DNS providers)
  • SMTP passwords are stored in the SQLite settings table (encrypted at rest only if you use full-disk encryption on the host)

Dependencies

Core runtime dependencies are minimal:

Package Purpose
go-chi/chi HTTP routing
modernc.org/sqlite SQLite (no CGo)
go-sql-driver/mysql optional MariaDB backend
golang.org/x/crypto bcrypt + TOTP primitives
pquerna/otp TOTP code generation/verification

All dependencies can be audited in go.mod / go.sum.

AI Assistance

Development is assisted by Claude (Anthropic). No secrets, credentials, database contents, or user data are shared with Claude — only code structure and logic.


Threat Model Notes

  • CaddyUI is intended for private/internal network deployment. Exposing it directly to the public internet without authentication hardening (strong password + TOTP) is not recommended.
  • The Caddy admin API (CADDY_ADMIN_URL) should not be exposed outside the Docker network. Default Docker Compose configuration keeps it on an internal bridge.
  • Database backups contain hashed passwords and TOTP secrets — protect them accordingly.
Learn more about advisories related to X4Applegate/caddyui in the GitHub Advisory Database