Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
16 changes: 16 additions & 0 deletions plugins/module_utils/parse_secrets_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -539,3 +539,19 @@ def _inject_field(self, secret_name, f):
self.parsed_secrets[secret_name]["fields"][f["name"]] = secret

return

def get_unique_vault_prefixes(self):
"""
Extract all unique vault prefixes from parsed secrets.

This is useful for creating fine-grained Vault policies for each
unique prefix path (e.g., apps/qtodo, hub/infra/keycloak).

Returns:
list: Sorted list of unique vault prefixes
"""
prefixes = set()
for secret in self.parsed_secrets.values():
for prefix in secret.get("vault_prefixes", []):
prefixes.add(prefix)
return sorted(list(prefixes))
1 change: 1 addition & 0 deletions plugins/modules/parse_secrets_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ def run(module):
results["parsed_secrets"] = parsed_secret_obj.parsed_secrets
results["kubernetes_secret_objects"] = parsed_secret_obj.kubernetes_secret_objects
results["secret_store_namespace"] = parsed_secret_obj.secret_store_namespace
results["unique_vault_prefixes"] = parsed_secret_obj.get_unique_vault_prefixes()

module.exit_json(**results)

Expand Down
152 changes: 152 additions & 0 deletions roles/vault_utils/tasks/vault_app_policies.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
---
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When are these tasks executed? I haven't been able to find the include

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad. I only included this file in my local testing ansible tasks which is outside of this repo. Now I have this file integrated into the push_parsed_secrets.yaml flow. The task file is automatically included when processing secrets with custom vaultPrefix values.

# vault_app_policies.yaml
# Creates fine-grained Vault policies for application-level isolation
#
# This task file creates individual policies for each vaultPrefix provided.
# Each application gets its own policy that only allows access to its
# specific path, enabling application-level secret isolation.
#
# Required variables:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add these variables and their descriptions to defaults/main.yml

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added all app policy variables to defaults/main.yml.

# app_prefixes: List of app prefixes to create policies for
# e.g., ["apps/qtodo", "hub/infra/keycloak"]
#
# Optional variables:
# app_capabilities: Capabilities for app policies (default: read)
# app_update_hub_role: Whether to update hub-role with new policies (default: true)
# app_create_jwt_roles: Whether to create JWT roles per app (default: false)
# app_jwt_role_config: Dict mapping app names to JWT role config
#
# Example usage:
# - name: Create app-specific vault policies
# ansible.builtin.include_tasks:
# file: vault_app_policies.yaml
# vars:
# app_prefixes:
# - apps/qtodo
# - hub/infra/keycloak
# app_update_hub_role: true

- name: Debug - Show app prefixes to process
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that these debug messages are useful during the development phase, do we need them during the execution phase?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! Added verbosity: 1 to the debug tasks so they only appear when running with -v flag. This keeps normal execution outoput clean while still providing troubleshooting info when needed.

ansible.builtin.debug:
msg: "Processing app prefixes: {{ app_prefixes }}"
when: app_prefixes is defined and app_prefixes | length > 0

- name: Skip if no app prefixes defined
ansible.builtin.debug:
msg: "No app_prefixes defined, skipping app policy creation"
when: app_prefixes is not defined or app_prefixes | length == 0

# Create policy template for each app prefix
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of creating a file with an echo, let's use the k8s_cp, copy or template module if possible

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored to pipe policy content directly to vault policy write via stdin, avoiding temp file creation. This follows the same pattern used in vault_jwt.yaml. Using k8s_cp would require creating local temp files and add complexity, the stdin approach is cleaner and consistent with existing code.

- name: Configure app policy template
kubernetes.core.k8s_exec:
namespace: "{{ vault_ns }}"
pod: "{{ vault_pod }}"
command: >
bash -e -c "echo 'path \"secret/data/{{ item }}/*\" { capabilities = [\"read\"] }' > /tmp/policy-{{ item | replace('/', '-') }}.hcl"
loop: "{{ app_prefixes }}"
loop_control:
label: "Creating policy template for {{ item }}"
when: app_prefixes is defined and app_prefixes | length > 0

# Write the policy to Vault
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're writing policies whether they exist or not. One of Ansible's characteristics is idempotence. It should only run when a change is needed. Existing policies should be validated first, and then, based on the current state, new policies should be written.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added idempotency check. The task now queries existing Vault policies first (vault polich list) and only creates policies that don't already exist, using ansible's difference filter to compare.

- name: Configure app policy in Vault
kubernetes.core.k8s_exec:
namespace: "{{ vault_ns }}"
pod: "{{ vault_pod }}"
command: >
vault policy write {{ item | replace('/', '-') }}-k8s-secret
/tmp/policy-{{ item | replace('/', '-') }}.hcl
loop: "{{ app_prefixes }}"
loop_control:
label: "Writing policy {{ item | replace('/', '-') }}-k8s-secret"
when: app_prefixes is defined and app_prefixes | length > 0

# Build list of new policy names
- name: Build app policy names list
ansible.builtin.set_fact:
_app_policy_names: "{{ app_prefixes | map('replace', '/', '-') | map('regex_replace', '$', '-k8s-secret') | list }}"
when: app_prefixes is defined and app_prefixes | length > 0

# Get current hub-role policies
- name: Get current hub-role policies
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_result
failed_when: false
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a command that doesn't change anything (it only reads), we might want to add changed_when: false to it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added changed_when: false in this task.

when:
- app_prefixes is defined and app_prefixes | length > 0
- app_update_hub_role | default(true) | bool

# Parse current policies and merge with new ones
- name: Set current policies fact
ansible.builtin.set_fact:
_current_policies: "{{ (_hub_role_result.stdout | from_json).data.token_policies | default(['default', vault_global_policy ~ '-secret', vault_pushsecrets_policy ~ '-secret', vault_hub ~ '-secret']) }}"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of hardcoding the default values ​​manually on this line, it's better to put them in a variable in defaults/main.yaml and retrieve them from there.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Moved the default policies to defaults/main.yml as vault_hub_role_default_policies and updated the task to reference it.

when:
- app_prefixes is defined and app_prefixes | length > 0
- app_update_hub_role | default(true) | bool
- _hub_role_result.rc == 0

- name: Set default policies if hub-role not found
ansible.builtin.set_fact:
_current_policies: "{{ ['default', vault_global_policy ~ '-secret', vault_pushsecrets_policy ~ '-secret', vault_hub ~ '-secret'] }}"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move this to a variable in defaults/main.yml

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done: both instances now reference vault_hub_role_default_policies from defautls/main.yml

when:
- app_prefixes is defined and app_prefixes | length > 0
- app_update_hub_role | default(true) | bool
- _hub_role_result.rc != 0

# Merge current and new policies (removing duplicates)
- name: Merge policies
ansible.builtin.set_fact:
_merged_policies: "{{ (_current_policies + _app_policy_names) | unique }}"
when:
- app_prefixes is defined and app_prefixes | length > 0
- app_update_hub_role | default(true) | bool

# Update hub-role with merged policies
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We execute this unconditionally, whether the policies are already configured or not?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added idempotency check. The hub-role is now only updated whent he merged policies differ from the current policies. Sorted comparison is used to handle order differences.

- 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 }}"
when:
- app_prefixes is defined and app_prefixes | length > 0
- app_update_hub_role | default(true) | bool

- name: Display updated hub-role policies
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug messages are useful during development, but we should remove it in execution

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug messages are now gated with verbosity: 1.

ansible.builtin.debug:
msg: "hub-role policies updated to: {{ _merged_policies | join(', ') }}"
when:
- app_prefixes is defined and app_prefixes | length > 0
- app_update_hub_role | default(true) | bool

# Optionally create JWT roles for app-level isolation
# This requires app_jwt_role_config to be defined
- name: Configure JWT role for app (if configured)
kubernetes.core.k8s_exec:
namespace: "{{ vault_ns }}"
pod: "{{ vault_pod }}"
command: >
vault write auth/"{{ vault_hub }}"/role/"{{ _app_name }}"-role
bound_service_account_names="{{ _role_config.service_account_names }}"
bound_service_account_namespaces="{{ _role_config.service_account_namespaces }}"
policies="default,{{ vault_global_policy }}-secret,{{ item | replace('/', '-') }}-k8s-secret"
ttl="{{ _role_config.ttl | default(vault_hub_ttl) }}"
loop: "{{ app_prefixes }}"
loop_control:
label: "Creating JWT role for {{ _app_name }}"
vars:
_app_name: "{{ item | basename }}"
_role_config: "{{ app_jwt_role_config[_app_name] | default({}) }}"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of using an alternative dictionary with role configuration values ​​and a key that matches the app name, wouldn't it be better to have app_prefixes be a list with dictionaries and their related properties in the same object?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored app_prefixes to use a unified dict structure with prefix and optional jwt_role in the same object.

when:
- app_prefixes is defined and app_prefixes | length > 0
- app_create_jwt_roles | default(false) | bool
- app_jwt_role_config is defined
- _app_name in app_jwt_role_config
13 changes: 13 additions & 0 deletions roles/vault_utils/tasks/vault_jwt.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,19 @@
not jwt_config_oidc_discovery_url == oidc_discovery_url or
not jwt_config_default_role == default_role | default('default')

# Create JWT auth policies from vault_jwt_policies variable
# These are manually defined policies for SPIFFE workloads that need direct Vault access
- name: Write JWT policies directly to Vault
kubernetes.core.k8s_exec:
namespace: "{{ vault_ns }}"
pod: "{{ vault_pod }}"
command: >
bash -e -c "echo '{{ item.policy | b64encode }}' | base64 -d | vault policy write {{ item.name }} -"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I imagine this base64 encoding and decoding issue is due to escape characters, but we should be able to solve it with escape characters or by creating HCL files as is done in vault_app_policies.yaml or with the global policy

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! Replaced the base64 encode/decode with a heredoc which handles escape characters naturally and is more readable.

loop: "{{ vault_jwt_policies | default([]) }}"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition to adding the variable vault_jwt_policies here, let's add it to defaults/main.yml so that anyone reading that file is aware of the variable's existence and can override it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added vault_jwt_policies: [] to defaults/main.yml.

loop_control:
label: "Writing JWT policy {{ item.name }}"
when: vault_jwt_policies is defined and vault_jwt_policies | length > 0

- name: Run JWT role tasks
ansible.builtin.include_tasks: vault_jwt_roles.yaml
loop: "{{ vault_jwt_roles | default([]) }}"
Expand Down
40 changes: 38 additions & 2 deletions roles/vault_utils/tasks/vault_secrets_init.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,48 @@
pod: "{{ vault_pod }}"
command: "vault policy write {{ vault_hub }}-secret /tmp/policy-{{ vault_hub }}.hcl"

- name: Configure kubernetes role for hub
# Get current hub-role policies to preserve any custom policies added by patterns
- name: Get current hub-role policies
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_result
failed_when: false
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
failed_when: false
failed_when: false
changed_when: false


# Define the default policies that VP framework requires
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd move this to the role's defaults file (defaults/main.yml). The fact name reflects it's a default value and it makes more sense if this values, which may be modified in future, to reside in the defaults file rather than a task file.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved the default policies to defaults/main.yml

- name: Set default hub-role policies list
ansible.builtin.set_fact:
_default_hub_policies:
- default
- "{{ vault_global_policy }}-secret"
- "{{ vault_pushsecrets_policy }}-secret"
- "{{ vault_hub }}-secret"

# 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([]) }}"
when: _hub_role_result.rc == 0

- name: Set empty current policies if hub-role not found
ansible.builtin.set_fact:
_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 + _default_hub_policies) | unique }}"

- name: Configure kubernetes role for hub with merged policies
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're going to check existing policies, we could take advantage of this and add a conditional to this execution and only run the command when the policies have changed or the role is created for the first time.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The task checks _hub_role_needs_update which is true only when the role doesn't exist or policies have changed.

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="default,{{ vault_global_policy }}-secret,{{ vault_pushsecrets_policy }}-secret,{{ vault_hub }}-secret" ttl="{{ vault_hub_ttl }}"
policies="{{ _merged_hub_policies | join(',') }}"
ttl="{{ vault_hub_ttl }}"