Keycloak

Wer in Applikationen keine Benutzerschicht implementieren möchte, nutzt Keycloak. Keycloak bietet

  • Benutzer- und Gruppenmanagement

  • Session Management

  • Brute Force Detection

  • Passwort-Policies, Verify email, …

  • User Registration

  • User Self Service (Forgot Password, Edit User Data etc.)

  • User Consent

  • 2-Factor-Auth (z.B. mit FreeOTP)

  • Identity Providers (OpenID Connect oder SAML 2.0): GitHub, Twitter, Facebook, …

  • User Federation über Kerberos (AD) oder LDAP

  • Templating (Layout, E-Mails etc.)

Wird hier als Standalone Server beschrieben, nicht in der geclusterten Variante. Benötigt ca. 300 MB auf der Festplatte.

Die sogenannte „Overlay“ Download-Variante von Keycloak kommt ohne WildFly-Server daher und wird auf einem bestehenden als Add-On installiert. Von der Community nicht supportet, falls auf diesem noch weitere Applikationen laufen.

Keycloak beherrscht

  • OIDC (OpenID Connect, also OAuth 2.0 plus Login- und Profil-Informationen)

  • SAML 2.0

Im Fall von OpenID Connect/OAuth wird die Applikation durch einen OAuth-Client geschützt, indem dieser bei nicht vorhandener Session den Keycloak-Server um Authentifizierung bittet. Der Authentication Flow ist dabei wie folgt:

  1. Die Web-App (genauer: ein OAuth-Client) leitet den Benutzer auf Keylcoak um.

  2. Der Benutzer authentifiziert sich mit seinen Keycloak-Credentials.

  3. Der Benutzer wird mit einem Token auf die Web-App umgeleitet.

  4. Die Web-App bekommt einen Access-Token zugewiesen.

  5. Mit Hilfe des Access-Tokens kann die Web-App weitere Benutzerdaten abfragen.

Entweder bringt die Web-App einen OAuth-Client mit (wie z.B. Grafana - bevorzugter Lösungsansatz), man nutzt einen speziellen OAuth-Proxy (z.B. oauth2-proxy), oder man installiert einen im Webserver (z.B. für Apache mod_auth_openidc). Die verschiedenen Arten in der Übersicht:

              :443            :443         :443        :443
           oauth2-proxy      HAProxy      Apache    AnyRevProxy
                |               |            |           |
                |             :4180          |       Web-App +
                |          oauth2-proxy      |      OAuth-Client
                |               |            |           |
Keycloak <------+---------------+------------+-----------+
                |               |            |
             Web-App         Web-App      Web-App

Wichtige Keycloak-URLs - System:

  • User Account Console: /auth/realms/$REALM/account

Wichtige Keycloak-URLs - OpenID:

  • OpenID-Config: /auth/realms/$REALM/.well-known/openid-configuration

  • Authorization-URL (Login): /auth/realms/$REALM/protocol/openid-connect/auth

  • Token-URL (Redeem): /auth/realms/$REALM/protocol/openid-connect/token

  • Introspection-URL: /auth/realms/$REALM/protocol/openid-connect/token/introspect

  • UserInfo-URL (Profile): /auth/realms/$REALM/protocol/openid-connect/userinfo

  • End-Session-URL: /auth/realms/$REALM/protocol/openid-connect/logout

  • JWKS-URI: /auth/realms/$REALM/protocol/openid-connect/certs

  • Check-Session-iFrame: /auth/realms/$REALM/protocol/openid-connect/login-status-iframe.html

Ports:

  • 8080/tcp

  • Admin-Console: 9990/tcp

Breaking Changes:

  • Keycloak 17+: Der mitgelieferte Applikationsserver ist jetzt nicht mehr WildFly/Undertow, sondern Quarkus/Vertx, was logischerweise eine Reihe von Änderungen bei der Konfiguration von Keycloak und der Bereitstellung von benutzerdefinierten Providern mit sich bringt. Der Support für WildFly-basierte Keycloak-Systeme endet im Juni 2022.

  • Keycloak 16.0.0: Kommt mit WildFly 25. WildFly 25 wirft das alte Sicherheitssubsystem über Bord, das unter anderem für die Konfiguration von TLS verwendet wurde. Aufgrund des Umfangs der Änderungen gibt es nur den Weg, mit der Standardkonfiguration von Keycloak 16 zu beginnen und die relevanten Änderungen vorzunehmen, anstatt die Konfigurationsdateien von früheren Versionen von Keycloak zu kopieren.

Links

Installation bis v16 (WildFly-based)

yum -y install wget java

VER=16.0.0
cd /tmp
wget https://github.com/keycloak/keycloak/releases/download/$VER/keycloak-$VER.tar.gz
tar xvzf keycloak-$VER.tar.gz
mv keycloak-$VER /opt

cd /opt
ln -s keycloak-$VER keycloak

Keycloak soll auf jedem Interface hören, und Node-ID ändern:

cp /opt/keycloak/standalone/configuration/standalone.xml /opt/keycloak/standalone/configuration/standalone.xml.orig

sed --in-place --expression 's,inet-address value="${jboss.bind.address.management:127.0.0.1}",any-address,g' /opt/keycloak/standalone/configuration/standalone.xml
sed --in-place --expression 's,inet-address value="${jboss.bind.address:127.0.0.1}",any-address,g' /opt/keycloak/standalone/configuration/standalone.xml

NODE_ID=$(( ( RANDOM % 100000 )  + 1 ))
sed --in-place --expression "s,jboss.tx.node.id:1,jboss.tx.node.id:$NODE_ID,g" /opt/keycloak/standalone/configuration/standalone.xml

Benutzer anlegen, unter dem später der integrierte WildFly-Server laufen soll:

useradd --shell /sbin/nologin keycloak
chown -R keycloak:keycloak /opt/keycloak*

Bemerkung

Die WildFly-Konfigurationsdatei sollte nicht im laufenden Betrieb geändert werden - WildFly könnte sie überschreiben. Statt dessen verwendet man zur Laufzeit dessen CLI.

Test-Start:

cd /opt/keycloak
bin/standalone.sh

Systemd-Unit-File:

/etc/systemd/system/keycloak.service
[Unit]
Description=The Keycloak Server
After=syslog.target network.target
Before=httpd.service

[Service]
Type=idle
User=keycloak
Group=keycloak
ExecStart=/opt/keycloak/bin/standalone.sh -b 0.0.0.0
TimeoutStartSec=180
TimeoutStopSec=180
# Java programs sometimes don't send back the expected exit status when shutting down in response to SIGTERM.
# Adding the following fixes the problem:
SuccessExitStatus=143
LimitNOFILE=102642
StandardOutput=null

[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable keycloak
systemctl start keycloak

Admin-Account im Keycloak (nicht im WildFly) für den Zugriff auf die Weboberfläche (Default: http://keycloak:8080/auth) anlegen:

cd /opt/keycloak
bin/add-user-keycloak.sh --user keycloak-admin

systemctl restart keycloak

Bei Bedarf noch Benutzer für den WildFly (nicht in Keycloak) anlegen, um beispielsweise WildFly direkt überwachen zu können:

cd /opt/keycloak
bin/add-user.sh

systemctl restart keycloak

Anschliessend http://keycloak:8080/auth aufrufen, in der „Administration Console“ einloggen und das eigene Profil des Admin-Accounts konfigurieren.

Upgrade

Achtung

Keycloak 16 kommt mit WildFly 25, bei dem sich fundamental Änderungen in der Konfiguration ergeben haben. Ein Upgrade von Keycloak x auf Keycloak 16 muss daher nach https://www.keycloak.org/docs/latest/upgrading/#wildfly-25-upgrade durchgeführt werden: We recommend that rather than copying configuration files from previous versions of Keycloak that you start with the default configuration files provided in Keycloak 16 and apply the relevant changes.

Die Datenbankstrukturen werden automatisch aktualisiert, sofern in der standalone.xml der Wert migrationStrategy auf update steht (was dem Default-Verhalten entspricht).

OLD=16.0.0
NEW=16.1.0

cd /tmp
wget https://github.com/keycloak/keycloak/releases/download/$NEW/keycloak-$NEW.tar.gz
tar xvzf keycloak-$NEW.tar.gz
mv keycloak-$NEW /opt

systemctl stop keycloak
rm -rf /opt/keycloak/standalone/data/tx-object-store/*
unlink /opt/keycloak

cd /opt
ln -s keycloak-$NEW keycloak

\cp -a /opt/keycloak-$OLD/standalone/* /opt/keycloak/standalone/
cd /opt/keycloak
# for running DB migration and seeing output:
bin/jboss-cli.sh --file=bin/migrate-standalone.cli
bin/standalone.sh

systemctl start keycloak

Siehe http://www.keycloak.org/docs/latest/upgrading/index.html

Keycloak hinter einem Reverse Proxy

Der Reverse Proxy muss das ursprünglich angeforderte Protokoll durchreichen.

Nicht vergessen:

  • im Apache vHost: RequestHeader set X-Forwarded-Proto "https"

  • auf dem HAProxy: im Keycloak-Backend http-request add-header X-Forwarded-Proto https

Keycloak selbst muss wie folgt konfiguriert werden:

standalone.xml
<subsystem xmlns="urn:jboss:domain:undertow:12.0" ...>
   ...
   <server name="default-server">
      ...
      <http-listener name="default" socket-binding="http" redirect-socket="https" enable-http2="true"
          proxy-address-forwarding="true"/>
      ...
   </server>
   ...
</subsystem>

SQL-Datenbank Backend

Eine SQL-Datenbank wie MariaDB oder PostgreSQL ist nur bei grösseren Installationen oder bei einem Keycloak-Cluster (dann zwingend und dediziert) nötig, ansonsten verwendet Keycloak intern die Java SQL H2-Datenbank. In diesem Fall liegen die DB-Dateien in standalone/data/. Die H2-DB lässt sich per java -jar modules/system/layers/base/com/h2database/h2/main/h2-*.jar administrieren (auf de lokalen Admin-Maschine per sshfs ausführen - es wird ein Browser gestartet).

LDAP per FreeIPA

Benutzer aus dem FreeIPA-LDAP synchronisieren. Im passenden Realm auf User Federation > Add provider > LDAP klicken. Dann:

  • Console Display Name: FreeIPA LDAP

  • Edit Mode: WRITABLE (falls die Benutzer ihr Passwort o.ä. per Keycloak ändern dürfen)

  • Vendor: Red Hat Directory Server

    • Username LDAP attribute: uid

    • RDN LDAP attribute: uid

    • UUID LDAP attribute: nsuniqueid

    • User Object Classes: inetOrgPerson, organizationalPerson

  • Connection URL: ldap://freeipa:389

  • Users DN: cn=users,cn=accounts,dc=linuxfabrik,dc=it

  • Bind Type: simple

  • Bind DN: cn=Directory Manager

Im Abschnitt „Sync Settings“ lässt sich die Art (Full vs. Partial) sowie die Häufigkeit der Synchronisation konfigurieren.

Wer LDAP-Gruppenzugehörigkeit auf Roles in Keycloak mappen möchte, erstellt unter „User Federation > LDAP > Mappers“ einen „role-ldap-mapper“, z.B. „mygroup-role-ldap-mapper“:

  • Name: mygroup-role-ldap-mapper

  • Mapper Type: role-ldap-mapper

  • LDAP Roles DN: cn=groups,cn=accounts,dc=linuxfabrik,dc=it (dort finden sich unter „member“ die zur Gruppe gehörenden Benutzer)

  • LDAP Filter: (cn=mygroup)

  • Nach dem Klick auf „Sync LDAP Groups To Keycloak“ taucht die gewünschte Gruppe im Keycloak-Realm in der obersten Ebene unter „Groups“ auf.

Realms

Realm einrichten

Ein Realm bezeichnet in der Regel eine Organisation (z.B. Linuxfabrik), in der verschiedene Clients (also Applikationen wie beispielsweise Nextcloud, Grafana usw.) mit unterschiedlichen Methoden (SAML, OIDC) konfiguriert werden. Ein Realm verwaltet zudem eine User-Base.

Die Screenshots zeigen die Einrichtung eines Realms „example“ für eine zu schützende Client-Applikation (PHP), die sich auf den OAuth-Client im Apache verlässt. In den Screenshots wird dem Realm die UUID „3323f2c6-5389-4bc3-9a25-a8d8ae64c6d4“ zugewiesen, die so auch in der Webserver/Reverse Proxy Konfiguration verwendet werden muss.

Im Beispiel stellvertretend für eine Applikation ohne eigene Authentifizierungs-Mechanismen ein „dummes“ PHP-Skript, welches sich bei der Authentifizierung auf Keycloak verlässt, aber die von Keycloak gelieferten Daten ausliest:

test.php
<h1>MyApp</h1>
<p>
    <?php
        echo 'Welcome, ' . $_SERVER['OIDC_CLAIM_email'];
        echo '<pre>';
        print_r($_SERVER);
        echo '</pre>';
    ?>
</p>
Neuen Realm hinzufügen:
../_images/keycloak-realm-01.png
../_images/keycloak-realm-02.png
../_images/keycloak-realm-03.png
../_images/keycloak-realm-04.png
Client (zu schützende Applikation) einrichten:
../_images/keycloak-realm-05.png

Der Tab „Credentials“ erscheint nur, wenn „Access Type: confidential“ gewählt wurde. Bedeutet, dass der OAuth-Client eine Client-ID und ein Secret verwenden muss.

../_images/keycloak-realm-06.png

„Secret“ notieren, wird für die Einrichtung der Webserver-vHosts benötigt.

Authentication Policies für die Benutzer festlegen:
../_images/keycloak-realm-07.png
../_images/keycloak-realm-08.png
../_images/keycloak-realm-09.png
Benutzer anlegen (sofern sie nicht aus einem LDAP stammen):
../_images/keycloak-realm-10.png
../_images/keycloak-realm-11.png
Event-Logging konfigurieren:
../_images/keycloak-realm-12.png

Testen eines Realms

Was liefert Keycloak auf Anfragen zurück? Kann auf jedem beliebigen Host ausgeführt werden:

# Setttings
KEYCLOAK_HOST=https://idp.linuxfabrik.ch
KEYCLOAK_USERNAME=<Keycloak username>
KEYCLOAK_PASSWORD=<Keycloak password>
KEYCLOAK_REALM=<Keycloak realm name>
KEYCLOAK_CLIENT_SECRET=<Keycloak client secret>
KEYCLOAK_CLIENT_ID=<Client ID>

# Get token
TOKEN=$(curl -s \
    -d "client_id=$KEYCLOAK_CLIENT_ID" \
    -d "client_secret=$KEYCLOAK_CLIENT_SECRET" \
    -d "username=$KEYCLOAK_USERNAME" \
    -d "password=$KEYCLOAK_PASSWORD" \
    -d "grant_type=password" \
    "$KEYCLOAK_HOST/auth/realms/$KEYCLOAK_REALM/protocol/openid-connect/token" | jq -r '.access_token')

# Use token to get userinfo
curl --silent \
    -H "Authorization: bearer $TOKEN" \
    $KEYCLOAK_HOST/auth/realms/$KEYCLOAK_REALM/protocol/openid-connect/userinfo | jq

2FA: One Time Passwords (OTP)

Konfiguriert sich ein Benutzer ein OTP (Nutzung per FreeOTP oder Google Authenticator) in auth/realms/$REALM/account/totp, codiert der QR-Code u.a.

  • TOTP (Time-based OTP)

  • Realm-Name

  • Benutzername

  • gemeinsames Secret: 32 stellig, alphanumerisch, wechselt bei jedem Seiten-Reload

  • OTP-Länge: genau 6 Zeichen

  • Algorithmus: SHA1

  • period - Wechsel des OTP nach: 30 Sekunden

Diese Default-Vorgaben lassen sich in Authentication > OTP Policy ändern. Unter Authentication > Required Actions > Configure OTP lässt sich einstellen, dass alle neuen Benutzer ein OTP verwenden müssen.

Hinweis: die Google Authenticator App auf Android hat Stand 2020-02 mit 8 statt 6 Zeichen und SHA256 statt SHA1 ein Problem; auf dem iPhone dagegen funktioniert der Google Authenticator tadellos. Mit FreeOTP klappt es dagegen immer und auf beiden Plattformen.

Troubleshooting

tail -f /opt/keycloak/standalone/log/server.log

Werden die richtigen Header gesendet, z.B. vom Reverse Proxy? Keycloak beenden und mit nc einen Service auf Port 8080 simulieren, damit man die eingehenden Anfragen an den vermeintlichen Keycloak-Server sieht und auswerten kann.

nc -l 8080

Ergibt z.B.:

GET /auth/realms/example/protocol/openid-connect/auth?response_type=code&scope=openid%20email&client_id=example&state=HcKXBTixVaDEPkm_64sfFr8lvTo&redirect_uri=https%3A%2F%2Fexample.linuxfabrik.ch%2F%2A&nonce=ZdCA4AEt9BmDvPEew_BGCvvcWNwfB1vli77embyRxkw HTTP/1.1
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng, */*;q=0.8
Connection: Keep-Alive
Cookie: AUTH_SESSION_ID=3d0208c1-80...6dd4821b36.myserver; KEYCLOAK_IDENTITY=eyJhbGc...-zYQ; KEYCLOAK_SESSION=example/cb49c45b-...f-460a3504ca02/3d0208c1-80...736dd4821b36
Host: 192.168.122.235:8080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Fedora; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.75 Safari/537.36
X-Forwarded-For: 192.168.26.1
X-Forwarded-Host: 192.168.122.235:8080
X-Forwarded-Port: 443
X-Forwarded-Proto: https
X-Forwarded-Server: 192.168.122.235:8080
Failed to send email javax.mail.MessagingException: Could not convert socket to TLS; nested exception is: javax.net.ssl.SSLException: Unexpected error: java.security.InvalidAlgorithmParameterException: the trustAnchors parameter must be non-empty

Als erstes versuchen, den Keycloak-Service neuzustarten, danach mit dem Debugging anfangen.