Alex Daichendt

Securing a Caddy endpoint with LLDAP

4 min read


For my small home network, I was looking around for a solution to synchronize user accounts across services. I host various services like a file server or smaller web applications that are accessed by my significant other and a couple of friends. In the past, I manually created accounts and passed on the password, which is slightly tedious. Another pain point is, that my web applications are unsecured at the moment. I would like to have an SSO login screen before being able to access a service. As a reverse proxy, I already deployed Caddy.

After researching I found out that for me and the services I am running, LDAP would be the best choice as it has the best compatibility and appears to be the industry standard for this kind of problem. Looking for a server proofed hard: most of them are rather heavy with lots of functions that I don’t need or are tedious to configure. Consequently, I searched for a lightweight server and found one: LLDAP is exactly what I was looking for. The project includes a lightweight LDAP server, which only supports a bare minimum of features (users, groups). Passwords can be reset by users in a small - admittedly ugly - web interface. Perfect for me. For locking down web applications there is caddy-security - an addon that allows interaction with LDAP before granting access to a site.

Setup

Since I installed a Caddy Docker image with caddy-security already integrated, I did not have to do anything else. For example, Alexander Henderson’s image comes with several useful modules preinstalled, that I require for other projects anyway. If that’s not an option, you can easily create your own Docker image including caddy-security.

The installation is quickly done with Docker. I mounted a folder instead of the suggested volume. The initial password can be reset in the administration panel. I utilize a custom bridge for networking so that I can resolve my other services with DNS. After that, we can navigate to http://IP:17170 and are presented with the administration panel, where we can create users and groups.

LLDAP Userinterface

Integration with Caddy

Assume we have a domain example.xyz. You can already create A or CNAME records for the domains auth.example.xyz and example.xyz and point them at your Caddy server.

Now we need to prepare the authentication part in the global block of the Caddy file as follows. In your Caddy docker-compose file make sure to add the two env vars for LLDAP_ADMIN_PASSWORD and JWT_SHARED_KEY. The part with search_filter and group is crucial. The search_filter is the query that is used to find your user object in the domain. Once it is found, depending on the groups your user is in, groups get assigned within the Caddy authentication procedure. In this example, a domain user that belongs to the group user gets assigned the authp/user group.

The file is an adoption of the caddy-security documentation 1 to interact with LLDAP:

order authenticate before respond
order authorize before basicauth

security {
    ldap identity store example.xyz {
        realm example.xyz
        servers {
            ldap://lldap:3890
        }
        attributes {
            name displayName
            surename cn
            username uid
            member_of memberOf
            email mail
        }
        username "CN=admin,OU=people,DC=example,DC=xyz"
        password "{env.LLDAP_ADMIN_PASSWORD}"
        search_base_dn "DC=example,DC=xyz"
        search_filter "(&(uid=%s)(objectClass=person))"
        groups {
            "uid=user,ou=groups,dc=example,dc=xyz" authp/user
        }
    }

    authentication portal myportal {
        crypto default token lifetime 3600
        crypto key sign-verify {env.JWT_SHARED_KEY}
        enable identity store example.xyz
        cookie domain example.xyz
        ui {
            logo url "https://caddyserver.com/resources/images/caddy-circle-lock.svg"
            logo description "Caddy"
            links {
                "My Identity" "/whoami" icon "las la-user"
            }
        }
    }

    authorization policy mypolicy {
        # disable auth redirect
        set auth url https://auth.example.xyz

        crypto key verify {env.JWT_SHARED_KEY}
        allow roles authp/user
    }
}

Finally, we need to create endpoints for auth and whatever service we need to protect. Make sure that the names myportal and mypolicy match with the previously declared ones. Note: the import tls originated from my globally defined TLS setup and is not relevant to this guide.

auth.example.xyz {
    route {
        authenticate with myportal
    }
}

example.xyz {
    root * /config/html
    authorize with mypolicy
    encode gzip
    file_server browse
    import tls
}

I strongly recommend consulting the documentation 1 2 for in-depth information.

Happy authenticating!

Footnotes