feat(vault): add a HashiCorp Vault role

This commit is contained in:
NaeiKinDus 2024-06-26 00:00:00 +00:00
parent 688bdae6a1
commit ddf406fd37
Signed by: WoodSmellParticle
GPG key ID: 8E52ADFF7CA8AE56
17 changed files with 477 additions and 0 deletions

View file

@ -0,0 +1,21 @@
---
hc_vault_api_port: 8200
hc_vault_architecture: linux_amd64
hc_vault_cluster_nodes:
hc_vault_default_binary_path: /usr/local/bin/vault
hc_vault_default_shell: /usr/sbin/nologin
hc_vault_default_umask: '0077'
hc_vault_enable_ui: false
hc_vault_environment_vars: {}
hc_vault_gpg_key_fingerprint_regexp: '\bC874[[:space:]]+011F[[:space:]]+0AB4[[:space:]]+0511[[:space:]]+0D02[[:space:]]+1055[[:space:]]+3436[[:space:]]+5D94[[:space:]]+72D7[[:space:]]+468F\b'
hc_vault_gpg_key_id_regexp: 34365D9472D7468F
hc_vault_initialize: false
hc_vault_raft_cluster_port: 8201
hc_vault_root_dir: /srv/vault
hc_vault_runas: vault
hc_vault_server_config:
hc_vault_server_tls_cert_data:
hc_vault_server_tls_key_data:
hc_vault_init_key_shares_count: 1
hc_vault_init_key_threshold: 1
hc_vault_init_provisioner_data_filepath: /tmp

View file

@ -0,0 +1,20 @@
---
- name: 'reload vault service'
become: true
ansible.builtin.systemd_service:
name: vault.service
enabled: true
state: reloaded
- name: 'restart vault service'
become: true
ansible.builtin.systemd_service:
name: vault.service
daemon_reload: true
enabled: true
state: restarted
- name: 'load firewall rules'
become: true
ansible.builtin.command: /usr/sbin/nft -f /etc/nftables.d/vault.nft
when: nft_rules.changed

View file

@ -0,0 +1,21 @@
---
galaxy_info:
author: Florian L.
namespace: nullified
description: Install HashiCorp Vault
# issue_tracker_url: http://example.com/issue/tracker
license: MIT
min_ansible_version: 2.15
# https://galaxy.ansible.com/api/v1/platforms/
platforms:
- name: Debian
versions:
- bookworm
galaxy_tags:
- hashicorp
- vault
- secrets
dependencies: []

View file

@ -0,0 +1,38 @@
---
- name: initialize vault
become: true
no_log: true
ansible.builtin.command:
cmd: >
vault operator init
-tls-skip-verify -non-interactive -format=yaml
-key-shares={{ hc_vault_init_key_shares_count }}
-key-threshold={{ hc_vault_init_key_threshold }}
chdir: '{{ hc_vault_root_dir }}'
environment:
VAULT_ADDR: 'https://127.0.0.1:8200'
DBUS_SESSION_BUS_ADDRESS: '/dev/null'
register: init_data
- name: set init data filename
no_log: true
ansible.builtin.set_fact:
hc_vault_init_data_filename: "{{ hc_vault_init_data_filepath | default('/tmp', True) }}/vault_{{ ansible_facts['fqdn'] }}_init.yml"
- name: save initialization data
connection: local
no_log: true
block:
- name: save content to temp file
ansible.builtin.copy:
content: '{{ init_data.stdout }}'
dest: '{{ hc_vault_init_data_filename }}'
mode: '0600'
owner: "{{ ansible_facts['user_id'] }}"
group: "{{ ansible_facts['user_id'] }}"
- name: print init data file location
no_log: true
ansible.builtin.debug:
msg: 'Initialization data is stored in file "{{ hc_vault_init_data_filename }}". MAKE SURE TO SAVE IT SOMEWHERE SAFE!'
verbosity: 0

View file

@ -0,0 +1,151 @@
---
- name: '[setup] gather facts if not already done'
ansible.builtin.setup:
gather_subset:
- all_ipv4_addresses
- default_ipv4
- dns
- name: install vault binary
when: not hc_vault_binary_installed or hc_vault_local_binary_version != hc_vault_version
notify:
- 'vault : restart vault service'
block:
- name: download archive
ansible.builtin.get_url:
url: 'https://releases.hashicorp.com/vault/{{ hc_vault_version }}/vault_{{ hc_vault_version }}_{{ hc_vault_architecture }}.zip'
dest: '{{ tmp_file.path }}/vault_{{ hc_vault_version }}_{{ hc_vault_architecture }}.zip'
mode: '0600'
- name: download SHASUMs file signature
ansible.builtin.get_url:
url: 'https://releases.hashicorp.com/vault/{{ hc_vault_version }}/vault_{{ hc_vault_version }}_SHA256SUMS.sig'
dest: '{{ tmp_file.path }}/shasums.sig'
mode: '0600'
- name: download SHASUMs files for vault releases
ansible.builtin.get_url:
url: 'https://releases.hashicorp.com/vault/{{ hc_vault_version }}/vault_{{ hc_vault_version }}_SHA256SUMS'
dest: '{{ tmp_file.path }}/shasums.txt'
mode: '0600'
- name: Verify downloaded files integrity
block:
- name: check SHASUMs file integrity
ansible.builtin.command: 'gpg --verify {{ tmp_file.path }}/shasums.sig {{ tmp_file.path }}/shasums.txt'
- name: check SHASUM of the downloaded archive
ansible.builtin.command:
cmd: 'sha256sum -c {{ tmp_file.path }}/shasums.txt'
chdir: '{{ tmp_file.path }}'
register: shasum_check
failed_when: 'search_string not in shasum_check.stdout'
vars:
search_string: 'vault_{{ hc_vault_version }}_{{ hc_vault_architecture }}.zip: OK'
- name: install vault package
become: true
ansible.builtin.shell: |
cd {{ tmp_file.path }}
unzip -o vault_{{ hc_vault_version }}_{{ hc_vault_architecture }}.zip
install -g {{ hc_vault_runas }} -o {{ hc_vault_runas }} -p -m 500 ./vault {{ hc_vault_binary_path }}
{{ hc_vault_binary_path }} -h > /dev/null || (echo "Unexpected return, binary might be invalid")
- name: prepare directory layout
become: true
ansible.builtin.file:
path: '{{ item }}'
owner: '{{ hc_vault_runas }}'
group: '{{ hc_vault_runas }}'
mode: '0700'
state: directory
loop:
- '{{ hc_vault_root_dir }}/config'
- '{{ hc_vault_root_dir }}/data'
- '{{ hc_vault_root_dir }}/tls'
- name: install systemd unit file
become: true
notify:
- 'vault : restart vault service'
ansible.builtin.template:
src: ../templates/vault-unit.service.j2
dest: /lib/systemd/system/vault.service
mode: '0644'
owner: root
group: root
- name: install default vault configuration file
become: true
notify:
- 'vault : restart vault service'
no_log: true
ansible.builtin.template:
src: ../templates/config.hcl.j2
dest: '{{ hc_vault_root_dir }}/config/main.hcl'
mode: '0600'
owner: '{{ hc_vault_runas }}'
group: '{{ hc_vault_runas }}'
- name: install environment file
become: true
notify:
- 'vault : restart vault service'
no_log: true
ansible.builtin.template:
src: ../templates/env.j2
dest: '{{ hc_vault_root_dir }}/config/vault.env'
mode: '0600'
owner: '{{ hc_vault_runas }}'
group: '{{ hc_vault_runas }}'
- name: install TLS certificate and key
become: true
notify:
- 'vault : reload vault service'
no_log: true
block:
- name: copy provided data
block:
- name: TLS certificate
ansible.builtin.copy:
content: '{{ hc_vault_server_tls_cert_data }}'
dest: '{{ hc_vault_root_dir }}/tls/tls.cert'
owner: '{{ hc_vault_runas }}'
group: '{{ hc_vault_runas }}'
mode: '0600'
- name: Private key
ansible.builtin.copy:
content: '{{ hc_vault_server_tls_key_data }}'
dest: '{{ hc_vault_root_dir }}/tls/tls.key'
owner: '{{ hc_vault_runas }}'
group: '{{ hc_vault_runas }}'
mode: '0600'
when: hc_vault_server_tls_cert_data and hc_vault_server_tls_key_data
- name: generate new files
block:
- name: generate ECDSA key
ansible.builtin.command:
cmd: openssl ecparam -name prime256v1 -genkey -out tls.key
chdir: '{{ hc_vault_root_dir }}/tls'
creates: '{{ hc_vault_root_dir }}/tls/tls.key'
- name: generate certificate
ansible.builtin.command:
cmd: >
openssl req -new -days 3650 -nodes -x509
-subj "/C=FR/ST=Void/L=Void/O=IT/OU=Vault/CN={{ ansible_facts['fqdn'] }}"
-addext "subjectAltName = DNS:localhost, DNS:{{ ansible_facts['hostname'] }}, IP:{{ ansible_facts['default_ipv4']['address'] | default(ansible_facts['all_ipv4_addresses'][0]) }}, IP:127.0.0.1"
-addext "extendedKeyUsage = serverAuth"
-addext "keyUsage = nonRepudiation, digitalSignature, keyEncipherment"
-addext "basicConstraints = CA:FALSE"
-key tls.key -out tls.cert
chdir: '{{ hc_vault_root_dir }}/tls'
creates: '{{ hc_vault_root_dir }}/tls/tls.cert'
- name: update files ownership
ansible.builtin.file:
path: '{{ hc_vault_root_dir }}/tls/{{ item }}'
state: file
owner: '{{ hc_vault_runas }}'
group: '{{ hc_vault_runas }}'
mode: '0600'
loop:
- tls.key
- tls.cert
when: not hc_vault_server_tls_cert_data or not hc_vault_server_tls_key_data
- name: flush handlers
meta: flush_handlers

View file

@ -0,0 +1,61 @@
---
- name: create temp directory
ansible.builtin.tempfile:
state: directory
register: tmp_file
changed_when: false
- name: find vault path
ansible.builtin.command: 'bash -c "command -v vault"'
register: output_vault_binary_path
failed_when: false
changed_when: false
- name: find local vault binary version
become: true
environment:
DBUS_SESSION_BUS_ADDRESS: /dev/null
VAULT_ADDR: 'https://127.0.0.1:8200'
ansible.builtin.shell: "{{ output_vault_binary_path.stdout }} version | sed -E 's/Vault[[:space:]]+v([0-9.-]+)(\\b|$).*$/\\1/'"
when: output_vault_binary_path.rc == 0
register: output_vault_binary_version
changed_when: false
- name: set binary facts
ansible.builtin.set_fact:
hc_vault_binary_installed: "{{ 'true' if output_vault_binary_path.rc == 0 else 'false' }}"
hc_vault_binary_path: "{{ output_vault_binary_path.stdout | default(hc_vault_default_binary_path, true) }}"
hc_vault_local_binary_version: "{{ output_vault_binary_version.get('stdout', None) }}"
- name: run prerequisite tasks
ansible.builtin.import_tasks: prerequisites.yml
- name: install vault
ansible.builtin.import_tasks: install.yml
- name: run security configuration
ansible.builtin.import_tasks: security.yml
- name: find vault initialization status
ansible.builtin.command: '{{ output_vault_binary_path.stdout }} operator init -status -tls-skip-verify'
become: true
register: hc_vault_init_status
environment:
DBUS_SESSION_BUS_ADDRESS: /dev/null
VAULT_ADDR: 'https://127.0.0.1:8200'
failed_when: hc_vault_init_status.rc == 1
changed_when: false
- name: initialize vault
ansible.builtin.import_tasks: initialize.yml
when: hc_vault_initialize and hc_vault_init_status.rc == 2
- name: cleanup
become: true
ansible.builtin.file:
path: '{{ tmp_file.path }}'
state: absent
changed_when: false
- name: flush handlers
meta: flush_handlers

View file

@ -0,0 +1,62 @@
---
# APT repository is unreliable, not working when this code was developed, so the zip solution is favored
- name: install required packages
become: true
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
force_apt_get: true
pkg:
- gpg
- curl
- coreutils
- name: create vault group
become: true
ansible.builtin.group:
name: '{{ hc_vault_runas }}'
system: true
- name: create vault user
become: true
ansible.builtin.user:
comment: vault dedicated user
create_home: true
home: '{{ hc_vault_root_dir }}'
group: '{{ hc_vault_runas }}'
name: '{{ hc_vault_runas }}'
password_lock: true
shell: '{{ hc_vault_default_shell }}'
state: present
system: true
umask: '{{ hc_vault_default_umask }}'
- name: check HC GPG key is imported
become: true
ansible.builtin.command: gpg --list-keys 'HashiCorp Security'
register: gpg_list_keys
changed_when: false
failed_when: false
- name: import and verify HC GPG key
become: true
block:
- name: fetch HC GPG key
ansible.builtin.get_url:
url: 'https://www.hashicorp.com/.well-known/pgp-key.txt'
dest: '{{ tmp_file.path }}/pgp-key.txt'
mode: '0600'
- name: import HC GPG key
ansible.builtin.command: 'gpg --import {{ tmp_file.path }}/pgp-key.txt'
- name: check GPG key ID
ansible.builtin.command: "gpg --list-keys 'HashiCorp Security' | grep -iE '{{ hc_vault_gpg_key_id_regexp }}'"
- name: check GPG key fingerprint
ansible.builtin.command: "gpg --fingerprint --list-signatures 'HashiCorp Security' | grep -iE '{{ hc_vault_gpg_key_fingerprint_regexp }}'"
when: gpg_list_keys.rc != 0
rescue:
- name: remove invalid GPG key
ansible.builtin.command: "gpg --delete-keys --batch --yes 'HashiCorp Security'"
- name: stop the playbook run
ansible.builtin.debug:
msg: 'Task "{{ ansible_failed_task }}" found an inconsistency with the imported GPG key; something somewhere is deeply wrong.'
failed_when: true

View file

@ -0,0 +1,14 @@
---
- name: install firewall rules
become: true
ansible.builtin.template:
src: ../templates/vault.nft.j2
dest: /etc/nftables.d/vault.nft
mode: '0600'
owner: root
group: root
vars:
firewall_lb_ips: '{{ hc_vault_security_lb_ips | default({}, True) }}'
firewall_cluster_nodes_ips: '{{ hc_vault_security_cluster_nodes | default({}, True) }}'
notify:
- 'vault : load firewall rules'

View file

@ -0,0 +1,18 @@
{%- if not hc_vault_server_config %}
ui = {% if hc_vault_enable_ui %}true{% else %}false{% endif +%}
disable_mlock = false
storage "file" {
path = "{{ hc_vault_root_dir }}/data"
}
# HTTPS listener
listener "tcp" {
address = "0.0.0.0:8200"
tls_cert_file = "{{ hc_vault_root_dir }}/tls/tls.cert"
tls_key_file = "{{ hc_vault_root_dir }}/tls/tls.key"
tls_min_version = "tls13"
}
{%- else %}
{{ hc_vault_server_config }}
{% endif %}

View file

@ -0,0 +1,3 @@
{% for item in hc_vault_environment_vars.keys() -%}
{{ item }} = {{ hc_vault_environment_vars[item] }}
{% endfor %}

View file

@ -0,0 +1,39 @@
[Unit]
Description="HashiCorp Vault"
Requires=network-online.target
After=network-online.target
StartLimitIntervalSec=120
StartLimitBurst=4
ConditionCapability=CAP_IPC_LOCK
ConditionCapability=CAP_SYSLOG
ConditionFileNotEmpty={{ hc_vault_root_dir }}/config/main.hcl
ConditionPathIsDirectory={{ hc_vault_root_dir }}/tls
[Install]
WantedBy=multi-user.target
[Service]
AmbientCapabilities=CAP_IPC_LOCK
CapabilityBoundingSet=CAP_IPC_LOCK CAP_SYSLOG
EnvironmentFile={{ hc_vault_root_dir }}/config/vault.env
ExecStart={{ hc_vault_binary_path }} server -config={{ hc_vault_root_dir }}/config/main.hcl
Group={{ hc_vault_runas }}
KillMode=process
KillSignal=SIGINT
LimitCORE=0
LimitMEMLOCK=infinity
LimitNOFILE=65536
LockPersonality=yes
NoNewPrivileges=yes
OOMScoreAdjust=-500
PrivateDevices=yes
PrivateTmp=yes
ProtectHome=yes
ProtectSystem=full
Restart=on-failure
RestartSec=5
SecureBits=keep-caps
TimeoutSec=30
Type=notify-reload
UMask=0077
User={{ hc_vault_runas }}

View file

@ -0,0 +1,13 @@
{%- set api_source_ips = firewall_lb_ips | default({}, True) -%}
{%- set noop = api_source_ips.update(firewall_cluster_nodes_ips) -%}
table inet filter {
chain input {
{% if firewall_lb_ips %}ip saddr { {{ api_source_ips | join (', ') }} } {% endif %}tcp dport {{ hc_vault_api_port }} accept
{% if firewall_cluster_nodes_ips %}ip saddr { {{ firewall_cluster_nodes_ips | join(', ') }} } tcp dport {{ hc_vault_raft_cluster_port }}{% endif +%}
}
chain output {
{% if firewall_lb_ips %}ip daddr { {{ api_source_ips | join (', ') }} } {% endif %}tcp sport {{ hc_vault_api_port }} accept
{% if firewall_cluster_nodes_ips %}ip daddr { {{ firewall_cluster_nodes_ips | join(', ') }} } tcp sport {{ hc_vault_raft_cluster_port }}{% endif +%}
}
}

View file

@ -0,0 +1,2 @@
hc_vault_version: '{{ vault_hc_vault_version }}'
hc_vault_architecture: linux_amd64

View file

@ -30,3 +30,7 @@ k3s_cluster_additional_tf_resources:
tfvars_content: '{{ vault_invoice_ninja_tfvars }}'
tfstate_path: '{{ vault_invoice_ninja_tfstate_path }}'
# storage_dir:
hc_vault_server_tls_cert_data: '{{ vault_hc_vault_server_tls_cert_data }}'
hc_vault_server_tls_key_data: '{{ vault_hc_vault_server_tls_key_data }}'
hc_vault_initialize: true

View file

@ -17,6 +17,13 @@
ansible.builtin.include_role:
name: nullified.infrastructure.server
- name: setup vault
hosts: internal:&vault
tasks:
- name: include vault role
ansible.builtin.include_role:
name: nullified.infrastructure.vault
- name: setup mariadb servers
hosts: internal:&mariadb
tasks: