Skip to content

feat: support migration from Consul to Raft #80

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
- openbao
- openbao_ha
- vault
- vault_raft_migration
steps:
- name: Github Checkout 🛎
uses: actions/checkout@v4
Expand All @@ -39,7 +40,7 @@
run: |
pipx uninstall ansible-core
python3 -m pip install --upgrade pip
python3 -m pip install ansible-core==${{ matrix.ansible_version }}.* docker git+https://github.com/TerryHowe/ansible-modules-hashivault@c22434d887f0b8a5ac3ebda710664a027291e71c
python3 -m pip install ansible-core==${{ matrix.ansible_version }}.* docker jmespath git+https://github.com/TerryHowe/ansible-modules-hashivault@c22434d887f0b8a5ac3ebda710664a027291e71c

Check warning on line 43 in .github/workflows/pull_request.yml

View workflow job for this annotation

GitHub Actions / lint / Ansible 2.14 lint

yaml[line-length]

Line too long (195 > 160 characters)
ansible-galaxy collection build
ansible-galaxy collection install *.tar.gz
ansible-galaxy collection install community.general
Expand Down
3 changes: 3 additions & 0 deletions roles/vault/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ s (default: Omitted)
* `vault_write_keys_file`: Whether to write the root token and unseal keys to a file. Default `false`
* `vault_write_keys_file_host`: Host on which to write root token and unseal keys. Default `localhost`
* `vault_write_keys_file_path`: Path of file to write root token and unseal keys. Default `vault-keys.json`
* `vault_storage_type`: The type of storage to be used by vault either `Consul` or `Raft`. Note if vault is already deployed with `Consul` then you can change this inconjunction with `vault_migrate_consul_to_raft` to perform a one-way migration from `Consul` to `Raft`. Default `consul`.
* `vault_migrate_consul_to_raft`: if set true perform the necessary steps to convert the storage backend from `Consul` to `Raft`. Default `false`
* `vault_raft_leaders`: List of IPs belonging to Raft leaders. Expected that the first and only entry is the IP address of the first Vault instance as this would be initialised whereas as the others will not.

Root and unseal keys
--------------------
Expand Down
24 changes: 24 additions & 0 deletions roles/vault/defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ vault_tls_cert: ""

vault_config_dir: ""

vault_raft_leaders: []

vault_storage_type: "consul"

vault_config: >
{
"cluster_name": "{{ vault_cluster_name }}",
Expand All @@ -49,10 +53,17 @@ vault_config: >
{% endif %}
}],
"storage": {
{% if vault_storage_type == 'consul' %}
"consul": {
"address": "127.0.0.1:{{ consul_bind_port }}",
"path": "vault/"
}
{% elif vault_storage_type == 'raft' %}
"raft": {
"node_id": "raft_{{ inventory_hostname }}",
"path": "/vault/file"
}
{% endif %}
},
"telemetry": {
"prometheus_retention_time": "30s",
Expand Down Expand Up @@ -97,3 +108,16 @@ vault_write_keys_file: false
vault_write_keys_file_host: localhost
# Path of file to write root token and unseal keys.
vault_write_keys_file_path: vault-keys.json

vault_migrate_consul_to_raft: false

vault_raft_migration_config: >
storage_source "consul" {
address = "{{ consul_bind_ip }}:{{ consul_bind_port }}"
path = "vault/"
}
storage_destination "raft" {
node_id = "raft_{{ inventory_hostname }}"
path = "/vault/file"
}
cluster_addr = "{{ vault_protocol }}://{{ vault_bind_address }}:8201"
4 changes: 4 additions & 0 deletions roles/vault/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,9 @@
- name: "Deploy Consul"
import_tasks: consul.yml

- name: "Migrate from Consul to Raft"
import_tasks: raft_migration.yml
when: vault_migrate_consul_to_raft | bool

- name: "Deploy Vault"
import_tasks: vault.yml
26 changes: 26 additions & 0 deletions roles/vault/tasks/raft_migration.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
- name: Take Consul snapshot
ansible.builtin.command: >
docker exec {{ consul_docker_name }} /bin/sh -c "consul snapshot save /consul/data/backup_{{ ansible_facts.date_time.iso8601_basic_short }}.snap"
become: true
register: consul_snapshot
changed_when: true

- name: Write migration configuration
ansible.builtin.copy:
content: "{{ vault_raft_migration_config }}"
dest: "{{ vault_config_dir }}/migration.hcl"
mode: "0644"
become: true

- name: Perform migration
ansible.builtin.command: >
docker exec {{ vault_docker_name }} /bin/sh -c "vault operator migrate -config /vault/config/migration.hcl"
become: true
changed_when: true

- name: Change ownership of raft data directory
ansible.builtin.command: >
docker exec {{ vault_docker_name }} /bin/sh -c "chown -R vault:vault /vault/file"
become: true
changed_when: true
183 changes: 183 additions & 0 deletions tests/test_vault_raft_migration.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
---
- name: Prepare for vault role
gather_facts: true
hosts: consul
vars:
vault_config_dir: "/etc/vault"
vault_log_keys: true
vault_protocol: http
vault_set_keys_fact: true
vault_write_keys_file: true
tasks:
- name: Ensure /etc/vault exists
file:
path: /etc/vault
state: directory
mode: "0700"
become: true

- name: Include vault role
include_role:
name: vault

- name: Include vault role (idempotence test)
include_role:
name: vault

- name: Include Vault keys
ansible.builtin.include_vars:
file: "vault-keys.json"
name: vault_keys

- name: Unseal vault
include_role:
name: vault_unseal
vars:
vault_unseal_keys: "{{ vault_keys.keys_base64 }}"

- name: Configure PKI - create root/intermediate and generate certificates
vars:
vault_pki_certificate_subject:
- role: 'ServerCert'
common_name: "OS-CERT-TEST"
extra_params:
ttl: "8760h"
ip_sans: "127.0.0.1"
alt_names: "example.com"
exclude_cn_from_sans: true
vault_pki_certificates_directory: "/tmp/"
vault_pki_generate_certificates: true
vault_pki_intermediate_ca_name: "OS-TLS-INT"
vault_pki_intermediate_create: true
vault_pki_intermediate_roles:
- name: "ServerCert"
config:
max_ttl: 8760h
ttl: 8760h
allow_any_name: true
allow_ip_sans: true
require_cn: false
server_flag: true
key_type: rsa
key_bits: 4096
country: ["UK"]
locality: ["Bristol"]
organization: ["StackHPC"]
ou: ["HPC"]
vault_pki_root_ca_name: "OS-TLS-ROOT"
vault_pki_root_create: true
vault_pki_write_certificate_files: true
vault_pki_write_int_ca_to_file: true
vault_pki_write_pem_bundle: false
vault_pki_write_root_ca_to_file: true
vault_token: "{{ vault_keys.root_token }}"
block:
- name: Configure PKI - create root/intermediate and generate certificates
include_role:
name: vault_pki

- name: Configure PKI - create root/intermediate and generate certificates (idempotence test)
include_role:
name: vault_pki

- name: Configure PKI - generate certificate pem bundle
vars:
vault_pki_certificate_subject:
- role: 'ServerCert'
common_name: "OS-CERT-TEST2"
extra_params:
ttl: "8760h"
ip_sans: "192.168.38.72"
exclude_cn_from_sans: true
vault_pki_certificates_directory: "/tmp/"
vault_pki_generate_certificates: true
vault_pki_intermediate_ca_name: "OS-TLS-INT"
vault_pki_intermediate_create: false
vault_pki_root_ca_name: "OS-TLS-ROOT"
vault_pki_root_create: false
vault_pki_write_certificate_files: true
vault_pki_write_pem_bundle: true
vault_token: "{{ vault_keys.root_token }}"
block:
- name: Configure PKI - generate certificate pem bundle
include_role:
name: vault_pki

- name: Configure PKI - generate certificate pem bundle (idempotence test)
include_role:
name: vault_pki

- name: Validate if certificates exist
stat:
path: "/tmp/{{ item }}"
register: stat_result
failed_when: not stat_result.stat.exists
loop:
- OS-CERT-TEST.crt
- OS-CERT-TEST2.pem

- name: Concatenate CAs
shell: |
cat /tmp/OS-TLS-ROOT.pem /tmp/OS-TLS-INT.crt > /tmp/CA-CHAIN.pem
args:
executable: /bin/bash
become: true
changed_when: true

- name: Verify certificate chain
command: |
openssl verify -CAfile /tmp/CA-CHAIN.pem
/tmp/{{ item }}
register: verify_result
failed_when: verify_result.rc != 0
loop:
- OS-CERT-TEST.crt
- OS-CERT-TEST2.pem
changed_when: false

- name: Migrate vault to raft
include_role:
name: vault
vars:
vault_storage_type: raft
vault_migrate_consul_to_raft: true

- name: Unseal vault
include_role:
name: vault_unseal
vars:
vault_unseal_keys: "{{ vault_keys.keys_base64 }}"

- name: Validate vault is using raft
ansible.builtin.command: >
docker exec -e VAULT_ADDR=http://127.0.0.1:8200 vault /bin/sh -c "vault status -format=json"
register: vault_status
become: true
changed_when: false

- name: Validate vault is using raft
ansible.builtin.assert:
that:
- vault_status.stdout | from_json | json_query('storage_type') == 'raft'
fail_msg: "Vault is not using raft storage backend"
success_msg: "Vault is using raft storage backend"

- name: Read CA certificate from vault
hashivault_read:
url: http://127.0.0.1:8200
mount_point: OS-TLS-ROOT
secret: cert/ca
token: "{{ vault_keys.root_token }}"
register: vault_ca_cert

- name: Read CA from file
ansible.builtin.slurp:
src: /tmp/OS-TLS-ROOT.pem
register: ca_chain

- name: Validate ROOT CA
ansible.builtin.assert:
that:
- vault_ca_cert.value.certificate == (ca_chain.content | b64decode).rstrip('\n')
fail_msg: "ROOT CA certificate do not match"
success_msg: "ROOT CA certificate do match"
Loading