Установка 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
- Созданный ConfigMap с корпоративными корневыми сертификатами монтируется в директорию /custom_certs/.
- Переменная X509_CA_BUNDLE указывает на путь к файлу с сертификатами и при старте контейнера используется скриптом /opt/jboss/tools/x509.sh, который импортирует сертификаты из файла в keystore.
- Переменная PROXY_ADDRESS_FORWARDING нужна для нормального проксирования заголовков X-Forwarded-For. Если ёе не задать, то при попытке открыть Keycloak Administration Console можно будет увидеть лишь пустую страницу.
- Переменная JAVA_OPTS переопределяется для того, чтобы добавить -Dkeycloak.profile.feature.upload_scripts=enabled. Это нужно для того, чтобы нормально работал импорт ранее забекапленных (экспортированных) реалмов. Без этого параметра, при попытке импорта ранее экспортированного реалма в логах будет ошибка:
keycloak Uncaught server error: java.lang.RuntimeException: Script upload is disabled
Ошибки при обновлении
При обновлении с версии 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
Discussion