Skip to content
Merged
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
6 changes: 6 additions & 0 deletions playbooks/lego_openbao.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
- name: "Install and configure lego for OpenBao"
hosts: "{{ lego_openbao_hosts | default('lego_openbao') }}"
become: true
roles:
- role: "lego_openbao"
2 changes: 1 addition & 1 deletion roles/lego/defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ lego_certificate_store_user: "{{ lego_user_res.uid | default(lego_user) }}"
lego_certificate_store_group: "{{ lego_user_res.group | default(lego_user) }}"
lego_certificate_store_mode: "0755"
lego_systemd_path: "/etc/systemd/system"
lego_version: "4.27.0"
lego_version: "4.35.1"
lego_system_type: "linux"
lego_system_arch: >-2
{{
Expand Down
19 changes: 19 additions & 0 deletions roles/lego_openbao/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# lego for OpenBao ACME+EAB

- https://github.com/go-acme/lego
- https://openbao.org/api-docs/secret/pki/#acme-certificate-issuance

## Installation

This rolle assumes that a lego binary is already installed (use the `lego` role for that).
It sets up an ACME account for lego on an OpenBao instance using External Account Binding (EAB) and configures a separate systemd service to fetch certificates using that account.

## Usage

Set the following variables

- `lego_openbao_approle`: Approle credentials in the form `{"role_id": "some string", "secret_id": "some string"}` with permission to get new EAB tokens.
- `lego_openbao_issuer_hostname`: Hostname of the OpenBao instance.
- `lego_openbao_issuer_path` (default: `/v1/pki-int/roles/lego/acme`): Full path to the OpenBao role with ACME support.

The options regarding ACME challenges are the same as documented in the `lego` role, just prefixed with `lego_openbao_` instead of `lego_`.
60 changes: 60 additions & 0 deletions roles/lego_openbao/defaults/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
lego_openbao_base_path: "/opt/lego"
lego_openbao_certificate_store: "{{ lego_openbao_base_path }}/certificates"
lego_openbao_certificate_store_user: "{{ lego_openbao_user_res.uid | default(lego_openbao_user) }}"
lego_openbao_certificate_store_group: "{{ lego_openbao_user_res.group | default(lego_openbao_user) }}"
lego_openbao_certificate_store_mode: "0750"
lego_openbao_systemd_path: "/etc/systemd/system"

lego_openbao_executable: "{{ lego_openbao_base_path }}/lego"
lego_openbao_account_base_path: "{{ lego_openbao_base_path }}/accounts"

lego_openbao_enable_metrics_textfile: false

lego_openbao_user: "lego"
lego_openbao_timer_on_boot: ~
lego_openbao_timer_on_calendar: "*-*-* 00,12:00:00"
lego_openbao_timer_interval: ~
lego_openbao_timer_accuracy: ~
lego_openbao_timer_random_delay: "43200"
lego_openbao_timer_persistent: true

lego_openbao_certificate:
domains:
- "instance.domain.company.com"
email: ops@domain.company.com

lego_openbao_auth_backend_name: "approle"
lego_openbao_auth_backend_login_path: "v1/auth/{{ lego_openbao_auth_backend_name }}/login"
lego_openbao_approle:
role_id: ~
secret_id: ~

lego_openbao_force_renew_eab_account: false

lego_openbao_issuer_hostname: "vault.example.local"
lego_openbao_issuer_path: "/v1/pki-int/roles/lego/acme"

lego_openbao_challenge:
type: "http"
lego_openbao_challenge_http_port: 80

lego_openbao_tasks:
playbook: "run"
systemd: "renew"

lego_openbao_certificate_renewal_days: 30
lego_openbao_configuration_default_key_type: "ec256"

lego_openbao_configuration_defaults:
command_parameters:
global:
server: "https://{{ lego_openbao_issuer_hostname }}{{ lego_openbao_issuer_path }}/directory"
accept-tos: true
email: "{{ lego_openbao_certificate.email }}"
path: "{{ lego_openbao_base_path }}"
key-type: "{{ lego_openbao_configuration_default_key_type }}"
run: {}
renew:
days: "{{ lego_openbao_certificate_renewal_days }}"
environment: {}
20 changes: 20 additions & 0 deletions roles/lego_openbao/handlers/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
- name: "Run lego-openbao"
become: true
become_user: "{{ lego_openbao_user_res.name }}"
environment: "{{ lego_openbao_configuration_merged.environment }}"
ansible.builtin.command: "{{ lego_openbao_command_playbook }}"
register: lego_openbao_run
changed_when: lego_openbao_run.rc == 0

- name: "Recurse certificate store ownership"
ansible.builtin.file:
path: "{{ lego_openbao_certificate_store }}"
state: directory
owner: "{{ lego_openbao_certificate_store_user }}"
group: "{{ lego_openbao_certificate_store_group }}"
recurse: true

- name: "Reload systemd"
ansible.builtin.systemd:
daemon_reload: true
197 changes: 197 additions & 0 deletions roles/lego_openbao/tasks/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
---

- name: "Ensure lego user is created"
ansible.builtin.user:
name: "{{ lego_openbao_user }}"
state: "present"
system: true
register: "lego_openbao_user_res"
tags: [ "prepare", "prepare-lego" ]

Check failure on line 9 in roles/lego_openbao/tasks/main.yml

View workflow job for this annotation

GitHub Actions / ansible / Run ansible-lint / Run Ansible lint (3.11, 8.3.0)

yaml[brackets]

Too many spaces inside brackets

- name: "Ensure base directory for lego is created"
ansible.builtin.file:
path: "{{ lego_openbao_base_path }}"
state: "directory"
owner: "{{ lego_openbao_certificate_store_user }}"
group: "{{ lego_openbao_certificate_store_group }}"
mode: "0755"
tags: [ "prepare", "prepare-lego" ]

Check failure on line 18 in roles/lego_openbao/tasks/main.yml

View workflow job for this annotation

GitHub Actions / ansible / Run ansible-lint / Run Ansible lint (3.11, 8.3.0)

yaml[brackets]

Too many spaces inside brackets

- name: "Ensure certificate directory exists and has the configured permissions"
ansible.builtin.file:
path: "{{ lego_openbao_certificate_store }}"
state: "directory"
owner: "{{ lego_openbao_certificate_store_user }}"
group: "{{ lego_openbao_certificate_store_group }}"
mode: "{{ lego_openbao_certificate_store_mode }}"
tags: [ "prepare", "prepare-lego" ]

Check failure on line 27 in roles/lego_openbao/tasks/main.yml

View workflow job for this annotation

GitHub Actions / ansible / Run ansible-lint / Run Ansible lint (3.11, 8.3.0)

yaml[brackets]

Too many spaces inside brackets
notify: [ "Recurse certificate store ownership" ]

Check failure on line 28 in roles/lego_openbao/tasks/main.yml

View workflow job for this annotation

GitHub Actions / ansible / Run ansible-lint / Run Ansible lint (3.11, 8.3.0)

yaml[brackets]

Too many spaces inside brackets

- name: "Ensure lego metrics textfile generator exists"
ansible.builtin.template:
src: "metrics-textfile.py.j2"
dest: "{{ lego_openbao_base_path }}/metrics-textfile.py"
owner: "root"
group: "root"
mode: "0755"
when: "lego_openbao_enable_metrics_textfile"
tags: [ "prepare", "prepare-lego" ]

Check failure on line 38 in roles/lego_openbao/tasks/main.yml

View workflow job for this annotation

GitHub Actions / ansible / Run ansible-lint / Run Ansible lint (3.11, 8.3.0)

yaml[brackets]

Too many spaces inside brackets

- name: "Check if lego lego_openbao_executable exists"
ansible.builtin.stat:
path: "{{ lego_openbao_executable }}"
check_mode: false
changed_when: false
register: "lego_openbao_executable_file"
failed_when:
- "not lego_openbao_executable_file.stat.exists"
- "not lego_openbao_executable_file.stat.executable"
tags: [ "prepare", "prepare-lego" ]

Check failure on line 49 in roles/lego_openbao/tasks/main.yml

View workflow job for this annotation

GitHub Actions / ansible / Run ansible-lint / Run Ansible lint (3.11, 8.3.0)

yaml[brackets]

Too many spaces inside brackets

- name: "Check if lego account file exists"
ansible.builtin.stat:
path: "{{ lego_openbao_acme_account_path }}"
check_mode: false
changed_when: false
ignore_errors: true
register: "lego_openbao_acme_account_file"
tags: [ "prepare", "prepare-lego" ]

Check failure on line 58 in roles/lego_openbao/tasks/main.yml

View workflow job for this annotation

GitHub Actions / ansible / Run ansible-lint / Run Ansible lint (3.11, 8.3.0)

yaml[brackets]

Too many spaces inside brackets

- name: "Create ACME account using EAB"
when: "not lego_openbao_acme_account_file.stat.exists or lego_openbao_force_renew_eab_account"
block:
# https://openbao.org/api-docs/auth/approle/#login-with-approle
- name: Login to OpenBao
ansible.builtin.uri:
method: POST
url: "https://{{ lego_openbao_issuer_hostname }}/{{ lego_openbao_auth_backend_login_path }}"
body:
role_id: "{{ lego_openbao_approle.role_id }}"
secret_id: "{{ lego_openbao_approle.secret_id }}"
body_format: json
timeout: 180
register: lego_openbao_login_data

# https://openbao.org/api-docs/secret/pki/#get-acme-eab-binding-token
- name: Get ACME EAB binding token
ansible.builtin.uri:
method: POST
url: "https://{{ lego_openbao_issuer_hostname }}{{ lego_openbao_issuer_path }}/new-eab"
body: {}
body_format: json
headers:
X-Vault-Token: "{{ lego_openbao_login_data.json.auth.client_token }}"
timeout: 180
register: lego_openbao_eab_token_data

- name: "Run lego with EAB credentials"
become: true
become_user: "{{ lego_openbao_user_res.name }}"
environment: "{{ lego_openbao_configuration_merged.environment }}"
ansible.builtin.command: "{{ lego_openbao_command_eab }}"
vars:
lego_openbao_eab_kid: "{{ lego_openbao_eab_token_data.json.data.id }}"
lego_openbao_eab_hmac: "{{ lego_openbao_eab_token_data.json.data.key }}"
register: lego_openbao_run_eab
changed_when: lego_openbao_run_eab.rc == 0

- name: "Check if certificate file exists"
ansible.builtin.stat:
path: "{{ lego_openbao_certificate_store }}/{{
lego_openbao_certificate.domains[0] }}.crt"
register: "lego_openbao_certificate_stat"
changed_when: "not lego_openbao_certificate_stat.stat.exists"
notify: [ "Run lego-openbao" ]

Check failure on line 104 in roles/lego_openbao/tasks/main.yml

View workflow job for this annotation

GitHub Actions / ansible / Run ansible-lint / Run Ansible lint (3.11, 8.3.0)

yaml[brackets]

Too many spaces inside brackets
tags: [ "deploy", "deploy-lego" ]

Check failure on line 105 in roles/lego_openbao/tasks/main.yml

View workflow job for this annotation

GitHub Actions / ansible / Run ansible-lint / Run Ansible lint (3.11, 8.3.0)

yaml[brackets]

Too many spaces inside brackets

- name: "Check if certificate is nonexistent or differs from wanted state"
when:
- lego_openbao_certificate_stat.stat.exists
- lego_openbao_certificate_stat.stat.size > 0
block:
- name: "Fetch certificate info"
community.crypto.x509_certificate_info:
path: "{{ lego_openbao_certificate_store }}/{{
lego_openbao_certificate.domains[0] }}.crt"
register: "lego_openbao_certificate_info"
tags: [ "deploy", "deploy-lego" ]

# you have to seperatly loop because map with regex_replace does not like getting AnsibleUnsafeText objects
- name: "Remove DNS at the start of the SAN"
ansible.builtin.set_fact:
lego_openbao_certificate_sans: "{{ lego_openbao_certificate_sans | default([]) +
[item | regex_replace('^DNS:', '')] }}"
loop: "{{ lego_openbao_certificate_info.subject_alt_name }}"
tags: [ "deploy", "deploy-lego" ]

- name: "Compare SANs, notify handler if they differ"
ansible.builtin.set_fact:
lego_openbao_certificate_sans_equal: >-
{{
lego_openbao_certificate.domains is superset(lego_openbao_certificate_sans)
and lego_openbao_certificate.domains is subset(lego_openbao_certificate_sans)
}}
changed_when: "not lego_openbao_certificate_sans_equal"
notify: [ "Run lego-openbao" ]
tags: [ "deploy", "deploy-lego" ]

- name: "Compare pubkey type, notify handler if it differs"
ansible.builtin.set_fact:
lego_openbao_certificate_pubkey_type_differs: "{{
lego_openbao_certificate_info.public_key_type !=
lego_openbao_certificate_wanted_keytype }}"
vars:
lego_openbao_certificate_wanted_keytype: >-
{{
"ECC" if "ec" in lego_openbao_configuration_merged.command_parameters.global["key-type"]
else "RSA" if "rsa" in lego_openbao_configuration_merged.command_parameters.global["key-type"]
}}
changed_when: "lego_openbao_certificate_pubkey_type_differs"
notify: [ "Run lego-openbao" ]
tags: [ "deploy", "deploy-lego" ]

- name: "Notify handler if pubkey is empty"
ansible.builtin.debug:
msg: "pubkey file exists, but is empty"
when:
- lego_openbao_certificate_stat.stat.exists
- lego_openbao_certificate_stat.stat.size == 0
changed_when: true
notify: [ "Run lego-openbao" ]
tags: [ "deploy", "deploy-lego" ]

- name: "Ensure systemd unit file for lego-openbao is templated"
ansible.builtin.template:
src: "lego-openbao.service.j2"
dest: "{{ lego_openbao_systemd_path }}/lego-openbao.service"
owner: "root"
group: "root"
mode: "0640"
notify: [ "Reload systemd" ]
tags: [ "deploy", "deploy-lego" ]

- name: "Ensure systemd timer for lego-openbao is templated"
ansible.builtin.template:
src: "lego-openbao.timer.j2"
dest: "{{ lego_openbao_systemd_path }}/lego-openbao.timer"
owner: "root"
group: "root"
mode: "0640"
notify: [ "Reload systemd" ]
tags: [ "deploy", "deploy-lego" ]

- name: "Flush handlers to ensure systemd has loaded the unit files"
ansible.builtin.meta: "flush_handlers"

- name: "Enable lego-openbao.service"
ansible.builtin.systemd:
name: "lego-openbao.service"
enabled: true
tags: [ "deploy", "deploy-lego" ]

- name: "Enable lego-openbao.timer"
ansible.builtin.systemd:
name: "lego-openbao.timer"
state: "started"
enabled: true
tags: [ "deploy", "deploy-lego" ]
39 changes: 39 additions & 0 deletions roles/lego_openbao/templates/lego-openbao.service.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
[Unit]
Description=Fetch certificates from OpenBao

[Service]
StandardOutput=journal
StandardError=journal
SyslogIdentifier=lego-openbao
{% for envvar in lego_openbao_configuration_merged.environment %}
Environment={{ envvar }}={{ lego_openbao_configuration_merged.environment[envvar] }}
{% endfor %}
Type=oneshot
Group={{ lego_openbao_user_res.group }}
User={{ lego_openbao_user_res.name }}
WorkingDirectory={{ lego_openbao_base_path }}
ExecStartPre=+-chown {{ lego_openbao_certificate_store_user }}:{{ lego_openbao_certificate_store_group }} {{ lego_openbao_certificate_store }}
ExecStartPre=+-chmod {{ lego_openbao_certificate_store_mode }} {{ lego_openbao_certificate_store }}
{% if lego_openbao_pre_renewal_script is defined %}
ExecStartPre=+-{{ lego_openbao_pre_renewal_script }}
{% endif %}
ExecStart={{ lego_openbao_command_systemd }}
ExecStartPost=+-/bin/sh -c 'chown {{ lego_openbao_certificate_store_user }}:{{ lego_openbao_certificate_store_group }} {{ lego_openbao_certificate_store }}/*'
ExecStartPost=+-/bin/sh -c 'chmod 0640 {{ lego_openbao_certificate_store }}/*'
{% if lego_openbao_enable_metrics_textfile %}
ExecStartPost=+-/usr/bin/python3 {{ lego_openbao_base_path }}/metrics-textfile.py
{% endif %}
{% if lego_openbao_post_renewal_script is defined %}
{# see https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_tests.html#type-tests #}
{# list is not string or mapping but iterable #}
{% if lego_openbao_post_renewal_script is not string and lego_openbao_post_renewal_script is not mapping and lego_openbao_post_renewal_script is iterable and lego_openbao_post_renewal_script | length > 0 %}
{% for hook in lego_openbao_post_renewal_script %}
ExecStartPost=+-{{ hook }}
{% endfor %}
{% else %}
ExecStartPost=+-{{ lego_openbao_post_renewal_script }}
{% endif %}
{% endif %}

[Install]
WantedBy=multi-user.target
26 changes: 26 additions & 0 deletions roles/lego_openbao/templates/lego-openbao.timer.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[Unit]
Description=Timer for lego-openbao

[Timer]
{% if lego_openbao_timer_on_boot | bool %}
OnBootSec={{ lego_openbao_timer_on_boot }}
{% endif %}
{% if lego_openbao_timer_on_calendar %}
OnCalendar={{ lego_openbao_timer_on_calendar }}
{% else %}
OnActiveSec={{ lego_openbao_timer_interval }}
{% endif %}
{% if lego_openbao_timer_accuracy %}
AccuraySec={{ lego_openbao_timer_accuracy }}
{% endif %}
{% if lego_openbao_timer_random_delay %}
RandomizedDelaySec={{ lego_openbao_timer_random_delay }}
{% endif %}
{% if lego_openbao_timer_persistent %}
Persistent={{ lego_openbao_timer_persistent | string }}
{% endif %}

Unit=lego-openbao.service

[Install]
WantedBy=timers.target
Loading
Loading