Skip to content

Access lists & mTLS

Two ways to put something in front of a host that does not involve a login page. For the login-page version, see forward auth.

Access listsPick a list to see its members and the hosts it protects - Board preview has none.DemoNothing you change here is saved
  • unused
  • empty
4 lists, 6 members
StagingEverything not meant for the public yet
UsernameAddedActions
avery04/01/2026
sam04/01/2026
priya22/01/2026

Multi-account HTTP basic auth, assignable per proxy host. Passwords are bcrypt-hashed. One list can protect several hosts, and adding an account to the list grants it everywhere the list is used.

An empty list fails closed. A host whose list has no users and no IP rules refuses every request rather than serving it unauthenticated. The access lists page flags an empty list, and warns when a host is still using one; the host editor marks empty lists in its picker and warns when one is chosen.

A list’s Network tab holds allow and deny rules on client addresses - single IPs, CIDR ranges or hostnames, IPv4 or IPv6 - checked from the top, where the first rule that matches decides. When no rule matches sets what everyone else gets; it defaults to deny, so a list of allow rules is an allowlist. The address is the one Caddy resolves after trusted proxies, so a CDN or load balancer in front is seen through rather than allowed or denied as a whole.

The same rules can guard an L4 host, where only they apply - a list’s users and passwords have no meaning below HTTP. Either way the rules can only see the address that reaches Caddy: with Docker’s userland proxy in the path, some setups show every client as the bridge gateway, which the L4 page explains.

A rule can name a host instead of an address - a dynamic-DNS name for someone’s home connection, say. Caddy’s IP matchers only take ranges, so the controller does the lookup: it resolves A and AAAA records, writes the answer into the config, and looks the name up again whenever the record’s TTL runs out (clamped to between a minute and an hour), reloading Caddy only when the answer changed. A name is looked up as soon as it is saved, and the Network tab shows what each one currently stands for, or why it stands for nothing.

  • IPv4 answers are exact; IPv6 answers cover their /64. A home connection’s IPv6 address moves around inside its /64 as privacy addresses rotate, so one address would stop matching within hours. End the name in a prefix length to change that: home.example.com/56 for a whole /56, home.example.com/128 for the exact address. Anything wider than /48 is refused.
  • A failed lookup keeps the last answer for 24 hours, retried every minute. After that, and for a name that has never resolved, the name stands for no addresses at all.
  • No addresses fails closed for allow, and open for deny. An allow rule with nothing behind it admits nobody; a deny rule with nothing behind it denies nobody, and the Network tab warns about it. A list whose only rule is an unresolved allow shuts everyone out - on an L4 host too.
  • A name counts as one rule towards a list’s limit of 500, and stands for at most 16 addresses. Lookups go to the servers under Settings → DNS → DNS Resolvers when those are enabled, and to the controller’s own resolver otherwise - so a name that only resolves inside a network needs a controller that can see that network’s DNS.
An access list's Network tabReorder, add or remove rules and save; nothing leaves the page.DemoNothing you change here is saved
Allow or deny by the client's IP address, checked top to bottom - the first rule that matches decides. Addresses are read after Settings > Network > Trusted Proxies, so a CDN or load balancer in front is seen through. A hostname, such as a dynamic-DNS name, is looked up again as its record expires and stands for every address it resolves to: an IPv4 address exactly, an IPv6 one with its /64 (end the name in /56, /128 and so on to change that).
Resolves to 2001:db8:4f2:1a00::/64, 203.0.113.24/32
Only used while there is at least one rule.

A list can hold IP rules alone, users alone, or both. With both, Satisfy decides how they combine:

  • All (the default): the address has to be allowed and the password right.
  • Any: an allowed address gets straight in, and everyone else - a denied address included - is asked for the password. The office network walks in; working from home means logging in.

A location rule can use a list of its own instead of the host’s, or none at all: keep /admin/* to a stricter list, or leave /.well-known/* open on an otherwise protected host.

A list a host still uses cannot be deleted - whether the host itself, a location rule on it, an L4 host or the dashboard host uses it. The page names the hosts, and the REST API answers 409. Choose another list on them first, so nothing that was protected quietly stops being so.

By default the upstream never sees the credentials a list checked: CPM removes the Authorization header before forwarding the request. Turn on Pass auth to host for an application that reads the username from it itself. Lists made before this setting existed have it on, which is how they always behaved.

Simple, and appropriate for exactly the cases basic auth is appropriate for: an internal tool, a staging site, something you want off the open internet without standing up an identity provider.

Adding an account to an access listGenerate, reveal and copy all work.DemoNothing you change here is saved
Generated passwords are 24 characters from a mixed alphabet.

mTLS asks the client for a certificate. A visitor without one does not get a connection at all, which is a stronger boundary than any password - there is nothing to phish and nothing to guess.

CPM includes a CA for this. Issue client certificates from the Certificates page, hand them to the people or machines that need them, and enable mTLS on the hosts they should reach. The CA’s private key is encrypted at rest like every other secret; Caddy only ever gets the CA certificate.

Revocation is fail-closed. Revoking a certificate rejects it immediately, and revoking every certificate rejects every connection rather than falling open to none-required.

Certificates can carry roles, and roles can be given path rules per host. So one certificate reaches the application while another also reaches /admin/*:

Path Requires
/* Any valid client certificate
/admin/* The ops role

Paths can also be excluded, which is how a health check endpoint or an ACME challenge path stays reachable without a certificate.

Roles are assigned to issued certificates, so revoking one certificate does not disturb the others holding the same role.

mTLS on a proxy hostTrust by role, or pick certificates one at a time.DemoNothing you change here is saved
Mutual TLS (mTLS)Require clients to present a trusted certificate to connect
mTLS requires TLS on this host
A certificate must be set. Select roles and/or individual certificates to allow.
Leave empty to require an identity for the whole host. Comma-separated paths gate only those routes.
Paths to exclude from mTLS. These paths bypass client certificate enforcement while all other paths remain protected. Ignored if Protected Paths is set.
Trusted Roles
  • opsReaches /admin/*1 cert
  • staff1 cert
Trusted Certificates
0/2
Certificates issued by Internal CA
  • avery@laptopexpires Jan 4, 2027
  • backup-runnerexpires Jan 4, 2027