01_vault.yml 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  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. - name: "Vault | Set initialization status fact"
  122. ansible.builtin.set_fact:
  123. vault_is_initialized: "{{ vault_init_check.status != 501 }}"
  124. tags:
  125. - vault-init
  126. - name: "Vault | Initialize Vault"
  127. ansible.builtin.command:
  128. cmd: >-
  129. vault operator init
  130. -key-shares=1 -key-threshold=1 -format=json
  131. environment:
  132. VAULT_ADDR: "{{ vault_addr }}"
  133. register: vault_init_output
  134. when: not vault_is_initialized
  135. changed_when: true
  136. tags:
  137. - vault-init
  138. - name: "Vault | Ensure local vault directory exists on control node"
  139. ansible.builtin.file:
  140. path: "{{ playbook_dir }}/../vault"
  141. state: directory
  142. mode: "0700"
  143. delegate_to: localhost
  144. become: false
  145. when: not vault_is_initialized
  146. tags:
  147. - vault-init
  148. - name: "Vault | Save init output to control node"
  149. ansible.builtin.copy:
  150. content: "{{ vault_init_output.stdout }}"
  151. dest: "{{ vault_init_file }}"
  152. mode: "0600"
  153. delegate_to: localhost
  154. become: false
  155. when: not vault_is_initialized
  156. tags:
  157. - vault-init
  158. - name: "Vault | Parse init output"
  159. ansible.builtin.set_fact:
  160. vault_init_data: "{{ vault_init_output.stdout | from_json }}"
  161. when: not vault_is_initialized
  162. tags:
  163. - vault-init
  164. - name: "Vault | Display unseal key and root token"
  165. ansible.builtin.debug:
  166. msg:
  167. - "============================================="
  168. - " VAULT INITIALIZATION COMPLETE"
  169. - "============================================="
  170. - " Unseal Key: {{ vault_init_data.unseal_keys_b64[0] }}"
  171. - " Root Token: {{ vault_init_data.root_token }}"
  172. - "============================================="
  173. - " SAVE THESE VALUES SECURELY!"
  174. - "============================================="
  175. when: not vault_is_initialized
  176. tags:
  177. - vault-init
  178. # ── Load existing init data if already initialized ────────────────
  179. - name: "Vault | Load existing init data from control node"
  180. ansible.builtin.slurp:
  181. src: "{{ vault_init_file }}"
  182. delegate_to: localhost
  183. become: false
  184. register: vault_init_file_content
  185. when: vault_is_initialized
  186. tags:
  187. - vault-unseal
  188. - name: "Vault | Parse existing init data"
  189. ansible.builtin.set_fact:
  190. vault_init_data: "{{ vault_init_file_content.content | b64decode | from_json }}"
  191. when: vault_is_initialized
  192. tags:
  193. - vault-unseal
  194. # ── Unseal ────────────────────────────────────────────────────────
  195. - name: "Vault | Check seal status"
  196. ansible.builtin.uri:
  197. url: "{{ vault_addr }}/v1/sys/seal-status"
  198. method: GET
  199. register: vault_seal_status
  200. tags:
  201. - vault-unseal
  202. - name: "Vault | Unseal Vault"
  203. ansible.builtin.uri:
  204. url: "{{ vault_addr }}/v1/sys/unseal"
  205. method: PUT
  206. body_format: json
  207. body:
  208. key: "{{ vault_init_data.unseal_keys_b64[0] }}"
  209. status_code: 200
  210. when: vault_seal_status.json.sealed | default(true)
  211. tags:
  212. - vault-unseal
  213. - name: "Vault | Set root token fact"
  214. ansible.builtin.set_fact:
  215. vault_root_token: "{{ vault_init_data.root_token }}"
  216. tags:
  217. - vault-configure
  218. # ── Enable KV v2 secrets engine ───────────────────────────────────
  219. - name: "Vault | Check existing secrets engines"
  220. ansible.builtin.uri:
  221. url: "{{ vault_addr }}/v1/sys/mounts"
  222. method: GET
  223. headers:
  224. X-Vault-Token: "{{ vault_root_token }}"
  225. status_code: 200
  226. register: vault_mounts
  227. tags:
  228. - vault-configure
  229. - name: "Vault | Enable KV v2 secrets engine at 'secret'"
  230. ansible.builtin.uri:
  231. url: "{{ vault_addr }}/v1/sys/mounts/secret"
  232. method: POST
  233. headers:
  234. X-Vault-Token: "{{ vault_root_token }}"
  235. body_format: json
  236. body:
  237. type: kv
  238. options:
  239. version: "2"
  240. status_code: [200, 204]
  241. when: "'secret/' not in vault_mounts.json"
  242. tags:
  243. - vault-configure
  244. # ── Create ansible policy ─────────────────────────────────────────
  245. - name: "Vault | Create ansible-policy"
  246. ansible.builtin.uri:
  247. url: "{{ vault_addr }}/v1/sys/policies/acl/ansible-policy"
  248. method: PUT
  249. headers:
  250. X-Vault-Token: "{{ vault_root_token }}"
  251. body_format: json
  252. body:
  253. policy: |
  254. path "{{ vault_secret_prefix }}/*" {
  255. capabilities = ["create", "read", "update", "delete", "list"]
  256. }
  257. path "{{ vault_secret_meta_prefix }}/*" {
  258. capabilities = ["list", "read", "delete"]
  259. }
  260. path "{{ vault_secret_meta_prefix }}" {
  261. capabilities = ["list"]
  262. }
  263. path "secret/metadata/" {
  264. capabilities = ["list"]
  265. }
  266. status_code: [200, 204]
  267. tags:
  268. - vault-configure
  269. # ── Create ansible token ──────────────────────────────────────────
  270. - name: "Vault | Create ansible token with ansible-policy"
  271. ansible.builtin.uri:
  272. url: "{{ vault_addr }}/v1/auth/token/create"
  273. method: POST
  274. headers:
  275. X-Vault-Token: "{{ vault_root_token }}"
  276. body_format: json
  277. body:
  278. policies:
  279. - ansible-policy
  280. display_name: ansible
  281. ttl: "8760h"
  282. renewable: true
  283. no_parent: true
  284. status_code: 200
  285. register: ansible_token_result
  286. tags:
  287. - vault-configure
  288. - name: "Vault | Save ansible token to control node"
  289. ansible.builtin.copy:
  290. content: "{{ ansible_token_result.json.auth.client_token }}"
  291. dest: "{{ vault_token_file }}"
  292. mode: "0600"
  293. delegate_to: localhost
  294. become: false
  295. tags:
  296. - vault-configure
  297. # ── Enable AppRole auth ───────────────────────────────────────────
  298. - name: "Vault | Check existing auth methods"
  299. ansible.builtin.uri:
  300. url: "{{ vault_addr }}/v1/sys/auth"
  301. method: GET
  302. headers:
  303. X-Vault-Token: "{{ vault_root_token }}"
  304. status_code: 200
  305. register: vault_auth_methods
  306. tags:
  307. - vault-approle
  308. - name: "Vault | Enable AppRole auth method"
  309. ansible.builtin.uri:
  310. url: "{{ vault_addr }}/v1/sys/auth/approle"
  311. method: POST
  312. headers:
  313. X-Vault-Token: "{{ vault_root_token }}"
  314. body_format: json
  315. body:
  316. type: approle
  317. status_code: [200, 204]
  318. when: "'approle/' not in vault_auth_methods.json"
  319. tags:
  320. - vault-approle
  321. - name: "Vault | Create {{ vault_approle_name }} AppRole"
  322. ansible.builtin.uri:
  323. url: "{{ vault_addr }}/v1/auth/approle/role/{{ vault_approle_name }}"
  324. method: POST
  325. headers:
  326. X-Vault-Token: "{{ vault_root_token }}"
  327. body_format: json
  328. body:
  329. token_policies:
  330. - ansible-policy
  331. token_ttl: "1h"
  332. token_max_ttl: "4h"
  333. secret_id_ttl: "0"
  334. status_code: [200, 204]
  335. tags:
  336. - vault-approle
  337. # ── Generate and populate secrets (only write if absent) ──────────
  338. # Each secret is checked first — existing secrets are never overwritten.
  339. # To rotate a credential, delete its Vault path and re-run this playbook.
  340. - name: "Vault | Check existing secrets"
  341. ansible.builtin.uri:
  342. url: "{{ vault_addr }}/v1/{{ vault_secret_prefix }}/{{ item }}"
  343. method: GET
  344. headers:
  345. X-Vault-Token: "{{ vault_root_token }}"
  346. status_code: [200, 404]
  347. loop:
  348. - ollama
  349. - openwebui
  350. - keycloak
  351. register: existing_secrets
  352. tags:
  353. - vault-secrets
  354. - name: "Vault | Build existing secrets map"
  355. ansible.builtin.set_fact:
  356. secret_exists: "{{ secret_exists | default({}) | combine({item.item: item.status == 200}) }}"
  357. loop: "{{ existing_secrets.results }}"
  358. tags:
  359. - vault-secrets
  360. # ── Ollama ────────────────────────────────────────────────────────
  361. - name: "Vault | Generate Ollama API key"
  362. ansible.builtin.command: openssl rand -hex 32
  363. register: ollama_api_key
  364. changed_when: false
  365. delegate_to: localhost
  366. become: false
  367. when: not secret_exists['ollama']
  368. tags:
  369. - vault-secrets
  370. - name: "Vault | Store Ollama secrets"
  371. ansible.builtin.uri:
  372. url: "{{ vault_addr }}/v1/{{ vault_secret_prefix }}/ollama"
  373. method: POST
  374. headers:
  375. X-Vault-Token: "{{ vault_root_token }}"
  376. body_format: json
  377. body:
  378. data:
  379. api_key: "{{ ollama_api_key.stdout }}"
  380. status_code: [200, 204]
  381. when: not secret_exists['ollama']
  382. tags:
  383. - vault-secrets
  384. # ── Open WebUI ────────────────────────────────────────────────────
  385. - name: "Vault | Generate Open WebUI secret key"
  386. ansible.builtin.command: openssl rand -hex 32
  387. register: openwebui_secret_key
  388. changed_when: false
  389. delegate_to: localhost
  390. become: false
  391. when: not secret_exists['openwebui']
  392. tags:
  393. - vault-secrets
  394. - name: "Vault | Store Open WebUI secrets"
  395. ansible.builtin.uri:
  396. url: "{{ vault_addr }}/v1/{{ vault_secret_prefix }}/openwebui"
  397. method: POST
  398. headers:
  399. X-Vault-Token: "{{ vault_root_token }}"
  400. body_format: json
  401. body:
  402. data:
  403. secret_key: "{{ openwebui_secret_key.stdout }}"
  404. status_code: [200, 204]
  405. when: not secret_exists['openwebui']
  406. tags:
  407. - vault-secrets
  408. # ── Keycloak ──────────────────────────────────────────────────────
  409. - name: "Vault | Generate Keycloak admin password"
  410. ansible.builtin.command: openssl rand -base64 16
  411. register: keycloak_admin_password
  412. changed_when: false
  413. delegate_to: localhost
  414. become: false
  415. when: not secret_exists['keycloak']
  416. tags:
  417. - vault-secrets
  418. - name: "Vault | Generate Keycloak client secret"
  419. ansible.builtin.command: openssl rand -hex 32
  420. register: keycloak_client_secret
  421. changed_when: false
  422. delegate_to: localhost
  423. become: false
  424. when: not secret_exists['keycloak']
  425. tags:
  426. - vault-secrets
  427. - name: "Vault | Generate Keycloak realm admin password"
  428. ansible.builtin.command: openssl rand -base64 16
  429. register: keycloak_realm_admin_password
  430. changed_when: false
  431. delegate_to: localhost
  432. become: false
  433. when: not secret_exists['keycloak']
  434. tags:
  435. - vault-secrets
  436. - name: "Vault | Store Keycloak secrets"
  437. ansible.builtin.uri:
  438. url: "{{ vault_addr }}/v1/{{ vault_secret_prefix }}/keycloak"
  439. method: POST
  440. headers:
  441. X-Vault-Token: "{{ vault_root_token }}"
  442. body_format: json
  443. body:
  444. data:
  445. admin_password: "{{ keycloak_admin_password.stdout }}"
  446. client_secret: "{{ keycloak_client_secret.stdout }}"
  447. realm_admin_password: "{{ keycloak_realm_admin_password.stdout }}"
  448. status_code: [200, 204]
  449. when: not secret_exists['keycloak']
  450. tags:
  451. - vault-secrets
  452. # ── OpenClaw (always write when token is provided) ────────────────
  453. - name: "Vault | Store Telegram token in Vault"
  454. ansible.builtin.uri:
  455. url: "{{ vault_addr }}/v1/{{ vault_secret_prefix }}/openclaw"
  456. method: POST
  457. headers:
  458. X-Vault-Token: "{{ vault_root_token }}"
  459. body_format: json
  460. body:
  461. data:
  462. telegram_token: "{{ telegram_token }}"
  463. status_code: [200, 204]
  464. when: telegram_token | length > 0
  465. tags:
  466. - vault-secrets
  467. handlers:
  468. - name: Restart vault
  469. ansible.builtin.systemd:
  470. name: vault
  471. state: restarted
  472. daemon_reload: true