Add a collection for managing encryption of secret data

Best practices should referring to at least basic encryption of data
including SSH keypairs, PKI certificates, user_secrets, etc.

This collection aims to help/assist with managing data in encrypted
state, in case ansuble_vault is used as an encryption mechanism.

The collection should allow adding more supproted mechanism,
like SOPS for managing data encryption in the future.

Change-Id: I8af3118946682af4ec31bb1d4f6bea93be34f68c
This commit is contained in:
Dmitriy Rabotyagov
2025-03-09 16:27:51 +01:00
parent da0ddb5102
commit 6a600eb981
20 changed files with 780 additions and 0 deletions

1
.gitignore vendored
View File

@@ -45,6 +45,7 @@ logs/*
# OS generated files #
######################
._*
.ansible
.tox
*.egg-info
.eggs

View File

@@ -0,0 +1,2 @@
.. include:: ../../encrypt_secrets/README.rst

View File

@@ -8,6 +8,7 @@ OpenStack-Ansible Operator Tooling
swift_storage_mount_drives
elk_metrics
mcapi
encrypt_secrets
OpenStack-Ansible Diff Generator
--------------------------------

131
encrypt_secrets/README.rst Normal file
View File

@@ -0,0 +1,131 @@
==================
Encrypting secrets
==================
This document describes the supported operations for encrypting secrets and explains how to perform them using the appropriate tooling.
Ansible-Vault
=============
OpenStack-Ansible provides tooling to encrypt and rotate secret files and keypairs using Ansible Vault.
Role Defaults
-------------
.. literalinclude:: ../../encrypt_secrets/roles/ansible_vault/defaults/main.yml
:language: yaml
:start-after: under the License.
Installing the Collection
-------------------------
To install the collection, define it in your region deployment configuration file, located at `/etc/openstack_deploy/user-collection-requirements.yml`, as shown below:
.. code-block:: yaml
- name: osa_ops.encrypt_secrets
type: git
version: master
source: https://opendev.org/openstack/openstack-ansible-ops#/encrypt_secrets
Then, run `./scripts/bootstrap-ansible.sh` to install the collection.
Initial Encryption of Secret Files
----------------------------------
When initializing a region for the first time, you should encrypt secrets and generated private keys before storing them in Git. You can perform this process locally or on the deployment host.
.. NOTE::
You must re-run the encryption process whenever new services or keypairs are generated, which may occur at later deployment stages.
Encrypting Secrets Locally
~~~~~~~~~~~~~~~~~~~~~~~~~~
The process for encrypting secrets locally is similar to running it on the deploy host, but some context-specific variables required by OpenStack-Ansible may be unavailable and must be supplied manually.
Ensure you have a Python virtual environment with Ansible installed before proceeding.
1. Generate a password for the Ansible Vault and store it securely:
.. code-block:: bash
pwgen 36 1 > /tmp/vault.secret
2. Run the encryption playbook:
.. code-block:: bash
ansible-playbook osa_ops.encrypt_secrets.ansible_vault -e ansible_vault_region=${REGION_NAME} -e ansible_vault_pw=/tmp/vault.secret
3. Copy the contents of `/tmp/vault.secret` to the deployment host, for example to `/etc/openstack/vault.secret`.
4. Define the vault secret path in `/etc/openstack_deploy/user.rc`:
.. code-block:: bash
export ANSIBLE_VAULT_PASSWORD_FILE=/etc/openstack/vault.secret
5. Store the password securely in your preferred password manager.
6. Push the changes to your Git repository.
7. Ensure that the deploy host decrypts any required secrets.
Encrypting Secrets on the Deployment Host
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Follow these steps to encrypt secrets directly on the deployment host:
1. Generate a password and store it securely:
.. code-block:: bash
pwgen 36 1 > /etc/openstack/vault.secret
2. Define the vault secret path in `/etc/openstack_deploy/user.rc`:
.. code-block:: bash
export ANSIBLE_VAULT_PASSWORD_FILE=/etc/openstack/vault.secret
3. Run the encryption playbook:
.. code-block:: bash
openstack-ansible osa_ops.encrypt_secrets.ansible_vault
4. Commit and push changes to `/etc/openstack_deploy` in your Git repository.
5. Save the vault password (`/etc/openstack/vault.secret`) in a secure password manager.
6. Decrypt any necessary secrets before running OpenStack playbooks.
Decrypting Keypairs on the Deploy Host
--------------------------------------
The OpenStack-Ansible PKI role does not support storing private keys in encrypted format on the deployment host. Instead, configure a pipeline that decrypts the keys after placing them on the deploy host.
Encrypted keypairs should be committed to the Git repository, but stored unencrypted on the deployment host.
To decrypt them, run the following playbook:
.. code-block:: bash
openstack-ansible osa_ops.encrypt_secrets.ansible_vault -e ansible_vault_action=decrypt
Rotating the Ansible Vault Secret
---------------------------------
Rotating the Ansible Vault password requires re-encrypting all secrets in the repository. Assuming the original password is stored in `/tmp/vault.secret`, follow these steps:
1. Generate a new vault password/encryption key:
.. code-block:: bash
pwgen 45 1 > /tmp/vault.secret.new
2. Re-encrypt all secrets using the new password:
.. code-block:: bash
ANSIBLE_VAULT_PASSWORD_FILE=/tmp/vault.secret ansible-playbook osa_ops.encrypt_secrets.ansible_vault -e ansible_vault_action=rotate
3. Transfer the new password to the deployment host and store it securely in a password manager.

View File

@@ -0,0 +1,61 @@
### REQUIRED
# The namespace of the collection. This can be a company/brand/organization or product namespace under which all
# content lives. May only contain alphanumeric lowercase characters and underscores. Namespaces cannot start with
# underscores or numbers and cannot contain consecutive underscores
namespace: osa_ops
# The name of the collection. Has the same character restrictions as 'namespace'
name: encrypt_secrets
# The version of the collection. Must be compatible with semantic versioning
version: 0.1.0
# The path to the Markdown (.md) readme file. This path is relative to the root of the collection
readme: README.md
# A list of the collection's content authors. Can be just the name or in the format 'Full Name <email> (url)
# @nicks:irc/im.site#channel'
authors:
- Dmitriy Rabotyagov <dmitriy.rabotyagov@advanced.host>
### OPTIONAL but strongly recommended
# A short summary description of the collection
description: Encrypt and manage encrypted files for OpenStack-Ansible
# Either a single license or a list of licenses for content inside of a collection. Ansible Galaxy currently only
# accepts L(SPDX,https://spdx.org/licenses/) licenses. This key is mutually exclusive with 'license_file'
license:
- Apache-2.0
# The path to the license file for the collection. This path is relative to the root of the collection. This key is
# mutually exclusive with 'license'
license_file: ''
# A list of tags you want to associate with the collection for indexing/searching. A tag name has the same character
# requirements as 'namespace' and 'name'
tags: []
# Collections that this collection requires to be installed for it to be usable. The key of the dict is the
# collection label 'namespace.name'. The value is a version range
# L(specifiers,https://python-semanticversion.readthedocs.io/en/latest/#requirement-specification). Multiple version
# range specifiers can be set and are separated by ','
dependencies: {}
# The URL of the originating SCM repository
repository: https://opendev.org/openstack/openstack-ansible-ops
# The URL to any online docs
documentation: https://docs.openstack.org/openstack-ansible-ops
# The URL to the homepage of the collection/project
homepage: https://docs.openstack.org/openstack-ansible
# The URL to the collection issue tracker
issues: https://bugs.launchpad.net/openstack-ansible
# A list of file glob-like patterns used to filter any files or directories that should not be included in the build
# artifact. A pattern is matched from the relative path of the file or directory of the collection directory. This
# uses 'fnmatch' to match the files or directories. Some directories and files like 'galaxy.yml', '*.pyc', '*.retry',
# and '.git' are always filtered
build_ignore: []

View File

@@ -0,0 +1,11 @@
---
- name: Encrypt secrets
hosts: encrypt-default
tasks:
- name: Importing ansible_vault role
ansible.builtin.import_role:
name: ansible_vault
vars:
ansible_vault_action: encrypt

View File

@@ -0,0 +1,55 @@
---
dependency:
name: galaxy
options:
requirements-file: requirements.yml
driver:
name: docker
platforms:
- name: "encrypt-${MOLECULE_SCENARIO_NAME}"
image: "${DOCKER_REGISTRY:-quay.io/gotmax23}/${DOCKER_IMAGE_TAG:-debian-systemd:bookworm}"
command: ${DOCKER_COMMAND:-""}
pre_build_image: true
privileged: true
systemd: true
provisioner:
name: ansible
lint:
name: ansible-lint
env:
ANSIBLE_ROLES_PATH: ../../roles
inventory:
group_vars:
all:
ansible_vault_repo_path: /etc/openstack_deploy
ansible_vault_pw: /etc/openstack_deploy/vault_pw
ansible_vault_region: molecule
_molecule_password_mapping:
keystone_container_mysql_password: oequ0iejahgh8amaiy3Qua1Moo3weicaazo4
keystone_auth_admin_password: chaumei2Hoh5eisiesaip5goodees9eesahs
keystone_oslomsg_rpc_password: ei6Ooraenuavahleijuv3oos7asheih6Aidi
config_options:
defaults:
inject_facts_as_vars: false
scenario:
name: default
test_sequence:
- dependency
- cleanup
- destroy
- syntax
- create
- prepare
- converge
- idempotence
# NOTE: We don't use side-effect due to bug preventing to define multiple of them:
# https://github.com/ansible/molecule/issues/3617
- verify verify_converge.yml
- verify verify_rotate.yml
- verify verify_decrypt.yml
- cleanup
- destroy

View File

@@ -0,0 +1,54 @@
---
- name: Generate data for role verification
hosts: encrypt-default
tasks:
- name: Install required packages
ansible.builtin.package:
name:
- python3-cryptography
- ansible-core
update_cache: "{{ (ansible_facts['os_family'] | lower == 'debian') | ternary(true, omit) }}"
- name: Create required directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
recurse: true
mode: "0755"
loop:
- /etc/openstack_deploy/pki/certs/private
- /etc/openstack_deploy/pki/certs/certs
- /etc/openstack_deploy/pki/roots/TestRoot/private
- /etc/openstack_deploy/ssh_keypairs
- name: Generate ansible-vault secrets to use for data encryption
ansible.builtin.copy:
content: "{{ item.content }}"
dest: "{{ item.dest }}"
mode: "0600"
loop:
- dest: /etc/openstack_deploy/vault_pw
content: "{{ lookup('ansible.builtin.password', '/dev/null', chars=['ascii_lowercase', 'digits'], length=32) }}"
- dest: /etc/openstack_deploy/vault_pw.new
content: "{{ lookup('ansible.builtin.password', '/dev/null', chars=['ascii_lowercase', 'digits'], length=32) }}"
- dest: /etc/openstack_deploy/user_secrets.yml
content: |
---
{{ _molecule_password_mapping | to_yaml }}
- name: Generate private keys
community.crypto.openssl_privatekey:
path: "{{ item }}"
loop:
- /etc/openstack_deploy/pki/certs/private/noop.key.pem
- /etc/openstack_deploy/pki/roots/TestRoot/private/TestRoot.key.pem
- name: Generate test certificate
community.crypto.x509_certificate:
path: /etc/openstack_deploy/pki/certs/certs/noop.crt
privatekey_path: /etc/openstack_deploy/pki/certs/private/noop.key.pem
provider: selfsigned
- name: Generate ssh keypair
community.crypto.openssh_keypair:
path: /etc/openstack_deploy/ssh_keypairs/noop_keypair

View File

@@ -0,0 +1,70 @@
---
- name: Verify encryption of data
hosts: encrypt-default
tasks:
- name: Fetch test files to verify they're encrypted
ansible.builtin.slurp:
src: "{{ item }}"
loop:
- /etc/openstack_deploy/pki/certs/private/noop.key.pem
- /etc/openstack_deploy/pki/roots/TestRoot/private/TestRoot.key.pem
- /etc/openstack_deploy/ssh_keypairs/noop_keypair
register: encrypted_files
- name: Fetch test files to verify they were not encrypted
ansible.builtin.slurp:
src: "{{ item }}"
loop:
- /etc/openstack_deploy/pki/certs/certs/noop.crt
- /etc/openstack_deploy/ssh_keypairs/noop_keypair.pub
register: plaintext_files
- name: Ensure that encrypted files contain proper IDs
ansible.builtin.assert:
quiet: true
that:
- item['content'] | b64decode | split('\n') | first == "$ANSIBLE_VAULT;1.2;AES256;MOLECULE"
loop: "{{ encrypted_files['results'] }}"
loop_control:
label: "{{ item['source'] }}"
- name: Ensure that not encrypted files do NOT contain ANSIBLE_VAULT header
ansible.builtin.assert:
quiet: true
that:
- "'ANSIBLE_VAULT' not in item['content'] | b64decode"
loop: "{{ plaintext_files['results'] }}"
loop_control:
label: "{{ item['source'] }}"
- name: Ensure that encrypted files can be decrypted with expected password
ansible.builtin.command: "ansible-vault view {{ item['source'] }} --vault-password-file /etc/openstack_deploy/vault_pw"
changed_when: false
loop: "{{ encrypted_files['results'] }}"
loop_control:
label: "{{ item['source'] }}"
- name: Verify that we can not read user_secrets without VAULT password
ansible.builtin.command: "ansible -e @/etc/openstack_deploy/user_secrets.yml -m debug -a var={{ item }} -i localhost, localhost"
failed_when:
- not (failed_secrets_read.rc == 2 and 'Attempting to decrypt but no vault secrets found' not in failed_secrets_read.stderr)
changed_when: false
loop: "{{ _molecule_password_mapping.keys() }}"
register: failed_secrets_read
- name: Verify that we can read user_secrets with supplied password
ansible.builtin.command: "ansible -e @/etc/openstack_deploy/user_secrets.yml -m debug -a var={{ item }} -i localhost, localhost"
environment:
ANSIBLE_VAULT_PASSWORD_FILE: /etc/openstack_deploy/vault_pw
changed_when: false
loop: "{{ _molecule_password_mapping.keys() }}"
register: success_secrets_read
- name: Verify that values are correct
ansible.builtin.assert:
quiet: true
that:
- _molecule_password_mapping[item['item']] in item['stdout']
loop: "{{ success_secrets_read['results'] }}"
loop_control:
label: "{{ item['item'] }}"

View File

@@ -0,0 +1,39 @@
---
- name: Verify decryption
hosts: encrypt-default
tasks:
- name: Importing ansible_vault role
ansible.builtin.import_role:
name: ansible_vault
vars:
ansible_vault_action: decrypt
# NOTE: At this point we have rotated the secret, so "new" one should be used
ansible_vault_pw: /etc/openstack_deploy/vault_pw.new
post_tasks:
- name: Fetch test files to verify they were not encrypted
ansible.builtin.slurp:
src: "{{ item }}"
loop:
- /etc/openstack_deploy/pki/certs/certs/noop.crt
- /etc/openstack_deploy/ssh_keypairs/noop_keypair.pub
- /etc/openstack_deploy/pki/certs/private/noop.key.pem
- /etc/openstack_deploy/pki/roots/TestRoot/private/TestRoot.key.pem
- /etc/openstack_deploy/ssh_keypairs/noop_keypair
register: plaintext_files
- name: Ensure that not encrypted files do NOT contain ANSIBLE_VAULT header
ansible.builtin.assert:
quiet: true
that:
- "'ANSIBLE_VAULT' not in item['content'] | b64decode"
loop: "{{ plaintext_files['results'] }}"
loop_control:
label: "{{ item['source'] }}"
- name: Verify that user_secrets remain encrypted
ansible.builtin.command: "ansible -e @/etc/openstack_deploy/user_secrets.yml -m debug -a var={{ item }} -i localhost, localhost"
failed_when:
- not (failed_secrets_read.rc == 2 and 'Attempting to decrypt but no vault secrets found' not in failed_secrets_read.stderr)
changed_when: false
loop: "{{ _molecule_password_mapping.keys() }}"
register: failed_secrets_read

View File

@@ -0,0 +1,70 @@
---
- name: Verify rotation
hosts: encrypt-default
tasks:
# NOTE: While all actions are expected to run on "localhost", only
# rotate job is sensetive to environment, as needs to load
# and decrypt variables through ansible hostvars and not ansible-vault
# binary.
- name: Importing ansible_vault role
ansible.builtin.import_role:
name: ansible_vault
vars:
ansible_vault_action: rotate
# NOTE: We actually do not test in-line secrets rotation due to the
# reason above
ansible_vault_secrets_paths:
- "{{ ansible_vault_repo_path }}/group_vars/all/secrets.yml"
post_tasks:
- name: Fetch test files that were re-encrypted
ansible.builtin.slurp:
src: "{{ item }}"
loop:
- /etc/openstack_deploy/pki/certs/private/noop.key.pem
- /etc/openstack_deploy/pki/roots/TestRoot/private/TestRoot.key.pem
- /etc/openstack_deploy/ssh_keypairs/noop_keypair
register: encrypted_files
- name: Fetch test files to verify they are still not encrypted
ansible.builtin.slurp:
src: "{{ item }}"
loop:
- /etc/openstack_deploy/pki/certs/certs/noop.crt
- /etc/openstack_deploy/ssh_keypairs/noop_keypair.pub
register: plaintext_files
- name: Ensure that encrypted files contain same Vault ID
ansible.builtin.assert:
quiet: true
that:
- item['content'] | b64decode | split('\n') | first == "$ANSIBLE_VAULT;1.2;AES256;MOLECULE"
loop: "{{ encrypted_files['results'] }}"
loop_control:
label: "{{ item['source'] }}"
- name: Ensure that not encrypted files still do NOT contain ANSIBLE_VAULT header
ansible.builtin.assert:
quiet: true
that:
- "'ANSIBLE_VAULT' not in item['content'] | b64decode"
loop: "{{ plaintext_files['results'] }}"
loop_control:
label: "{{ item['source'] }}"
- name: Ensure that encrypted files can be decrypted with new password
ansible.builtin.command: "ansible-vault view {{ item['source'] }} --vault-password-file /etc/openstack_deploy/vault_pw.new"
changed_when: false
loop: "{{ encrypted_files['results'] }}"
loop_control:
label: "{{ item['source'] }}"
- name: Ensure that encrypted files can NOT be decrypted with old password
ansible.builtin.command: "ansible-vault view {{ item['source'] }} --vault-password-file /etc/openstack_deploy/vault_pw"
changed_when: false
failed_when:
- not (decrypt_invalid_pw.rc == 1 and 'no vault secrets were found that could decrypt' in decrypt_invalid_pw.stderr)
register: decrypt_invalid_pw
loop: "{{ encrypted_files['results'] }}"
loop_control:
label: "{{ item['source'] }}"

View File

@@ -0,0 +1,7 @@
---
- name: Using ansible-vault for managing data encryption
hosts: localhost
tasks:
- name: Importing ansible_vault role
ansible.builtin.import_role:
name: osa_ops.encrypt_secrets.ansible_vault

View File

@@ -0,0 +1,6 @@
---
collections:
- name: community.crypto
source: https://github.com/ansible-collections/community.crypto
type: git
version: 2.22.3

View File

@@ -0,0 +1,39 @@
---
# Copyright 2025, Advanced Hosters B.V.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Allowed values: "encrypt", "decrypt" and "rotate"
ansible_vault_action: encrypt
# Path to the OpenStack-Ansible configuration (openstack_deploy) folder
ansible_vault_repo_path: "{{ lookup('ansible.builtin.env', 'OSA_CONFIG_DIR') | default(lookup('ansible.builtin.env', 'PWD') ~ '/openstack_deploy', True) }}"
# Name of the region, which will be used as vault id
ansible_vault_region: "{{ service_region | default('RegionOne') }}"
# Path to the ansible-vault password file
ansible_vault_pw: "{{ lookup('ansible.builtin.env', 'ANSIBLE_VAULT_PASSWORD_FILE') }}"
# Path to the freshly generated ansible-vault password file. Used for rotation only
ansible_vault_new_pw: "{{ ansible_vault_pw ~ '.new' }}"
# If in-place copy is enabled, role will completely override the resulting file
# When disabled, Ansible will produce a managed block for each managed variable
ansible_vault_in_place_copy: true
# Paths to files, where individual variables needs to be encrypted
ansible_vault_secrets_paths:
- "{{ ansible_vault_repo_path }}/user_secrets.yml"
- "{{ ansible_vault_repo_path }}/group_vars/all/secrets.yml"
# Instead of defining paths to files explicitly, you can search filesystem for
# files with individually encrypted secrets. Results will be combined with
# `ansible_vault_secrets_paths`
ansible_vault_secrets_search_paths: []
ansible_vault_secrets_search_pattern: "secrets.yml"
# Can be overriden to a specific destination in case venv is not activated
ansible_vault_binary: ansible-vault

View File

@@ -0,0 +1,95 @@
---
# Copyright 2025, Advanced Hosters B.V.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
- name: Try to read secrets file as yaml or fallback to decrypt secrets
block:
- name: Fetch the file
ansible.builtin.slurp:
src: "{{ file['stat']['path'] }}"
register: _secrets_file
- name: Read file as unencrypted
ansible.builtin.set_fact:
_secrets: "{{ _secrets_file['content'] | b64decode | from_yaml }}"
rescue:
- name: Skipping file as it is likely already encrypted
ansible.builtin.debug:
msg: "We failed to read file as YAML, which means that it's likely already encrypted. Skipping..."
when:
- ansible_vault_action == "encrypt"
- name: Loading encrypted variables to re-encrypt
ansible.builtin.include_vars:
file: "{{ file['stat']['path'] }}"
when:
- ansible_vault_action == "rotate"
- name: Read current secrets file for rotation
vars:
_secret_vars: "{{ _secrets_file['content'] | b64decode | regex_findall('(.*):\\s!vault\\s\\|\\n') }}"
ansible.builtin.set_fact:
_secrets: |-
{% set secrets_mapping = {} %}
{% for var in _secret_vars %}
{% set _ = secrets_mapping.update({var: hostvars['localhost'][var]}) %}
{% endfor %}
{{ secrets_mapping }}
when:
- ansible_vault_action == "rotate"
always:
- name: Encrypt individual secrets from unencrypted file
ansible.builtin.command:
argv:
- "{{ ansible_vault_binary }}"
- encrypt_string
- --vault-id
- "{{ ansible_vault_region | upper }}@{{ _ansible_vault_encrypt_file }}"
- --encrypt-vault-id
- "{{ ansible_vault_region | upper }}"
- '{{ item.value }}'
- --name
- "{{ item.key }}"
with_dict: "{{ _secrets | default({}) }}"
no_log: true
register: new_secrets
changed_when: false
- name: Place encrypted secrets in-place
ansible.builtin.copy:
content: "---\n{{ new_secrets.results | map(attribute='stdout') | join('\n') }}\n"
dest: "{{ file['stat']['path'] }}"
mode: "0600"
when:
- _secrets is defined
- _secrets | length > 0
- ansible_vault_in_place_copy
- name: Place encrypted secrets in independent blocks
ansible.builtin.blockinfile:
block: "{{ item['stdout'] }}"
dest: "{{ file['stat']['path'] }}"
marker: "# {mark} ANSIBLE MANAGED {{ item.item['key'] }}"
mode: "0600"
loop: "{{ new_secrets.results }}"
when:
- _secrets is defined
- _secrets | length > 0
- not ansible_vault_in_place_copy
- name: Undefine the secrets variable
ansible.builtin.set_fact:
_secrets: {}

View File

@@ -0,0 +1,94 @@
---
# Copyright 2025, Advanced Hosters B.V.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
- name: Encrypt/Rotate files with individual secrets encrypted
when:
- ansible_vault_action in ['encrypt', 'rotate']
block:
- name: Find secrets file for region
ansible.builtin.find:
paths: "{{ ansible_vault_secrets_search_paths }}"
patterns: "{{ ansible_vault_secrets_search_pattern }}"
recurse: true
register: _found_secret_files
- name: Verify existance of expected files
vars:
_ansible_vault_found_secrets_paths: "{{ _found_secret_files.get('files', []) | map(attribute='path') }}"
ansible.builtin.stat:
path: "{{ item }}"
loop: "{{ _ansible_vault_found_secrets_paths + ansible_vault_secrets_paths }}"
register: _encrypt_string_files
- name: Encrypt individual secrets in files
ansible.builtin.include_tasks:
file: ansible_vault_strings.yml
loop: "{{ _encrypt_string_files.results | selectattr('stat.exists') }}"
loop_control:
loop_var: file
label: "{{ file['stat']['path'] }}"
- name: Encrypt private keys for PKI/SSH
block:
- name: Find private keys in the directory
ansible.builtin.find:
paths: "{{ ansible_vault_repo_path }}/pki/"
patterns: "*.key.pem"
recurse: true
register: __private_keys
- name: Find SSH private keys in the directory
ansible.builtin.find:
paths: "{{ ansible_vault_repo_path }}/ssh_keypairs/"
patterns: "^(?!.*\\.(pub|info)($|\\?)).*"
use_regex: true
recurse: false
register: __ssh_keys
- name: Decrypt private keys
ansible.builtin.command:
argv:
- "{{ ansible_vault_binary }}"
- decrypt
- --vault-id
- "{{ ansible_vault_region | upper }}@{{ ansible_vault_pw }}"
- "{{ item }}"
failed_when:
- not (_decrypt_keys.rc == 1 and 'input is not vault encrypted data' in _decrypt_keys.stderr)
- not _decrypt_keys.rc == 0
changed_when:
- _decrypt_keys.rc == 0
register: _decrypt_keys
with_items: "{{ __private_keys.files | map(attribute='path') | list + __ssh_keys.files | map(attribute='path') | list }}"
when:
- ansible_vault_action in ['decrypt', 'rotate']
- name: Encrypt private keys
ansible.builtin.command:
argv:
- "{{ ansible_vault_binary }}"
- encrypt
- --vault-id
- "{{ ansible_vault_region | upper }}@{{ _ansible_vault_encrypt_file }}"
- "{{ item }}"
register: _encrypt_keys
failed_when:
- not (_encrypt_keys.rc == 1 and 'input is already encrypted' in _encrypt_keys.stderr)
- not _encrypt_keys.rc == 0
changed_when:
- _encrypt_keys.rc == 0
with_items: "{{ __private_keys.files | map(attribute='path') | list + __ssh_keys.files | map(attribute='path') | list }}"
when:
- ansible_vault_action in ['encrypt', 'rotate']

View File

@@ -0,0 +1,3 @@
---
_ansible_vault_encrypt_file: "{{ (ansible_vault_action == 'rotate') | ternary(ansible_vault_new_pw, ansible_vault_pw) }}"

21
tox.ini
View File

@@ -64,3 +64,24 @@ commands =
bash -c "{toxinidir}/tests/common/test-ansible-env-prep.sh"
{[testenv:pep8]commands}
{[testenv:bashate]commands}
[testenv:molecule]
deps =
-c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
-r{env:OSA_TEST_REQUIREMENTS_FILE:https://opendev.org/openstack/openstack-ansible/raw/branch/{env:TEST_BRANCH:master}/test-requirements.txt}
commands =
molecule test
passenv =
{[testenv]passenv}
DOCKER_REGISTRY
DOCKER_IMAGE_TAG
DOCKER_COMMAND
[testenv:molecule-encrypt-secrets]
changedir={toxinidir}/encrypt_secrets
deps = {[testenv:molecule]deps}
commands = {[testenv:molecule]commands}
passenv = {[testenv:molecule]passenv}

View File

@@ -91,3 +91,19 @@
parent: openstack-ansible-deploy-aio_magnum_octavia_capi_kvm-ubuntu-jammy
files:
- ^mcapi_vexxhost/.*
- job:
name: openstack-ansible-tox-molecule-encrypt-secrets-rockylinux-9
parent: openstack-ansible-tox-molecule-rockylinux-9
files:
- encrypt_secrets
vars:
tox_envlist: molecule-encrypt-secrets
- job:
name: openstack-ansible-tox-molecule-encrypt-secrets-ubuntu-noble
parent: openstack-ansible-tox-molecule-ubuntu-noble
files:
- encrypt_secrets
vars:
tox_envlist: molecule-encrypt-secrets

View File

@@ -25,6 +25,8 @@
- openstack-ansible-deploy-aio_magnum_octavia_capi_kvm_ops-ubuntu-jammy-v1.29.6
- openstack-ansible-deploy-aio_magnum_octavia_capi_kvm_ops-ubuntu-jammy-v1.30.2
- openstack-ansible-deploy-aio_magnum_octavia_capi_kvm_ops-ubuntu-jammy-v1.31.1
- openstack-ansible-tox-molecule-encrypt-secrets-rockylinux-9
- openstack-ansible-tox-molecule-encrypt-secrets-ubuntu-noble
gate:
jobs:
- openstack-ansible-linters
@@ -32,3 +34,5 @@
- openstack-ansible-deploy-aio_magnum_octavia_capi_kvm_ops-ubuntu-jammy-v1.29.6
- openstack-ansible-deploy-aio_magnum_octavia_capi_kvm_ops-ubuntu-jammy-v1.30.2
- openstack-ansible-deploy-aio_magnum_octavia_capi_kvm_ops-ubuntu-jammy-v1.31.1
- openstack-ansible-tox-molecule-encrypt-secrets-rockylinux-9
- openstack-ansible-tox-molecule-encrypt-secrets-ubuntu-noble