05_keycloak.yml 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. ---
  2. # playbooks/05_keycloak.yml
  3. # Deploy Keycloak on ai_server, create realm, client, roles, and admin user
  4. - name: "Keycloak | Deploy and configure Keycloak"
  5. hosts: ai_server
  6. become: true
  7. gather_facts: true
  8. tags:
  9. - keycloak
  10. vars:
  11. vault_token_file: "{{ playbook_dir }}/../vault/.vault-token"
  12. vault_url: "http://{{ ai_server_ip }}:{{ vault_port }}"
  13. keycloak_container_name: keycloak
  14. keycloak_port: 8180
  15. keycloak_data_dir: /mnt/ai_data/keycloak
  16. keycloak_realm: "{{ vault_project_slug }}"
  17. keycloak_base_url: "http://localhost:8180"
  18. tasks:
  19. # ── Secret resolution: Vault if available, otherwise generate locally ─
  20. - name: "Keycloak | Check if Vault token exists"
  21. ansible.builtin.stat:
  22. path: "{{ vault_token_file }}"
  23. register: vault_token_stat
  24. delegate_to: localhost
  25. become: false
  26. tags:
  27. - keycloak-secrets
  28. - name: "Keycloak | Set vault_available fact"
  29. ansible.builtin.set_fact:
  30. vault_available: "{{ vault_token_stat.stat.exists }}"
  31. tags:
  32. - keycloak-secrets
  33. # -- From Vault (when available) --
  34. - name: "Keycloak | Retrieve admin password from Vault"
  35. ansible.builtin.set_fact:
  36. keycloak_admin_password: "{{ lookup('community.hashi_vault.hashi_vault', vault_secret_prefix ~ '/keycloak:admin_password token=' ~ lookup('ansible.builtin.file', vault_token_file) ~ ' url=' ~ vault_url) }}"
  37. when: vault_available
  38. tags:
  39. - keycloak-secrets
  40. - name: "Keycloak | Retrieve client secret from Vault"
  41. ansible.builtin.set_fact:
  42. keycloak_client_secret: "{{ lookup('community.hashi_vault.hashi_vault', vault_secret_prefix ~ '/keycloak:client_secret token=' ~ lookup('ansible.builtin.file', vault_token_file) ~ ' url=' ~ vault_url) }}"
  43. when: vault_available
  44. tags:
  45. - keycloak-secrets
  46. - name: "Keycloak | Retrieve realm admin password from Vault"
  47. ansible.builtin.set_fact:
  48. keycloak_realm_admin_password: "{{ lookup('community.hashi_vault.hashi_vault', vault_secret_prefix ~ '/keycloak:realm_admin_password token=' ~ lookup('ansible.builtin.file', vault_token_file) ~ ' url=' ~ vault_url) }}"
  49. when: vault_available
  50. tags:
  51. - keycloak-secrets
  52. # -- Generate locally (when Vault not available) --
  53. - name: "Keycloak | Generate admin password locally"
  54. ansible.builtin.command: openssl rand -base64 16
  55. register: _kc_admin_pass_gen
  56. delegate_to: localhost
  57. become: false
  58. changed_when: false
  59. when: not vault_available
  60. tags:
  61. - keycloak-secrets
  62. - name: "Keycloak | Generate client secret locally"
  63. ansible.builtin.command: openssl rand -hex 32
  64. register: _kc_client_secret_gen
  65. delegate_to: localhost
  66. become: false
  67. changed_when: false
  68. when: not vault_available
  69. tags:
  70. - keycloak-secrets
  71. - name: "Keycloak | Generate realm admin password locally"
  72. ansible.builtin.command: openssl rand -base64 16
  73. register: _kc_realm_pass_gen
  74. delegate_to: localhost
  75. become: false
  76. changed_when: false
  77. when: not vault_available
  78. tags:
  79. - keycloak-secrets
  80. - name: "Keycloak | Set locally generated secrets as facts"
  81. ansible.builtin.set_fact:
  82. keycloak_admin_password: "{{ _kc_admin_pass_gen.stdout }}"
  83. keycloak_client_secret: "{{ _kc_client_secret_gen.stdout }}"
  84. keycloak_realm_admin_password: "{{ _kc_realm_pass_gen.stdout }}"
  85. when: not vault_available
  86. tags:
  87. - keycloak-secrets
  88. - name: "Keycloak | Save generated credentials to vault/keycloak-credentials.txt"
  89. ansible.builtin.copy:
  90. content: |
  91. # Keycloak credentials — generated {{ ansible_date_time.iso8601 }}
  92. # Vault was not available; store these securely.
  93. keycloak_admin_password={{ keycloak_admin_password }}
  94. keycloak_client_secret={{ keycloak_client_secret }}
  95. keycloak_realm_admin_password={{ keycloak_realm_admin_password }}
  96. dest: "{{ playbook_dir }}/../vault/keycloak-credentials.txt"
  97. mode: "0600"
  98. delegate_to: localhost
  99. become: false
  100. when: not vault_available
  101. tags:
  102. - keycloak-secrets
  103. # ── Container deployment ─────────────────────────────────────────
  104. - name: "Keycloak | Create data directory"
  105. ansible.builtin.file:
  106. path: "{{ keycloak_data_dir }}"
  107. state: directory
  108. mode: "0755"
  109. owner: "1000"
  110. group: "1000"
  111. tags:
  112. - keycloak-deploy
  113. - name: "Keycloak | Run Keycloak container"
  114. community.docker.docker_container:
  115. name: "{{ keycloak_container_name }}"
  116. image: quay.io/keycloak/keycloak:latest
  117. state: started
  118. restart_policy: unless-stopped
  119. ports:
  120. - "{{ keycloak_port }}:8080"
  121. env:
  122. KEYCLOAK_ADMIN: admin
  123. KEYCLOAK_ADMIN_PASSWORD: "{{ keycloak_admin_password }}"
  124. KC_PROXY_HEADERS: xforwarded
  125. KC_HOSTNAME: "https://idm.{{ domain }}"
  126. KC_HTTP_ENABLED: "true"
  127. volumes:
  128. - "{{ keycloak_data_dir }}:/opt/keycloak/data"
  129. command: start
  130. tags:
  131. - keycloak-deploy
  132. - name: "Keycloak | Wait for Keycloak to be ready"
  133. ansible.builtin.uri:
  134. url: "{{ keycloak_base_url }}/realms/master"
  135. method: GET
  136. status_code: 200
  137. timeout: 10
  138. register: keycloak_ready
  139. retries: 30
  140. delay: 10
  141. until: keycloak_ready.status == 200
  142. tags:
  143. - keycloak-deploy
  144. # ── Admin token ──────────────────────────────────────────────────
  145. - name: "Keycloak | Get admin access token"
  146. ansible.builtin.uri:
  147. url: "{{ keycloak_base_url }}/realms/master/protocol/openid-connect/token"
  148. method: POST
  149. body_format: form-urlencoded
  150. body:
  151. grant_type: password
  152. client_id: admin-cli
  153. username: admin
  154. password: "{{ keycloak_admin_password }}"
  155. status_code: 200
  156. register: keycloak_admin_token
  157. tags:
  158. - keycloak-configure
  159. - name: "Keycloak | Set admin token fact"
  160. ansible.builtin.set_fact:
  161. kc_token: "{{ keycloak_admin_token.json.access_token }}"
  162. tags:
  163. - keycloak-configure
  164. # ── Create realm ─────────────────────────────────────────────────
  165. - name: "Keycloak | Check if realm exists"
  166. ansible.builtin.uri:
  167. url: "{{ keycloak_base_url }}/admin/realms/{{ keycloak_realm }}"
  168. method: GET
  169. headers:
  170. Authorization: "Bearer {{ kc_token }}"
  171. status_code: [200, 404]
  172. register: realm_check
  173. tags:
  174. - keycloak-realm
  175. - name: "Keycloak | Create realm {{ keycloak_realm }}"
  176. ansible.builtin.uri:
  177. url: "{{ keycloak_base_url }}/admin/realms"
  178. method: POST
  179. headers:
  180. Authorization: "Bearer {{ kc_token }}"
  181. Content-Type: application/json
  182. body_format: json
  183. body:
  184. realm: "{{ keycloak_realm }}"
  185. displayName: "{{ keycloak_realm_display }}"
  186. enabled: true
  187. status_code: [201, 409]
  188. when: realm_check.status == 404
  189. tags:
  190. - keycloak-realm
  191. # ── Create client ────────────────────────────────────────────────
  192. - name: "Keycloak | Check if open-webui client exists"
  193. ansible.builtin.uri:
  194. url: "{{ keycloak_base_url }}/admin/realms/{{ keycloak_realm }}/clients?clientId=open-webui"
  195. method: GET
  196. headers:
  197. Authorization: "Bearer {{ kc_token }}"
  198. status_code: 200
  199. register: client_check
  200. tags:
  201. - keycloak-client
  202. - name: "Keycloak | Create open-webui client"
  203. ansible.builtin.uri:
  204. url: "{{ keycloak_base_url }}/admin/realms/{{ keycloak_realm }}/clients"
  205. method: POST
  206. headers:
  207. Authorization: "Bearer {{ kc_token }}"
  208. Content-Type: application/json
  209. body_format: json
  210. body:
  211. clientId: open-webui
  212. enabled: true
  213. protocol: openid-connect
  214. publicClient: false
  215. clientAuthenticatorType: client-secret
  216. secret: "{{ keycloak_client_secret }}"
  217. redirectUris:
  218. - "{{ keycloak_redirect_uri }}"
  219. webOrigins:
  220. - "{{ openwebui_url }}"
  221. standardFlowEnabled: true
  222. directAccessGrantsEnabled: false
  223. status_code: [201, 409]
  224. when: client_check.json | length == 0
  225. tags:
  226. - keycloak-client
  227. # ── Add realm roles → userinfo protocol mapper ──────────────────
  228. - name: "Keycloak | Get open-webui client internal ID"
  229. ansible.builtin.uri:
  230. url: "{{ keycloak_base_url }}/admin/realms/{{ keycloak_realm }}/clients?clientId=open-webui"
  231. method: GET
  232. headers:
  233. Authorization: "Bearer {{ kc_token }}"
  234. status_code: 200
  235. register: openwebui_client_info
  236. tags:
  237. - keycloak-client
  238. - name: "Keycloak | Add realm roles userinfo mapper to open-webui client"
  239. ansible.builtin.uri:
  240. url: "{{ keycloak_base_url }}/admin/realms/{{ keycloak_realm }}/clients/{{ openwebui_client_info.json[0].id }}/protocol-mappers/models"
  241. method: POST
  242. headers:
  243. Authorization: "Bearer {{ kc_token }}"
  244. Content-Type: application/json
  245. body_format: json
  246. body:
  247. name: realm-roles-userinfo
  248. protocol: openid-connect
  249. protocolMapper: oidc-usermodel-realm-role-mapper
  250. config:
  251. claim.name: realm_access.roles
  252. jsonType.label: String
  253. multivalued: "true"
  254. userinfo.token.claim: "true"
  255. id.token.claim: "true"
  256. access.token.claim: "true"
  257. status_code: [201, 409]
  258. when: openwebui_client_info.json | length > 0
  259. tags:
  260. - keycloak-client
  261. # ── Create realm roles ───────────────────────────────────────────
  262. - name: "Keycloak | Create ai-user role"
  263. ansible.builtin.uri:
  264. url: "{{ keycloak_base_url }}/admin/realms/{{ keycloak_realm }}/roles"
  265. method: POST
  266. headers:
  267. Authorization: "Bearer {{ kc_token }}"
  268. Content-Type: application/json
  269. body_format: json
  270. body:
  271. name: ai-user
  272. description: "Standard AI platform user"
  273. status_code: [201, 409]
  274. tags:
  275. - keycloak-roles
  276. - name: "Keycloak | Create ai-admin role"
  277. ansible.builtin.uri:
  278. url: "{{ keycloak_base_url }}/admin/realms/{{ keycloak_realm }}/roles"
  279. method: POST
  280. headers:
  281. Authorization: "Bearer {{ kc_token }}"
  282. Content-Type: application/json
  283. body_format: json
  284. body:
  285. name: ai-admin
  286. description: "AI platform administrator"
  287. status_code: [201, 409]
  288. tags:
  289. - keycloak-roles
  290. # ── Create realm admin user ──────────────────────────────────────
  291. - name: "Keycloak | Check if realm admin user exists"
  292. ansible.builtin.uri:
  293. url: "{{ keycloak_base_url }}/admin/realms/{{ keycloak_realm }}/users?username={{ keycloak_realm_admin_user }}"
  294. method: GET
  295. headers:
  296. Authorization: "Bearer {{ kc_token }}"
  297. status_code: 200
  298. register: admin_user_check
  299. tags:
  300. - keycloak-users
  301. - name: "Keycloak | Create realm admin user"
  302. ansible.builtin.uri:
  303. url: "{{ keycloak_base_url }}/admin/realms/{{ keycloak_realm }}/users"
  304. method: POST
  305. headers:
  306. Authorization: "Bearer {{ kc_token }}"
  307. Content-Type: application/json
  308. body_format: json
  309. body:
  310. username: "{{ keycloak_realm_admin_user }}"
  311. enabled: true
  312. emailVerified: true
  313. credentials:
  314. - type: password
  315. value: "{{ keycloak_realm_admin_password }}"
  316. temporary: false
  317. status_code: [201, 409]
  318. when: admin_user_check.json | length == 0
  319. tags:
  320. - keycloak-users
  321. - name: "Keycloak | Get realm admin user ID"
  322. ansible.builtin.uri:
  323. url: "{{ keycloak_base_url }}/admin/realms/{{ keycloak_realm }}/users?username={{ keycloak_realm_admin_user }}"
  324. method: GET
  325. headers:
  326. Authorization: "Bearer {{ kc_token }}"
  327. status_code: 200
  328. register: admin_user_info
  329. tags:
  330. - keycloak-users
  331. - name: "Keycloak | Get ai-admin role representation"
  332. ansible.builtin.uri:
  333. url: "{{ keycloak_base_url }}/admin/realms/{{ keycloak_realm }}/roles/ai-admin"
  334. method: GET
  335. headers:
  336. Authorization: "Bearer {{ kc_token }}"
  337. status_code: 200
  338. register: ai_admin_role
  339. tags:
  340. - keycloak-users
  341. - name: "Keycloak | Assign ai-admin role to realm admin user"
  342. ansible.builtin.uri:
  343. url: "{{ keycloak_base_url }}/admin/realms/{{ keycloak_realm }}/users/{{ admin_user_info.json[0].id }}/role-mappings/realm"
  344. method: POST
  345. headers:
  346. Authorization: "Bearer {{ kc_token }}"
  347. Content-Type: application/json
  348. body_format: json
  349. body:
  350. - id: "{{ ai_admin_role.json.id }}"
  351. name: ai-admin
  352. status_code: [200, 204]
  353. when: admin_user_info.json | length > 0
  354. tags:
  355. - keycloak-users
  356. # ── Store OIDC URL in Vault ──────────────────────────────────────
  357. - name: "Keycloak | Store OIDC URL in Vault"
  358. ansible.builtin.uri:
  359. url: "{{ vault_api_addr }}/v1/{{ vault_secret_prefix }}/keycloak"
  360. method: POST
  361. headers:
  362. X-Vault-Token: "{{ lookup('ansible.builtin.file', vault_token_file) }}"
  363. body_format: json
  364. body:
  365. data:
  366. admin_password: "{{ keycloak_admin_password }}"
  367. client_secret: "{{ keycloak_client_secret }}"
  368. realm_admin_password: "{{ keycloak_realm_admin_password }}"
  369. oidc_url: "{{ keycloak_oidc_url }}"
  370. status_code: [200, 204]
  371. tags:
  372. - keycloak-vault