Files
paskia/docs/proxy/caddy.md
T
LeoVasanko 10af29f92d MultiSite: one instance serves authentication across many domains (#4)
- Serve multiple domains (RP IDs) from one instance: host-based dispatch,
  per-domain credentials and sessions, domains managed at runtime in the
  admin UI — previously one RP per instance
- Cross-domain sign-in via Related Origin Requests: per-domain related-origins
  list with a served .well-known/webauthn document
- Explicit per-domain origin lists with shell-glob wildcards (**. for apex +
  any subdomain depth, *. for one level), editable in the admin UI with
  validation and self-lockout guards
- Per-domain auth hosts: the account/admin UI can live on a different host
  per domain, no longer confined to subdomains of a single RP
- CLI: 'paskia init <rp-id [rp-name]' initializes or adds a domain to an
  existing database; 'paskia migrate' converts legacy databases

BREAKING CHANGES (v2.0):
- Database schema: config is now per-domain and credentials/sessions carry
  an rp_id — existing databases must be converted with 'paskia migrate'
- Origins are now explicit: main implicitly allowed every subdomain of the
  RP; configure '**.' origins to reproduce that behavior
- CLI: the flat '--rp-id/--rp-name/--origin/--auth/--save' flags are
  replaced by the 'init' and 'migrate' subcommandsReviewed-on: #4
2026-09-07 22:02:06 +00:00

4.2 KiB

Paskia Caddy Configuration

/auth/api/forward · Proxy guides

Caddy is a modern web server that makes setting up web services easy. We provide a few Caddy snippets that make the configuration even easier, although the forward_auth directive of Caddy can be used directly as well. Place the auth folder with the snippets require and setup where your config file is (e.g. /etc/caddy/auth)

What these snippets do

  • setup: Mount the auth UI at /auth/ proxying to :4401
  • require: Use /auth/api/forward for access control
  • Render a login page or a permission denied page if needed (without changing URL)

Your backend may not use authentication at all, or it can make use of the user information passed via Remote-* headers by the authentication system, see trusted headers for details.

We assume the normal unprotected Caddyfile for your site looks like this:

app.example.com {
    @public path /.well-known/* /favicon.ico
    handle @public {
        root * /var/www/
        file_server
    }

    handle {
        reverse_proxy :3000  # Your app backend
    }
}

Note: We use the handle @name approach rather than handle_path to keep the path unaltered. Unlike bare directives, these blocks will be tried in sequence and each can contain what you'd typically put in your site definition (by default reverse_proxy takes precedence and nothing reaches the static files).

We will adapt from this to protect your app.

Protect your site (auth/setup, auth/require)

app.example.com {
    import auth/setup

    @public path /.well-known/* /favicon.ico
    handle @public {
        root * /var/www/
        file_server
    }

    @reports path /reports
    handle @reports {
        import auth/require perm=myapp:reports
        reverse_proxy :3000
    }

    handle {
        import auth/require max-age=12h
        reverse_proxy :3000
    }
}

The above setup allows unauthenticated access to certain files, then implements two different access controls for your backend app depending on which path is accessed. Note that the perm and max-age options may be combined, e.g. perm=myapp:admin&max-age=5min on a very sensitive endpoint. This will require additional authentication if the passkey hasn't been used in the last 5 minutes (automatic session renewals don't affect this). Use "" if you only want the user to be authenticated with no time or perm requirements.

Public access (public=1)

For routes where anonymous visitors are allowed but logged-in users should still be identified, add public=1 to the same snippet:

handle {
    import auth/require "public=1"
    reverse_proxy :3000
}

The auth check then always passes (204): anonymous requests and users lacking a requested perm reach your backend marked with a Remote-Public header (anonymous, forbidden or authenticated) instead of getting a 401/403. Your backend must check Remote-Public before treating the request as authorized — see trusted headers. A max_age reauth requirement still renders the authentication page, even on public routes.

Dedicated Authentication Site

When you setup a separate subdomain for the authentication site, just add to your config another section for the auth host:

auth.example.com {
    reverse_proxy :4401
}

Remember to set the auth host for the domain in the admin panel's Domains section to restrict the authentication services to this domain.

Note that we still reserve /auth/ on each site for logout page and any APIs your application may require, while full user profile and global options are only available on the auth host.

Paskia does not require CORS configuration, but it can access the authentication and registration of auth host WS API from the other sites as WebSockets don't require any CORS.

Override the paskia backend address (AUTH_UPSTREAM)

By default, the auth service is contacted at localhost port 4401. You can point Caddy to a different address by setting the AUTH_UPSTREAM environment variable for Caddy.

If unset, the snippets use :4401 by default.