01_vault.yml 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. ---
  2. # playbooks/01_vault.yml
  3. # Deploy HashiCorp Vault as a native systemd service on ai_server.
  4. # Using the official HashiCorp RPM avoids Docker networking/SELinux issues.
  5. - name: "Vault | Deploy and configure HashiCorp Vault"
  6. hosts: ai_server
  7. become: true
  8. gather_facts: true
  9. tags:
  10. - vault
  11. vars:
  12. vault_port: 8202
  13. vault_config_dir: /etc/vault.d
  14. vault_data_dir: /mnt/ai_data/vault/data
  15. vault_addr: "http://127.0.0.1:{{ vault_port }}"
  16. vault_init_file: "{{ playbook_dir }}/../vault/.vault-init.json"
  17. vault_token_file: "{{ playbook_dir }}/../vault/.vault-token"
  18. vars_prompt:
  19. - name: telegram_token
  20. prompt: "Telegram Bot Token (from @BotFather). Press ENTER to skip"
  21. private: false
  22. default: ""
  23. tasks:
  24. # ── Install Vault via official HashiCorp RPM ──────────────────────
  25. - name: "Vault | Remove any stale HashiCorp repo file"
  26. ansible.builtin.file:
  27. path: /etc/yum.repos.d/hashicorp.repo
  28. state: absent
  29. tags:
  30. - vault-install
  31. - name: "Vault | Install dnf-plugins-core"
  32. ansible.builtin.dnf:
  33. name: dnf-plugins-core
  34. state: present
  35. tags:
  36. - vault-install
  37. - name: "Vault | Download HashiCorp RPM repo file"
  38. ansible.builtin.get_url:
  39. url: https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
  40. dest: /etc/yum.repos.d/hashicorp.repo
  41. mode: "0644"
  42. tags:
  43. - vault-install
  44. - name: "Vault | Install vault package"
  45. ansible.builtin.dnf:
  46. name: vault
  47. state: present
  48. tags:
  49. - vault-install
  50. # ── Data directory ────────────────────────────────────────────────
  51. - name: "Vault | Create data directory"
  52. ansible.builtin.file:
  53. path: "{{ vault_data_dir }}"
  54. state: directory
  55. mode: "0750"
  56. owner: vault
  57. group: vault
  58. tags:
  59. - vault-dirs
  60. # ── Configuration ─────────────────────────────────────────────────
  61. - name: "Vault | Template vault.hcl configuration"
  62. ansible.builtin.template:
  63. src: "{{ playbook_dir }}/../templates/vault/vault.hcl.j2"
  64. dest: "{{ vault_config_dir }}/vault.hcl"
  65. mode: "0640"
  66. owner: vault
  67. group: vault
  68. notify: Restart vault
  69. tags:
  70. - vault-config
  71. - name: "Vault | Show rendered vault.hcl"
  72. ansible.builtin.command: cat {{ vault_config_dir }}/vault.hcl
  73. changed_when: false
  74. register: vault_hcl_content
  75. tags:
  76. - vault-config
  77. - name: "Vault | Display vault.hcl"
  78. ansible.builtin.debug:
  79. var: vault_hcl_content.stdout_lines
  80. tags:
  81. - vault-config
  82. # ── Firewall ───────────────────────────────────────────────────────
  83. - name: "Vault | Open Vault port in firewalld"
  84. ansible.posix.firewalld:
  85. port: "{{ vault_port }}/tcp"
  86. permanent: true
  87. immediate: true
  88. state: enabled
  89. tags:
  90. - vault-service
  91. # ── Start service ─────────────────────────────────────────────────
  92. - name: "Vault | Enable and start vault.service"
  93. ansible.builtin.systemd:
  94. name: vault
  95. state: started
  96. enabled: true
  97. daemon_reload: true
  98. tags:
  99. - vault-service
  100. - name: "Vault | Wait for Vault to become ready"
  101. ansible.builtin.uri:
  102. url: "{{ vault_addr }}/v1/sys/health"
  103. method: GET
  104. status_code: [200, 429, 472, 473, 501, 503]
  105. timeout: 5
  106. register: vault_health
  107. retries: 30
  108. delay: 5
  109. until: vault_health.status in [200, 429, 472, 473, 501, 503]
  110. tags:
  111. - vault-health
  112. # ── Initialization ────────────────────────────────────────────────
  113. - name: "Vault | Check if Vault is already initialized"
  114. ansible.builtin.uri:
  115. url: "{{ vault_addr }}/v1/sys/health"
  116. method: GET
  117. status_code: [200, 429, 472, 473, 501, 503]
  118. register: vault_init_check
  119. tags:
  120. - vault-init
  121. - vault-unseal
  122. - name: "Vault | Set initialization status fact"
  123. ansible.builtin.set_fact:
  124. vault_is_initialized: "{{ vault_init_check.status != 501 }}"
  125. tags:
  126. - vault-init
  127. - vault-unseal
  128. - name: "Vault | Initialize Vault"
  129. ansible.builtin.command:
  130. cmd: >-
  131. vault operator init
  132. -key-shares=1 -key-threshold=1 -format=json
  133. environment:
  134. VAULT_ADDR: "{{ vault_addr }}"
  135. register: vault_init_output
  136. when: not vault_is_initialized
  137. changed_when: true
  138. tags:
  139. - vault-init
  140. - name: "Vault | Ensure local vault directory exists on control node"
  141. ansible.builtin.file:
  142. path: "{{ playbook_dir }}/../vault"
  143. state: directory
  144. mode: "0700"
  145. delegate_to: localhost
  146. become: false
  147. when: not vault_is_initialized
  148. tags:
  149. - vault-init
  150. - name: "Vault | Save init output to control node"
  151. ansible.builtin.copy:
  152. content: "{{ vault_init_output.stdout }}"
  153. dest: "{{ vault_init_file }}"
  154. mode: "0600"
  155. delegate_to: localhost
  156. become: false
  157. when: not vault_is_initialized
  158. tags:
  159. - vault-init
  160. - name: "Vault | Parse init output"
  161. ansible.builtin.set_fact:
  162. vault_init_data: "{{ vault_init_output.stdout | from_json }}"
  163. when: not vault_is_initialized
  164. tags:
  165. - vault-init
  166. - name: "Vault | Display unseal key and root token"
  167. ansible.builtin.debug:
  168. msg:
  169. - "============================================="
  170. - " VAULT INITIALIZATION COMPLETE"
  171. - "============================================="
  172. - " Unseal Key: {{ vault_init_data.unseal_keys_b64[0] }}"
  173. - " Root Token: {{ vault_init_data.root_token }}"
  174. - "============================================="
  175. - " SAVE THESE VALUES SECURELY!"
  176. - "============================================="
  177. when: not vault_is_initialized
  178. tags:
  179. - vault-init
  180. # ── Load existing init data if already initialized ────────────────
  181. - name: "Vault | Load existing init data from control node"
  182. ansible.builtin.slurp:
  183. src: "{{ vault_init_file }}"
  184. delegate_to: localhost
  185. become: false
  186. register: vault_init_file_content
  187. when: vault_is_initialized
  188. tags:
  189. - vault-unseal
  190. - name: "Vault | Parse existing init data"
  191. ansible.builtin.set_fact:
  192. vault_init_data: "{{ vault_init_file_content.content | b64decode | from_json }}"
  193. when: vault_is_initialized
  194. tags:
  195. - vault-unseal
  196. # ── Unseal ────────────────────────────────────────────────────────
  197. - name: "Vault | Check seal status"
  198. ansible.builtin.uri:
  199. url: "{{ vault_addr }}/v1/sys/seal-status"
  200. method: GET
  201. register: vault_seal_status
  202. tags:
  203. - vault-unseal
  204. - name: "Vault | Unseal Vault"
  205. ansible.builtin.uri:
  206. url: "{{ vault_addr }}/v1/sys/unseal"
  207. method: PUT
  208. body_format: json
  209. body:
  210. key: "{{ vault_init_data.unseal_keys_b64[0] }}"
  211. status_code: 200
  212. when: vault_seal_status.json.sealed | default(true)
  213. tags:
  214. - vault-unseal
  215. # ── Auto-unseal on reboot ─────────────────────────────────────────
  216. - name: "Vault | Deploy unseal key to server"
  217. ansible.builtin.copy:
  218. content: "{{ vault_init_data.unseal_keys_b64[0] }}"
  219. dest: /etc/vault.d/unseal.key
  220. owner: root
  221. group: root
  222. mode: "0400"
  223. tags:
  224. - vault-unseal
  225. - vault-autounseal
  226. - name: "Vault | Deploy vault-unseal.sh"
  227. ansible.builtin.template:
  228. src: "{{ playbook_dir }}/../templates/vault/vault-unseal.sh.j2"
  229. dest: /usr/local/bin/vault-unseal.sh
  230. owner: root
  231. group: root
  232. mode: "0750"
  233. tags:
  234. - vault-autounseal
  235. - name: "Vault | Deploy vault-unseal.service"
  236. ansible.builtin.template:
  237. src: "{{ playbook_dir }}/../templates/vault/vault-unseal.service.j2"
  238. dest: /etc/systemd/system/vault-unseal.service
  239. owner: root
  240. group: root
  241. mode: "0644"
  242. notify: Reload systemd and restart vault-unseal
  243. tags:
  244. - vault-autounseal
  245. - name: "Vault | Enable vault-unseal.service"
  246. ansible.builtin.systemd:
  247. name: vault-unseal.service
  248. enabled: true
  249. daemon_reload: true
  250. tags:
  251. - vault-autounseal
  252. - name: "Vault | Set root token fact"
  253. ansible.builtin.set_fact:
  254. vault_root_token: "{{ vault_init_data.root_token }}"
  255. tags:
  256. - vault-configure
  257. # ── Enable KV v2 secrets engine ───────────────────────────────────
  258. - name: "Vault | Check existing secrets engines"
  259. ansible.builtin.uri:
  260. url: "{{ vault_addr }}/v1/sys/mounts"
  261. method: GET
  262. headers:
  263. X-Vault-Token: "{{ vault_root_token }}"
  264. status_code: 200
  265. register: vault_mounts
  266. tags:
  267. - vault-configure
  268. - name: "Vault | Enable KV v2 secrets engine at 'secret'"
  269. ansible.builtin.uri:
  270. url: "{{ vault_addr }}/v1/sys/mounts/secret"
  271. method: POST
  272. headers:
  273. X-Vault-Token: "{{ vault_root_token }}"
  274. body_format: json
  275. body:
  276. type: kv
  277. options:
  278. version: "2"
  279. status_code: [200, 204]
  280. when: "'secret/' not in vault_mounts.json"
  281. tags:
  282. - vault-configure
  283. # ── Create ansible policy ─────────────────────────────────────────
  284. - name: "Vault | Create ansible-policy"
  285. ansible.builtin.uri:
  286. url: "{{ vault_addr }}/v1/sys/policies/acl/ansible-policy"
  287. method: PUT
  288. headers:
  289. X-Vault-Token: "{{ vault_root_token }}"
  290. body_format: json
  291. body:
  292. policy: |
  293. path "{{ vault_secret_prefix }}/*" {
  294. capabilities = ["create", "read", "update", "delete", "list"]
  295. }
  296. path "{{ vault_secret_meta_prefix }}/*" {
  297. capabilities = ["list", "read", "delete"]
  298. }
  299. path "{{ vault_secret_meta_prefix }}" {
  300. capabilities = ["list"]
  301. }
  302. path "secret/metadata/" {
  303. capabilities = ["list"]
  304. }
  305. status_code: [200, 204]
  306. tags:
  307. - vault-configure
  308. # ── Create ansible token ──────────────────────────────────────────
  309. - name: "Vault | Create ansible token with ansible-policy"
  310. ansible.builtin.uri:
  311. url: "{{ vault_addr }}/v1/auth/token/create"
  312. method: POST
  313. headers:
  314. X-Vault-Token: "{{ vault_root_token }}"
  315. body_format: json
  316. body:
  317. policies:
  318. - ansible-policy
  319. display_name: ansible
  320. ttl: "8760h"
  321. renewable: true
  322. no_parent: true
  323. status_code: 200
  324. register: ansible_token_result
  325. tags:
  326. - vault-configure
  327. - name: "Vault | Save ansible token to control node"
  328. ansible.builtin.copy:
  329. content: "{{ ansible_token_result.json.auth.client_token }}"
  330. dest: "{{ vault_token_file }}"
  331. mode: "0600"
  332. delegate_to: localhost
  333. become: false
  334. tags:
  335. - vault-configure
  336. # ── Enable AppRole auth ───────────────────────────────────────────
  337. - name: "Vault | Check existing auth methods"
  338. ansible.builtin.uri:
  339. url: "{{ vault_addr }}/v1/sys/auth"
  340. method: GET
  341. headers:
  342. X-Vault-Token: "{{ vault_root_token }}"
  343. status_code: 200
  344. register: vault_auth_methods
  345. tags:
  346. - vault-approle
  347. - name: "Vault | Enable AppRole auth method"
  348. ansible.builtin.uri:
  349. url: "{{ vault_addr }}/v1/sys/auth/approle"
  350. method: POST
  351. headers:
  352. X-Vault-Token: "{{ vault_root_token }}"
  353. body_format: json
  354. body:
  355. type: approle
  356. status_code: [200, 204]
  357. when: "'approle/' not in vault_auth_methods.json"
  358. tags:
  359. - vault-approle
  360. - name: "Vault | Create {{ vault_approle_name }} AppRole"
  361. ansible.builtin.uri:
  362. url: "{{ vault_addr }}/v1/auth/approle/role/{{ vault_approle_name }}"
  363. method: POST
  364. headers:
  365. X-Vault-Token: "{{ vault_root_token }}"
  366. body_format: json
  367. body:
  368. token_policies:
  369. - ansible-policy
  370. token_ttl: "1h"
  371. token_max_ttl: "4h"
  372. secret_id_ttl: "0"
  373. status_code: [200, 204]
  374. tags:
  375. - vault-approle
  376. # ── Generate and populate secrets (only write if absent) ──────────
  377. # Each secret is checked first — existing secrets are never overwritten.
  378. # To rotate a credential, delete its Vault path and re-run this playbook.
  379. - name: "Vault | Check existing secrets"
  380. ansible.builtin.uri:
  381. url: "{{ vault_addr }}/v1/{{ vault_secret_prefix }}/{{ item }}"
  382. method: GET
  383. headers:
  384. X-Vault-Token: "{{ vault_root_token }}"
  385. status_code: [200, 404]
  386. loop:
  387. - ollama
  388. - openwebui
  389. - keycloak
  390. register: existing_secrets
  391. tags:
  392. - vault-secrets
  393. - name: "Vault | Build existing secrets map"
  394. ansible.builtin.set_fact:
  395. secret_exists: "{{ secret_exists | default({}) | combine({item.item: item.status == 200}) }}"
  396. loop: "{{ existing_secrets.results }}"
  397. tags:
  398. - vault-secrets
  399. # ── Ollama ────────────────────────────────────────────────────────
  400. - name: "Vault | Generate Ollama API key"
  401. ansible.builtin.command: openssl rand -hex 32
  402. register: ollama_api_key
  403. changed_when: false
  404. delegate_to: localhost
  405. become: false
  406. when: not secret_exists['ollama']
  407. tags:
  408. - vault-secrets
  409. - name: "Vault | Store Ollama secrets"
  410. ansible.builtin.uri:
  411. url: "{{ vault_addr }}/v1/{{ vault_secret_prefix }}/ollama"
  412. method: POST
  413. headers:
  414. X-Vault-Token: "{{ vault_root_token }}"
  415. body_format: json
  416. body:
  417. data:
  418. api_key: "{{ ollama_api_key.stdout }}"
  419. status_code: [200, 204]
  420. when: not secret_exists['ollama']
  421. tags:
  422. - vault-secrets
  423. # ── Open WebUI ────────────────────────────────────────────────────
  424. - name: "Vault | Generate Open WebUI secret key"
  425. ansible.builtin.command: openssl rand -hex 32
  426. register: openwebui_secret_key
  427. changed_when: false
  428. delegate_to: localhost
  429. become: false
  430. when: not secret_exists['openwebui']
  431. tags:
  432. - vault-secrets
  433. - name: "Vault | Store Open WebUI secrets"
  434. ansible.builtin.uri:
  435. url: "{{ vault_addr }}/v1/{{ vault_secret_prefix }}/openwebui"
  436. method: POST
  437. headers:
  438. X-Vault-Token: "{{ vault_root_token }}"
  439. body_format: json
  440. body:
  441. data:
  442. secret_key: "{{ openwebui_secret_key.stdout }}"
  443. status_code: [200, 204]
  444. when: not secret_exists['openwebui']
  445. tags:
  446. - vault-secrets
  447. # ── Keycloak ──────────────────────────────────────────────────────
  448. - name: "Vault | Generate Keycloak admin password"
  449. ansible.builtin.command: openssl rand -base64 16
  450. register: keycloak_admin_password
  451. changed_when: false
  452. delegate_to: localhost
  453. become: false
  454. when: not secret_exists['keycloak']
  455. tags:
  456. - vault-secrets
  457. - name: "Vault | Generate Keycloak client secret"
  458. ansible.builtin.command: openssl rand -hex 32
  459. register: keycloak_client_secret
  460. changed_when: false
  461. delegate_to: localhost
  462. become: false
  463. when: not secret_exists['keycloak']
  464. tags:
  465. - vault-secrets
  466. - name: "Vault | Generate Keycloak realm admin password"
  467. ansible.builtin.command: openssl rand -base64 16
  468. register: keycloak_realm_admin_password
  469. changed_when: false
  470. delegate_to: localhost
  471. become: false
  472. when: not secret_exists['keycloak']
  473. tags:
  474. - vault-secrets
  475. - name: "Vault | Store Keycloak secrets"
  476. ansible.builtin.uri:
  477. url: "{{ vault_addr }}/v1/{{ vault_secret_prefix }}/keycloak"
  478. method: POST
  479. headers:
  480. X-Vault-Token: "{{ vault_root_token }}"
  481. body_format: json
  482. body:
  483. data:
  484. admin_password: "{{ keycloak_admin_password.stdout }}"
  485. client_secret: "{{ keycloak_client_secret.stdout }}"
  486. realm_admin_password: "{{ keycloak_realm_admin_password.stdout }}"
  487. status_code: [200, 204]
  488. when: not secret_exists['keycloak']
  489. tags:
  490. - vault-secrets
  491. # ── OpenClaw (always write when token is provided) ────────────────
  492. - name: "Vault | Store Telegram token in Vault"
  493. ansible.builtin.uri:
  494. url: "{{ vault_addr }}/v1/{{ vault_secret_prefix }}/openclaw"
  495. method: POST
  496. headers:
  497. X-Vault-Token: "{{ vault_root_token }}"
  498. body_format: json
  499. body:
  500. data:
  501. telegram_token: "{{ telegram_token }}"
  502. status_code: [200, 204]
  503. when: telegram_token | length > 0
  504. tags:
  505. - vault-secrets
  506. handlers:
  507. - name: Restart vault
  508. ansible.builtin.systemd:
  509. name: vault
  510. state: restarted
  511. daemon_reload: true
  512. - name: Reload systemd and restart vault-unseal
  513. ansible.builtin.systemd:
  514. name: vault-unseal.service
  515. state: restarted
  516. daemon_reload: true