【DevOps工具篇】使用Ansible部署Keycloak oauth2proxy 和 单点登录(SSO)设置
@TOC
推荐超级课程:
Ansible 是一种基础设施即代码定义语言,Keycloak 是一种 OpenID-Connect 提供者、认证代理和可以处理用户联合的工具。
在本文中,我将描述如何通过 Ansible 完全基于模型创建可扩展的 Keycloak 单点登录(SSO)设置。要跟进,您需要对 Docker(compose)、Ansible、代理、Linux 和 OpenID-Connect 有基本了解。
Ansible 基础知识
我假设您已经对 Ansible 有一些基本了解,但通常您需要以下目录:
# 文件模板 Keycloak
mkdir ./roles/keycloak/templates
# Ansible 任务 Keycloak
mkdir ./roles/keycloak/tasks
# 文件模板部署
mkdir ./roles/deployments/templates
# Ansible 任务部署
mkdir ./roles/deployments/tasks
# 变量
mkdir ./group_vars/
如果您想要一个用于存储机密的 Ansible 保险库,而不是使用 group_vars/all.yaml
,请参考 Ansible 保险库文档
。
部署 Keycloak
OIDC 需要 https,这意味着您需要在 Keycloak 前面设置一个 TLS 设置。最简单的方法是使用 nginx 和 Let’s Encrypt
。Keycloak 的外部 https 地址将从现在起被引用为 {{ keycloak_address }}
。
让我们从 Keycloak compose 文件开始。如果您选择使用 Ansible,这应该放在 roles/keycloak/templates
中,并命名为 keycloak.yaml
。
---
version: '3.3'
services:
keycloak:
container_name: keycloak-container
command: start --hostname-strict=false --log-level=WARNING
image: quay.io/keycloak/keycloak:23.0.3 # <- 截至 2023 年 12 月版本
environment:
- KEYCLOAK_ADMIN=admin
- KEYCLOAK_ADMIN_PASSWORD={{ keycloak_admin_password }}
- PROXY_ADDRESS_FORWARDING=true
- KC_PROXY=edge
- KC_LOG_LEVEL=ALL
- KC_DB=postgres # <- 不再是默认值
- KC_DB_URL_HOST=postgres
- KC_DB_USERNAME=keycloak
- KC_DB_PASSWORD={{ keycloak_postgres_password }}
- KC_HEALTH_ENABLED=true
- KC_METRICS_ENABLED=true
- KEYCLOAK_LOGLEVEL=WARN
restart: unless-stopped
ports:
- 5050:8080
depends_on:
- postgres
postgres:
container_name: postgres-container
image: postgres:15.1
environment:
- POSTGRES_DB=keycloak
- POSTGRES_PASSWORD_FILE=/run/secrets/postgres_password
- POSTGRES_USER=keycloak
restart: unless-stopped
secrets:
- postgres_password
volumes:
- /data/keycloak-postgres/:/var/lib/postgresql/data
secrets:
postgres_password:
file: postgres_password
...
这里有很多内容,首先我们必须在 compose 文件中定义引用的变量/机密,这些变量是 keycloak_admin_password
和 keycloak_postgres_password
,我们还需要用于机密文件的 secrets 文件。其次,我们有一个卷,它将主机的文件系统路径挂载到 postgres 容器中。实际上,如果您所有的配置都是在 Ansible 中进行建模,这并不是绝对必要的,但这也意味着您无需在每次重新创建容器时都运行 Playbook。
出于演示目的,我们将这些机密定义在 group_vars/all.yaml
中,但通常机密应该在保险库中定义,并且只适用于个别主机:
# group_vars/all.yaml
keycloak_admin_password=adminpassword
keycloak_postgres_password=pgpassword
最后,我们需要定义 Ansible 任务来:
- 安装系统上必要的软件包
- 创建
/data/
目录的卷数据 - 创建用于 docker-compose 部署的目标目录
- 将 compose 文件模板化并复制到远程
- 部署 compose 文件
- 在开始配置之前等待 Keycloak 启动
作为一个 Ansible 任务文件,它应该如下所示:
# roles/keycloak/tasks/main.yaml
- name: Install docker-compose
package:
name:
- docker-compose # 应包括系统的容器管理器
state: present
- name: Create data-dir
file:
name: /data/
state: directory
- name: Create keycloak psql volume-mount
file:
name: /data/keycloak-postgres/
state: directory
- name: Create compose directory keycloak
file:
name: "/opt/keycloak/"
state: directory
- name: Copy compose templates keycloak
template:
src: "keycloak.yaml"
dest: "/opt/keycloak/"
- name: Copy compose postgres secret file
copy:
content: "{{ keycloak_postgres_password }}"
dest: "/opt/keycloak/postgres_password"
- name: Deploy compose templates
community.docker.docker_compose:
project_src: "/opt/keycloak/"
pull: true
files:
- "keycloak.yaml"
- name: Check/Wait for Keycloak to be up
uri:
url: https://keycloak.atlantishq.de/health
method: GET
return_content: yes
status_code: 200
body_format: json
register: result
until: result.status == 200 and result.json.status == "UP"
retries: 10
delay: 20
check_mode: false
创建 OIDC-客户端
现在让我们使用 oauth2proxy 来保护一个本身不支持 OIDC 的应用程序。如果您已经在使用,Traeffic 也支持此功能。
要创建一个可工作的设置,我们需要:
- 在 Keycloak 中创建一个 OIDC 客户端
- 配置并部署一个 oauth2proxy 容器在我们的应用程序之前
我们将使用 Ansible 模块 keycloak-client 作为 local_action 。
首先,让我们为我们的客户端定义必要的变量,并以一种可以轻松部署多个客户端的方式构建 Ansible 任务。
请注意,这些机密需要精确给定的长度,您应该使用所提供的命令(由同名包提供)来创建它们,这是您可以在 Ansible 中执行的操作,但我会将它们静态配置为变量。
# 这将放入 group_vars/all.yaml
keycloak_clients:
client_name:
party_secret: "$(pwgen -s 16 -n 1)"
client_id: name_of_your_client
client_secret: "$(pwgen -s 32 -n 1)"
redirect_uris:
- "https://target_subdomain.example.com/*"
description: "在 Keycloak 中显示的描述"
keycloak_id: "00000000-0000-0000-0000-000000000001"
groups:
# groups: "group1,group2"
master_address: "https://target_subdomain.example.com"
skips:
- "/logo/light.svg"
- groups 可选地定义了用户必须是其一部分才能继续的一组组
- redirect_uris 定义了一组允许的重定向 URL,这意味着在登录后将重定向到的 URL。这很重要,因为页面在重定向到登录页面时可以请求任意重定向 URL。
- master_address 定义了默认重定向,如果在登录请求中没有给出
- skips 可选地定义了一个路径列表,这些路径将在未经身份验证的情况下转发,您可以将其用于健康端点、图标或其他未经特权的页面
现在我们在任务中使用 Ansible 列表 keycloak_clients
来为我们的部署创建和更新这些客户端,就像这样:
# 这将放入 roles/keycloak/task/main.yaml,在等待任务完成之后
- name: Create Keycloak Clients
local_action:
module: keycloak_client
auth_client_id: admin-cli
auth_keycloak_url: https://keycloak.atlantishq.de/
auth_realm: master
auth_username: admin
auth_password: "{{ keycloak_admin_password }}"
state: present
realm: master
client_id: '{{ keycloak_clients[item]["client_id"] }}'
id: '{{ keycloak_clients[item]["keycloak_id"] }}'
name: '{{ keycloak_clients[item]["client_id"] }}'
description: '{{ keycloak_clients[item]["description"] }}'
enabled: True
client_authenticator_type: client-secret
public_client: false
secret: '{{ keycloak_clients[item]["client_secret"] }}'
authorization_services_enabled: true
service_accounts_enabled: true
redirect_uris: '{{ keycloak_clients[item]["redirect_uris"] }}'
web_origins: '{{ keycloak_clients[item]["redirect_uris"] }}'
frontchannel_logout: False
protocol: openid-connect
# >> 下面解释 <<
protocol_mappers:
- config:
accesss.token.claim: true
claim.name: "groups"
id.token.claim: true
userinfo.token.claim: true
full.path: false
id: "{{ keycloak_clients[item]['keycloak_id'] | regex_replace('^(?P<X>.{2})(.)', '\\g<X>' ~ '1') }}"
consentRequired: false
protocol: "openid-connect"
protocolMapper: "oidc-group-membership-mapper"
name: "client-group-mapper"
- config:
included.client.audience: '{{ keycloak_clients[item]["client_id"] }}'
id.token.claim: false
access.token.claim: true
id: "{{ keycloak_clients[item]['keycloak_id'] | regex_replace('^(?P<X>.{2})(.)', '\\g<X>' ~ '2') }}"
consentRequired: false
protocol: "openid-connect"
protocolMapper: "oidc-audience-mapper"
name: "aud-mapper-client"
with_items: "{{ keycloak_clients.keys() | list }}"
该任务对我们在之前步骤中在 group_vars/all.yaml
中定义的 keycloak_clients 列表进行迭代。第一部分应该相当明了。但第二部分呢?
这些是所谓的 OIDC 范围声明 ,简而言之,它们定义了应该传递给客户端的 OIDC 服务器上的信息。在我们的情况下,我们传递了两个特殊信息:
- 用户所属的组
- 预期的“受众”即我们正在进行身份验证的客户端的名称(oauth2proxy 在内部需要)
regex_replace
可能看起来很奇怪,但它只用一个 1 和 2 分别替换 ID 中的单个数字,以便为每个客户端创建唯一的关联 ID。意思是:
# keycloak 客户端基础 ID
00000000-0000-0000-0000-000000000001
# 变为
00100000-0000-0000-0000-000000000001
# 和
00200000-0000-0000-0000-000000000001
..您可以稍后使用类似的策略来管理更复杂映射器或声明的 ID。
创建 oauth2proxy 部署
准备工作完成后,我们现在终于可以部署一个带有应用程序的 oauth2proxy 容器。为此,首先再次创建一个 compose 模板(请注意 UPSTREAM
地址和端口,必须是目标应用程序正在运行的端口和地址):
version: "3.7"
services:
oauth2-proxy-{{ item }}:
image: bitnami/oauth2-proxy:7.3.0
depends_on:
- redis
restart: always
command:
{% if keycloak_clients[item].get("skips") %}
{% for route in keycloak_clients[item].skips %}
- --skip-auth-route
- {{ route }}
{% endfor %}
{% endif %}
- --http-address
- 0.0.0.0:{{ services[item].port }}
ports:
- {{ services[item].port }}:{{ services[item].port }}
environment:
OAUTH2_PROXY_SCOPE: openid email profile
OAUTH2_PROXY_UPSTREAMS: http://{{ ansible_default_ipv4.address }}:5000
OAUTH2_PROXY_EMAIL_DOMAINS: '*'
OAUTH2_PROXY_PROVIDER: keycloak-oidc
OAUTH2_PROXY_PROVIDER_DISPLAY_NAME: "Display Name"
OAUTH2_PROXY_REDIRECT_URL: "{{ keycloak_clients[item].master_address }}/oauth2/callback"
OAUTH2_PROXY_OIDC_ISSUER_URL: "https://{{ keycloak_address }}/realms/master"
OAUTH2_PROXY_CLIENT_ID: "{{ keycloak_clients[item].client_id }}"
OAUTH2_PROXY_CLIENT_SECRET: "{{ keycloak_clients[item].client_secret }}"
{% if keycloak_clients[item].groups %}
OAUTH2_PROXY_ALLOWED_GROUPS: {{ keycloak_clients[item].groups }}
{% endif %}
OAUTH2_PROXY_OIDC_EMAIL_CLAIM: sub
OAUTH2_PROXY_SET_XAUTHREQUEST: "true"
OAUTH2_PROXY_SESSION_STORE_TYPE: redis
OAUTH2_PROXY_REDIS_CONNECTION_URL: redis://redis
OAUTH2_PROXY_COOKIE_REFRESH: 15m
OAUTH2_PROXY_COOKIE_NAME: SESSION
OAUTH2_PROXY_COOKIE_SECRET: "{{ keycloak_clients[item].party_secret }}"
OAUTH2_PROXY_REVERSE_PROXY: "true"
OAUTH2_PROXY_SKIP_PROVIDER_BUTTON: "true"
OAUTH2_PROXY_WHITELIST_DOMAIN: "{{ keycloak_address }}"
#作为 compose 文件的一部分,我们还需要一个会话存储
redis:
image: redis:7.2.4-alpine
restart: always
volumes:
- cache:/data
#没有挂载,因为会话存储是短暂的
volumes:
cache:
driver: local
..然后使用 Ansible 任务部署它:
# 这将放入 roles/deployments/tasks/main.yaml
- name: Create opt-dir
file:
name: /opt/
state: directory
- name: OAuth2Proxy directories
file:
path: "/opt/oauth2proxy/{{ item }}/"
state: directory
recurse: yes
with_items:
- client_name
- name: Deploy OAuth2Proxy compose files
template:
src: oauth-standalone-docker-compose.yaml
dest: "/opt/oauth2proxy/{{ item }}/docker-compose.yaml"
with_items:
- client_name
- name: Deploy OAuth2Proxy
community.docker.docker_compose:
project_src: /opt/oauth2proxy/{{ item }}/
pull: true
with_items:
- client_name
现在,如果您想要进行测试,您可以尝试将其与您的应用程序一起使用,或者通过在正确的端口上运行一个简单的 Web 服务器来尝试如下:
docker run -p 5000:80 nginx
顶级 Ansible Playbook
如果您想要在您的服务器上部署整个系统,我们需要定义一些额外的 Ansible 代码。所有以下文件将会放在您的 Ansible 项目的根目录中(与 group_vars
和 roles
目录所在的同一目录)。
Host.ini
描述一组 主机 的文件,例如:
# host.ini
[keycloak]
192.168.122.1
[deployments]
192.168.122.1
playbook.yaml
描述应在哪个 主机 上运行哪些 角色 的 Playbook 文件:
- hosts: keycloak
roles:
- keycloak
- hosts: deployments
roles:
- deployments
准备好这些文件后,我们最终可以运行整个系统:
ansible-playbook -i hosts.ini playbook.yaml --diff