Rewrote playbook as a role.
This commit is contained in:
commit
c8086b5d82
0
.gitignore
vendored
Normal file
0
.gitignore
vendored
Normal file
17
LICENSE
Normal file
17
LICENSE
Normal file
@ -0,0 +1,17 @@
|
||||
MIT No Attribution License
|
||||
|
||||
Copyright (c) 2023 Brian Lee
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the “Software”), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
47
README.md
Normal file
47
README.md
Normal file
@ -0,0 +1,47 @@
|
||||
# Ansible Role: lego
|
||||
|
||||
This role runs [acme-lego](https://go-acme.github.io/lego/) on the localhost, such that the acme account and DNS api credentials are never communicated to the server. It also creates boilerplate nginx configuration in accordance with the Mozilla's recomended TLS configuration.
|
||||
|
||||
## Requirements
|
||||
|
||||
The `nginx_config` role which is distributed in the nginx_core collection.
|
||||
|
||||
```yaml
|
||||
collections:
|
||||
- name: nginxinc.nginx_core
|
||||
```
|
||||
|
||||
It's not listed in the `meta` dependencies since that would run the role out of sequence.
|
||||
|
||||
## Role Variables
|
||||
|
||||
You can configure multiple providers and domains with a single ACME account.
|
||||
|
||||
```yaml
|
||||
acme_email: acme@example.com
|
||||
acme_domains:
|
||||
- { domain: myhost.example.com, provider: easydns }
|
||||
```
|
||||
|
||||
## Secrets
|
||||
|
||||
The api keys are sprinkled throughout the task as environment variables until I come up with a smarter way to do that.
|
||||
|
||||
## File Permissions
|
||||
|
||||
File access to the certificates and keys are controlled by way of unix permissions. The files are owned by the acme system user/group, and each service that needs to use the certificates just need to belong to the acme group.
|
||||
|
||||
The playbook could be used to renew certificates from multiple DNS providers. The only provider I'm using currently is Name cheap. You will need to edit the environment variables in the following files if you want to use other providers:
|
||||
|
||||
* main.yml
|
||||
* tasks/certificates.yml
|
||||
|
||||
You can test your results: ssllabs.com/ssltest
|
||||
|
||||
## Example Playbook
|
||||
|
||||
```yaml
|
||||
- hosts: all
|
||||
roles:
|
||||
- bleetube.lego
|
||||
```
|
43
defaults/example.yml
Normal file
43
defaults/example.yml
Normal file
@ -0,0 +1,43 @@
|
||||
---
|
||||
acme_email: acme@example.com
|
||||
acme_domains:
|
||||
- { domain: "{{ inventory_hostname }}", provider: namecheap }
|
||||
|
||||
nginx_config_dhparam: /etc/ssl/dhparams.pem
|
||||
nginx_config_http_template_enable: true
|
||||
nginx_config_http_template:
|
||||
- template_file: http/default.conf.j2
|
||||
deployment_location: "/etc/nginx/acme_{{ acme_domains[0].domain }}.conf"
|
||||
backup: false
|
||||
config:
|
||||
core:
|
||||
server_name: "{{ acme_domains[0].domain }}"
|
||||
ssl:
|
||||
certificate: "{{ acme_path }}/certificates/{{ acme_domains[0].domain }}.crt"
|
||||
certificate_key: "{{ acme_path }}/certificates/{{ acme_domains[0].domain }}.key"
|
||||
trusted_certificate: "{{ acme_path }}/certificates/{{ acme_domains[0].domain }}.issuer.crt"
|
||||
ciphers: ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
|
||||
dhparam: "{{ nginx_config_dhparam }}"
|
||||
ecdh_curve: X25519:secp521r1:secp384r1
|
||||
prefer_server_ciphers: true
|
||||
protocols:
|
||||
- TLSv1.2
|
||||
- TLSv1.3
|
||||
session_cache:
|
||||
shared:
|
||||
name: "{{ acme_domains[0].domain }}"
|
||||
size: 1M
|
||||
session_tickets: false
|
||||
session_timeout: 1d
|
||||
ocsp: true
|
||||
ocsp_cache:
|
||||
name: cache
|
||||
size: 64k
|
||||
stapling: true
|
||||
stapling_verify: true
|
||||
ocsp_responder: http://r3.o.lencr.org
|
||||
headers:
|
||||
add_headers:
|
||||
- name: Strict-Transport-Security
|
||||
value: '"max-age=7776000"'
|
||||
always: true
|
15
defaults/main.yml
Normal file
15
defaults/main.yml
Normal file
@ -0,0 +1,15 @@
|
||||
---
|
||||
acme_path: /var/acme
|
||||
acme_system_user: acme
|
||||
acme_system_group: acme
|
||||
nginx_config_dhparam: "{{ acme_path }}/dhparams.pem"
|
||||
nginx_user: nginx
|
||||
#EASYDNS_TOKEN: "{{ lookup('ansible.builtin.env', 'EASYDNS_TOKEN') }}"
|
||||
#EASYDNS_KEY: "{{ lookup('ansible.builtin.env', 'EASYDNS_KEY') }}"
|
||||
NAMECHEAP_API_USER: "{{ lookup('ansible.builtin.env', 'NAMECHEAP_API_USER') }}"
|
||||
NAMECHEAP_API_KEY: "{{ lookup('ansible.builtin.env', 'NAMECHEAP_API_KEY') }}"
|
||||
|
||||
# lego_path refers to the local ansible user's home directory, used in delegate_to: localhost
|
||||
lego_path: ~/.secrets/acme
|
||||
# This might work if the playbook is executing as the local user:
|
||||
#lego_path: "{{ ansible_env.HOME }}/.secrets/acme/certificates"
|
2
meta/main.yml
Normal file
2
meta/main.yml
Normal file
@ -0,0 +1,2 @@
|
||||
---
|
||||
dependencies: []
|
75
tasks/certificates.yml
Normal file
75
tasks/certificates.yml
Normal file
@ -0,0 +1,75 @@
|
||||
---
|
||||
# These tasks run in a loop for each domain so that we can check for existing certificates
|
||||
# and only order new ones if they don't already exist.
|
||||
|
||||
- name: "Check for an existing certificate for {{ acme_domain.domain }}"
|
||||
ansible.builtin.stat:
|
||||
path: "{{ lego_path }}/certificates/{{ acme_domain.domain }}.crt"
|
||||
register: lego_cert
|
||||
delegate_to: localhost
|
||||
tags: lego
|
||||
|
||||
- name: Instruct lego to register an account and order a new certificate if one doesn't already exist.
|
||||
set_fact:
|
||||
lego_command: "{{ 'renew' if lego_cert.stat.exists else 'run'}}"
|
||||
delegate_to: localhost
|
||||
tags: lego
|
||||
|
||||
- name: Order acme certificates without waiting for propogation of TXT record to all authoritative name servers.
|
||||
ansible.builtin.command:
|
||||
cmd: >
|
||||
lego --path {{ lego_path }} --dns {{ acme_domain.provider }} --domains {{ acme_domain.domain }} --email {{ acme_email }} --dns.disable-cp --accept-tos {{ lego_command }}
|
||||
register: lego_result
|
||||
delegate_to: localhost
|
||||
changed_when: False
|
||||
ignore_errors: true
|
||||
tags: lego
|
||||
environment:
|
||||
# EASYDNS_TOKEN: "{{ EASYDNS_TOKEN }}"
|
||||
# EASYDNS_KEY: "{{ EASYDNS_KEY }}"
|
||||
NAMECHEAP_API_USER: "{{ NAMECHEAP_API_USER }}"
|
||||
NAMECHEAP_API_KEY: "{{ NAMECHEAP_API_KEY }}"
|
||||
|
||||
- name: Print lego output with dns.disable-cp
|
||||
ansible.builtin.debug:
|
||||
var: lego_result
|
||||
delegate_to: localhost
|
||||
tags: lego
|
||||
|
||||
# --dns.disable-cp: disables the need to wait the propagation of the TXT record to all authoritative name servers.
|
||||
# I haven't yet figured out why it only works sporadically with or without this option.
|
||||
- name: Retry the last command if necessary, but wait for propogation of TXT record to all authoritative name servers.
|
||||
ansible.builtin.command:
|
||||
cmd: >
|
||||
lego --path {{ lego_path }} --dns {{ acme_domain.provider }} --domains {{ acme_domain.domain }} --email {{ acme_email }} --accept-tos {{ lego_command }}
|
||||
when: lego_result.failed
|
||||
register: lego_result
|
||||
delegate_to: localhost
|
||||
changed_when: False
|
||||
tags: lego
|
||||
environment:
|
||||
# EASYDNS_TOKEN: "{{ EASYDNS_TOKEN }}"
|
||||
# EASYDNS_KEY: "{{ EASYDNS_KEY }}"
|
||||
NAMECHEAP_API_USER: "{{ NAMECHEAP_API_USER }}"
|
||||
NAMECHEAP_API_KEY: "{{ NAMECHEAP_API_KEY }}"
|
||||
|
||||
- name: Print lego output without dns.disable-cp
|
||||
ansible.builtin.debug:
|
||||
var: lego_result
|
||||
delegate_to: localhost
|
||||
tags: lego
|
||||
|
||||
- name: "Copy certificate files for {{ acme_domain.domain }}."
|
||||
ansible.builtin.copy:
|
||||
src: "{{ lego_path }}/certificates/{{ acme_domain.domain }}.{{ file_extension }}"
|
||||
dest: "{{ acme_path }}/certificates/"
|
||||
owner: "{{ acme_system_user }}"
|
||||
group: "{{ acme_system_group }}"
|
||||
mode: '0640'
|
||||
tags: lego
|
||||
loop:
|
||||
- crt
|
||||
- key
|
||||
- issuer.crt
|
||||
loop_control:
|
||||
loop_var: file_extension
|
21
tasks/dhparams.yml
Normal file
21
tasks/dhparams.yml
Normal file
@ -0,0 +1,21 @@
|
||||
---
|
||||
- name: Check for a pre-generated dhparams file.
|
||||
ansible.builtin.stat:
|
||||
path: files/dhparams.pem
|
||||
register: dhparams
|
||||
delegate_to: localhost
|
||||
tags: dhparams
|
||||
|
||||
- name: Use previously generated dhparams to reduce deployment time by several minutes.
|
||||
ansible.builtin.copy:
|
||||
src: dhparams.pem
|
||||
dest: "{{ acme_path }}/dhparams.pem"
|
||||
force: false
|
||||
when: dhparams.stat.exists
|
||||
tags: dhparams
|
||||
|
||||
# https://docs.ansible.com/ansible/latest/collections/community/crypto/openssl_dhparam_module.html
|
||||
- name: Generate Diffie-Hellman parameters with the default size (4096 bits)
|
||||
community.crypto.openssl_dhparam:
|
||||
path: "{{ acme_path }}/dhparams.pem"
|
||||
tags: dhparams
|
37
tasks/main.yml
Normal file
37
tasks/main.yml
Normal file
@ -0,0 +1,37 @@
|
||||
---
|
||||
- name: Assert all secrets have been configured.
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
# - EASYDNS_TOKEN != ''
|
||||
# - EASYDNS_KEY != ''
|
||||
- NAMECHEAP_API_USER != ''
|
||||
- NAMECHEAP_API_KEY != ''
|
||||
fail_msg: "FAILED: Secrets have not been configured."
|
||||
no_log: true
|
||||
|
||||
- name: Set up the acme system user and group.
|
||||
import_tasks: setup-acme.yml
|
||||
|
||||
- name: Add nginx user to the acme group.
|
||||
ansible.builtin.user:
|
||||
name: "{{ nginx_user }}"
|
||||
groups: "{{ acme_system_group }}"
|
||||
append: true
|
||||
when: acme_system_user != "root"
|
||||
|
||||
- name: Run lego looped task to order or renew certificates for all acme domains.
|
||||
include_tasks: certificates.yml
|
||||
loop: "{{ acme_domains }}"
|
||||
loop_control:
|
||||
loop_var: acme_domain
|
||||
tags: lego
|
||||
|
||||
- name: Loop through the domain list (again) to configure nginx for each ACME domain
|
||||
include_tasks: nginx_conf.yml
|
||||
loop: "{{ acme_domains }}"
|
||||
loop_control:
|
||||
loop_var: acme_domain
|
||||
tags: nginx
|
||||
|
||||
- import_tasks: dhparams.yml
|
||||
tags: dhparams
|
44
tasks/nginx_conf.yml
Normal file
44
tasks/nginx_conf.yml
Normal file
@ -0,0 +1,44 @@
|
||||
---
|
||||
- name: Configure nginx TLSv1.2 for {{ acme_domain.domain }}
|
||||
ansible.builtin.import_role:
|
||||
name: nginxinc.nginx_core.nginx_config
|
||||
allow_duplicates: true
|
||||
tags: nginx
|
||||
vars:
|
||||
nginx_config_http_template_enable: true
|
||||
nginx_config_http_template:
|
||||
- template_file: http/default.conf.j2
|
||||
deployment_location: "/etc/nginx/acme_{{ acme_domain.domain }}.conf"
|
||||
backup: false
|
||||
config:
|
||||
core:
|
||||
server_name: "{{ acme_domain.domain }}"
|
||||
ssl:
|
||||
certificate: "{{ acme_path }}/certificates/{{ acme_domain.domain }}.crt"
|
||||
certificate_key: "{{ acme_path }}/certificates/{{ acme_domain.domain }}.key"
|
||||
trusted_certificate: "{{ acme_path }}/certificates/{{ acme_domain.domain }}.issuer.crt"
|
||||
ciphers: ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
|
||||
dhparam: "{{ nginx_config_dhparam }}"
|
||||
ecdh_curve: X25519:secp521r1:secp384r1
|
||||
prefer_server_ciphers: true
|
||||
protocols:
|
||||
- TLSv1.2
|
||||
- TLSv1.3
|
||||
session_cache:
|
||||
shared:
|
||||
name: "{{ acme_domain.domain }}"
|
||||
size: 1M
|
||||
session_tickets: false
|
||||
session_timeout: 1d
|
||||
ocsp: true
|
||||
ocsp_cache:
|
||||
name: cache
|
||||
size: 64k
|
||||
stapling: true
|
||||
stapling_verify: true
|
||||
ocsp_responder: http://r3.o.lencr.org
|
||||
headers:
|
||||
add_headers:
|
||||
- name: Strict-Transport-Security
|
||||
value: '"max-age=7776000"'
|
||||
always: true
|
35
tasks/setup-acme.yml
Normal file
35
tasks/setup-acme.yml
Normal file
@ -0,0 +1,35 @@
|
||||
---
|
||||
- name: Get nologin path for acme user
|
||||
ansible.builtin.find:
|
||||
paths:
|
||||
- /bin
|
||||
- /sbin
|
||||
- /usr/bin
|
||||
- /usr/sbin
|
||||
patterns: nologin
|
||||
register: nologin_bin
|
||||
|
||||
- name: Create the acme group
|
||||
ansible.builtin.group:
|
||||
name: "{{ acme_system_group }}"
|
||||
state: present
|
||||
system: true
|
||||
when: acme_system_group != "root"
|
||||
|
||||
- name: Create the acme system user
|
||||
ansible.builtin.user:
|
||||
name: "{{ acme_system_user }}"
|
||||
groups: "{{ acme_system_group }}"
|
||||
shell: "{{ nologin_bin.files[0].path }}"
|
||||
system: true
|
||||
create_home: false
|
||||
home: "{{ acme_path }}"
|
||||
when: acme_system_user != "root"
|
||||
|
||||
- name: Ensure acme_path exists.
|
||||
ansible.builtin.file:
|
||||
path: "{{ acme_path }}"
|
||||
owner: "{{ acme_system_user }}"
|
||||
group: "{{ acme_system_group }}"
|
||||
state: directory
|
||||
mode: '0750'
|
Loading…
Reference in New Issue
Block a user