diff --git a/roles/vault_utils/defaults/main.yml b/roles/vault_utils/defaults/main.yml index ca3fa04..fb44b2e 100644 --- a/roles/vault_utils/defaults/main.yml +++ b/roles/vault_utils/defaults/main.yml @@ -19,13 +19,16 @@ vault_global_policy: global vault_global_capabilities: '[\\\"read\\\"]' vault_pushsecrets_policy: pushsecrets vault_pushsecrets_capabilities: '[\\\"create\\\",\\\"read\\\",\\\"update\\\",\\\"list\\\",\\\"delete\\\"]' +# Legacy community chart (golang-external-secrets) legacy_external_secrets_ns: golang-external-secrets legacy_external_secrets_sa: golang-external-secrets legacy_external_secrets_secret: golang-external-secrets +# Red Hat openshift-external-secrets chart external_secrets_ns: external-secrets -# The service account cannot be called "external-secrets" as that SA is used by the downstream ESO +# The service account cannot be called "external-secrets" as that SA is used by the ESO operator external_secrets_sa: ocp-external-secrets external_secrets_secret: ocp-external-secrets +# hub-role binds every SA whose token secret exists (see detect_external_secrets_auth.yml) unseal_secret: "vaultkeys" unseal_namespace: "imperative" vault_jwt_config: false diff --git a/roles/vault_utils/tasks/detect_external_secrets_auth.yml b/roles/vault_utils/tasks/detect_external_secrets_auth.yml new file mode 100644 index 0000000..d783d7f --- /dev/null +++ b/roles/vault_utils/tasks/detect_external_secrets_auth.yml @@ -0,0 +1,103 @@ +--- +# Detect which External Secrets Operator service account tokens exist on the +# cluster and build hub-role bindings. Supports both: +# - openshift-external-secrets (ocp-external-secrets / external-secrets) +# - golang-external-secrets (legacy community chart) +# +# Sets facts used by vault_secrets_init.yaml and vault_app_policies.yaml: +# hub_bound_service_account_names - comma-separated for vault write +# hub_bound_service_account_namespaces - comma-separated for vault write +# hub_bound_service_account_names_list +# hub_bound_service_account_namespaces_list +# token_data / sa_token - reviewer JWT (prefers openshift) +# active_external_secrets_sa - primary SA (for backwards compatibility) +# active_external_secrets_ns +# active_external_secrets_secret + +- name: Check for openshift external secrets service account token + no_log: '{{ hide_sensitive_output | default(true) }}' + kubernetes.core.k8s_info: + kind: Secret + namespace: "{{ external_secrets_ns }}" + name: "{{ external_secrets_secret }}" + api_version: v1 + register: external_secrets_token_data + failed_when: false + +- name: Check for golang external secrets service account token + no_log: '{{ hide_sensitive_output | default(true) }}' + kubernetes.core.k8s_info: + kind: Secret + namespace: "{{ legacy_external_secrets_ns }}" + name: "{{ legacy_external_secrets_secret }}" + api_version: v1 + register: legacy_external_secrets_token_data + failed_when: false + +- name: Build hub-role service account binding lists + ansible.builtin.set_fact: + hub_bound_service_account_names_list: >- + {{ + ([] if external_secrets_token_data.resources | length == 0 else [external_secrets_sa]) + + ([] if legacy_external_secrets_token_data.resources | length == 0 else [legacy_external_secrets_sa]) + }} + hub_bound_service_account_namespaces_list: >- + {{ + ([] if external_secrets_token_data.resources | length == 0 else [external_secrets_ns]) + + ([] if legacy_external_secrets_token_data.resources | length == 0 else [legacy_external_secrets_ns]) + }} + +- name: Set comma-separated hub-role bindings for vault write + ansible.builtin.set_fact: + hub_bound_service_account_names: "{{ hub_bound_service_account_names_list | join(',') }}" + hub_bound_service_account_namespaces: "{{ hub_bound_service_account_namespaces_list | join(',') }}" + +- name: Fail if no external secrets service account tokens are found + ansible.builtin.fail: + msg: >- + No External Secrets service account tokens found. Expected at least one of: + {{ external_secrets_ns }}/{{ external_secrets_secret }} (openshift-external-secrets) or + {{ legacy_external_secrets_ns }}/{{ legacy_external_secrets_secret }} (golang-external-secrets). + Deploy openshift-external-secrets or golang-external-secrets before running load-secrets. + when: hub_bound_service_account_names_list | length == 0 + +- name: Set token reviewer and primary ESO facts (prefer openshift over legacy) + no_log: '{{ hide_sensitive_output | default(true) }}' + ansible.builtin.set_fact: + token_data: >- + {{ + external_secrets_token_data + if (external_secrets_token_data.resources | length > 0) + else legacy_external_secrets_token_data + }} + active_external_secrets_sa: >- + {{ + external_secrets_sa + if (external_secrets_token_data.resources | length > 0) + else legacy_external_secrets_sa + }} + active_external_secrets_ns: >- + {{ + external_secrets_ns + if (external_secrets_token_data.resources | length > 0) + else legacy_external_secrets_ns + }} + active_external_secrets_secret: >- + {{ + external_secrets_secret + if (external_secrets_token_data.resources | length > 0) + else legacy_external_secrets_secret + }} + +- name: Set sa_token fact + no_log: '{{ hide_sensitive_output | default(true) }}' + ansible.builtin.set_fact: + sa_token: "{{ token_data.resources[0].data.token | b64decode }}" + +- name: Debug - External secrets auth detection + ansible.builtin.debug: + msg: >- + ESO hub-role bindings: names={{ hub_bound_service_account_names }}, + namespaces={{ hub_bound_service_account_namespaces }}, + token reviewer from {{ active_external_secrets_ns }}/{{ active_external_secrets_secret }} + verbosity: 1 diff --git a/roles/vault_utils/tasks/vault_app_policies.yaml b/roles/vault_utils/tasks/vault_app_policies.yaml index 2740ff2..452168a 100644 --- a/roles/vault_utils/tasks/vault_app_policies.yaml +++ b/roles/vault_utils/tasks/vault_app_policies.yaml @@ -40,6 +40,10 @@ verbosity: 1 when: app_prefixes is not defined or app_prefixes | length == 0 +- name: Detect external secrets service accounts for hub-role + ansible.builtin.include_tasks: detect_external_secrets_auth.yml + when: app_prefixes is defined and app_prefixes | length > 0 + # Build list of policy names we need - name: Build app policy names list ansible.builtin.set_fact: @@ -125,53 +129,28 @@ - _hub_role_result.rc != 0 # Merge current and new policies (removing duplicates) -- name: Merge policies +- name: Merge hub-role policies with app policies ansible.builtin.set_fact: - _merged_policies: "{{ (_current_policies + _app_policy_names) | unique }}" + _merged_hub_policies: "{{ (_current_policies + _app_policy_names) | unique }}" when: - app_prefixes is defined and app_prefixes | length > 0 - app_update_hub_role | default(true) | bool -# Check if hub-role policies need updating -- name: Check if hub-role policies changed - ansible.builtin.set_fact: - _hub_role_policies_changed: "{{ _current_policies | sort != _merged_policies | sort }}" - when: - - app_prefixes is defined and app_prefixes | length > 0 - - app_update_hub_role | default(true) | bool - -- name: Debug - Hub role policy change status - ansible.builtin.debug: - msg: "Hub role policies changed: {{ _hub_role_policies_changed }} (current: {{ _current_policies | sort }}, merged: {{ _merged_policies | sort }})" - verbosity: 1 - when: - - app_prefixes is defined and app_prefixes | length > 0 - - app_update_hub_role | default(true) | bool - -# Update hub-role only if policies have changed -- name: Update hub-role with app policies - kubernetes.core.k8s_exec: - namespace: "{{ vault_ns }}" - pod: "{{ vault_pod }}" - command: > - vault write auth/{{ vault_hub }}/role/{{ vault_hub }}-role - bound_service_account_names="{{ active_external_secrets_sa | default('golang-external-secrets') }}" - bound_service_account_namespaces="{{ active_external_secrets_ns | default('golang-external-secrets') }}" - policies="{{ _merged_policies | join(',') }}" - ttl="{{ vault_hub_ttl }}" +- name: Write hub-role with app policies and detected ESO service accounts + ansible.builtin.include_tasks: write_hub_role.yml when: - app_prefixes is defined and app_prefixes | length > 0 - app_update_hub_role | default(true) | bool - - _hub_role_policies_changed | bool - name: Display updated hub-role policies ansible.builtin.debug: - msg: "hub-role policies updated to: {{ _merged_policies | join(', ') }}" + msg: >- + hub-role policies: {{ _merged_hub_policies | join(', ') }}; + bindings: {{ hub_bound_service_account_names }}/{{ hub_bound_service_account_namespaces }} verbosity: 1 when: - app_prefixes is defined and app_prefixes | length > 0 - app_update_hub_role | default(true) | bool - - _hub_role_policies_changed | bool # Optionally create JWT roles for app-level isolation # Only creates roles for entries that have jwt_role defined diff --git a/roles/vault_utils/tasks/vault_secrets_init.yaml b/roles/vault_utils/tasks/vault_secrets_init.yaml index f8ba36c..a41926c 100644 --- a/roles/vault_utils/tasks/vault_secrets_init.yaml +++ b/roles/vault_utils/tasks/vault_secrets_init.yaml @@ -31,43 +31,8 @@ command: "vault auth enable -path={{ vault_hub }} kubernetes" when: kubernetes_enabled.rc != 0 -- name: Check for external secrets namespace and secret - no_log: '{{ hide_sensitive_output | default(true) }}' - kubernetes.core.k8s_info: - kind: Secret - namespace: "{{ external_secrets_ns }}" - name: "{{ external_secrets_secret }}" - api_version: v1 - register: external_secrets_token_data - failed_when: false - -- name: Check for legacy external secrets namespace and secret - no_log: '{{ hide_sensitive_output | default(true) }}' - kubernetes.core.k8s_info: - kind: Secret - namespace: "{{ legacy_external_secrets_ns }}" - name: "{{ legacy_external_secrets_secret }}" - api_version: v1 - register: legacy_external_secrets_token_data - failed_when: false - when: external_secrets_token_data.resources | length == 0 - -- name: Set external secrets configuration to use (prefer new over legacy) - ansible.builtin.set_fact: - active_external_secrets_ns: "{{ external_secrets_ns if external_secrets_token_data.resources | length > 0 else legacy_external_secrets_ns }}" - active_external_secrets_sa: "{{ external_secrets_sa if external_secrets_token_data.resources | length > 0 else legacy_external_secrets_sa }}" - active_external_secrets_secret: "{{ external_secrets_secret if external_secrets_token_data.resources | length > 0 else legacy_external_secrets_secret }}" - token_data: "{{ external_secrets_token_data if external_secrets_token_data.resources | length > 0 else legacy_external_secrets_token_data }}" - -- name: Fail if neither external secrets nor legacy external secrets are found - ansible.builtin.fail: - msg: "Neither {{ external_secrets_ns }}/{{ external_secrets_secret }} nor {{ legacy_external_secrets_ns }}/{{ legacy_external_secrets_secret }} secret found" - when: token_data.resources | length == 0 - -- name: Set sa_token fact - no_log: '{{ hide_sensitive_output | default(true) }}' - ansible.builtin.set_fact: - sa_token: "{{ token_data.resources[0].data.token | b64decode }}" +- name: Detect external secrets service accounts for hub-role + ansible.builtin.include_tasks: detect_external_secrets_auth.yml - name: Configure hub kubernetes backend no_log: '{{ hide_sensitive_output | default(true) }}' @@ -130,7 +95,7 @@ pod: "{{ vault_pod }}" command: "vault policy write {{ vault_hub }}-secret /tmp/policy-{{ vault_hub }}.hcl" -# Get current hub-role policies to preserve any custom policies added by patterns +# Preserve custom policies while ensuring VP defaults are present - name: Get current hub-role policies kubernetes.core.k8s_exec: namespace: "{{ vault_ns }}" @@ -141,7 +106,6 @@ failed_when: false changed_when: false -# Get existing policies (empty list if role doesn't exist yet) - name: Set current policies fact from existing hub-role ansible.builtin.set_fact: _current_hub_policies: "{{ (_hub_role_result.stdout | from_json).data.token_policies | default([]) }}" @@ -152,29 +116,9 @@ _current_hub_policies: [] when: _hub_role_result.rc != 0 -# Merge existing policies with defaults (preserves custom policies, ensures defaults present) - name: Merge hub-role policies ansible.builtin.set_fact: _merged_hub_policies: "{{ (_current_hub_policies + vault_hub_role_default_policies) | unique }}" -# Determine if we need to update the hub-role -- name: Check if hub-role policies need updating - ansible.builtin.set_fact: - _hub_role_needs_update: "{{ (_hub_role_result.rc != 0) or (_current_hub_policies | sort != _merged_hub_policies | sort) }}" - -- name: Debug - Hub role update status - ansible.builtin.debug: - msg: "Hub role needs update: {{ _hub_role_needs_update }} (role exists: {{ _hub_role_result.rc == 0 }}, current: {{ _current_hub_policies | sort }}, merged: {{ _merged_hub_policies | sort }})" - verbosity: 1 - -- name: Configure kubernetes role for hub with merged policies - kubernetes.core.k8s_exec: - namespace: "{{ vault_ns }}" - pod: "{{ vault_pod }}" - command: > - vault write auth/"{{ vault_hub }}"/role/"{{ vault_hub }}"-role - bound_service_account_names="{{ active_external_secrets_sa }}" - bound_service_account_namespaces="{{ active_external_secrets_ns }}" - policies="{{ _merged_hub_policies | join(',') }}" - ttl="{{ vault_hub_ttl }}" - when: _hub_role_needs_update | bool +- name: Write hub-role with all detected ESO service accounts + ansible.builtin.include_tasks: write_hub_role.yml diff --git a/roles/vault_utils/tasks/write_hub_role.yml b/roles/vault_utils/tasks/write_hub_role.yml new file mode 100644 index 0000000..c21c2fb --- /dev/null +++ b/roles/vault_utils/tasks/write_hub_role.yml @@ -0,0 +1,78 @@ +--- +# Write or update auth//role/-role using hub_bound_service_account_* facts. +# Requires: detect_external_secrets_auth.yml (or equivalent facts) +# Policy list: _merged_hub_policies (vault_secrets_init) or _merged_policies (vault_app_policies) + +- name: Normalize merged hub-role policies fact + ansible.builtin.set_fact: + _merged_hub_policies: >- + {{ + _merged_hub_policies + | default(_merged_policies) + | default(vault_hub_role_default_policies) + }} + +- name: Get current hub-role for comparison + kubernetes.core.k8s_exec: + namespace: "{{ vault_ns }}" + pod: "{{ vault_pod }}" + command: > + vault read -format=json auth/{{ vault_hub }}/role/{{ vault_hub }}-role + register: _hub_role_read + failed_when: false + changed_when: false + +- name: Parse existing hub-role policies + ansible.builtin.set_fact: + _current_hub_policies: "{{ (_hub_role_read.stdout | from_json).data.token_policies | default([]) }}" + when: _hub_role_read.rc == 0 + +- name: Set empty hub-role policies when role does not exist + ansible.builtin.set_fact: + _current_hub_policies: [] + when: _hub_role_read.rc != 0 + +- name: Parse existing hub-role service account bindings + ansible.builtin.set_fact: + _current_hub_bound_names: "{{ (_hub_role_read.stdout | from_json).data.bound_service_account_names | default([]) }}" + _current_hub_bound_namespaces: "{{ (_hub_role_read.stdout | from_json).data.bound_service_account_namespaces | default([]) }}" + when: _hub_role_read.rc == 0 + +- name: Set empty hub-role bindings when role does not exist + ansible.builtin.set_fact: + _current_hub_bound_names: [] + _current_hub_bound_namespaces: [] + when: _hub_role_read.rc != 0 + +- name: Check if hub-role needs updating + ansible.builtin.set_fact: + _hub_role_needs_update: >- + {{ + (_hub_role_read.rc != 0) + or (_current_hub_policies | sort != _merged_hub_policies | sort) + or (_current_hub_bound_names | sort != hub_bound_service_account_names_list | sort) + or (_current_hub_bound_namespaces | sort != hub_bound_service_account_namespaces_list | sort) + }} + +- name: Debug - Hub role update status + ansible.builtin.debug: + msg: >- + Hub role needs update: {{ _hub_role_needs_update }} + (policies changed: {{ _current_hub_policies | sort != _merged_hub_policies | sort }}, + bindings changed: {{ + (_current_hub_bound_names | sort != hub_bound_service_account_names_list | sort) + or (_current_hub_bound_namespaces | sort != hub_bound_service_account_namespaces_list | sort) + }}) + verbosity: 1 + +- name: Configure kubernetes role for hub + kubernetes.core.k8s_exec: + namespace: "{{ vault_ns }}" + pod: "{{ vault_pod }}" + command: > + vault write auth/"{{ vault_hub }}"/role/"{{ vault_hub }}"-role + bound_service_account_names="{{ hub_bound_service_account_names }}" + bound_service_account_namespaces="{{ hub_bound_service_account_namespaces }}" + policies="{{ _merged_hub_policies | join(',') }}" + ttl="{{ vault_hub_ttl }}" + when: _hub_role_needs_update | bool