Central Authentication and SSO

Introduction

So far, we’ve been pretty blasé about our use of superadmin and root accounts to do all our work. In a single user environment, this isn’t that big a deal. Any production environment you would run into is not a single user environment, and it’s important to understand how the big boys handle authentication. For this we will be learning SSO

TL;DR

in this article, we will:

  • Set up keycloak for single sign on
  • Set gitea and portainer to authenticate through keycloak

What is SSO?

SSO (Single Sign On) is the idea that the web service using an account does not control the authentication of the account. Instead of having a database with (dubiously implemented) encrypted passwords, they just throw up their hands and say “Here SSO server, you handle it”. They check with the SSO server if a person trying to access their service can access their service, and trust the server to tell them yes or no.

SSO is a subset of centralized authentication. Typically central auth can be broken into two components: On premise authentication protocols (such as LDAP or Active Directory) and web authentication (such as OIDC and SAML). We will focus on web authentication.

SSO is incredibly important for security: aside from reducing the amount of separate accounts people require, it allows for a central contact point for authorization. If you disable an account on the SSO server, it gets disabled everywhere. It also allows you to pay extra attention to the SSO server’s security policies: It’s much easier to set up multi-factor authentication, and strong user policies in one place than 20.

Deploying Keycloak

There is a couple different options for single sign-on, but one of the most secure options is keycloak. Your SSO provider needs to be as secure as possible: it’s acting as the authority for multiple different web services. Keycloak is the upstream service for Red Hat’s identity management and is about as secure as you can get.

Instead of logging into portainer, let’s log into Gitea and document our config file:

version: '3'

services:
  keycloak-db:
      container_name: keycloak-db
      image: postgres:13
      restart: always
      volumes:
        - /mnt/containers/keycloak/container-data/db:/var/lib/postgresql/data:Z
      environment:
        POSTGRES_DB: keycloak
        POSTGRES_USER: keycloak
        POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      networks:
        - keycloak-nw
  keycloak:
      container_name: keycloak
      image: quay.io/keycloak/keycloak:latest
      restart: always
      environment:
        DB_VENDOR: POSTGRES
        DB_ADDR: keycloak-db
        DB_DATABASE: keycloak
        DB_USER: keycloak
        DB_SCHEMA: public
        DB_PASSWORD: ${POSTGRES_PASSWORD}
        KEYCLOAK_USER: admin
        KEYCLOAK_PASSWORD: ${KEYCLOAK_PASSWORD}
        PROXY_ADDRESS_FORWARDING: 'true'
      # ports:
        # - 8080:8080
      depends_on:
        - keycloak-db
      networks:
        - reverseproxy-nw
        - keycloak-nw
        
networks:
    keycloak-nw:
    reverseproxy-nw:
        external: true

We also need to set our dns for auth.<mydomain>.<tld> at our router (or public DNS provider)

and set the url/SSL in nginx proxy manager:

Deploying Keycloak with Gitea

So far, we have been manually updating our gitea documentation after applying it in portainer (or raw docker-compose). This isn’t ideal since unless you’re diligent, the two configs can diverge. If that happens, bad things happen. Like meetings in the business world.

We’ve been constrained to doing this manually because trying to pull from Gitea without the required scaffolding ends up in a chicken-and-egg situation. How do you use a config from Gitea when the config is for Gitea?

Well now that we have our base infrastructure in place, we can do away with this double handling for all future services. Let’s show that now.

  • In portainer, create a new stack. Call it keycloak and set it to use a git repository. Set the url to http://gitea:3000/homelab/docker-infrastructure. Set the compose path to keycloak/docker-compose.yaml.

Because portainer and gitea are on the same docker reverseproxy-nw network, we can refer to gitea by its internal name and use unencrypted http

  • in Environment Variables set your POSTGRES_PASSWORD and KEYCLOAK_PASSWORD to randomly generated values and deploy

Your new deployment is now tied to your gitea config! If you change the gitea config, within 5 minutes portainer will automatically redeploy the new config. No double handling, and easier maintenance of your docker environment. Cool

Getting started with Keycloak

Alright, we’ve got a login! At least we do if it all went well. Go ahead and log into auth.<yourdomain>.<tld> with the user admin and the password specified in portainer:

At first glance keycloak is… kind of overwhelming. That’s because it’s designed with very large scales in mind, and accordingly there’s a lot of knobs to tweak. The good news is that the defaults are mostly OK.

First step is we need a user and a group for our central authentication. Create a homelab group first:

and create a user:

Under roles, also give your user admin privileges

Log off and log back on with your primary user. This is now the user we will use to authenticate with other services.

Typically once this step is completed, the super admin account will get a hardened password and alerts upon sign in. This type of super admin account is called a break glass account. This also applies to portainer later on. For Gitea we will simply link the oidc login to the existing user: in a production environment, the superadmin would be a dedicated account.

You now have an authentication server with robust security controls. For example, you can set up multi-factor authentication for your user, and every application you authorize with keycloak will also get multifactor authentication.

Registering Gitea as a Client

In keycloak, create a new client in the clients sidebar. Create the client as gitea and URL as https://git.<yourdomain>.<tld>. set the type as open ID connect:

Once created, change the Access type to confidential. Save at the bottom.

Under Credentials, copy out the secret. We will need this shortly.

Setting Gitea Up

  • In cockpit, let’s edit the base gitea config, and make sure the ROOT_URL is set to https://git.<mydomain>.<tld>. If it’s not set correctly, keycloak and gitea won’t talk nice to eachother.
micro /mnt/containers/gitea/container-data/data/gitea/conf/app.ini

  • You can also disable registration at the same time (if you prefer)

  • Quit with control+q and save with y. Now restart gitea in portainer, or if you want a shortcut you can simply run docker restart gitea in the terminal.

in the Gitea interface, navigate to site administration.

under Authentication Sources, Add a source. Set the Authentication name to keycloak and the Oauth Provider to OpenID Connect. The Key is gitea and the client secret is the one you copied earlier. The Icon URL is just a link to an image, let’s grab it off keycloak’s website with https://www.keycloak.org/resources/images/keycloak_logo_480x108.png. The OIDC auto discovery URL is https://auth.<yourdomain>.<tld>/auth/realms/master/.well-known/openid-configuration (change your domain of course).

Testing Login

Phew, that was quite a few steps! If all goes well you can sign out, and get a shiny new icon to sign in. First sign in it’ll ask you to link to an existing account (since we disabled registration), and bam! You can now log in using your keycloak credentials, no additional password required. You can see this below:

Single Sign on with Portainer

This article is starting to get a bit long, but let’s do this one more time. Create another client in keycloak called portainer.

Same thing, set the access to confidential and save. Copy out the secret.

in portainer, under settings→authentication, create a custom Oauth provider. We can also turn on automatic user provisioning (also referred to as just-in-time provisioning) to have keycloak generate user accounts in portainer on the fly.

Set the following:

Client ID: portainer
Client Secret: <your secret>
Authorization URL: https://auth.<yourdomain>.<tld>/auth/realms/master/protocol/openid-connect/auth
Access Token URL: https://auth.<yourdomain>.<tld>/auth/realms/master/protocol/openid-connect/token
Resource URL: https://auth.<yourdomain>.<tld>/auth/realms/master/protocol/openid-connect/userinfo
Redirect URL: https://portainer.<yourdomain>.<tld>
Logout URL: https://auth.<yourdomain>.<tld>/auth/realms/master/protocol/openid-connect/logout
User Identifier: email
Scopes: email

If you are curious where those values came from, they are from the url we gave gitea earlier. Portainer doesn’t support automatically filling those values like Gitea does.

Give it a test! Open an incognito window and log in with portainer and keycloak. It should log you in, even though there is no account in portainer yet!

Of course you don’t get assigned any roles, so you’re locked out of everything (this is good). We logged in via incognito so we can assign the correct roles from the super admin account.

Moving on

Alright! That article was a bit of a marathon, but now we have a single account we can use to log into Keycloak, Gitea and Portainer. Fantastic!

Fedora does not get the same treatment as Linux cannot talk SSO protocols by default. Instead, Fedora expects an LDAP or active directory authorization system (which we don’t plan to go into).

Now we have most of our docker infrastructure in place. What we really need for a cherry on top is a kickass wiki! Let’s combine all of the infrastructure tools we have in place to deploy outline in Deploying Outline Wiki