11_vault_oidc.yml 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. ---
  2. # playbooks/11_vault_oidc.yml
  3. # Configure Keycloak as an OIDC provider for Vault UI login.
  4. # Run this after 05_keycloak.yml — requires the realm and admin user to exist.
  5. - name: "Vault OIDC | Configure Keycloak authentication for Vault"
  6. hosts: ai_server
  7. become: true
  8. gather_facts: false
  9. tags:
  10. - vault-oidc
  11. vars:
  12. vault_token_file: "{{ playbook_dir }}/../vault/.vault-token"
  13. vault_url: "http://{{ ai_server_ip }}:{{ vault_port }}"
  14. vault_addr: "http://127.0.0.1:{{ vault_port }}"
  15. keycloak_base_url: "http://localhost:8180"
  16. vault_oidc_client_id: "vault"
  17. tasks:
  18. # ── Load root token ───────────────────────────────────────────────
  19. - name: "Vault OIDC | Load Vault root token"
  20. ansible.builtin.set_fact:
  21. vault_root_token: "{{ lookup('ansible.builtin.file', playbook_dir ~ '/../vault/.vault-init.json') | from_json | json_query('root_token') }}"
  22. delegate_to: localhost
  23. become: false
  24. tags: always
  25. # ── Resolve client secret (reuse existing or generate new) ────────
  26. - name: "Vault OIDC | Check for existing vault OIDC client secret"
  27. ansible.builtin.uri:
  28. url: "{{ vault_addr }}/v1/{{ vault_secret_prefix }}/vault-oidc"
  29. method: GET
  30. headers:
  31. X-Vault-Token: "{{ vault_root_token }}"
  32. status_code: [200, 404]
  33. register: existing_oidc_secret
  34. tags: always
  35. - name: "Vault OIDC | Use existing client secret"
  36. ansible.builtin.set_fact:
  37. vault_oidc_client_secret: "{{ existing_oidc_secret.json.data.data.client_secret }}"
  38. when: existing_oidc_secret.status == 200
  39. tags: always
  40. - name: "Vault OIDC | Generate new client secret"
  41. ansible.builtin.command: openssl rand -hex 32
  42. register: _new_secret
  43. changed_when: false
  44. delegate_to: localhost
  45. become: false
  46. when: existing_oidc_secret.status == 404
  47. tags: always
  48. - name: "Vault OIDC | Set new client secret fact"
  49. ansible.builtin.set_fact:
  50. vault_oidc_client_secret: "{{ _new_secret.stdout }}"
  51. when: existing_oidc_secret.status == 404
  52. tags: always
  53. # ── Get Keycloak admin token ──────────────────────────────────────
  54. - name: "Vault OIDC | Retrieve Keycloak admin password from Vault"
  55. ansible.builtin.set_fact:
  56. 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) }}"
  57. tags:
  58. - vault-oidc-keycloak
  59. - name: "Vault OIDC | Get Keycloak admin token"
  60. ansible.builtin.uri:
  61. url: "{{ keycloak_base_url }}/realms/master/protocol/openid-connect/token"
  62. method: POST
  63. body_format: form-urlencoded
  64. body:
  65. grant_type: password
  66. client_id: admin-cli
  67. username: admin
  68. password: "{{ keycloak_admin_password }}"
  69. status_code: 200
  70. register: kc_token_result
  71. tags:
  72. - vault-oidc-keycloak
  73. - name: "Vault OIDC | Set Keycloak token fact"
  74. ansible.builtin.set_fact:
  75. kc_token: "{{ kc_token_result.json.access_token }}"
  76. tags:
  77. - vault-oidc-keycloak
  78. # ── Create vault OIDC client in Keycloak ─────────────────────────
  79. - name: "Vault OIDC | Check if vault client exists in Keycloak"
  80. ansible.builtin.uri:
  81. url: "{{ keycloak_base_url }}/admin/realms/{{ keycloak_realm }}/clients?clientId={{ vault_oidc_client_id }}"
  82. method: GET
  83. headers:
  84. Authorization: "Bearer {{ kc_token }}"
  85. status_code: 200
  86. register: vault_client_check
  87. tags:
  88. - vault-oidc-keycloak
  89. - name: "Vault OIDC | Create vault client in Keycloak"
  90. ansible.builtin.uri:
  91. url: "{{ keycloak_base_url }}/admin/realms/{{ keycloak_realm }}/clients"
  92. method: POST
  93. headers:
  94. Authorization: "Bearer {{ kc_token }}"
  95. Content-Type: application/json
  96. body_format: json
  97. body:
  98. clientId: "{{ vault_oidc_client_id }}"
  99. enabled: true
  100. protocol: openid-connect
  101. publicClient: false
  102. clientAuthenticatorType: client-secret
  103. secret: "{{ vault_oidc_client_secret }}"
  104. redirectUris:
  105. - "https://vault.{{ domain }}/ui/vault/auth/oidc/oidc/callback"
  106. - "http://localhost:8250/oidc/callback"
  107. webOrigins:
  108. - "https://vault.{{ domain }}"
  109. standardFlowEnabled: true
  110. directAccessGrantsEnabled: false
  111. status_code: [201, 409]
  112. when: vault_client_check.json | length == 0
  113. tags:
  114. - vault-oidc-keycloak
  115. # When the client already exists, Keycloak is the source of truth for the secret.
  116. # POST .../client-secret regenerates a new random secret — it cannot be used to
  117. # set a specific value. Instead, read whatever Keycloak currently has and use that.
  118. - name: "Vault OIDC | Read existing vault client secret from Keycloak"
  119. ansible.builtin.uri:
  120. url: "{{ keycloak_base_url }}/admin/realms/{{ keycloak_realm }}/clients/{{ vault_client_check.json[0].id }}/client-secret"
  121. method: GET
  122. headers:
  123. Authorization: "Bearer {{ kc_token }}"
  124. status_code: 200
  125. register: keycloak_vault_secret
  126. when: vault_client_check.json | length > 0
  127. tags:
  128. - vault-oidc-keycloak
  129. - name: "Vault OIDC | Use Keycloak client secret as canonical value"
  130. ansible.builtin.set_fact:
  131. vault_oidc_client_secret: "{{ keycloak_vault_secret.json.value }}"
  132. when: vault_client_check.json | length > 0
  133. tags:
  134. - vault-oidc-keycloak
  135. # ── Add realm roles mapper to vault client ID token ───────────────
  136. - name: "Vault OIDC | Re-fetch vault client to get UUID"
  137. ansible.builtin.uri:
  138. url: "{{ keycloak_base_url }}/admin/realms/{{ keycloak_realm }}/clients?clientId={{ vault_oidc_client_id }}"
  139. method: GET
  140. headers:
  141. Authorization: "Bearer {{ kc_token }}"
  142. status_code: 200
  143. register: vault_client_fetched
  144. tags:
  145. - vault-oidc-keycloak
  146. - name: "Vault OIDC | Set vault client UUID"
  147. ansible.builtin.set_fact:
  148. vault_kc_client_uuid: "{{ vault_client_fetched.json[0].id }}"
  149. tags:
  150. - vault-oidc-keycloak
  151. - name: "Vault OIDC | List existing vault client protocol mappers"
  152. ansible.builtin.uri:
  153. url: "{{ keycloak_base_url }}/admin/realms/{{ keycloak_realm }}/clients/{{ vault_kc_client_uuid }}/protocol-mappers/models"
  154. method: GET
  155. headers:
  156. Authorization: "Bearer {{ kc_token }}"
  157. status_code: 200
  158. register: vault_client_mappers
  159. tags:
  160. - vault-oidc-keycloak
  161. - name: "Vault OIDC | Add realm roles mapper to ID token"
  162. ansible.builtin.uri:
  163. url: "{{ keycloak_base_url }}/admin/realms/{{ keycloak_realm }}/clients/{{ vault_kc_client_uuid }}/protocol-mappers/models"
  164. method: POST
  165. headers:
  166. Authorization: "Bearer {{ kc_token }}"
  167. Content-Type: application/json
  168. body_format: json
  169. body:
  170. name: "realm-roles-id-token"
  171. protocol: openid-connect
  172. protocolMapper: "oidc-usermodel-realm-role-mapper"
  173. config:
  174. "claim.name": "realm_access.roles"
  175. "jsonType.label": "String"
  176. "multivalued": "true"
  177. "id.token.claim": "true"
  178. "access.token.claim": "true"
  179. "userinfo.token.claim": "true"
  180. status_code: [201, 409]
  181. when: vault_client_mappers.json | selectattr('name', 'equalto', 'realm-roles-id-token') | list | length == 0
  182. tags:
  183. - vault-oidc-keycloak
  184. # ── Persist vault OIDC client secret in Vault ────────────────────
  185. - name: "Vault OIDC | Store vault OIDC client secret in Vault"
  186. ansible.builtin.uri:
  187. url: "{{ vault_addr }}/v1/{{ vault_secret_prefix }}/vault-oidc"
  188. method: POST
  189. headers:
  190. X-Vault-Token: "{{ vault_root_token }}"
  191. body_format: json
  192. body:
  193. data:
  194. client_secret: "{{ vault_oidc_client_secret }}"
  195. status_code: [200, 204]
  196. tags:
  197. - vault-oidc-vault
  198. # ── Enable and configure Vault OIDC auth method ───────────────────
  199. - name: "Vault OIDC | Check existing Vault auth methods"
  200. ansible.builtin.uri:
  201. url: "{{ vault_addr }}/v1/sys/auth"
  202. method: GET
  203. headers:
  204. X-Vault-Token: "{{ vault_root_token }}"
  205. status_code: 200
  206. register: vault_auth_methods
  207. tags:
  208. - vault-oidc-vault
  209. - name: "Vault OIDC | Enable OIDC auth method in Vault"
  210. ansible.builtin.uri:
  211. url: "{{ vault_addr }}/v1/sys/auth/oidc"
  212. method: POST
  213. headers:
  214. X-Vault-Token: "{{ vault_root_token }}"
  215. body_format: json
  216. body:
  217. type: oidc
  218. status_code: [200, 204]
  219. when: "'oidc/' not in vault_auth_methods.json"
  220. tags:
  221. - vault-oidc-vault
  222. - name: "Vault OIDC | Configure Vault OIDC auth method"
  223. ansible.builtin.uri:
  224. url: "{{ vault_addr }}/v1/auth/oidc/config"
  225. method: POST
  226. headers:
  227. X-Vault-Token: "{{ vault_root_token }}"
  228. body_format: json
  229. body:
  230. oidc_discovery_url: "https://idm.{{ domain }}/realms/{{ keycloak_realm }}"
  231. oidc_client_id: "{{ vault_oidc_client_id }}"
  232. oidc_client_secret: "{{ vault_oidc_client_secret }}"
  233. default_role: default
  234. status_code: [200, 204]
  235. tags:
  236. - vault-oidc-vault
  237. # ── Create Vault policy for Keycloak-authenticated users ──────────
  238. - name: "Vault OIDC | Create vault-admin policy"
  239. ansible.builtin.uri:
  240. url: "{{ vault_addr }}/v1/sys/policies/acl/vault-admin"
  241. method: PUT
  242. headers:
  243. X-Vault-Token: "{{ vault_root_token }}"
  244. body_format: json
  245. body:
  246. policy: |
  247. path "secret/*" {
  248. capabilities = ["create", "read", "update", "delete", "list"]
  249. }
  250. path "sys/health" {
  251. capabilities = ["read"]
  252. }
  253. path "sys/policies/acl" {
  254. capabilities = ["list"]
  255. }
  256. path "sys/policies/acl/*" {
  257. capabilities = ["read", "list"]
  258. }
  259. status_code: [200, 204]
  260. tags:
  261. - vault-oidc-vault
  262. # ── Create Vault OIDC role ────────────────────────────────────────
  263. - name: "Vault OIDC | Create default OIDC role"
  264. ansible.builtin.uri:
  265. url: "{{ vault_addr }}/v1/auth/oidc/role/default"
  266. method: POST
  267. headers:
  268. X-Vault-Token: "{{ vault_root_token }}"
  269. body_format: json
  270. body:
  271. user_claim: sub
  272. allowed_redirect_uris:
  273. - "https://vault.{{ domain }}/ui/vault/auth/oidc/oidc/callback"
  274. - "http://localhost:8250/oidc/callback"
  275. bound_claims:
  276. "/realm_access/roles": ["ai-admin"]
  277. token_policies:
  278. - vault-admin
  279. token_ttl: "4h"
  280. token_max_ttl: "8h"
  281. oidc_scopes:
  282. - openid
  283. - profile
  284. - email
  285. status_code: [200, 204]
  286. tags:
  287. - vault-oidc-vault
  288. - name: "Vault OIDC | Display login instructions"
  289. ansible.builtin.debug:
  290. msg: |
  291. Vault OIDC login configured.
  292. In the Vault UI, select method: OIDC, role: default, then click Sign in with OIDC Provider.
  293. You will be redirected to Keycloak to authenticate.
  294. tags:
  295. - vault-oidc-vault