diff --git a/TODO.txt b/TODO.txt index a758463..ffb4646 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1 +1 @@ -- Replace alvaroaleman.freeipa-client with https://galaxy.ansible.com/freeipa/ansible_freeipa \ No newline at end of file + - Setup Grafana to use Keycloak \ No newline at end of file diff --git a/ansible-navigator.yml.old b/ansible-navigator.yml.old new file mode 100644 index 0000000..fd82057 --- /dev/null +++ b/ansible-navigator.yml.old @@ -0,0 +1,63 @@ +# cspell:ignore cmdline, workdir +--- +ansible-navigator: + ansible: + config: + help: false + # Inventory is set in ansible.cfg. Override at runtime with -i if needed. + + execution-environment: + container-engine: podman + enabled: true + image: aap.toal.ca/ee-demo:latest + pull: + policy: missing + + environment-variables: + pass: + - OP_SERVICE_ACCOUNT_TOKEN # 1Password service account (vault) + - OP_CONNECT_HOST # 1Password Connect server (alternative) + - OP_CONNECT_TOKEN + - CONTROLLER_HOST # AAP / AWX controller + - CONTROLLER_OAUTH_TOKEN + - CONTROLLER_USERNAME + - CONTROLLER_PASSWORD + - AAP_HOSTNAME # Newer AAP naming (same controller) + - AAP_USERNAME + - AAP_PASSWORD + - SATELLITE_SERVER_URL + - SATELLITE_USERNAME + - SATELLITE_PASSWORD + - SATELLITE_VALIDATE_CERTS + - NETBOX_API + - NETBOX_API_TOKEN + - NETBOX_TOKEN + + # Volume mounts are not merged across config files - all required mounts + # must be listed here when a project config is present. + volume-mounts: + # 1Password SSH agent socket (required for vault-id-from-op-client.sh) + - src: "/home/ptoal/.1password/agent.sock" + dest: "/root/.1password/agent.sock" + options: "Z" + # Ansible utilities + - src: "/home/ptoal/.ansible/utils/" + dest: "/root/.ansible/utils" + options: "Z" + # Project-local collections (toallab.infra and others not in the EE image) + - src: "collections" + dest: "/runner/project/collections" + options: "Z" + - src: "~/.kube/config" + dest: "/root/.kube/config" + options: "ro" + + + logging: + level: warning + file: /tmp/ansible-navigator.log + + mode: stdout + + playbook-artifact: + enable: false diff --git a/playbooks/deploy_aap.yml b/playbooks/deploy_aap.yml index 1da75cd..7076ecf 100644 --- a/playbooks/deploy_aap.yml +++ b/playbooks/deploy_aap.yml @@ -51,8 +51,12 @@ # e.g. platform 'aap' in namespace 'aap' → aap-aap.apps.openshift.toal.ca __aap_platform_name: "{{ aap_operator_platform_name | default('aap') }}" __aap_namespace: "{{ aap_operator_namespace | default('aap') }}" + # Use custom gateway hostname if set, otherwise fall back to auto-generated route + __aap_gateway_host: >- + {{ aap_operator_gateway_route_host + | default(__aap_platform_name + '-' + __aap_namespace + '.apps.' + ocp_cluster_name + '.' + ocp_base_domain) }} __aap_oidc_redirect_uris: - - "https://{{ __aap_platform_name }}-{{ __aap_namespace }}.apps.{{ ocp_cluster_name }}.{{ ocp_base_domain }}/accounts/profile/callback/" + - "https://{{ __aap_gateway_host }}/accounts/profile/callback/" module_defaults: middleware_automation.keycloak.keycloak_client: @@ -119,7 +123,7 @@ - " Redirect : {{ __aap_oidc_redirect_uris | join(', ') }}" - "" - "Set in host_vars for the aap host:" - - " aap_gateway_url: https://{{ __aap_platform_name }}-{{ __aap_namespace }}.apps.{{ ocp_cluster_name }}.{{ ocp_base_domain }}" + - " aap_gateway_url: https://{{ __aap_gateway_host }}" - " aap_oidc_issuer: {{ __aap_keycloak_api_url }}/realms/{{ keycloak_realm }}" - "" - "Then run: --tags aap_configure_oidc to register the authenticator in AAP." diff --git a/playbooks/deploy_vault.yml b/playbooks/deploy_vault.yml new file mode 100644 index 0000000..24a2019 --- /dev/null +++ b/playbooks/deploy_vault.yml @@ -0,0 +1,151 @@ +--- +# Deploy and configure HashiCorp Vault CE on TrueNAS Scale. +# +# Vault is deployed as a TrueNAS custom app (Docker Compose). +# This playbook handles post-deploy configuration only — it does NOT install Vault. +# See: docs/ for the TrueNAS compose YAML and vault.hcl required before running. +# +# Prerequisites: +# - Vault running on TrueNAS and accessible at vault_url +# - vault host/group in inventory with vault_url and vault_oidc_issuer set +# +# Keycloak OIDC prerequisites (--tags vault_configure_keycloak,vault_configure_oidc): +# - Keycloak realm exists (configured via deploy_openshift.yml) +# - vault_vault_oidc_client_secret in 1Password (or it will be generated and displayed) +# - In host_vars for the vault host: +# vault_url: "http://nas.lan.toal.ca:8200" +# vault_oidc_issuer: "https://keycloak.apps../realms/" +# +# Play order: +# Play 0: vault_configure_keycloak — Create Keycloak OIDC client for Vault +# Play 1: vault_init — Initialize Vault, display keys for 1Password +# Play 2: (default) — Unseal + configure OIDC authentication +# +# Usage: +# ansible-navigator run playbooks/deploy_vault.yml --tags vault_configure_keycloak +# ansible-navigator run playbooks/deploy_vault.yml --tags vault_init +# ansible-navigator run playbooks/deploy_vault.yml +# ansible-navigator run playbooks/deploy_vault.yml --tags vault_configure_keycloak,vault_init + +# --------------------------------------------------------------------------- +# Play 0: Create Keycloak OIDC client for Vault (optional) +# Runs on openshift hosts to access keycloak_url/keycloak_realm host vars. +# Creates the OIDC client in Keycloak with the correct Vault callback URIs. +# --------------------------------------------------------------------------- +- name: Configure Keycloak OIDC client for Vault + hosts: openshift + gather_facts: false + connection: local + + tags: + - never + - vault_configure_keycloak + + vars: + __vault_keycloak_api_url: "{{ keycloak_url }}{{ keycloak_context | default('') }}" + __vault_oidc_client_id: "{{ vault_oidc_client_id | default('vault') }}" + __vault_url: "{{ hostvars[groups['vault'][0]]['vault_url'] | default('http://nas.lan.toal.ca:8200') }}" + + module_defaults: + middleware_automation.keycloak.keycloak_client: + auth_client_id: admin-cli + auth_keycloak_url: "{{ __vault_keycloak_api_url }}" + auth_realm: master + auth_username: "{{ keycloak_admin_user }}" + auth_password: "{{ vault_keycloak_admin_password }}" + validate_certs: "{{ keycloak_validate_certs | default(true) }}" + + tasks: + - name: Set Vault OIDC client secret (vault value or generated) + ansible.builtin.set_fact: + __vault_oidc_client_secret: "{{ vault_vault_oidc_client_secret | default(lookup('community.general.random_string', length=32, special=false)) }}" + __vault_oidc_secret_generated: "{{ vault_vault_oidc_client_secret is not defined }}" + no_log: true + + - name: Create Vault OIDC client in Keycloak + middleware_automation.keycloak.keycloak_client: + realm: "{{ keycloak_realm }}" + client_id: "{{ __vault_oidc_client_id }}" + name: "HashiCorp Vault" + description: "OIDC client for Vault on TrueNAS" + enabled: true + protocol: openid-connect + public_client: false + standard_flow_enabled: true + implicit_flow_enabled: false + direct_access_grants_enabled: false + service_accounts_enabled: false + secret: "{{ __vault_oidc_client_secret }}" + redirect_uris: + - "{{ __vault_url }}/ui/vault/auth/oidc/oidc/callback" + - "http://localhost:8250/oidc/callback" + web_origins: + - "+" + protocol_mappers: + - name: groups + protocol: openid-connect + protocolMapper: oidc-group-membership-mapper + config: + full.path: "false" + id.token.claim: "true" + access.token.claim: "true" + userinfo.token.claim: "true" + claim.name: groups + state: present + no_log: "{{ keycloak_no_log | default(true) }}" + + - name: Display generated client secret (save this to vault!) + ansible.builtin.debug: + msg: + - "*** GENERATED VAULT OIDC CLIENT SECRET — SAVE THIS TO 1PASSWORD ***" + - "vault_vault_oidc_client_secret: {{ __vault_oidc_client_secret }}" + - "" + - "Save to 1Password and reference as vault_vault_oidc_client_secret." + when: __vault_oidc_secret_generated | bool + + - name: Display Keycloak Vault OIDC configuration summary + ansible.builtin.debug: + msg: + - "Keycloak Vault OIDC client configured:" + - " Realm : {{ keycloak_realm }}" + - " Client : {{ __vault_oidc_client_id }}" + - " Issuer : {{ __vault_keycloak_api_url }}/realms/{{ keycloak_realm }}" + - "" + - "Set in host_vars for the vault host:" + - " vault_oidc_issuer: {{ __vault_keycloak_api_url }}/realms/{{ keycloak_realm }}" + - "" + - "Then run: --tags vault_init (if not done) then the default play." + verbosity: 1 + +# --------------------------------------------------------------------------- +# Play 1: Initialize Vault (optional, one-time) +# Initializes Vault and displays root token + unseal keys for saving to 1Password. +# Fails after init intentionally — save credentials then run the default play. +# --------------------------------------------------------------------------- +- name: Initialize Vault + hosts: vault + gather_facts: false + connection: local + + tags: + - never + - vault_init + + tasks: + - name: Run Vault init tasks + ansible.builtin.include_role: + name: vault_setup + tasks_from: init.yml + +# --------------------------------------------------------------------------- +# Play 2: Unseal and configure Vault OIDC authentication (default) +# Requires vault_vault_root_token and vault_vault_oidc_client_secret in 1Password. +# Optionally unseals if vault_unseal_keys is provided and Vault is sealed. +# --------------------------------------------------------------------------- +- name: Configure Vault OIDC authentication + hosts: vault + gather_facts: false + connection: local + + roles: + - role: vault_setup diff --git a/roles/aap_operator/defaults/main.yml b/roles/aap_operator/defaults/main.yml index 64118f2..c628fd7 100644 --- a/roles/aap_operator/defaults/main.yml +++ b/roles/aap_operator/defaults/main.yml @@ -25,5 +25,8 @@ aap_operator_hub_file_storage_size: 10Gi aap_operator_admin_user: admin # --- Routing (optional) --- +# Set to a custom hostname to override the auto-generated Gateway route (primary UI/API entry point) +# aap_operator_gateway_route_host: aap.example.com # Set to a custom hostname to override the auto-generated Controller route -# aap_operator_controller_route_host: aap.example.com +# aap_operator_controller_route_host: controller.example.com + diff --git a/roles/aap_operator/meta/argument_specs.yml b/roles/aap_operator/meta/argument_specs.yml index 14d9707..de9b4d9 100644 --- a/roles/aap_operator/meta/argument_specs.yml +++ b/roles/aap_operator/meta/argument_specs.yml @@ -58,10 +58,18 @@ argument_specs: description: Admin username for the platform. type: str default: admin - aap_operator_controller_route_host: + aap_operator_gateway_route_host: description: > - Custom hostname for the Automation Controller Route. - When set, overrides the auto-generated route hostname (e.g. aap.example.com). + Custom hostname for the AAP Gateway Route (primary UI/API entry point in AAP 2.5+). + When set, overrides the auto-generated gateway route (e.g. aap.example.com). Leave unset to use the default apps subdomain route. type: str required: false + aap_operator_controller_route_host: + description: > + Custom hostname for the Automation Controller Route. + When set, overrides the auto-generated controller route hostname. + Leave unset to use the default apps subdomain route. + type: str + required: false + diff --git a/roles/aap_operator/tasks/main.yml b/roles/aap_operator/tasks/main.yml index 8bbbcf6..5a9e005 100644 --- a/roles/aap_operator/tasks/main.yml +++ b/roles/aap_operator/tasks/main.yml @@ -116,6 +116,9 @@ # PostgreSQL storage for all components (RWO) database: postgres_storage_class: "{{ aap_operator_storage_class }}" + # Gateway is the primary UI/API entry point in AAP 2.5+ + gateway: + route_host: "{{ aap_operator_gateway_route_host | default(omit) }}" # Component toggles and per-component config controller: disabled: "{{ aap_operator_controller_disabled | bool }}" @@ -129,6 +132,48 @@ eda: disabled: "{{ aap_operator_eda_disabled | bool }}" +# ------------------------------------------------------------------ +# Step 3a: Clear controller route_host if not explicitly set +# strategic-merge-patch (state: present) does not remove existing fields, +# so we must explicitly remove /spec/controller/route_host if the user +# hasn't set aap_operator_controller_route_host (e.g. after switching to +# gateway-based routing). +# ------------------------------------------------------------------ +# The platform operator propagates route_host to the child AutomationController CR. +# Both must be cleared or the controller operator will continue to set the old hostname. +- name: Remove controller route_host from platform CR (use auto-generated route) + kubernetes.core.k8s_json_patch: + api_version: aap.ansible.com/v1alpha1 + kind: AnsibleAutomationPlatform + namespace: "{{ aap_operator_namespace }}" + name: "{{ aap_operator_platform_name }}" + patch: + - op: remove + path: /spec/controller/route_host + when: aap_operator_controller_route_host is not defined + failed_when: false # no-op if field doesn't exist + +- name: Remove controller route_host from child AutomationController CR + kubernetes.core.k8s_json_patch: + api_version: automationcontroller.ansible.com/v1beta1 + kind: AutomationController + namespace: "{{ aap_operator_namespace }}" + name: "{{ aap_operator_platform_name }}-controller" + patch: + - op: remove + path: /spec/route_host + when: aap_operator_controller_route_host is not defined + failed_when: false # no-op if field doesn't exist or CR not yet created + +- name: Delete aap-controller Route so operator recreates with correct hostname + kubernetes.core.k8s: + api_version: route.openshift.io/v1 + kind: Route + namespace: "{{ aap_operator_namespace }}" + name: "{{ aap_operator_platform_name }}-controller" + state: absent + when: aap_operator_controller_route_host is not defined + # ------------------------------------------------------------------ # Step 4: Wait for platform to be ready # ------------------------------------------------------------------ diff --git a/roles/vault_setup/defaults/main.yml b/roles/vault_setup/defaults/main.yml new file mode 100644 index 0000000..1afadd5 --- /dev/null +++ b/roles/vault_setup/defaults/main.yml @@ -0,0 +1,22 @@ +--- +# --- Vault API --- +vault_url: "http://nas.lan.toal.ca:8200" +vault_validate_certs: false + +# --- Init --- +vault_init_key_shares: 5 +vault_init_key_threshold: 3 + +# --- OIDC --- +vault_oidc_client_id: vault +vault_oidc_admin_group: vault-admins +vault_oidc_default_ttl: 1h +vault_oidc_max_ttl: 8h + +# --- Unseal --- +# vault_unseal_keys: [] # list of 3+ unseal key strings (from 1Password) + +# --- Secrets (required, set via vault or host_vars) --- +# vault_vault_root_token: # root token from 1Password (required for Play 2) +# vault_vault_oidc_client_secret: # OIDC client secret from Keycloak (required for Play 2) +# vault_oidc_issuer: # e.g. https://keycloak.apps.openshift.toal.ca/realms/toallab diff --git a/roles/vault_setup/meta/argument_specs.yml b/roles/vault_setup/meta/argument_specs.yml new file mode 100644 index 0000000..40a87c3 --- /dev/null +++ b/roles/vault_setup/meta/argument_specs.yml @@ -0,0 +1,61 @@ +--- +argument_specs: + main: + short_description: Configure a running HashiCorp Vault instance + description: + - Unseals Vault if sealed and unseal keys are provided. + - Enables and configures OIDC authentication using Keycloak. + - Creates an admin policy and maps a Keycloak group to it. + - Requires Vault to already be initialized (use vault_init tag first). + options: + vault_url: + description: Base URL of the Vault API. + type: str + default: "http://nas.lan.toal.ca:8200" + vault_validate_certs: + description: Whether to validate TLS certificates for Vault API calls. + type: bool + default: false + vault_vault_root_token: + description: Vault root token for API authentication. Required. + type: str + required: true + vault_oidc_issuer: + description: OIDC discovery URL base (Keycloak realm URL). Required. + type: str + required: true + vault_vault_oidc_client_secret: + description: OIDC client secret from Keycloak. Required. + type: str + required: true + vault_oidc_client_id: + description: OIDC client ID registered in Keycloak. + type: str + default: vault + vault_oidc_admin_group: + description: Keycloak group name to map to the Vault admin policy. + type: str + default: vault-admins + vault_oidc_default_ttl: + description: Default token TTL for OIDC-authenticated tokens. + type: str + default: 1h + vault_oidc_max_ttl: + description: Maximum token TTL for OIDC-authenticated tokens. + type: str + default: 8h + vault_unseal_keys: + description: >- + List of unseal key strings. If provided and Vault is sealed, + the role will attempt to unseal using these keys. + type: list + elements: str + default: [] + vault_init_key_shares: + description: Number of key shares for vault operator init. + type: int + default: 5 + vault_init_key_threshold: + description: Number of key shares required to unseal. + type: int + default: 3 diff --git a/roles/vault_setup/tasks/configure_oidc.yml b/roles/vault_setup/tasks/configure_oidc.yml new file mode 100644 index 0000000..1c24884 --- /dev/null +++ b/roles/vault_setup/tasks/configure_oidc.yml @@ -0,0 +1,136 @@ +--- +# Configure Keycloak OIDC authentication in Vault. +# +# Creates: +# - OIDC auth method (auth/oidc) +# - OIDC config pointing to Keycloak realm +# - Default OIDC role with groups claim +# - Admin ACL policy +# - External identity group mapped to vault_oidc_admin_group Keycloak group + +- name: Set Vault API auth headers + ansible.builtin.set_fact: + __vault_headers: + X-Vault-Token: "{{ vault_vault_root_token }}" + +- name: Enable OIDC auth method + ansible.builtin.uri: + url: "{{ vault_url }}/v1/sys/auth/oidc" + method: POST + headers: "{{ __vault_headers }}" + validate_certs: "{{ vault_validate_certs }}" + body_format: json + body: + type: oidc + description: Keycloak OIDC + status_code: [200, 204, 400] + register: __vault_enable_oidc + no_log: true + changed_when: __vault_enable_oidc.status in [200, 204] + +- name: Configure OIDC provider (Keycloak) + ansible.builtin.uri: + url: "{{ vault_url }}/v1/auth/oidc/config" + method: POST + headers: "{{ __vault_headers }}" + validate_certs: "{{ vault_validate_certs }}" + body_format: json + body: + oidc_discovery_url: "{{ vault_oidc_issuer }}" + oidc_client_id: "{{ vault_oidc_client_id }}" + oidc_client_secret: "{{ vault_vault_oidc_client_secret }}" + default_role: default + status_code: [200, 204] + no_log: true + +- name: Create default OIDC role + ansible.builtin.uri: + url: "{{ vault_url }}/v1/auth/oidc/role/default" + method: POST + headers: "{{ __vault_headers }}" + validate_certs: "{{ vault_validate_certs }}" + body_format: json + body: + bound_audiences: + - "{{ vault_oidc_client_id }}" + allowed_redirect_uris: + - "{{ vault_url }}/ui/vault/auth/oidc/oidc/callback" + - "http://localhost:8250/oidc/callback" + user_claim: preferred_username + groups_claim: groups + token_policies: + - default + token_ttl: "{{ vault_oidc_default_ttl }}" + token_max_ttl: "{{ vault_oidc_max_ttl }}" + status_code: [200, 204] + no_log: true + +- name: Create admin ACL policy + ansible.builtin.uri: + url: "{{ vault_url }}/v1/sys/policies/acl/admin" + method: POST + headers: "{{ __vault_headers }}" + validate_certs: "{{ vault_validate_certs }}" + body_format: json + body: + policy: | + path "*" { + capabilities = ["create", "read", "update", "delete", "list", "sudo"] + } + status_code: [200, 204] + no_log: true + +- name: Create external identity group for admin + ansible.builtin.uri: + url: "{{ vault_url }}/v1/identity/group" + method: POST + headers: "{{ __vault_headers }}" + validate_certs: "{{ vault_validate_certs }}" + body_format: json + body: + name: "{{ vault_oidc_admin_group }}" + type: external + policies: + - admin + status_code: [200, 204] + register: __vault_admin_group + no_log: true + +- name: Get OIDC auth method accessor + ansible.builtin.uri: + url: "{{ vault_url }}/v1/sys/auth" + method: GET + headers: "{{ __vault_headers }}" + validate_certs: "{{ vault_validate_certs }}" + status_code: 200 + register: __vault_auth_list + no_log: true + +- name: Set OIDC accessor fact + ansible.builtin.set_fact: + __vault_oidc_accessor: "{{ __vault_auth_list.json['oidc/'].accessor }}" + +- name: Create group alias mapping Keycloak group to Vault admin group + ansible.builtin.uri: + url: "{{ vault_url }}/v1/identity/group-alias" + method: POST + headers: "{{ __vault_headers }}" + validate_certs: "{{ vault_validate_certs }}" + body_format: json + body: + name: "{{ vault_oidc_admin_group }}" + mount_accessor: "{{ __vault_oidc_accessor }}" + canonical_id: "{{ __vault_admin_group.json.data.id }}" + status_code: [200, 204] + no_log: true + +- name: Display OIDC configuration summary + ansible.builtin.debug: + msg: + - "Vault OIDC configured:" + - " Provider : {{ vault_oidc_issuer }}" + - " Client : {{ vault_oidc_client_id }}" + - " Admin group: {{ vault_oidc_admin_group }}" + - "" + - "Login at: {{ vault_url }}/ui" + - "Select: OIDC → Sign in with Keycloak" diff --git a/roles/vault_setup/tasks/init.yml b/roles/vault_setup/tasks/init.yml new file mode 100644 index 0000000..908780b --- /dev/null +++ b/roles/vault_setup/tasks/init.yml @@ -0,0 +1,54 @@ +--- +# Initialize Vault. Idempotent: skips if already initialized. +# On success, displays root token and unseal keys for manual saving to 1Password. +# After saving, rerun the playbook (default play) to complete configuration. + +- name: Check Vault initialization status + ansible.builtin.uri: + url: "{{ vault_url }}/v1/sys/init" + method: GET + validate_certs: "{{ vault_validate_certs }}" + register: __vault_init_status + +- name: Skip init (already initialized) + ansible.builtin.debug: + msg: "Vault is already initialized. Skipping init." + when: __vault_init_status.json.initialized | bool + +- name: Initialize Vault + ansible.builtin.uri: + url: "{{ vault_url }}/v1/sys/init" + method: POST + validate_certs: "{{ vault_validate_certs }}" + body_format: json + body: + secret_shares: "{{ vault_init_key_shares }}" + secret_threshold: "{{ vault_init_key_threshold }}" + status_code: 200 + register: __vault_init_result + no_log: true + when: not __vault_init_status.json.initialized | bool + +- name: Display init output — SAVE TO 1PASSWORD NOW + ansible.builtin.debug: + msg: + - "*** VAULT INITIALIZED — SAVE THE FOLLOWING TO 1PASSWORD IMMEDIATELY ***" + - "" + - "Root Token:" + - " vault_vault_root_token: {{ __vault_init_result.json.root_token }}" + - "" + - "Unseal Keys (need {{ vault_init_key_threshold }} of {{ vault_init_key_shares }}):" + - "{% for key in __vault_init_result.json.keys_base64 %} unseal_key_{{ loop.index }}: {{ key }}{% endfor %}" + - "" + - "Save vault_unseal_keys as a list of {{ vault_init_key_threshold }} key strings in 1Password." + - "Save vault_vault_root_token to 1Password." + when: not __vault_init_status.json.initialized | bool + +- name: Fail after init — save credentials before continuing + ansible.builtin.fail: + msg: >- + Vault initialization complete. + SAVE the root token and unseal keys to 1Password before continuing. + Then run the default play to unseal and configure OIDC: + ansible-navigator run playbooks/deploy_vault.yml + when: not __vault_init_status.json.initialized | bool diff --git a/roles/vault_setup/tasks/main.yml b/roles/vault_setup/tasks/main.yml new file mode 100644 index 0000000..8ab9808 --- /dev/null +++ b/roles/vault_setup/tasks/main.yml @@ -0,0 +1,51 @@ +--- +# Configures a running, initialized HashiCorp Vault instance. +# +# Expects Vault to already be initialized (run --tags vault_init first). +# Unseals if sealed and vault_unseal_keys is defined. +# Then configures OIDC authentication with Keycloak. + +- name: Validate required variables + ansible.builtin.assert: + that: + - vault_url | length > 0 + - vault_vault_root_token | default('') | length > 0 + - vault_oidc_issuer | default('') | length > 0 + - vault_vault_oidc_client_secret | default('') | length > 0 + fail_msg: >- + vault_vault_root_token, vault_oidc_issuer, and vault_vault_oidc_client_secret + are required. Run --tags vault_init first, save credentials to 1Password, + then run --tags vault_configure_keycloak,vault_configure_oidc or default play. + +- name: Check Vault status + ansible.builtin.uri: + url: "{{ vault_url }}/v1/sys/health" + method: GET + validate_certs: "{{ vault_validate_certs }}" + status_code: [200, 429, 472, 473, 501, 503] + register: __vault_health + +- name: Assert Vault is initialized + ansible.builtin.assert: + that: + - __vault_health.json.initialized | bool + fail_msg: >- + Vault is not initialized. Run: + ansible-navigator run playbooks/deploy_vault.yml --tags vault_init + +- name: Unseal Vault if sealed + ansible.builtin.include_tasks: unseal.yml + when: + - __vault_health.json.sealed | bool + - vault_unseal_keys | default([]) | length > 0 + +- name: Assert Vault is unsealed + ansible.builtin.assert: + that: + - not __vault_health.json.sealed | bool or __vault_unsealed | default(false) | bool + fail_msg: >- + Vault is sealed. Provide vault_unseal_keys (list of unseal key strings) or + unseal manually via the Vault UI, then rerun. + +- name: Configure OIDC authentication + ansible.builtin.include_tasks: configure_oidc.yml diff --git a/roles/vault_setup/tasks/unseal.yml b/roles/vault_setup/tasks/unseal.yml new file mode 100644 index 0000000..37a04f2 --- /dev/null +++ b/roles/vault_setup/tasks/unseal.yml @@ -0,0 +1,37 @@ +--- +# Unseal Vault using keys from vault_unseal_keys list. +# Submits keys one at a time until Vault reports unsealed. +# Requires vault_init_key_threshold keys in vault_unseal_keys. + +- name: Submit unseal keys + ansible.builtin.uri: + url: "{{ vault_url }}/v1/sys/unseal" + method: POST + validate_certs: "{{ vault_validate_certs }}" + body_format: json + body: + key: "{{ item }}" + status_code: 200 + loop: "{{ vault_unseal_keys[:vault_init_key_threshold] }}" + register: __vault_unseal_result + no_log: true + +- name: Check unseal status + ansible.builtin.uri: + url: "{{ vault_url }}/v1/sys/health" + method: GET + validate_certs: "{{ vault_validate_certs }}" + status_code: [200, 429] + register: __vault_health + +- name: Assert Vault unsealed successfully + ansible.builtin.assert: + that: + - not __vault_health.json.sealed | bool + fail_msg: >- + Vault is still sealed after submitting {{ vault_init_key_threshold }} keys. + Check that vault_unseal_keys contains the correct keys and try again. + +- name: Register unseal success + ansible.builtin.set_fact: + __vault_unsealed: true