Table of Contents

Установка keycloak с помощью helm-чарта

Клонируем репозиторий с чартом:

mkdir ~/keycloak && cd ~/keycloak
git clone https://github.com/codecentric/helm-charts.git
echo '' > helm-charts/charts/keycloak/requirements.yaml

Cоздание базы на сервере postgres

user@postgres:~$ sudo su - postgres
postgres@postgres:~$ psql

postgres=# create database keycloak;
CREATE DATABASE
postgres=# create user keycloak with encrypted password 'keycloak_db_password';
CREATE ROLE
postgres=# grant all privileges on database keycloak to keycloak;
GRANT

Создание неймспейса и секретов (паролей и сертов)

Создаем неймспейс

kubectl create ns keycloak

Создаем секреты с дефолтным паролем админской учетки keycloak и паролем пользователя базы данных:

kubectl create secret generic keycloak-default-admin-password -n keycloak --from-literal=password=keycloak_console_password 
kubectl create secret generic keycloak-db-password -n keycloak --from-literal=password=keycloak_db_password --from-literal=username=keycloak 

Создаем секрет с ssl-сертификатом web-интерфейса:

openssl pkcs12 -in sso.domain.local.pfx -nocerts -out sso.domain.local.key
openssl rsa -in sso.domain.local.key -out sso.domain.local.key.pem
openssl pkcs12 -in sso.domain.local.pfx -clcerts -nokeys -out sso.domain.local.cert.crt
kubectl create secret generic -n keycloak sso-domain-local --from-file=tls.crt=./sso.domain.local.cert.crt --from-file=tls.key=./sso.domain.local.key.pem

Создаем ConfigMap с корпоративными корневыми сертификатами. Они должны быть в формате pem и оформлены стандартными разделителями - —–BEGIN CERTIFICATE—– и —–END CERTIFICATE—–.

kubectl -n keycloak create configmap ca-bundle --from-file=./ca.cer

keycloak_values.yaml

keycloak:
  replicas: 1
  image:
    tag: 8.0.2
  existingSecret: "keycloak-default-admin-password"

  extraVolumes: |
    - name: ca-bundle
      configMap:
        name: ca-bundle
  extraVolumeMounts: |
    - name: ca-bundle
      mountPath: /custom_certs/

  extraEnv: |
    - name: X509_CA_BUNDLE
      value: "/custom_certs/ca.cer"
    - name:  PROXY_ADDRESS_FORWARDING
      value: "true"
    - name: JAVA_OPTS
      value: |
        -server -Xms64m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true -Djboss.modules.system.pkgs=org.jboss.byteman -Djava.awt.headless=true -Dkeycloak.profile.feature.upload_scripts=enabled

  ingress:
    enabled: true
    path: /
    annotations: 
      kubernetes.io/ingress.class: nginx
    hosts:
      - sso.domain.local
    tls:
      - hosts:
        - sso.domain.local
        secretName: sso-domain-local

  persistence:
    deployPostgres: false
    dbVendor: "postgres"
    existingSecret: "keycloak-db-password"
    dbName: keycloak
    dbHost: 10.10.10.10
    dbPort: 5432

test:
  enabled: false

Ошибки при обновлении

При обновлении с версии 8.0.2 до версии 9.0.3 в логах возникла ошибка:

16:11:15,512 INFO  [org.keycloak.connections.jpa.updater.liquibase.LiquibaseJpaUpdaterProvider] (ServerService Thread Pool -- 68) Updating database. Using changelog META-INF/jpa-changelog-master.xml
16:11:15,595 ERROR [org.keycloak.connections.jpa.updater.liquibase.conn.DefaultLiquibaseConnectionProvider] (ServerService Thread Pool -- 68) Change Set META-INF/jpa-changelog-9.0.1.xml::9.0.1-KEYCLOAK-12579-add-not-null-constraint::keycloak failed.  Error: ERROR: duplicate key value violates unique constraint "sibling_names"
  Detail: Key (realm_id, parent_group, name)=(rdleas,  , 1_SrvDocs_DocumentationAIB_Owner) already exists. [Failed SQL: UPDATE public.KEYCLOAK_GROUP SET PARENT_GROUP = ' ' WHERE PARENT_GROUP IS NULL]
16:11:15,602 FATAL [org.keycloak.services] (ServerService Thread Pool -- 68) java.lang.RuntimeException: Failed to update database

Судя по всему - в базе две записи о группе 1_SrvDocs_DocumentationAIB_Owner.
Сначала - останавливаем keycloak. для этого редактируем statefullSet и ставим replicas: 0
Идем на сервер postgres, перевоплощаемся в пользователя postgres:

sudo su - postgres

и лечим. Для начала - восстановим базу из бекапа в исходное состояние:

dropdb keycloak
createdb keycloak
psql -d keycloak -f ./keycloak_10.06.20_15-03.bak

Дальше запускаем psql и смотрим список баз:

\l

Переключаемся на базу keycloak:

\c keycloak

Убеждаемся, что записей о группе две:

SELECT * FROM public.KEYCLOAK_GROUP WHERE name = '1_SrvDocs_DocumentationAIB_Owner';

Непонятно, какая из них правильная - удаляем обе:

DELETE FROM public.KEYCLOAK_GROUP WHERE name = '1_SrvDocs_DocumentationAIB_Owner';

Приложение для тестирования KeyCloak (python Flask)

Протестировать keycloak и убедиться, что он настроен правильно и работает можно с помощью простых приложений.

flask_oidc

https://gist.github.com/thomasdarimont/145dc9aa857b831ff2eff221b79d179a (https://gist.githubusercontent.com/thomasdarimont/145dc9aa857b831ff2eff221b79d179a/raw/a72a9462b2bd693913efba86cbc74f87c043121d/app.py)
https://gist.githubusercontent.com/thomasdarimont/145dc9aa857b831ff2eff221b79d179a/raw/a72a9462b2bd693913efba86cbc74f87c043121d/client_secrets.json
Ставим то что нужно:

sudo pip3 --trusted-host pypi.org --trusted-host files.pythonhosted.org --proxy=http://127.0.0.1:3130 install flask flask_oidc cherrypy cryptojwt cryptography>=2.8

Тестовое приложение на фреймворке flask - app.py.

#!/usr/bin/env python3
import json
import logging

from flask import Flask, g
from flask_oidc import OpenIDConnect
import requests

logging.basicConfig(level=logging.DEBUG)

app = Flask(__name__)
app.config.update({
    'SECRET_KEY': 'SomethingNotEntirelySecret',
    'TESTING': True,
    'DEBUG': True,
    'OIDC_CLIENT_SECRETS': 'client_secrets.json',
    'OIDC_ID_TOKEN_COOKIE_SECURE': False,
    'OIDC_REQUIRE_VERIFIED_EMAIL': False,
    'OIDC_USER_INFO_ENABLED': True,
    'OIDC_OPENID_REALM': 'flask-demo',
    'OIDC_SCOPES': ['openid', 'email', 'profile'],
    'OIDC_INTROSPECTION_AUTH_METHOD': 'client_secret_post'
})

oidc = OpenIDConnect(app)

@app.route('/')
def hello_world():
    if oidc.user_loggedin:
        return ('Hello, %s, <a href="/private">See private</a> '
                '<a href="/logout">Log out</a>') % \
            oidc.user_getfield('preferred_username')
    else:
        return 'Welcome anonymous, <a href="/private">Log in</a>'

@app.route('/private')
@oidc.require_login
def hello_me():
    """Example for protected endpoint that extracts private information from the OpenID Connect id_token.
       Uses the accompanied access_token to access a backend service.
    """

    info = oidc.user_getinfo(['preferred_username', 'email', 'sub', 'groups'])

    username = info.get('preferred_username')
    email = info.get('email')
    user_id = info.get('sub')
    groups = info.get('groups')
    greeting = 'Greeting!!!'
    """if user_id in oidc.credentials_store:
        try:
            from oauth2client.client import OAuth2Credentials
            access_token = OAuth2Credentials.from_json(oidc.credentials_store[user_id]).access_token
            print ('access_token=<%s>' % access_token)
            headers = {'Authorization': 'Bearer %s' % (access_token)}
            # YOLO
            # greeting = requests.get('http://localhost:8080/greeting', headers=headers).text
        except:
            print ("Could not access greeting-service")
            greeting = "Hello %s" % username
"""
    return (""" %s </br>
               Your email is %s </br>
               Your user_id is %s! </br>
               Your Groups - %s </br>
               <ul>
                 <li><a href="/">Home</a></li>
                 <li><a href="https://sso.rdleas.ru/auth/realms/rdleas/account?referrer=flask-app&referrer_uri=http://192.168.104.94:5000/private&">Account</a></li>
                </ul>""" %
            (greeting, email, user_id, groups))

@app.route('/api', methods=['POST'])
@oidc.accept_token(require_token=True, scopes_required=['openid'])
def hello_api():
    """OAuth 2.0 protected API endpoint accessible via AccessToken"""

    return json.dumps({'hello': 'Welcome %s' % g.oidc_token_info['sub']})

@app.route('/logout')
def logout():
    """Performs local logout by removing the session cookie."""

    oidc.logout()
    return 'Hi, you have been logged out! <a href="/">Return</a>'

if __name__ == '__main__':
     app.run(host= '0.0.0.0')
#    app.run()

И конфигурационный файл для него - client_secrets.json

{
    "web": {
        "issuer": "https://sso.rdleas.ru/auth/realms/rdleas",
        "auth_uri": "https://sso.rdleas.ru/auth/realms/rdleas/protocol/openid-connect/auth",
        "client_id": "test",
        "client_secret": "32e8263a-9edc-4159-902a-e23c22606b1d",
        "redirect_uris": [
            "http://192.168.104.94:5000/*"
        ],
        "userinfo_uri": "https://sso.rdleas.ru/auth/realms/rdleas/protocol/openid-connect/userinfo",
        "token_uri": "https://sso.rdleas.ru/auth/realms/rdleas/protocol/openid-connect/token",
        "token_introspection_uri": "https://sso.rdleas.ru/auth/realms/rdleas/protocol/openid-connect/token/introspect"
    }
}

Запускаем так:

./app.py

JWTConnect-Python-OidcRP

Вот еще один ваиант тестового приложения. Но его запуск я не осилил.

git clone https://github.com/openid/JWTConnect-Python-OidcRP.git
cd ./JWTConnect-Python-OidcRP/chrp
cp ./example_conf.py ./conf.py
./make_opbyuid_html.py conf > html/opbyuid.html
sudo pip3 --trusted-host pypi.org --trusted-host files.pythonhosted.org --proxy=http://127.0.0.1:3130 install cherrypy cryptojwt cryptography>=2.8