Skip to content

Commit ffd327e

Browse files
authored
Merge pull request #87 from minmzzhang/app-vault-policies
feat(vault): add fine-grained app policies and preserve hub-role
2 parents 1e81ec4 + f97ac86 commit ffd327e

8 files changed

Lines changed: 320 additions & 2 deletions

File tree

playbooks/process_secrets.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,4 @@
4848
kubernetes_secret_objects: "{{ secrets_results['kubernetes_secret_objects'] }}"
4949
vault_policies: "{{ secrets_results['vault_policies'] }}"
5050
parsed_secrets: "{{ secrets_results['parsed_secrets'] }}"
51+
unique_vault_prefixes: "{{ secrets_results['unique_vault_prefixes'] | default([]) }}"

plugins/module_utils/parse_secrets_v2.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,3 +388,19 @@ def _inject_field(self, secret_name, f):
388388
self.parsed_secrets[secret_name]["fields"][f["name"]] = secret
389389

390390
return
391+
392+
def get_unique_vault_prefixes(self):
393+
"""
394+
Extract all unique vault prefixes from parsed secrets.
395+
396+
This is useful for creating fine-grained Vault policies for each
397+
unique prefix path (e.g., apps/qtodo, hub/infra/keycloak).
398+
399+
Returns:
400+
list: Sorted list of unique vault prefixes
401+
"""
402+
prefixes = set()
403+
for secret in self.parsed_secrets.values():
404+
for prefix in secret.get("vault_prefixes", []):
405+
prefixes.add(prefix)
406+
return sorted(list(prefixes))

plugins/modules/parse_secrets_info.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ def run(module):
152152
results["parsed_secrets"] = parsed_secret_obj.parsed_secrets
153153
results["kubernetes_secret_objects"] = parsed_secret_obj.kubernetes_secret_objects
154154
results["secret_store_namespace"] = parsed_secret_obj.secret_store_namespace
155+
results["unique_vault_prefixes"] = parsed_secret_obj.get_unique_vault_prefixes()
155156

156157
module.exit_json(**results)
157158

roles/vault_utils/defaults/main.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,33 @@ unseal_secret: "vaultkeys"
3030
unseal_namespace: "imperative"
3131
vault_jwt_config: false
3232
vault_jwt_roles: []
33+
34+
# Default policies that VP framework requires for hub-role
35+
# Patterns can extend this list but these are always included
36+
vault_hub_role_default_policies:
37+
- default
38+
- "{{ vault_global_policy }}-secret"
39+
- "{{ vault_pushsecrets_policy }}-secret"
40+
- "{{ vault_hub }}-secret"
41+
42+
# JWT auth policies for workloads needing direct Vault access
43+
# List of {name: "policy-name", policy: "HCL policy content"}
44+
vault_jwt_policies: []
45+
46+
# App-specific Vault policy configuration (vault_app_policies.yaml)
47+
# List of app configs, each with 'prefix' and optional 'jwt_role' settings
48+
# Example:
49+
# app_prefixes:
50+
# - prefix: apps/qtodo
51+
# jwt_role:
52+
# service_account_names: "qtodo-sa"
53+
# service_account_namespaces: "qtodo"
54+
# ttl: "15m"
55+
# - prefix: hub/infra/keycloak
56+
app_prefixes: []
57+
# Capabilities for app policies
58+
app_capabilities: '[\"read\"]'
59+
# Whether to update hub-role with new app policies
60+
app_update_hub_role: true
61+
# Whether to create JWT roles per app (only for entries with jwt_role defined)
62+
app_create_jwt_roles: false

roles/vault_utils/tasks/push_parsed_secrets.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,23 @@
4646
when:
4747
- parsed_secrets is defined
4848
- parsed_secrets | length > 0
49+
50+
# Create app-specific Vault policies for custom vaultPrefixes
51+
# Filter out 'global' and 'hub' as they're handled by the VP framework
52+
- name: Filter custom vault prefixes for app policies
53+
ansible.builtin.set_fact:
54+
_custom_vault_prefixes: "{{ unique_vault_prefixes | default([]) | reject('equalto', 'global') | reject('equalto', 'hub') | list }}"
55+
56+
# Convert string list to dict format expected by vault_app_policies.yaml
57+
- name: Convert prefixes to app_prefixes format
58+
ansible.builtin.set_fact:
59+
_app_prefixes_list: "{{ _custom_vault_prefixes | map('regex_replace', '^(.*)$', '{\"prefix\": \"\\1\"}') | map('from_json') | list }}"
60+
when: _custom_vault_prefixes | length > 0
61+
62+
- name: Create app-specific Vault policies
63+
ansible.builtin.include_tasks:
64+
file: vault_app_policies.yaml
65+
vars:
66+
app_prefixes: "{{ _app_prefixes_list }}"
67+
app_update_hub_role: true
68+
when: _custom_vault_prefixes | length > 0
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
---
2+
# vault_app_policies.yaml
3+
# Creates fine-grained Vault policies for application-level isolation
4+
#
5+
# This task file creates individual policies for each vaultPrefix provided.
6+
# Each application gets its own policy that only allows access to its
7+
# specific path, enabling application-level secret isolation.
8+
#
9+
# Required variables:
10+
# app_prefixes: List of app configs with 'prefix' key and optional 'jwt_role'
11+
# e.g., [{prefix: "apps/qtodo"}, {prefix: "hub/infra/keycloak"}]
12+
#
13+
# Optional variables:
14+
# app_capabilities: Capabilities for app policies (default: read)
15+
# app_update_hub_role: Whether to update hub-role with new policies (default: true)
16+
# app_create_jwt_roles: Whether to create JWT roles per app (default: false)
17+
#
18+
# Example usage:
19+
# - name: Create app-specific vault policies
20+
# ansible.builtin.include_tasks:
21+
# file: vault_app_policies.yaml
22+
# vars:
23+
# app_prefixes:
24+
# - prefix: apps/qtodo
25+
# jwt_role:
26+
# service_account_names: "qtodo-sa"
27+
# service_account_namespaces: "qtodo"
28+
# - prefix: hub/infra/keycloak
29+
# app_update_hub_role: true
30+
31+
- name: Debug - Show app prefixes to process
32+
ansible.builtin.debug:
33+
msg: "Processing app prefixes: {{ app_prefixes }}"
34+
verbosity: 1
35+
when: app_prefixes is defined and app_prefixes | length > 0
36+
37+
- name: Skip if no app prefixes defined
38+
ansible.builtin.debug:
39+
msg: "No app_prefixes defined, skipping app policy creation"
40+
verbosity: 1
41+
when: app_prefixes is not defined or app_prefixes | length == 0
42+
43+
# Build list of policy names we need
44+
- name: Build app policy names list
45+
ansible.builtin.set_fact:
46+
_app_policy_names: "{{ app_prefixes | map(attribute='prefix') | map('replace', '/', '-') | map('regex_replace', '$', '-k8s-secret') | list }}"
47+
when: app_prefixes is defined and app_prefixes | length > 0
48+
49+
# Get existing policies from Vault to check what needs to be created
50+
- name: Get existing Vault policies
51+
kubernetes.core.k8s_exec:
52+
namespace: "{{ vault_ns }}"
53+
pod: "{{ vault_pod }}"
54+
command: vault policy list -format=json
55+
register: _existing_policies_result
56+
changed_when: false
57+
when: app_prefixes is defined and app_prefixes | length > 0
58+
59+
- name: Parse existing policies
60+
ansible.builtin.set_fact:
61+
_existing_policies: "{{ _existing_policies_result.stdout | from_json }}"
62+
when:
63+
- app_prefixes is defined and app_prefixes | length > 0
64+
- _existing_policies_result.rc == 0
65+
66+
# Filter to only policies that don't already exist
67+
- name: Determine policies to create
68+
ansible.builtin.set_fact:
69+
_policies_to_create: "{{ _app_policy_names | difference(_existing_policies | default([])) }}"
70+
when: app_prefixes is defined and app_prefixes | length > 0
71+
72+
- name: Debug - Show policies to create
73+
ansible.builtin.debug:
74+
msg: "Policies to create: {{ _policies_to_create | default([]) }} (existing: {{ _existing_policies | default([]) }})"
75+
verbosity: 1
76+
when: app_prefixes is defined and app_prefixes | length > 0
77+
78+
# Create only policies that don't exist
79+
- name: Configure app policy in Vault
80+
kubernetes.core.k8s_exec:
81+
namespace: "{{ vault_ns }}"
82+
pod: "{{ vault_pod }}"
83+
command: >
84+
bash -e -c "echo 'path \"secret/data/{{ _prefix }}/*\" { capabilities = [\"read\"] }' |
85+
vault policy write {{ _prefix | replace('/', '-') }}-k8s-secret -"
86+
loop: "{{ app_prefixes }}"
87+
loop_control:
88+
label: "Writing policy {{ _prefix | replace('/', '-') }}-k8s-secret"
89+
vars:
90+
_prefix: "{{ item.prefix }}"
91+
_policy_name: "{{ item.prefix | replace('/', '-') }}-k8s-secret"
92+
when:
93+
- app_prefixes is defined and app_prefixes | length > 0
94+
- _policy_name in (_policies_to_create | default([]))
95+
96+
# Get current hub-role policies
97+
- name: Get current hub-role policies
98+
kubernetes.core.k8s_exec:
99+
namespace: "{{ vault_ns }}"
100+
pod: "{{ vault_pod }}"
101+
command: >
102+
vault read -format=json auth/{{ vault_hub }}/role/{{ vault_hub }}-role
103+
register: _hub_role_result
104+
failed_when: false
105+
changed_when: false
106+
when:
107+
- app_prefixes is defined and app_prefixes | length > 0
108+
- app_update_hub_role | default(true) | bool
109+
110+
# Parse current policies and merge with new ones
111+
- name: Set current policies fact
112+
ansible.builtin.set_fact:
113+
_current_policies: "{{ (_hub_role_result.stdout | from_json).data.token_policies | default(vault_hub_role_default_policies) }}"
114+
when:
115+
- app_prefixes is defined and app_prefixes | length > 0
116+
- app_update_hub_role | default(true) | bool
117+
- _hub_role_result.rc == 0
118+
119+
- name: Set default policies if hub-role not found
120+
ansible.builtin.set_fact:
121+
_current_policies: "{{ vault_hub_role_default_policies }}"
122+
when:
123+
- app_prefixes is defined and app_prefixes | length > 0
124+
- app_update_hub_role | default(true) | bool
125+
- _hub_role_result.rc != 0
126+
127+
# Merge current and new policies (removing duplicates)
128+
- name: Merge policies
129+
ansible.builtin.set_fact:
130+
_merged_policies: "{{ (_current_policies + _app_policy_names) | unique }}"
131+
when:
132+
- app_prefixes is defined and app_prefixes | length > 0
133+
- app_update_hub_role | default(true) | bool
134+
135+
# Check if hub-role policies need updating
136+
- name: Check if hub-role policies changed
137+
ansible.builtin.set_fact:
138+
_hub_role_policies_changed: "{{ _current_policies | sort != _merged_policies | sort }}"
139+
when:
140+
- app_prefixes is defined and app_prefixes | length > 0
141+
- app_update_hub_role | default(true) | bool
142+
143+
- name: Debug - Hub role policy change status
144+
ansible.builtin.debug:
145+
msg: "Hub role policies changed: {{ _hub_role_policies_changed }} (current: {{ _current_policies | sort }}, merged: {{ _merged_policies | sort }})"
146+
verbosity: 1
147+
when:
148+
- app_prefixes is defined and app_prefixes | length > 0
149+
- app_update_hub_role | default(true) | bool
150+
151+
# Update hub-role only if policies have changed
152+
- name: Update hub-role with app policies
153+
kubernetes.core.k8s_exec:
154+
namespace: "{{ vault_ns }}"
155+
pod: "{{ vault_pod }}"
156+
command: >
157+
vault write auth/{{ vault_hub }}/role/{{ vault_hub }}-role
158+
bound_service_account_names="{{ active_external_secrets_sa | default('golang-external-secrets') }}"
159+
bound_service_account_namespaces="{{ active_external_secrets_ns | default('golang-external-secrets') }}"
160+
policies="{{ _merged_policies | join(',') }}"
161+
ttl="{{ vault_hub_ttl }}"
162+
when:
163+
- app_prefixes is defined and app_prefixes | length > 0
164+
- app_update_hub_role | default(true) | bool
165+
- _hub_role_policies_changed | bool
166+
167+
- name: Display updated hub-role policies
168+
ansible.builtin.debug:
169+
msg: "hub-role policies updated to: {{ _merged_policies | join(', ') }}"
170+
verbosity: 1
171+
when:
172+
- app_prefixes is defined and app_prefixes | length > 0
173+
- app_update_hub_role | default(true) | bool
174+
- _hub_role_policies_changed | bool
175+
176+
# Optionally create JWT roles for app-level isolation
177+
# Only creates roles for entries that have jwt_role defined
178+
- name: Configure JWT role for app (if configured)
179+
kubernetes.core.k8s_exec:
180+
namespace: "{{ vault_ns }}"
181+
pod: "{{ vault_pod }}"
182+
command: >
183+
vault write auth/"{{ vault_hub }}"/role/"{{ _app_name }}"-role
184+
bound_service_account_names="{{ item.jwt_role.service_account_names }}"
185+
bound_service_account_namespaces="{{ item.jwt_role.service_account_namespaces }}"
186+
policies="default,{{ vault_global_policy }}-secret,{{ item.prefix | replace('/', '-') }}-k8s-secret"
187+
ttl="{{ item.jwt_role.ttl | default(vault_hub_ttl) }}"
188+
loop: "{{ app_prefixes }}"
189+
loop_control:
190+
label: "Creating JWT role for {{ _app_name }}"
191+
vars:
192+
_app_name: "{{ item.prefix | basename }}"
193+
when:
194+
- app_prefixes is defined and app_prefixes | length > 0
195+
- app_create_jwt_roles | default(false) | bool
196+
- item.jwt_role is defined

roles/vault_utils/tasks/vault_jwt.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,21 @@
102102
not jwt_config_oidc_discovery_url == oidc_discovery_url or
103103
not jwt_config_default_role == default_role | default('default')
104104

105+
# Create JWT auth policies from vault_jwt_policies variable
106+
# These are manually defined policies for SPIFFE workloads that need direct Vault access
107+
- name: Write JWT policies directly to Vault
108+
kubernetes.core.k8s_exec:
109+
namespace: "{{ vault_ns }}"
110+
pod: "{{ vault_pod }}"
111+
command: |
112+
bash -e -c 'cat <<EOF | vault policy write {{ item.name }} -
113+
{{ item.policy }}
114+
EOF'
115+
loop: "{{ vault_jwt_policies | default([]) }}"
116+
loop_control:
117+
label: "Writing JWT policy {{ item.name }}"
118+
when: vault_jwt_policies is defined and vault_jwt_policies | length > 0
119+
105120
- name: Run JWT role tasks
106121
ansible.builtin.include_tasks: vault_jwt_roles.yaml
107122
loop: "{{ vault_jwt_roles | default([]) }}"

roles/vault_utils/tasks/vault_secrets_init.yaml

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,51 @@
130130
pod: "{{ vault_pod }}"
131131
command: "vault policy write {{ vault_hub }}-secret /tmp/policy-{{ vault_hub }}.hcl"
132132

133-
- name: Configure kubernetes role for hub
133+
# Get current hub-role policies to preserve any custom policies added by patterns
134+
- name: Get current hub-role policies
135+
kubernetes.core.k8s_exec:
136+
namespace: "{{ vault_ns }}"
137+
pod: "{{ vault_pod }}"
138+
command: >
139+
vault read -format=json auth/{{ vault_hub }}/role/{{ vault_hub }}-role
140+
register: _hub_role_result
141+
failed_when: false
142+
changed_when: false
143+
144+
# Get existing policies (empty list if role doesn't exist yet)
145+
- name: Set current policies fact from existing hub-role
146+
ansible.builtin.set_fact:
147+
_current_hub_policies: "{{ (_hub_role_result.stdout | from_json).data.token_policies | default([]) }}"
148+
when: _hub_role_result.rc == 0
149+
150+
- name: Set empty current policies if hub-role not found
151+
ansible.builtin.set_fact:
152+
_current_hub_policies: []
153+
when: _hub_role_result.rc != 0
154+
155+
# Merge existing policies with defaults (preserves custom policies, ensures defaults present)
156+
- name: Merge hub-role policies
157+
ansible.builtin.set_fact:
158+
_merged_hub_policies: "{{ (_current_hub_policies + vault_hub_role_default_policies) | unique }}"
159+
160+
# Determine if we need to update the hub-role
161+
- name: Check if hub-role policies need updating
162+
ansible.builtin.set_fact:
163+
_hub_role_needs_update: "{{ (_hub_role_result.rc != 0) or (_current_hub_policies | sort != _merged_hub_policies | sort) }}"
164+
165+
- name: Debug - Hub role update status
166+
ansible.builtin.debug:
167+
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 }})"
168+
verbosity: 1
169+
170+
- name: Configure kubernetes role for hub with merged policies
134171
kubernetes.core.k8s_exec:
135172
namespace: "{{ vault_ns }}"
136173
pod: "{{ vault_pod }}"
137174
command: >
138175
vault write auth/"{{ vault_hub }}"/role/"{{ vault_hub }}"-role
139176
bound_service_account_names="{{ active_external_secrets_sa }}"
140177
bound_service_account_namespaces="{{ active_external_secrets_ns }}"
141-
policies="default,{{ vault_global_policy }}-secret,{{ vault_pushsecrets_policy }}-secret,{{ vault_hub }}-secret" ttl="{{ vault_hub_ttl }}"
178+
policies="{{ _merged_hub_policies | join(',') }}"
179+
ttl="{{ vault_hub_ttl }}"
180+
when: _hub_role_needs_update | bool

0 commit comments

Comments
 (0)