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:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -45,6 +45,7 @@ logs/*
|
||||
# OS generated files #
|
||||
######################
|
||||
._*
|
||||
.ansible
|
||||
.tox
|
||||
*.egg-info
|
||||
.eggs
|
||||
|
2
doc/source/encrypt_secrets.rst
Normal file
2
doc/source/encrypt_secrets.rst
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
.. include:: ../../encrypt_secrets/README.rst
|
@@ -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
131
encrypt_secrets/README.rst
Normal 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.
|
61
encrypt_secrets/galaxy.yml
Normal file
61
encrypt_secrets/galaxy.yml
Normal 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: []
|
11
encrypt_secrets/molecule/default/converge.yml
Normal file
11
encrypt_secrets/molecule/default/converge.yml
Normal 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
|
55
encrypt_secrets/molecule/default/molecule.yml
Normal file
55
encrypt_secrets/molecule/default/molecule.yml
Normal 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
|
54
encrypt_secrets/molecule/default/prepare.yml
Normal file
54
encrypt_secrets/molecule/default/prepare.yml
Normal 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
|
70
encrypt_secrets/molecule/default/verify_converge.yml
Normal file
70
encrypt_secrets/molecule/default/verify_converge.yml
Normal 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'] }}"
|
39
encrypt_secrets/molecule/default/verify_decrypt.yml
Normal file
39
encrypt_secrets/molecule/default/verify_decrypt.yml
Normal 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
|
70
encrypt_secrets/molecule/default/verify_rotate.yml
Normal file
70
encrypt_secrets/molecule/default/verify_rotate.yml
Normal 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'] }}"
|
7
encrypt_secrets/playbooks/ansible_vault.yml
Normal file
7
encrypt_secrets/playbooks/ansible_vault.yml
Normal 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
|
6
encrypt_secrets/requirements.yml
Normal file
6
encrypt_secrets/requirements.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
collections:
|
||||
- name: community.crypto
|
||||
source: https://github.com/ansible-collections/community.crypto
|
||||
type: git
|
||||
version: 2.22.3
|
39
encrypt_secrets/roles/ansible_vault/defaults/main.yml
Normal file
39
encrypt_secrets/roles/ansible_vault/defaults/main.yml
Normal 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
|
@@ -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: {}
|
94
encrypt_secrets/roles/ansible_vault/tasks/main.yml
Normal file
94
encrypt_secrets/roles/ansible_vault/tasks/main.yml
Normal 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']
|
3
encrypt_secrets/roles/ansible_vault/vars/main.yml
Normal file
3
encrypt_secrets/roles/ansible_vault/vars/main.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
|
||||
_ansible_vault_encrypt_file: "{{ (ansible_vault_action == 'rotate') | ternary(ansible_vault_new_pw, ansible_vault_pw) }}"
|
21
tox.ini
21
tox.ini
@@ -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}
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user