Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion roles/vault_utils/defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
103 changes: 103 additions & 0 deletions roles/vault_utils/tasks/detect_external_secrets_auth.yml
Original file line number Diff line number Diff line change
@@ -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
43 changes: 11 additions & 32 deletions roles/vault_utils/tasks/vault_app_policies.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
66 changes: 5 additions & 61 deletions roles/vault_utils/tasks/vault_secrets_init.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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) }}'
Expand Down Expand Up @@ -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 }}"
Expand All @@ -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([]) }}"
Expand All @@ -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
78 changes: 78 additions & 0 deletions roles/vault_utils/tasks/write_hub_role.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
# Write or update auth/<vault_hub>/role/<vault_hub>-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