From a790f2ec263ba65f53284a1fa6fc60596edaeeec Mon Sep 17 00:00:00 2001 From: Ade Lee Date: Tue, 27 Oct 2026 16:11:52 -0400 Subject: [PATCH 1/3] [hooks] Add Barbican secret-store migrate live test hook Tempest cannot prove a secret changed backends. This hook discovers enabled stores via openstacksdk (catalogue UUIDs required by the migrate API), creates a secret on the global default, and migrates it to each other store with OSC and barbican-manage secret migrate, asserting secret_store_id via GET metadata (microversion 1.3). Coverage per destination: OSC round-trip, barbican-manage round-trip, dry-run; metadata-only conflict once. Related-Issue: #OSPRH-35280 Assisted-by: Composer --- .../barbican-secret-store-migrate/admin.yml | 30 ++++ .../barbican-secret-store-migrate/assert.yml | 35 ++++ .../metaonly.yml | 79 +++++++++ .../barbican-secret-store-migrate/migrate.yml | 152 ++++++++++++++++++ .../barbican-secret-store-migrate/move.yml | 21 +++ .../barbican-secret-store-migrate/pair.yml | 36 +++++ .../scenario.yml | 107 ++++++++++++ 7 files changed, 460 insertions(+) create mode 100644 hooks/playbooks/barbican-secret-store-migrate/admin.yml create mode 100644 hooks/playbooks/barbican-secret-store-migrate/assert.yml create mode 100644 hooks/playbooks/barbican-secret-store-migrate/metaonly.yml create mode 100644 hooks/playbooks/barbican-secret-store-migrate/migrate.yml create mode 100644 hooks/playbooks/barbican-secret-store-migrate/move.yml create mode 100644 hooks/playbooks/barbican-secret-store-migrate/pair.yml create mode 100644 hooks/playbooks/barbican-secret-store-migrate/scenario.yml diff --git a/hooks/playbooks/barbican-secret-store-migrate/admin.yml b/hooks/playbooks/barbican-secret-store-migrate/admin.yml new file mode 100644 index 000000000..3a5938592 --- /dev/null +++ b/hooks/playbooks/barbican-secret-store-migrate/admin.yml @@ -0,0 +1,30 @@ +--- +# Run barbican-manage secret migrate in the API pod. +# +# Operator CLI: reads barbican.conf / DB in-process and calls +# rewrap_secret. No Keystone token or HTTP to Barbican. +# +# Expects: cifmw_barbican_migrate_api_exec, *_admin_store_id, +# *_admin_secret_id, optional *_admin_dry_run. + +- name: "barbican-manage secret migrate ({{ cifmw_barbican_migrate_case }})" + ansible.builtin.command: + argv: "{{ _manage_argv }}" + vars: + _manage_argv: "{{ + cifmw_barbican_migrate_api_exec + + [ + 'barbican-manage', 'secret', 'migrate', + '--dest-store-id', cifmw_barbican_migrate_admin_store_id, + '--secret-id', cifmw_barbican_migrate_admin_secret_id, + '--error-file', + '/tmp/cifmw-barbican-manage-secret-migrate-errors.jsonl', + ] + + ( + ['--dry-run'] + if (cifmw_barbican_migrate_admin_dry_run | default(false) | bool) + else [] + ) + }}" + register: _migrate_admin + changed_when: false diff --git a/hooks/playbooks/barbican-secret-store-migrate/assert.yml b/hooks/playbooks/barbican-secret-store-migrate/assert.yml new file mode 100644 index 000000000..49ea797aa --- /dev/null +++ b/hooks/playbooks/barbican-secret-store-migrate/assert.yml @@ -0,0 +1,35 @@ +--- +# Assert a secret is on the expected Barbican secret-store catalogue entry. +# +# Microversion 1.3 secret metadata always includes secret_store_id and +# secret_store_ref (UUID/href, or null when there is no live payload). +# Use OSC — the patched client negotiates key-manager 1.3. +# +# Expects: +# cifmw_barbican_migrate_assert_label +# cifmw_barbican_migrate_assert_secret_id +# cifmw_barbican_migrate_assert_store (id, store_plugin, crypto_plugin) +# cifmw_barbican_migrate_osc_exec + +- name: "{{ cifmw_barbican_migrate_assert_label }}: get secret metadata" + ansible.builtin.command: + argv: "{{ cifmw_barbican_migrate_osc_exec + [ + 'openstack', 'secret', 'get', + cifmw_barbican_migrate_assert_secret_id, + '-f', 'json', + ] }}" + register: _assert_secret_get + changed_when: false + no_log: true + +- name: "{{ cifmw_barbican_migrate_assert_label }}: assert secret_store_id" + ansible.builtin.assert: + that: + - "'Secret store id' in (_assert_secret_get.stdout | from_json)" + - "'Secret store href' in (_assert_secret_get.stdout | from_json)" + - (_assert_secret_get.stdout | from_json)['Secret store id'] == cifmw_barbican_migrate_assert_store.id + - cifmw_barbican_migrate_assert_store.id in ((_assert_secret_get.stdout | from_json)['Secret store href'] | default('')) + fail_msg: >- + {{ cifmw_barbican_migrate_assert_label }}: expected secret_store_id + '{{ cifmw_barbican_migrate_assert_store.id }}', got + '{{ (_assert_secret_get.stdout | from_json)['Secret store id'] | default('') }}'. diff --git a/hooks/playbooks/barbican-secret-store-migrate/metaonly.yml b/hooks/playbooks/barbican-secret-store-migrate/metaonly.yml new file mode 100644 index 000000000..2a717e716 --- /dev/null +++ b/hooks/playbooks/barbican-secret-store-migrate/metaonly.yml @@ -0,0 +1,79 @@ +--- +# Metadata-only secrets have no payload and must fail migrate (HTTP 400). + +- name: Metadata-only migrate must conflict + block: + - name: Create metadata-only secret + ansible.builtin.command: + argv: "{{ cifmw_barbican_migrate_osc_exec + [ + 'openstack', 'secret', 'store', + '--name', 'ci-migrate-metaonly', + '-f', 'value', '-c', 'Secret href', + ] }}" + register: _meta_create + changed_when: false + no_log: true + + - name: Parse metadata-only secret + ansible.builtin.set_fact: + _meta_secret_href: "{{ _meta_create.stdout | trim }}" + + - name: Migrate metadata-only secret (expect conflict) + ansible.builtin.command: + argv: "{{ cifmw_barbican_migrate_osc_exec + [ + 'openstack', 'secret', 'migrate', + _meta_secret_href, + '--secret-store', cifmw_barbican_migrate_dst_store.id, + ] }}" + register: _meta_migrate + changed_when: false + failed_when: false + + - name: Assert metadata-only migrate failed + ansible.builtin.assert: + that: + - _meta_migrate.rc != 0 + - >- + ('400' in (_meta_migrate.stderr | default(''))) + or ('Bad Request' in (_meta_migrate.stderr | default(''))) + or ('no stored payload' in (_meta_migrate.stderr | default(''))) + or ('400' in (_meta_migrate.stdout | default(''))) + or ('Bad Request' in (_meta_migrate.stdout | default(''))) + or ('no stored payload' in (_meta_migrate.stdout | default(''))) + fail_msg: >- + Metadata-only migrate should fail with HTTP 400 / Bad Request. + rc={{ _meta_migrate.rc }} + stderr={{ _meta_migrate.stderr | default('') }} + + - name: Get metadata-only secret (expect null secret_store_id) + when: _meta_secret_href is defined + ansible.builtin.command: + argv: "{{ cifmw_barbican_migrate_osc_exec + [ + 'openstack', 'secret', 'get', + _meta_secret_href, '-f', 'json', + ] }}" + register: _meta_get + changed_when: false + no_log: true + + - name: Assert metadata-only secret has null store fields + when: _meta_get is defined and _meta_get is not skipped + ansible.builtin.assert: + that: + - "'Secret store id' in (_meta_get.stdout | from_json)" + - "'Secret store href' in (_meta_get.stdout | from_json)" + - (_meta_get.stdout | from_json)['Secret store id'] is none + - (_meta_get.stdout | from_json)['Secret store href'] is none + fail_msg: >- + Metadata-only secret should include Secret store id/href as null. + body={{ _meta_get.stdout | default('') }} + always: + - name: Delete metadata-only secret + when: _meta_secret_href is defined + ansible.builtin.command: + argv: "{{ cifmw_barbican_migrate_osc_exec + [ + 'openstack', 'secret', 'delete', _meta_secret_href, + ] }}" + changed_when: false + failed_when: false + no_log: true diff --git a/hooks/playbooks/barbican-secret-store-migrate/migrate.yml b/hooks/playbooks/barbican-secret-store-migrate/migrate.yml new file mode 100644 index 000000000..2ed593840 --- /dev/null +++ b/hooks/playbooks/barbican-secret-store-migrate/migrate.yml @@ -0,0 +1,152 @@ +--- +# Live-migrate Barbican secrets across every enabled secret store. +# +# Store set and catalogue UUIDs come from GET /v1/secret-stores +# (openstacksdk in the openstackclient pod) — the same ids the migrate +# API and microversion 1.3 secret_store_id assert require. Creates on +# the global-default store (OSC has no preferred-store command), then +# for each other store runs: OSC round-trip, barbican-manage secret +# migrate round-trip, dry-run, and (once) a metadata-only conflict +# against the first destination. +# +# Optional: cifmw_barbican_migrate_secret_type (e.g. symmetric) is +# passed through to openstack secret store when set. +# +# Wire as post_tests (after tempest on the default store): +# +# post_tests: +# - name: 90 Barbican secret-store migrate +# type: playbook +# source: "{{ ci_framework_base_src_dir }}/hooks/playbooks/barbican-secret-store-migrate/migrate.yml" +# +# Related: OSPRH-35280 / OSPRH-35261 / RHOSSTRAT-1435 + +- name: Test Barbican secret-store migrate via GET secret_store_id + hosts: "{{ cifmw_target_hook_host | default('localhost') }}" + gather_facts: false + vars: + cifmw_barbican_migrate_namespace: "{{ namespace | default(cifmw_openstack_namespace) | default('openstack') }}" + cifmw_barbican_migrate_osc_pod_name: openstackclient + cifmw_barbican_migrate_api_label: component=barbican-api + cifmw_barbican_migrate_api_container: barbican-api + _oc_env: + KUBECONFIG: "{{ cifmw_openshift_kubeconfig }}" + PATH: "{{ cifmw_path }}" + environment: "{{ _oc_env }}" + tasks: + - name: Get openstackclient pod + kubernetes.core.k8s_info: + kubeconfig: "{{ cifmw_openshift_kubeconfig }}" + api_key: "{{ cifmw_openshift_token | default(omit) }}" + context: "{{ cifmw_openshift_context | default(omit) }}" + api_version: v1 + kind: Pod + namespace: "{{ cifmw_barbican_migrate_namespace }}" + name: "{{ cifmw_barbican_migrate_osc_pod_name }}" + register: _osc_pod_info + + - name: Get Barbican API pods + kubernetes.core.k8s_info: + kubeconfig: "{{ cifmw_openshift_kubeconfig }}" + api_key: "{{ cifmw_openshift_token | default(omit) }}" + context: "{{ cifmw_openshift_context | default(omit) }}" + api_version: v1 + kind: Pod + namespace: "{{ cifmw_barbican_migrate_namespace }}" + label_selectors: + - "{{ cifmw_barbican_migrate_api_label }}" + field_selectors: + - status.phase=Running + register: _barbican_api_pod_info + + - name: Fail if required pods are missing + ansible.builtin.assert: + that: + - _osc_pod_info.resources | length > 0 + - _barbican_api_pod_info.resources | length > 0 + fail_msg: >- + Need Running openstackclient and a pod labelled + {{ cifmw_barbican_migrate_api_label }} in + {{ cifmw_barbican_migrate_namespace }}. + + - name: Set pod names and shared oc-exec argv prefixes + ansible.builtin.set_fact: + cifmw_barbican_migrate_osc_pod: "{{ _osc_pod_info.resources[0].metadata.name }}" + cifmw_barbican_migrate_api_pod: "{{ _barbican_api_pod_info.resources[0].metadata.name }}" + cifmw_barbican_migrate_osc_exec: "{{ [ + 'oc', '-n', cifmw_barbican_migrate_namespace, + 'exec', _osc_pod_info.resources[0].metadata.name, '--', + ] }}" + cifmw_barbican_migrate_api_exec: "{{ [ + 'oc', '-n', cifmw_barbican_migrate_namespace, + 'exec', _barbican_api_pod_info.resources[0].metadata.name, + '-c', cifmw_barbican_migrate_api_container, '--', + ] }}" + + # OSC has no secret-store list command; use openstacksdk in the OSC pod. + - name: List secret stores via openstacksdk + ansible.builtin.command: + argv: "{{ cifmw_barbican_migrate_osc_exec + [ + 'python3', '-c', _list_stores_py, + ] }}" + vars: + _list_stores_py: | + import json + import openstack + conn = openstack.connect() + stores = [] + for s in conn.key_manager.secret_stores(): + stores.append({ + 'id': s.id, + 'store_plugin': s.secret_store_plugin or '', + 'crypto_plugin': s.crypto_plugin or '', + 'global_default': bool(s.global_default), + }) + print(json.dumps(stores)) + register: _stores_raw + changed_when: false + no_log: true + + - name: Parse secret stores + ansible.builtin.set_fact: + _secret_stores: "{{ _stores_raw.stdout | from_json }}" + + - name: Require at least two stores and one global default + vars: + _defaults: "{{ _secret_stores | selectattr('global_default', 'in', [true, True, 'True', 'true', 1, '1']) | list }}" + ansible.builtin.assert: + that: + - _secret_stores | length >= 2 + - _defaults | length == 1 + fail_msg: >- + Need >=2 secret stores and exactly one global_default from + GET /v1/secret-stores (got {{ _secret_stores | length }} stores, + {{ _defaults | length }} defaults). Stores: {{ _secret_stores }} + + - name: Set global-default source and destinations + vars: + _defaults: "{{ _secret_stores | selectattr('global_default', 'in', [true, True, 'True', 'true', 1, '1']) | list }}" + ansible.builtin.set_fact: + _store_src: "{{ _defaults[0] }}" + _store_destinations: "{{ _secret_stores | rejectattr('id', 'equalto', _defaults[0].id) | list }}" + + - name: Choose a payload for this run + ansible.builtin.set_fact: + _payload: "ci-migrate-payload-{{ 999999 | random }}" + + - name: Migrate from global default to each other store + ansible.builtin.include_tasks: pair.yml + loop: "{{ _store_destinations }}" + loop_control: + loop_var: _store_dst + extended: true + label: "{{ _store_src.store_plugin }}/{{ _store_src.crypto_plugin }} → {{ _store_dst.store_plugin }}/{{ _store_dst.crypto_plugin }}" + vars: + cifmw_barbican_migrate_src_store: "{{ _store_src }}" + cifmw_barbican_migrate_dst_store: "{{ _store_dst }}" + # Pass-through; pair/scenario use | default('') — do not self-assign + # (ansible-lint jinja[invalid] recursive loop). + cifmw_barbican_migrate_pair_label: >- + {{ (_store_src.crypto_plugin or _store_src.store_plugin) ~ '_to_' + ~ (_store_dst.crypto_plugin or _store_dst.store_plugin) }} + cifmw_barbican_migrate_run_metaonly: "{{ ansible_loop.first }}" diff --git a/hooks/playbooks/barbican-secret-store-migrate/move.yml b/hooks/playbooks/barbican-secret-store-migrate/move.yml new file mode 100644 index 000000000..77fa5ac44 --- /dev/null +++ b/hooks/playbooks/barbican-secret-store-migrate/move.yml @@ -0,0 +1,21 @@ +--- +# Move _migrate_secret_href/_id onto cifmw_barbican_migrate_target_store +# via OSC or barbican-manage, per cifmw_barbican_migrate_via. + +- name: "OSC migrate ({{ cifmw_barbican_migrate_case }})" + when: cifmw_barbican_migrate_via | default('osc') == 'osc' + ansible.builtin.command: + argv: "{{ cifmw_barbican_migrate_osc_exec + [ + 'openstack', 'secret', 'migrate', + _migrate_secret_href, + '--secret-store', cifmw_barbican_migrate_target_store.id, + ] }}" + changed_when: false + +- name: "barbican-manage migrate ({{ cifmw_barbican_migrate_case }})" + when: cifmw_barbican_migrate_via | default('osc') == 'admin' + ansible.builtin.include_tasks: admin.yml + vars: + cifmw_barbican_migrate_admin_store_id: "{{ cifmw_barbican_migrate_target_store.id }}" + cifmw_barbican_migrate_admin_secret_id: "{{ _migrate_secret_id }}" + cifmw_barbican_migrate_admin_dry_run: "{{ cifmw_barbican_migrate_dry_run | default(false) | bool }}" diff --git a/hooks/playbooks/barbican-secret-store-migrate/pair.yml b/hooks/playbooks/barbican-secret-store-migrate/pair.yml new file mode 100644 index 000000000..62bc6e709 --- /dev/null +++ b/hooks/playbooks/barbican-secret-store-migrate/pair.yml @@ -0,0 +1,36 @@ +--- +# Run migrate scenarios for one (src → dst) pair. +# Expects cifmw_barbican_migrate_src_store / dst_store, _payload, +# cifmw_barbican_migrate_pair_label, cifmw_barbican_migrate_secret_type, +# and cifmw_barbican_migrate_run_metaonly (metaonly once across all pairs). + +- name: "Round-trip migrate via OSC ({{ cifmw_barbican_migrate_pair_label }})" + ansible.builtin.include_tasks: scenario.yml + vars: + cifmw_barbican_migrate_case: "osc_roundtrip_{{ cifmw_barbican_migrate_pair_label }}" + cifmw_barbican_migrate_via: osc + cifmw_barbican_migrate_roundtrip: true + cifmw_barbican_migrate_dry_run: false + cifmw_barbican_migrate_payload: "{{ _payload }}-osc-{{ cifmw_barbican_migrate_pair_label }}" + +- name: "Round-trip migrate via barbican-manage ({{ cifmw_barbican_migrate_pair_label }})" + ansible.builtin.include_tasks: scenario.yml + vars: + cifmw_barbican_migrate_case: "admin_roundtrip_{{ cifmw_barbican_migrate_pair_label }}" + cifmw_barbican_migrate_via: admin + cifmw_barbican_migrate_roundtrip: true + cifmw_barbican_migrate_dry_run: false + cifmw_barbican_migrate_payload: "{{ _payload }}-admin-{{ cifmw_barbican_migrate_pair_label }}" + +- name: "Dry-run barbican-manage secret migrate ({{ cifmw_barbican_migrate_pair_label }})" + ansible.builtin.include_tasks: scenario.yml + vars: + cifmw_barbican_migrate_case: "admin_dry_run_{{ cifmw_barbican_migrate_pair_label }}" + cifmw_barbican_migrate_via: admin + cifmw_barbican_migrate_roundtrip: false + cifmw_barbican_migrate_dry_run: true + cifmw_barbican_migrate_payload: "{{ _payload }}-dry-{{ cifmw_barbican_migrate_pair_label }}" + +- name: Metadata-only secret must fail migrate + when: cifmw_barbican_migrate_run_metaonly | default(false) | bool + ansible.builtin.include_tasks: metaonly.yml diff --git a/hooks/playbooks/barbican-secret-store-migrate/scenario.yml b/hooks/playbooks/barbican-secret-store-migrate/scenario.yml new file mode 100644 index 000000000..4e3d8cdf4 --- /dev/null +++ b/hooks/playbooks/barbican-secret-store-migrate/scenario.yml @@ -0,0 +1,107 @@ +--- +# One migrate scenario: create on src, migrate (and optional round-trip), +# assert secret_store_id, decrypt, delete. +# +# cifmw_barbican_migrate_via: osc | admin +# cifmw_barbican_migrate_dry_run: admin only; no rewrap +# cifmw_barbican_migrate_roundtrip: migrate back to src +# +# Expects migrate.yml facts (osc_exec, src/dst stores, …). + +- name: "{{ cifmw_barbican_migrate_case }} migrate scenario" + block: + - name: "Create secret for {{ cifmw_barbican_migrate_case }}" + ansible.builtin.command: + argv: "{{ _create_argv }}" + vars: + _create_argv: "{{ + cifmw_barbican_migrate_osc_exec + + [ + 'openstack', 'secret', 'store', + '--name', 'ci-migrate-' ~ cifmw_barbican_migrate_case, + '--payload', cifmw_barbican_migrate_payload, + '--payload-content-type', 'text/plain', + ] + + ( + ['--secret-type', cifmw_barbican_migrate_secret_type] + if (cifmw_barbican_migrate_secret_type | default('') | length) > 0 + else [] + ) + + ['-f', 'value', '-c', 'Secret href'] + }}" + register: _create_secret + changed_when: false + no_log: true + + - name: "Parse created secret for {{ cifmw_barbican_migrate_case }}" + ansible.builtin.set_fact: + _migrate_secret_href: "{{ _create_secret.stdout | trim }}" + _migrate_secret_id: "{{ _create_secret.stdout | trim | regex_replace('.*/', '') }}" + + - name: "Assert source store before migrate ({{ cifmw_barbican_migrate_case }})" + ansible.builtin.include_tasks: assert.yml + vars: + cifmw_barbican_migrate_assert_label: "{{ cifmw_barbican_migrate_case }} before" + cifmw_barbican_migrate_assert_secret_id: "{{ _migrate_secret_id }}" + cifmw_barbican_migrate_assert_store: "{{ cifmw_barbican_migrate_src_store }}" + + - name: "Migrate to destination ({{ cifmw_barbican_migrate_case }})" + ansible.builtin.include_tasks: move.yml + vars: + cifmw_barbican_migrate_target_store: "{{ cifmw_barbican_migrate_dst_store }}" + + - name: "Assert store after migrate ({{ cifmw_barbican_migrate_case }})" + ansible.builtin.include_tasks: assert.yml + vars: + cifmw_barbican_migrate_assert_label: "{{ cifmw_barbican_migrate_case }} after" + cifmw_barbican_migrate_assert_secret_id: "{{ _migrate_secret_id }}" + cifmw_barbican_migrate_assert_store: >- + {{ cifmw_barbican_migrate_src_store + if (cifmw_barbican_migrate_dry_run | default(false) | bool) + else cifmw_barbican_migrate_dst_store }} + + - name: "Migrate back to source ({{ cifmw_barbican_migrate_case }})" + when: + - cifmw_barbican_migrate_roundtrip | default(false) | bool + - not (cifmw_barbican_migrate_dry_run | default(false) | bool) + ansible.builtin.include_tasks: move.yml + vars: + cifmw_barbican_migrate_target_store: "{{ cifmw_barbican_migrate_src_store }}" + cifmw_barbican_migrate_dry_run: false + + - name: "Assert source store after roundtrip ({{ cifmw_barbican_migrate_case }})" + when: + - cifmw_barbican_migrate_roundtrip | default(false) | bool + - not (cifmw_barbican_migrate_dry_run | default(false) | bool) + ansible.builtin.include_tasks: assert.yml + vars: + cifmw_barbican_migrate_assert_label: "{{ cifmw_barbican_migrate_case }} roundtrip" + cifmw_barbican_migrate_assert_secret_id: "{{ _migrate_secret_id }}" + cifmw_barbican_migrate_assert_store: "{{ cifmw_barbican_migrate_src_store }}" + + - name: "Decrypt payload after migrate ({{ cifmw_barbican_migrate_case }})" + ansible.builtin.command: + argv: "{{ cifmw_barbican_migrate_osc_exec + [ + 'openstack', 'secret', 'get', '--payload', + _migrate_secret_href, '-f', 'value', '-c', 'Payload', + ] }}" + register: _payload_after + changed_when: false + no_log: true + + - name: "Assert payload unchanged ({{ cifmw_barbican_migrate_case }})" + ansible.builtin.assert: + that: + - _payload_after.stdout | trim == cifmw_barbican_migrate_payload + fail_msg: >- + {{ cifmw_barbican_migrate_case }}: payload mismatch after migrate. + always: + - name: "Delete secret for {{ cifmw_barbican_migrate_case }}" + when: _migrate_secret_href is defined + ansible.builtin.command: + argv: "{{ cifmw_barbican_migrate_osc_exec + [ + 'openstack', 'secret', 'delete', _migrate_secret_href, + ] }}" + changed_when: false + failed_when: false + no_log: true From abb450b01c0635354f341931a359c424fa20a160 Mon Sep 17 00:00:00 2001 From: Ade Lee Date: Tue, 27 Oct 2026 16:11:52 -0400 Subject: [PATCH 2/3] [hooks] Add modular Barbican secret-stores enable hook Drive prep/config from a job instance list (id/type/provider) so Fortanix, Luna, Proteccio, and software can be combined without combo-specific playbooks. Default list is software/simple_crypto. Provider settings live on each store entry. Shared resolve_from_param_or_file helper loads HSM PINs from a parameter or materialised file key. Fortanix is cert-only auth with PEMs fetched from lookaside URLs. Enforce operator MaxItems=1 for now. Assisted-by: Composer --- .../assemble_kustomization.yml | 20 +++ .../config/fortanix.yml | 26 ++++ .../barbican-secret-stores/config/luna.yml | 48 ++++++ .../config/proteccio.yml | 47 ++++++ .../config/software.yml | 5 + .../barbican-secret-stores/enable.yml | 143 ++++++++++++++++++ .../prepare/fortanix.yml | 114 ++++++++++++++ .../barbican-secret-stores/prepare/luna.yml | 111 ++++++++++++++ .../prepare/proteccio.yml | 116 ++++++++++++++ .../prepare/software.yml | 5 + .../barbican-secret-stores/process_store.yml | 14 ++ .../resolve_from_param_or_file.yml | 81 ++++++++++ .../93-barbican-secret-stores.yaml.j2 | 39 +++++ .../barbican-secret-stores/validate_store.yml | 80 ++++++++++ 14 files changed, 849 insertions(+) create mode 100644 hooks/playbooks/barbican-secret-stores/assemble_kustomization.yml create mode 100644 hooks/playbooks/barbican-secret-stores/config/fortanix.yml create mode 100644 hooks/playbooks/barbican-secret-stores/config/luna.yml create mode 100644 hooks/playbooks/barbican-secret-stores/config/proteccio.yml create mode 100644 hooks/playbooks/barbican-secret-stores/config/software.yml create mode 100644 hooks/playbooks/barbican-secret-stores/enable.yml create mode 100644 hooks/playbooks/barbican-secret-stores/prepare/fortanix.yml create mode 100644 hooks/playbooks/barbican-secret-stores/prepare/luna.yml create mode 100644 hooks/playbooks/barbican-secret-stores/prepare/proteccio.yml create mode 100644 hooks/playbooks/barbican-secret-stores/prepare/software.yml create mode 100644 hooks/playbooks/barbican-secret-stores/process_store.yml create mode 100644 hooks/playbooks/barbican-secret-stores/resolve_from_param_or_file.yml create mode 100644 hooks/playbooks/barbican-secret-stores/templates/93-barbican-secret-stores.yaml.j2 create mode 100644 hooks/playbooks/barbican-secret-stores/validate_store.yml diff --git a/hooks/playbooks/barbican-secret-stores/assemble_kustomization.yml b/hooks/playbooks/barbican-secret-stores/assemble_kustomization.yml new file mode 100644 index 000000000..546d1d6bd --- /dev/null +++ b/hooks/playbooks/barbican-secret-stores/assemble_kustomization.yml @@ -0,0 +1,20 @@ +--- +# Merge collected fragments into one OpenStackControlPlane kustomization. +# Operator CRD uses store *types* for enabledSecretStores / globalDefault; +# instance ids are a CI concern only (resolved earlier to _bss_global_default_type). + +- name: Deduplicate enabledSecretStores types (preserve order) + ansible.builtin.set_fact: + _bss_enabled_unique: "{{ _bss_enabled_types | unique | list }}" + +- name: Join customServiceConfig sections + ansible.builtin.set_fact: + _bss_custom_service_config: "{{ _bss_ini_sections | join('\n') }}" + no_log: true + +- name: Write Barbican secret-stores kustomization + ansible.builtin.template: + src: 93-barbican-secret-stores.yaml.j2 + dest: "{{ cifmw_manifests_dir }}/kustomizations/controlplane/93-barbican-secret-stores.yaml" + mode: "0644" + no_log: true diff --git a/hooks/playbooks/barbican-secret-stores/config/fortanix.yml b/hooks/playbooks/barbican-secret-stores/config/fortanix.yml new file mode 100644 index 000000000..a75ffa8bb --- /dev/null +++ b/hooks/playbooks/barbican-secret-stores/config/fortanix.yml @@ -0,0 +1,26 @@ +--- +# Fortanix KMIP → kmip CRD fragment + [kmip_plugin] customServiceConfig. +# Cert-only auth; PEMs mounted from ClientDataSpec under clientDataPath. +# Uses _bss_fortanix_prep from prepare/fortanix.yml. + +- name: Build kmip_plugin section for {{ store.id }} + ansible.builtin.set_fact: + _kmip_ini: | + [kmip_plugin] + host = {{ _bss_fortanix_prep.host }} + port = {{ _bss_fortanix_prep.port }} + ca_certs = {{ _bss_fortanix_prep.client_data_path }}/CACert.pem + certfile = {{ _bss_fortanix_prep.client_data_path }}/Client.pem + keyfile = {{ _bss_fortanix_prep.client_data_path }}/Client.key + # pkcs1_only is deprecated (Barbican 1011705); prefer + # public_key_format / private_key_format if Fortanix rejects + # the default public-key encoding. + pkcs1_only = false + +- name: Record kmip fragments for {{ store.id }} + ansible.builtin.set_fact: + _bss_enabled_types: "{{ _bss_enabled_types + ['kmip'] }}" + _bss_kmip_spec: + clientDataSecret: "{{ _bss_fortanix_prep.client_data_secret }}" + clientDataPath: "{{ _bss_fortanix_prep.client_data_path }}" + _bss_ini_sections: "{{ _bss_ini_sections + [_kmip_ini] }}" diff --git a/hooks/playbooks/barbican-secret-stores/config/luna.yml b/hooks/playbooks/barbican-secret-stores/config/luna.yml new file mode 100644 index 000000000..b92284d81 --- /dev/null +++ b/hooks/playbooks/barbican-secret-stores/config/luna.yml @@ -0,0 +1,48 @@ +--- +# Luna PKCS11 → pkcs11 CRD fragment + [p11_crypto_plugin] customServiceConfig. +# Plugin settings come from the store dict (not job-level cifmw_hsm_* globals). +# +# Required store fields: partition, mkek_label, hmac_label +# Optional: library_path (default Luna 64-bit Cryptoki path), +# key_wrap_mechanism (default CKM_AES_CBC_PAD) +# Uses _bss_luna_prep from prepare/luna.yml for secret names / mount path. + +- name: Require Luna config fields on store {{ store.id }} + ansible.builtin.assert: + that: + - store.partition is defined + - store.partition | length > 0 + - store.mkek_label is defined + - store.mkek_label | length > 0 + - store.hmac_label is defined + - store.hmac_label | length > 0 + fail_msg: >- + Luna store {{ store.id }} needs partition, mkek_label, and hmac_label. + +- name: Build Luna p11_crypto_plugin section for {{ store.id }} + ansible.builtin.set_fact: + _luna_ini: | + [p11_crypto_plugin] + plugin_name = PKCS11 + library_path = {{ store.library_path | default('/usr/local/luna/libs/64/libCryptoki2.so') }} + token_labels = {{ store.partition }} + mkek_label = {{ store.mkek_label }} + hmac_label = {{ store.hmac_label }} + encryption_mechanism = CKM_AES_GCM + aes_gcm_generate_iv = false + hmac_key_type = CKK_GENERIC_SECRET + hmac_keygen_mechanism = CKM_GENERIC_SECRET_KEY_GEN + hmac_mechanism = CKM_SHA256_HMAC + key_wrap_mechanism = {{ store.key_wrap_mechanism | default('CKM_AES_CBC_PAD') }} + key_wrap_generate_iv = true + always_set_cka_sensitive = true + os_locking_ok = false + +- name: Record Luna pkcs11 fragments for {{ store.id }} + ansible.builtin.set_fact: + _bss_enabled_types: "{{ _bss_enabled_types + ['pkcs11'] }}" + _bss_pkcs11_spec: + loginSecret: "{{ _bss_luna_prep.login_secret }}" + clientDataSecret: "{{ _bss_luna_prep.client_data_secret }}" + clientDataPath: "{{ _bss_luna_prep.client_data_path }}" + _bss_ini_sections: "{{ _bss_ini_sections + [_luna_ini] }}" diff --git a/hooks/playbooks/barbican-secret-stores/config/proteccio.yml b/hooks/playbooks/barbican-secret-stores/config/proteccio.yml new file mode 100644 index 000000000..0fe096568 --- /dev/null +++ b/hooks/playbooks/barbican-secret-stores/config/proteccio.yml @@ -0,0 +1,47 @@ +--- +# Proteccio PKCS11 → pkcs11 CRD fragment + [p11_crypto_plugin] customServiceConfig. +# Plugin settings come from the store dict. +# +# Required store fields: partition, mkek_label, hmac_label +# Optional: library_path (default libnethsm path), +# key_wrap_mechanism (default CKM_AES_CBC_PAD) +# Uses _bss_proteccio_prep from prepare/proteccio.yml. + +- name: Require Proteccio config fields on store {{ store.id }} + ansible.builtin.assert: + that: + - store.partition is defined + - store.partition | length > 0 + - store.mkek_label is defined + - store.mkek_label | length > 0 + - store.hmac_label is defined + - store.hmac_label | length > 0 + fail_msg: >- + Proteccio store {{ store.id }} needs partition, mkek_label, and hmac_label. + +- name: Build Proteccio p11_crypto_plugin section for {{ store.id }} + ansible.builtin.set_fact: + _proteccio_ini: | + [p11_crypto_plugin] + plugin_name = PKCS11 + library_path = {{ store.library_path | default('/usr/lib64/libnethsm.so') }} + token_labels = {{ store.partition }} + mkek_label = {{ store.mkek_label }} + hmac_label = {{ store.hmac_label }} + encryption_mechanism = CKM_AES_CBC + hmac_key_type = CKK_GENERIC_SECRET + hmac_keygen_mechanism = CKM_GENERIC_SECRET_KEY_GEN + hmac_mechanism = CKM_SHA256_HMAC + key_wrap_mechanism = {{ store.key_wrap_mechanism | default('CKM_AES_CBC_PAD') }} + key_wrap_generate_iv = true + always_set_cka_sensitive = true + os_locking_ok = false + +- name: Record Proteccio pkcs11 fragments for {{ store.id }} + ansible.builtin.set_fact: + _bss_enabled_types: "{{ _bss_enabled_types + ['pkcs11'] }}" + _bss_pkcs11_spec: + loginSecret: "{{ _bss_proteccio_prep.login_secret }}" + clientDataSecret: "{{ _bss_proteccio_prep.client_data_secret }}" + clientDataPath: "{{ _bss_proteccio_prep.client_data_path }}" + _bss_ini_sections: "{{ _bss_ini_sections + [_proteccio_ini] }}" diff --git a/hooks/playbooks/barbican-secret-stores/config/software.yml b/hooks/playbooks/barbican-secret-stores/config/software.yml new file mode 100644 index 000000000..b9d68fec7 --- /dev/null +++ b/hooks/playbooks/barbican-secret-stores/config/software.yml @@ -0,0 +1,5 @@ +--- +# simple_crypto contributes only its type to enabledSecretStores. +- name: Enable simple_crypto for {{ store.id }} + ansible.builtin.set_fact: + _bss_enabled_types: "{{ _bss_enabled_types + ['simple_crypto'] }}" diff --git a/hooks/playbooks/barbican-secret-stores/enable.yml b/hooks/playbooks/barbican-secret-stores/enable.yml new file mode 100644 index 000000000..6cbfb11dc --- /dev/null +++ b/hooks/playbooks/barbican-secret-stores/enable.yml @@ -0,0 +1,143 @@ +--- +# Enable Barbican secret stores from a job-declared instance list. +# +# Defaults (usual Barbican jobs need no vars): +# cifmw_barbican_secret_stores: +# - id: software +# type: simple_crypto +# cifmw_barbican_global_default_secret_store: software # first store id if unset +# +# Multi-store example (instance-oriented; supports multiple providers of the +# same type once the operator allows it). Provider settings live on each +# store dict — there are no job-level cifmw_hsm_* / cifmw_barbican_kmip_* +# fallbacks for plugin or client config: +# +# cifmw_barbican_secret_stores: +# - id: software +# type: simple_crypto +# - id: fortanix +# type: kmip +# provider: fortanix +# host: amer.smartkey.io +# ca_certs_src: https://…/CACert.pem # lookaside +# certfile_src: https://…/Client.pem +# keyfile_src: https://…/Client.key +# - id: luna +# type: pkcs11 +# provider: luna +# client_name: … +# partition: … +# mkek_label: … +# hmac_label: … +# key_wrap_mechanism: … +# # plus prepare fields: minclient_src, certs, … +# cifmw_barbican_global_default_secret_store: fortanix # id +# +# Today the barbican-operator CRD allows at most one kmip and one pkcs11 +# entry (MaxItems=1). This playbook enforces that and will relax when the +# CRD grows. clientDataPath values must already be unique across instances. +# +# Provider modules: prepare/.yml and config/.yml. +# Image bake tag overrides (cifmw_update_containers_*, cifmw_set_containers_images) +# remain job-level. + +- name: Enable Barbican secret stores from job instance list + hosts: "{{ cifmw_target_hook_host | default('localhost') }}" + vars: + _oc_env: + KUBECONFIG: "{{ cifmw_openshift_kubeconfig }}" + PATH: "{{ cifmw_path }}" + _bss_default_store_list: + - id: software + type: simple_crypto + environment: "{{ _oc_env }}" + tasks: + - name: Apply default secret-store list (simple_crypto only) + ansible.builtin.set_fact: + _bss_stores: "{{ cifmw_barbican_secret_stores | default(_bss_default_store_list) }}" + + - name: Apply default global-default id (first store when unset) + ansible.builtin.set_fact: + _bss_default_id: >- + {{ cifmw_barbican_global_default_secret_store + | default(_bss_stores[0].id) }} + + - name: Require a non-empty store list + ansible.builtin.assert: + that: + - _bss_stores | length > 0 + - _bss_default_id | length > 0 + fail_msg: >- + cifmw_barbican_secret_stores must be a non-empty list of + {id, type[, provider]} (default is software/simple_crypto). + + - name: Init fragment collectors + ansible.builtin.set_fact: + _bss_enabled_types: [] + _bss_ini_sections: [] + _bss_kmip_spec: {} + _bss_pkcs11_spec: {} + _bss_client_data_paths: [] + _bss_store_ids: [] + _bss_kmip_count: 0 + _bss_pkcs11_count: 0 + + - name: Validate each store entry + ansible.builtin.include_tasks: validate_store.yml + loop: "{{ _bss_stores }}" + loop_control: + loop_var: store + label: "{{ store.id | default('?') }}" + + - name: Assert unique store ids + ansible.builtin.assert: + that: + - _bss_store_ids | length == (_bss_store_ids | unique | list | length) + fail_msg: >- + Duplicate store id in cifmw_barbican_secret_stores: {{ _bss_store_ids }} + + - name: Assert unique clientDataPath values + ansible.builtin.assert: + that: + - _bss_client_data_paths | length == (_bss_client_data_paths | unique | list | length) + fail_msg: >- + clientDataPath values must be unique across secret-store instances + (got {{ _bss_client_data_paths }}). + + - name: Assert operator MaxItems limits (one kmip, one pkcs11 for now) + ansible.builtin.assert: + that: + - (_bss_kmip_count | int) <= 1 + - (_bss_pkcs11_count | int) <= 1 + fail_msg: >- + barbican-operator currently allows at most one kmip and one pkcs11 + store (MaxItems=1). Got kmip={{ _bss_kmip_count }}, + pkcs11={{ _bss_pkcs11_count }}. Drop the assert when the CRD + supports multiple providers of the same type. + + - name: Resolve global default id to store type + vars: + _default_matches: "{{ _bss_stores | selectattr('id', 'equalto', _bss_default_id) | list }}" + block: + - name: Require default id to match exactly one store + ansible.builtin.assert: + that: + - _default_matches | length == 1 + fail_msg: >- + cifmw_barbican_global_default_secret_store={{ _bss_default_id }} + must match exactly one store id in cifmw_barbican_secret_stores + (ids={{ _bss_stores | map(attribute='id') | list }}). + + - name: Set resolved global default type + ansible.builtin.set_fact: + _bss_global_default_type: "{{ _default_matches[0].type }}" + + - name: Prepare and configure each store + ansible.builtin.include_tasks: process_store.yml + loop: "{{ _bss_stores }}" + loop_control: + loop_var: store + label: "{{ store.id }} ({{ store.type }}/{{ store.provider | default('software') }})" + + - name: Assemble control-plane kustomization + ansible.builtin.include_tasks: assemble_kustomization.yml diff --git a/hooks/playbooks/barbican-secret-stores/prepare/fortanix.yml b/hooks/playbooks/barbican-secret-stores/prepare/fortanix.yml new file mode 100644 index 000000000..3a9063cfc --- /dev/null +++ b/hooks/playbooks/barbican-secret-stores/prepare/fortanix.yml @@ -0,0 +1,114 @@ +--- +# Fortanix KMIP — create client-data Secret (ClientDataSpec). +# Cert-only auth (no username/password). Client PEMs come from lookaside +# URLs on the store, same pattern as Luna/Proteccio client material. +# +# Required store fields: +# host +# ca_certs_src — URL to CA bundle / CA cert PEM +# certfile_src — URL to client certificate PEM +# keyfile_src — URL to client private key PEM +# Optional: +# port (default 5696), client_data_secret, client_data_path +# +# Opaque/passphrase KMIP mapping (SecretData): +# https://review.opendev.org/c/openstack/barbican/+/1011705 + +- name: Require Fortanix lookaside fields on store {{ store.id }} + ansible.builtin.assert: + that: + - store.host is defined + - store.host | length > 0 + - store.ca_certs_src is defined + - store.ca_certs_src | length > 0 + - store.certfile_src is defined + - store.certfile_src | length > 0 + - store.keyfile_src is defined + - store.keyfile_src | length > 0 + fail_msg: >- + Fortanix store {{ store.id }} needs host, ca_certs_src, certfile_src, + and keyfile_src (lookaside HTTPS URLs for PEM files). + +- name: Create temp dir for Fortanix PEMs ({{ store.id }}) + ansible.builtin.tempfile: + state: directory + suffix: "-fortanix-{{ store.id }}" + register: _fortanix_tmpdir + +- name: Fetch Fortanix PEMs from lookaside for {{ store.id }} + no_log: true + block: + - name: Download Fortanix CA certs + ansible.builtin.get_url: + url: "{{ store.ca_certs_src }}" + dest: "{{ _fortanix_tmpdir.path }}/CACert.pem" + mode: "0600" + + - name: Download Fortanix client certificate + ansible.builtin.get_url: + url: "{{ store.certfile_src }}" + dest: "{{ _fortanix_tmpdir.path }}/Client.pem" + mode: "0600" + + - name: Download Fortanix client key + ansible.builtin.get_url: + url: "{{ store.keyfile_src }}" + dest: "{{ _fortanix_tmpdir.path }}/Client.key" + mode: "0600" + + - name: Slurp Fortanix PEMs + ansible.builtin.slurp: + src: "{{ _fortanix_tmpdir.path }}/{{ item }}" + loop: + - CACert.pem + - Client.pem + - Client.key + register: _fortanix_pem_slurp + + - name: Map slurped Fortanix PEMs + ansible.builtin.set_fact: + _kmip_ca_certs: "{{ (_fortanix_pem_slurp.results | selectattr('item', 'equalto', 'CACert.pem') | first).content | b64decode }}" + _kmip_certfile: "{{ (_fortanix_pem_slurp.results | selectattr('item', 'equalto', 'Client.pem') | first).content | b64decode }}" + _kmip_keyfile: "{{ (_fortanix_pem_slurp.results | selectattr('item', 'equalto', 'Client.key') | first).content | b64decode }}" + no_log: true + + always: + - name: Remove Fortanix PEM temp dir + ansible.builtin.file: + path: "{{ _fortanix_tmpdir.path }}" + state: absent + +- name: Set Fortanix connection defaults for {{ store.id }} + ansible.builtin.set_fact: + _kmip_host: "{{ store.host }}" + _kmip_port: "{{ store.port | default('5696') }}" + _kmip_client_data_secret: "{{ store.client_data_secret | default('barbican-kmip-client-data') }}" + _kmip_client_data_path: "{{ store.client_data_path | default('/etc/kmip-client') }}" + +- name: Create or update KMIP client-data Secret for {{ store.id }} + kubernetes.core.k8s: + state: present + kubeconfig: "{{ cifmw_openshift_kubeconfig }}" + api_key: "{{ cifmw_openshift_token | default(omit) }}" + context: "{{ cifmw_openshift_context | default(omit) }}" + definition: + apiVersion: v1 + kind: Secret + metadata: + name: "{{ _kmip_client_data_secret }}" + namespace: "{{ namespace }}" + type: Opaque + stringData: + # Key names match barbican-operator config/samples/pods_with_kmip.yaml. + CACert.pem: "{{ _kmip_ca_certs }}" + Client.pem: "{{ _kmip_certfile }}" + Client.key: "{{ _kmip_keyfile }}" + no_log: true + +- name: Stash Fortanix prepare results for {{ store.id }} + ansible.builtin.set_fact: + _bss_fortanix_prep: + host: "{{ _kmip_host }}" + port: "{{ _kmip_port }}" + client_data_secret: "{{ _kmip_client_data_secret }}" + client_data_path: "{{ _kmip_client_data_path }}" diff --git a/hooks/playbooks/barbican-secret-stores/prepare/luna.yml b/hooks/playbooks/barbican-secret-stores/prepare/luna.yml new file mode 100644 index 000000000..648b2785b --- /dev/null +++ b/hooks/playbooks/barbican-secret-stores/prepare/luna.yml @@ -0,0 +1,111 @@ +--- +# Luna PKCS11 — bake Luna-client images and create client-data + login Secrets. +# All instance settings come from the store dict (see job cifmw_barbican_secret_stores). +# +# Required store fields: +# client_name, chrystoki_conf_src, minclient_src, server_cert_src, +# client_cert_src, and partition_password (or pin_file + pin_file_key) +# +# PIN resolution uses resolve_from_param_or_file.yml: +# partition_password: "{{ luna_hsm_password.password }}" +# # — or — +# pin_file: /var/tmp/qe-secrets/luna_pin.yaml +# pin_file_key: rdu2Pin +# +# Optional: ansible_role_repo, ansible_role_version, client_data_secret, +# login_secret, client_data_path, pin_file +# Image bake uses job-level cifmw_update_containers_* / +# cifmw_hsm_barbican_* overrides when set. + +- name: Resolve Luna partition password for {{ store.id }} + ansible.builtin.include_tasks: "{{ playbook_dir }}/resolve_from_param_or_file.yml" + vars: + rpf_name: "Luna store {{ store.id }} partition_password" + rpf_fact: _luna_password + rpf_value: "{{ store.partition_password | default('') }}" + rpf_file: "{{ store.pin_file | default('/var/tmp/qe-secrets/luna_pin.yaml') }}" + rpf_file_key: "{{ store.pin_file_key | default('') }}" + rpf_required: true + +- name: Require Luna prepare fields on store {{ store.id }} + ansible.builtin.assert: + that: + - store.client_name is defined + - store.client_name | length > 0 + - store.chrystoki_conf_src is defined + - store.chrystoki_conf_src | length > 0 + - store.minclient_src is defined + - store.minclient_src | length > 0 + - store.server_cert_src is defined + - store.server_cert_src | length > 0 + - store.client_cert_src is defined + - store.client_cert_src | length > 0 + fail_msg: >- + Luna store {{ store.id }} needs client_name, chrystoki_conf_src, + minclient_src, server_cert_src, and client_cert_src. + +- name: Check out the Luna HSM role git repo for {{ store.id }} + ansible.builtin.git: + dest: "./rhoso_luna_hsm" + repo: "{{ store.ansible_role_repo | default('https://github.com/openstack-k8s-operators/ansible-role-rhoso-luna-hsm.git') }}" + version: "{{ store.ansible_role_version | default('main') }}" + +- name: Create and upload Luna-baked barbican images for {{ store.id }} + ansible.builtin.include_role: + name: rhoso_luna_hsm + tasks_from: create_image + vars: + barbican_src_image_registry: >- + {{ cifmw_hsm_barbican_src_image_registry | + default(content_provider_registry_ip ~ ':5001') }} + barbican_src_image_namespace: >- + {{ cifmw_hsm_barbican_src_image_namespace | + default(cifmw_update_containers_org | + default(cifmw_default_container_image_namespace)) }} + barbican_src_image_tag: >- + {{ cifmw_hsm_barbican_src_image_tag | + default(cifmw_update_containers_tag | default('component-ci-testing')) }} + barbican_src_api_image_name: >- + {{ cifmw_hsm_barbican_src_api_image_name | + default('openstack-barbican-api') }} + barbican_src_worker_image_name: >- + {{ cifmw_hsm_barbican_src_worker_image_name | + default('openstack-barbican-worker') }} + barbican_dest_image_registry: >- + {{ cifmw_hsm_barbican_dest_image_registry | + default(content_provider_registry_ip ~ ':5001') }} + barbican_dest_image_namespace: >- + {{ cifmw_hsm_barbican_dest_image_namespace | + default(cifmw_update_containers_org | + default(cifmw_default_container_image_namespace)) }} + barbican_dest_image_tag: "{{ cifmw_update_containers_barbican_custom_tag }}" + barbican_dest_api_image_name: >- + {{ cifmw_hsm_barbican_dest_api_image_name | + default('openstack-barbican-api') }} + barbican_dest_worker_image_name: >- + {{ cifmw_hsm_barbican_dest_worker_image_name | + default('openstack-barbican-worker') }} + image_registry_verify_tls: "{{ cifmw_image_registry_verify_tls | default('false', true) }}" + luna_minclient_src: "{{ store.minclient_src }}" + +- name: Create Luna client-data and login secrets for {{ store.id }} + ansible.builtin.include_role: + name: rhoso_luna_hsm + tasks_from: create_secrets + vars: + luna_client_name: "{{ store.client_name }}" + chrystoki_conf_src: "{{ store.chrystoki_conf_src }}" + luna_server_cert_src: "{{ store.server_cert_src }}" + luna_client_cert_src: "{{ store.client_cert_src }}" + luna_partition_password: "{{ _luna_password }}" + kubeconfig_path: "{{ cifmw_openshift_kubeconfig }}" + oc_dir: "{{ cifmw_path }}" + luna_data_secret: "{{ store.client_data_secret | default('barbican-luna-client-data') }}" + login_secret: "{{ store.login_secret | default('barbican-luna-login') }}" + +- name: Stash Luna prepare results for {{ store.id }} + ansible.builtin.set_fact: + _bss_luna_prep: + client_data_secret: "{{ store.client_data_secret | default('barbican-luna-client-data') }}" + login_secret: "{{ store.login_secret | default('barbican-luna-login') }}" + client_data_path: "{{ store.client_data_path | default('/usr/local/luna/config') }}" diff --git a/hooks/playbooks/barbican-secret-stores/prepare/proteccio.yml b/hooks/playbooks/barbican-secret-stores/prepare/proteccio.yml new file mode 100644 index 000000000..3bf994518 --- /dev/null +++ b/hooks/playbooks/barbican-secret-stores/prepare/proteccio.yml @@ -0,0 +1,116 @@ +--- +# Proteccio PKCS11 — bake images and create client-data + login Secrets. +# All instance settings come from the store dict. +# +# Required: conf_src, client_src, client_crt_src, client_key_src, +# server_crt_src, and partition_password (or pin_file + pin_file_key) +# +# PIN resolution uses resolve_from_param_or_file.yml: +# partition_password: "{{ proteccio_hsm_password.password }}" +# # — or — +# pin_file: /var/tmp/qe-secrets/proteccio_pin.yaml +# pin_file_key: rdu2Pin +# +# Optional: ansible_role_repo, ansible_role_version, client_iso, +# client_data_secret, client_data_secret_namespace, login_secret, +# login_secret_field, client_data_path, pin_file + +- name: Resolve Proteccio partition password for {{ store.id }} + ansible.builtin.include_tasks: "{{ playbook_dir }}/resolve_from_param_or_file.yml" + vars: + rpf_name: "Proteccio store {{ store.id }} partition_password" + rpf_fact: _proteccio_password + rpf_value: "{{ store.partition_password | default('') }}" + rpf_file: "{{ store.pin_file | default('/var/tmp/qe-secrets/proteccio_pin.yaml') }}" + rpf_file_key: "{{ store.pin_file_key | default('') }}" + rpf_required: true + +- name: Require Proteccio prepare fields on store {{ store.id }} + ansible.builtin.assert: + that: + - store.conf_src is defined + - store.conf_src | length > 0 + - store.client_src is defined + - store.client_src | length > 0 + - store.client_crt_src is defined + - store.client_crt_src | length > 0 + - store.client_key_src is defined + - store.client_key_src | length > 0 + - store.server_crt_src is defined + - store.server_crt_src | length > 0 + fail_msg: >- + Proteccio store {{ store.id }} needs conf_src, client_src, + client_crt_src, client_key_src, and server_crt_src. + +- name: Check out the Proteccio HSM role git repo for {{ store.id }} + ansible.builtin.git: + dest: "./rhoso_proteccio_hsm" + repo: "{{ store.ansible_role_repo | default('https://github.com/openstack-k8s-operators/ansible-role-rhoso-proteccio-hsm.git') }}" + version: "{{ store.ansible_role_version | default('main') }}" + +- name: Create and upload Proteccio-baked Barbican images for {{ store.id }} + ansible.builtin.include_role: + name: rhoso_proteccio_hsm + tasks_from: create_image + vars: + barbican_src_api_image_name: >- + {{ cifmw_hsm_barbican_src_api_image_name | + default(cifmw_barbican_src_api_image_name | + default('openstack-barbican-api')) }} + barbican_src_worker_image_name: >- + {{ cifmw_hsm_barbican_src_worker_image_name | + default(cifmw_barbican_src_worker_image_name | + default('openstack-barbican-worker')) }} + barbican_src_image_registry: >- + {{ cifmw_hsm_barbican_src_image_registry | + default(content_provider_registry_ip ~ ':5001') }} + barbican_src_image_namespace: >- + {{ cifmw_hsm_barbican_src_image_namespace | + default(cifmw_update_containers_org | + default(cifmw_default_container_image_namespace)) }} + barbican_src_image_tag: >- + {{ cifmw_hsm_barbican_src_image_tag | + default(cifmw_update_containers_tag | default('component-ci-testing')) }} + barbican_dest_api_image_name: >- + {{ cifmw_hsm_barbican_dest_api_image_name | + default(cifmw_barbican_dest_api_image_name | + default('openstack-barbican-api')) }} + barbican_dest_worker_image_name: >- + {{ cifmw_hsm_barbican_dest_worker_image_name | + default(cifmw_barbican_dest_worker_image_name | + default('openstack-barbican-worker')) }} + barbican_dest_image_registry: >- + {{ cifmw_hsm_barbican_dest_image_registry | + default(content_provider_registry_ip ~ ':5001') }} + barbican_dest_image_namespace: >- + {{ cifmw_hsm_barbican_dest_image_namespace | + default(cifmw_update_containers_org | + default(cifmw_default_container_image_namespace)) }} + barbican_dest_image_tag: "{{ cifmw_update_containers_barbican_custom_tag }}" + image_registry_verify_tls: "{{ cifmw_image_registry_verify_tls | default('false', true) }}" + proteccio_client_src: "{{ store.client_src }}" + proteccio_client_iso: "{{ store.client_iso | default('Proteccio3.06.05.iso') }}" + +- name: Create Proteccio client-data and login secrets for {{ store.id }} + ansible.builtin.include_role: + name: rhoso_proteccio_hsm + tasks_from: create_secrets + vars: + proteccio_conf_src: "{{ store.conf_src }}" + proteccio_client_crt_src: "{{ store.client_crt_src }}" + proteccio_client_key_src: "{{ store.client_key_src }}" + proteccio_server_crt_src: "{{ store.server_crt_src }}" + proteccio_password: "{{ _proteccio_password }}" + kubeconfig_path: "{{ cifmw_openshift_kubeconfig }}" + oc_dir: "{{ cifmw_path }}" + proteccio_data_secret: "{{ store.client_data_secret | default('barbican-proteccio-client-data') }}" + proteccio_data_secret_namespace: "{{ store.client_data_secret_namespace | default(namespace) }}" + login_secret: "{{ store.login_secret | default('barbican-proteccio-login') }}" + login_secret_field: "{{ store.login_secret_field | default('PKCS11Pin') }}" + +- name: Stash Proteccio prepare results for {{ store.id }} + ansible.builtin.set_fact: + _bss_proteccio_prep: + client_data_secret: "{{ store.client_data_secret | default('barbican-proteccio-client-data') }}" + login_secret: "{{ store.login_secret | default('barbican-proteccio-login') }}" + client_data_path: "{{ store.client_data_path | default('/etc/proteccio') }}" diff --git a/hooks/playbooks/barbican-secret-stores/prepare/software.yml b/hooks/playbooks/barbican-secret-stores/prepare/software.yml new file mode 100644 index 000000000..5b61a1926 --- /dev/null +++ b/hooks/playbooks/barbican-secret-stores/prepare/software.yml @@ -0,0 +1,5 @@ +--- +# simple_crypto / software — nothing to bake or mount. +- name: No prepare steps for software secret store {{ store.id }} + ansible.builtin.debug: + msg: "Store {{ store.id }} (simple_crypto) needs no image bake or client secrets." diff --git a/hooks/playbooks/barbican-secret-stores/process_store.yml b/hooks/playbooks/barbican-secret-stores/process_store.yml new file mode 100644 index 000000000..8d56b1817 --- /dev/null +++ b/hooks/playbooks/barbican-secret-stores/process_store.yml @@ -0,0 +1,14 @@ +--- +# Prepare then configure one store instance (loop_var: store). + +- name: Resolve provider module name for {{ store.id }} + ansible.builtin.set_fact: + _bss_provider: >- + {{ store.provider + | default('software' if store.type == 'simple_crypto' else '') }} + +- name: Prepare provider {{ _bss_provider }} for {{ store.id }} + ansible.builtin.include_tasks: "prepare/{{ _bss_provider }}.yml" + +- name: Configure provider {{ _bss_provider }} for {{ store.id }} + ansible.builtin.include_tasks: "config/{{ _bss_provider }}.yml" diff --git a/hooks/playbooks/barbican-secret-stores/resolve_from_param_or_file.yml b/hooks/playbooks/barbican-secret-stores/resolve_from_param_or_file.yml new file mode 100644 index 000000000..551bd4267 --- /dev/null +++ b/hooks/playbooks/barbican-secret-stores/resolve_from_param_or_file.yml @@ -0,0 +1,81 @@ +--- +# Resolve a secret value from a direct parameter or a YAML file key. +# +# Used by provider prepare modules (Luna/Proteccio partition PIN). +# The job chooses the source per store field. +# +# Required caller vars: +# rpf_name — label for error messages (e.g. "Luna partition_password") +# rpf_fact — fact name to set with the resolved string +# +# Optional caller vars: +# rpf_value — direct value (store.password, store.partition_password, …) +# rpf_file — path to YAML file written by a pre-run playbook +# rpf_file_key — key inside that YAML file +# rpf_required — if true (default), fail when neither source yields a value +# +# Prefer rpf_value when non-empty; otherwise load rpf_file[rpf_file_key]. + +- name: Init resolve for {{ rpf_name }} + ansible.builtin.set_fact: + _rpf_resolved: "{{ rpf_value | default('') }}" + +- name: Load {{ rpf_name }} from file when parameter unset + when: + - (_rpf_resolved | length) == 0 + - (rpf_file_key | default('') | length) > 0 + block: + - name: Require rpf_file path for {{ rpf_name }} + ansible.builtin.assert: + that: + - rpf_file is defined + - rpf_file | length > 0 + fail_msg: >- + {{ rpf_name }}: rpf_file_key={{ rpf_file_key }} set but rpf_file + path is empty. + + - name: Check secret file exists for {{ rpf_name }} + ansible.builtin.stat: + path: "{{ rpf_file }}" + register: _rpf_stat + + - name: Require secret file for {{ rpf_name }} + ansible.builtin.assert: + that: + - _rpf_stat.stat.exists + fail_msg: >- + {{ rpf_name }}: file {{ rpf_file }} not found + (key={{ rpf_file_key }}). + + - name: Read secret file for {{ rpf_name }} + ansible.builtin.include_vars: + file: "{{ rpf_file }}" + name: _rpf_file_data + + - name: Require key in secret file for {{ rpf_name }} + ansible.builtin.assert: + that: + - rpf_file_key in _rpf_file_data + - _rpf_file_data[rpf_file_key] | length > 0 + fail_msg: >- + {{ rpf_name }}: key {{ rpf_file_key }} missing or empty in + {{ rpf_file }} (keys={{ _rpf_file_data.keys() | list }}). + + - name: Set {{ rpf_name }} from file key {{ rpf_file_key }} + ansible.builtin.set_fact: + _rpf_resolved: "{{ _rpf_file_data[rpf_file_key] }}" + no_log: true + +- name: Require resolved value for {{ rpf_name }} + when: (rpf_required | default(true) | bool) + ansible.builtin.assert: + that: + - _rpf_resolved | length > 0 + fail_msg: >- + {{ rpf_name }} could not be resolved. Set the parameter, or + rpf_file + rpf_file_key for a materialised secret file. + +- name: Publish resolved {{ rpf_name }} as {{ rpf_fact }} + ansible.builtin.set_fact: + "{{ rpf_fact }}": "{{ _rpf_resolved }}" + no_log: true diff --git a/hooks/playbooks/barbican-secret-stores/templates/93-barbican-secret-stores.yaml.j2 b/hooks/playbooks/barbican-secret-stores/templates/93-barbican-secret-stores.yaml.j2 new file mode 100644 index 000000000..f9767f8cf --- /dev/null +++ b/hooks/playbooks/barbican-secret-stores/templates/93-barbican-secret-stores.yaml.j2 @@ -0,0 +1,39 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +namespace: {{ namespace }} +patches: +- target: + kind: OpenStackControlPlane + name: .* + patch: |- + - op: add + path: /spec/barbican/template/globalDefaultSecretStore + value: {{ _bss_global_default_type }} + - op: add + path: /spec/barbican/template/enabledSecretStores + value: +{% for t in _bss_enabled_unique %} + - {{ t }} +{% endfor %} +{% if _bss_kmip_spec | length > 0 %} + - op: add + path: /spec/barbican/template/kmip + value: + clientDataSecret: {{ _bss_kmip_spec.clientDataSecret }} + clientDataPath: {{ _bss_kmip_spec.clientDataPath }} +{% endif %} +{% if _bss_pkcs11_spec | length > 0 %} + - op: add + path: /spec/barbican/template/pkcs11 + value: + loginSecret: {{ _bss_pkcs11_spec.loginSecret }} + clientDataSecret: {{ _bss_pkcs11_spec.clientDataSecret }} + clientDataPath: {{ _bss_pkcs11_spec.clientDataPath }} +{% endif %} +{% if _bss_custom_service_config | length > 0 %} + - op: add + path: /spec/barbican/template/customServiceConfig + value: | +{{ _bss_custom_service_config | indent(8, first=True) }} +{% endif %} diff --git a/hooks/playbooks/barbican-secret-stores/validate_store.yml b/hooks/playbooks/barbican-secret-stores/validate_store.yml new file mode 100644 index 000000000..9fccb69b8 --- /dev/null +++ b/hooks/playbooks/barbican-secret-stores/validate_store.yml @@ -0,0 +1,80 @@ +--- +# Validate one cifmw_barbican_secret_stores entry (loop_var: store). +# Updates _bss_store_ids, type counts, and collected clientDataPath values. + +- name: Require id and type on store entry + ansible.builtin.assert: + that: + - store.id is defined + - store.id | length > 0 + - store.type is defined + - store.type in ['simple_crypto', 'kmip', 'pkcs11'] + fail_msg: >- + Each store needs id and type in {simple_crypto, kmip, pkcs11}. Got: {{ store }} + +- name: Resolve provider for {{ store.id }} + ansible.builtin.set_fact: + _bss_provider: >- + {{ store.provider + | default('software' if store.type == 'simple_crypto' else '') }} + +- name: Require provider for non-software stores + ansible.builtin.assert: + that: + - _bss_provider | length > 0 + fail_msg: >- + Store {{ store.id }} (type={{ store.type }}) needs provider + (e.g. fortanix, luna, proteccio). + +- name: Check provider is known and consistent with type + vars: + _allowed: + simple_crypto: ['software'] + kmip: ['fortanix'] + pkcs11: ['luna', 'proteccio'] + ansible.builtin.assert: + that: + - _bss_provider in _allowed[store.type] + fail_msg: >- + Store {{ store.id }}: provider={{ _bss_provider }} is not valid for + type={{ store.type }} (allowed: {{ _allowed[store.type] }}). + +- name: Record store id + ansible.builtin.set_fact: + _bss_store_ids: "{{ _bss_store_ids + [store.id] }}" + +- name: Count kmip instances + when: store.type == 'kmip' + ansible.builtin.set_fact: + _bss_kmip_count: "{{ (_bss_kmip_count | int) + 1 }}" + +- name: Count pkcs11 instances + when: store.type == 'pkcs11' + ansible.builtin.set_fact: + _bss_pkcs11_count: "{{ (_bss_pkcs11_count | int) + 1 }}" + +- name: Collect clientDataPath when set on the store entry + when: store.client_data_path is defined + ansible.builtin.set_fact: + _bss_client_data_paths: "{{ _bss_client_data_paths + [store.client_data_path] }}" + +- name: Collect default KMIP clientDataPath + when: + - store.type == 'kmip' + - store.client_data_path is not defined + ansible.builtin.set_fact: + _bss_client_data_paths: "{{ _bss_client_data_paths + ['/etc/kmip-client'] }}" + +- name: Collect default Luna clientDataPath + when: + - store.provider | default('') == 'luna' + - store.client_data_path is not defined + ansible.builtin.set_fact: + _bss_client_data_paths: "{{ _bss_client_data_paths + ['/usr/local/luna/config'] }}" + +- name: Collect default Proteccio clientDataPath + when: + - store.provider | default('') == 'proteccio' + - store.client_data_path is not defined + ansible.builtin.set_fact: + _bss_client_data_paths: "{{ _bss_client_data_paths + ['/etc/proteccio'] }}" From 693fb1798f85d770b0b57b5d7ebf63d15db16141 Mon Sep 17 00:00:00 2001 From: Ade Lee Date: Wed, 28 Oct 2026 04:36:55 -0400 Subject: [PATCH 3/3] [hooks] Overlay KMIP Client.key at 0400 until operator fix Temporary ExtraMounts SubPath until barbican-operator#502 is in openstack-operator-index:latest. Matches testproject throwaway. Assisted-by: Composer Signed-off-by: Ade Lee --- .../93-barbican-secret-stores.yaml.j2 | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/hooks/playbooks/barbican-secret-stores/templates/93-barbican-secret-stores.yaml.j2 b/hooks/playbooks/barbican-secret-stores/templates/93-barbican-secret-stores.yaml.j2 index f9767f8cf..319bab9cf 100644 --- a/hooks/playbooks/barbican-secret-stores/templates/93-barbican-secret-stores.yaml.j2 +++ b/hooks/playbooks/barbican-secret-stores/templates/93-barbican-secret-stores.yaml.j2 @@ -22,6 +22,33 @@ patches: value: clientDataSecret: {{ _bss_kmip_spec.clientDataSecret }} clientDataPath: {{ _bss_kmip_spec.clientDataPath }} + # THROWAWAY: overlay Client.key at 0400 until barbican-operator + # mounts clientData with DefaultMode 0400 (draft PR; OPENSTACK_IMG + # index:latest still ships 0440). SubPath overlays the directory mount. + # Remove when quay …/openstack-operator-index:latest includes the fix. + - op: add + path: /spec/barbican/template/extraMounts + value: + - name: kmip-client-key-mode-0400 + extraVol: + - extraVolType: KmipClientKeyMode + propagation: + - BarbicanAPI + - BarbicanWorker + volumes: + - name: kmip-client-key-0400 + secret: + secretName: {{ _bss_kmip_spec.clientDataSecret }} + defaultMode: 256 + items: + - key: Client.key + path: Client.key + mode: 256 + mounts: + - name: kmip-client-key-0400 + mountPath: {{ _bss_kmip_spec.clientDataPath }}/Client.key + subPath: Client.key + readOnly: true {% endif %} {% if _bss_pkcs11_spec | length > 0 %} - op: add