Single Sign On (SSO) with subdomains using Caddy v2

A short guide on how to set up email/password SSO with Caddy v2 for multiple subdomains!

I've recently taken an interest in self-hosting simple open source applications — to have fun, take control of my privacy, and learn more about Linux, Docker and DevOps!

However, with this comes the need to add some form of authentication in front of all your services. For example, you probably don't want just anyone to be able to view all your RSS feeds, or use your markdown editor freely!

The most basic form is HTTP Basic Authentication, which is a pain as it must be configured and re-entered for each subdomain/service. You also need to type it out for each service, which can be challenging on mobile.

Single Sign On (SSO) on the other hand allows you to authenticate with different services with a single login. For example, if you have a Google account, you can go to,,, etc., without having to enter your login details again for each site — and each site has the same login details, saving you precious time!

There are already a few really useful guides and write-ups on using Caddy to set up an SSO system (e.g., see here and here), but I wasn't able to immediately figure out how to get this working with the latest Caddy v2 and use it on subdomains. I still wanted to stick with Caddy despite this, as it is extremely easy to set-up and provides automatic HTTPS certificates out-of-the-box, which is really useful when getting started!

This short blog post shows you how to configure a simple email/password SSO system with Caddy v2! By the end, it is as simple as adding a single line to add SSO to your subdomain!

[Update July 2021]: The plugins used were updated since I initially published this post. I have updated this post to work with the latest versions: caddy-auth-portal (v1.4.18) and caddy-auth-jwt (v1.3.14). If coming from elsewhere it might be useful to note that the auth_portal directive was renamed to authp in newer versions!

1. Download Caddy v2 with plugins

You need the plugins caddy-auth-portal and caddy-auth-jwt.

You can get Caddy with the plugins through a variety of options: manually building from source, downloading the pre-built version from their download page, or (the easiest) using this direct link with the plugins pre-populated!

2. Set up caddy-auth-jwt

At the top of your Caddyfile, add the following:

(sso) {
    jwt {
        set auth url
        allow roles authp/user

This creates a special 'snippet' that can be reused and imported in other blocks by simply using import sso (sso can be called whatever you want, as long as you're consistent with the naming throughout your Caddyfile!).

Note: this config will automatically generate your secrets. If you're using multiple servers or need something more advanced, check out the official docs (the crypto key fields)!

Make sure you update the auth url to whatever you want (e.g., for me it could be

I'm not going to go over roles in this post, but the official caddy-auth-portal docs explain exactly how they work! For my case, I'm only using it to authenticate my own account and want access to the /settings page to update my account details (password, keys, etc.), so authp/user should be fine!

3. Set up your Caddyfile's directive order

In Caddy v2, there's a pre-set order of precedence for directives. See this issue on GitHub for more details.

But jwt and authp (the directives from the two plugins we installed) aren't in that pre-set; so we need to tell Caddy where they lie in the order of precedence.

Just after your sso snippet, add the following:

    order jwt before reverse_proxy
    order authp before jwt

4. Configure your SSO!

Now you can use the authp directive to configure your SSO setup: {
    authp {
    	backend local /etc/caddy/auth/local/users.json local
        cookie domain
        cookie lifetime 86400 # 24 hours in seconds
        ui {
            links {
        registration {
            dropbox /etc/caddy/auth/local/users.json
            code "YOUR_CODE_HERE"
        transform user {
            match email
            action add role authp/user

Make sure you set the to be your actual domain (again, it doesn't have to be auth. — it can be whatever you want as long as you're consistent). You'll also need to update your domain's DNS settings to account for this new subdomain.

It's crucial to update the cookie domain — this is what makes it work for your subdomains! cookie lifetime allows you to choose how long your login cookies should last – note that it's in seconds!

If you want the users to be stored elsewhere then you can update the path in the backend local... directive. You'll also need to update this file to change user roles.

The ui directive is optional: if enabled, when authenticated on, you will be provided with a very handy list of all your services:

The registration directive is also optional: if enabled, there will be a registration option on the login screen, and one of the registration form fields will be the code as an extra step so only people you want (or who know the code) can register.

For simplicity, you might enable registration temporarily to create your own account, and then disable it — depending on any risk to your existing services! Alternatively, you can copy/paste the superuser in the JSON file you specified and update the duplicate to be your own details, e.g., change the password (using bcrypt), the roles, ID, username, etc.

The transform user directive allows us to add roles to users, and they can be quite powerful! For example, you might want to enable multi-factor authentication to certain (or all) users – check out the official docs for more info.

5. Use your SSO!

That's all the config done! Now whenever you want to enable SSO for a subdomain/service, just import sso.

There's a caveat though: one of your subdomains/routes needs to be marked as primary yes (for reasons explained here), but the sso snippet we defined didn't have this. So, you'll need to copy and paste the config into one of your routes and add primary yes before you can just use import sso in the rest.

For example, for the subdomain {
    jwt {
        primary yes # XXX: this is the addition
        set auth url
        allow roles authp/user

    reverse_proxy http://localhost:1234

And then for (and any other subdomains): {
    import sso
    reverse_proxy http://localhost:1234

Now whenever anyone navigates to or, if they are not logged in, they will be redirected to and will have to log in:

If you're already logged in (your browser will have a cookie), then it will just let you in!

The cookie will be shared across your subdomains, so you can freely switch between all your services without the pain of logging in every time!

What if you want some paths to be public but not all? You could do this: {
    @allow path /public /anothersafepath
    handle @allow {
        reverse_proxy http://localhost:1234
    import sso
    reverse_proxy http://localhost:1234

Because of the orders we set up earlier, this simply works from top-to-bottom because handle has higher precedence than jwt (from the import sso) and also reverse_proxy. Here's the pre-defined order for reference!

If you wanted to instead only keep some paths secret, you could change the second line to @reject not path /secret /anothersecretpath (and change @allow to @reject in the third line!).

I hope this post helps setting up your SSO with Caddy. I'd highly recommend trying it out if you find yourself always needing to authenticate with different services on your domain – and check out caddy-auth-portal's docs for even more advanced features!

Disclaimer: authentication is extremely important for a variety of services so please ensure your configuration works for you. In this blog I detail how you might use SSO but am in no way accountable for any privacy or confidentiality breaches as this is dependent on your configuration.