commit 0fb24730a104932671b5c5d0f2bc16223430be21 Author: Nick Janetakis Date: Sat Sep 29 11:19:18 2018 -0400 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..59053d4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +*/**.DS_Store +._* +.*.sw* +*~ +.idea/ +.vscode/ +*.retry diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..ebeda4b --- /dev/null +++ b/.travis.yml @@ -0,0 +1,17 @@ +--- + +services: "docker" + +env: + - distro: "ubuntu1604" + - distro: "ubuntu1804" + - distro: "debian8" + - distro: "debian9" + +script: + # Download test shim. + - wget -O ${PWD}/tests/test.sh https://gist.githubusercontent.com/nickjj/d12353b5b601e33cd62fda111359957a/raw + - chmod +x ${PWD}/tests/test.sh + + # Run tests. + - ${PWD}/tests/test.sh diff --git a/CHANGES.md b/CHANGES.md new file mode 100755 index 0000000..fca77f5 --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,7 @@ +# Changelog + +### v1.0.0 + +*Released: September 29th 2018* + +- Initial release diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c25a72d --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2018 Nick Janetakis nick.janetakis@gmail.com + +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, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2fef9b1 --- /dev/null +++ b/README.md @@ -0,0 +1,397 @@ +## What is ansible-acme-sh? [![Build Status](https://secure.travis-ci.org/nickjj/ansible-acme-sh.png)](http://travis-ci.org/nickjj/ansible-acme-sh) + +It is an [Ansible](http://www.ansible.com/home) role to: + +- Install acme.sh to issue, renew or remove Let's Encrypt based SSL certificates +- Issue certificates for single, multiple or wildcard domains +- Configure multiple domains through 1 certificate or separate certificates +- Issue DNS based challenges using acme.sh's automated DNS API feature +- Run custom acme.sh commands if the presets are not enough for you + +## Why would you want to use this role? + +This role uses [acme.sh](https://github.com/Neilpang/acme.sh) which is a self +contained Bash script to handle all of the complexities of issuing and +automatically renewing your SSL certificates. + +This role's goals are to be highly configurable but have enough sane defaults +so that you can get going by supplying nothing more than a list of domain names, +setting your DNS provider and supplying your DNS provider's API key. + +It's also idempotent for every task because that's the only way I roll! + +#### Why is DNS based challenges the only supported method? + +Having challenges done through DNS means you can set up your certificates before +your web server or proxy is provisioned. It also means your web server doesn't +need to know anything about how the ACME challenge works. All you have to do is +reference the final certificates this role generates. + +Another perk is if you're running a web server inside of Docker, you might not +have that up and running until after your server has been provisioned by Ansible. +For example, it's common to set up git based deploys to kick off an app deploy. + +Also, it's nice using DNS challenges because DNS challenges are the only way to +issue wildcard certificates using Let's Encrypt. Focusing efforts onto 1 solution +that works with all certificate types seemed like the right move. + +With that said, I probably won't be supporting other modes such as standalone, +webroot, nginx or Apache but nothing is set in stone. + +## Supported platforms + +- Ubuntu 16.04 LTS (Xenial) +- Ubuntu 18.04 LTS (Bionic) +- Debian 8 (Jessie) +- Debian 9 (Stretch) + +## Role variables + +``` +# The user on the system that acme.sh will run as. Keep in mind this user +# needs to already exist, this role will not create it. +acme_sh_become_user: "root" + +# The acme.sh repo to clone. +acme_sh_git_url: "https://github.com/Neilpang/acme.sh" + +# The branch, tag or commit that will be cloned. +acme_sh_git_version: "master" + +# By default if you were to clone this repo now and then 6 months from now you +# clonged it again, it will stick with the old master version from 6 months ago. +# If you want to pull the latest master version on every run, set this to True. +acme_sh_git_update: False + +# Where will this repo get cloned to? +acme_sh_git_clone_dest: "/usr/local/src/acme.sh" + +# When enabled, acme.me will upgrade itself to the latest version which is +# separate from updating the git repo. That's because acme.sh installs itself +# with an installer after cloning the source code. +# +# Enabling it could be good to get the latest release which may have bug fixes +# but keep in mind if you do this, you may get different results per run. I +# recommend occasionally setting this to True but keeping it disabled usually. +acme_sh_upgrade: False + +# When enabled the cloned source code, installation path, log files and renewal +# cron jobs will be removed. +# +# Installed certificates will not be removed. If you want to remove the installed +# certificates there is another option for that which we'll cover later. +acme_sh_uninstall: False + +# When creating an initial Let's Encrypt account, you can optionally supply an +# email address. By default this isn't set, but feel free to add your email +# address in if you want. If you do set it, you'll get emailed when your +# certificates are within 20 days of expiring. +# +# I highly recommend setting this because if all goes as planned you'll never +# get emailed unless acme.sh malfunctioned and failed to renew a certificate. +acme_sh_account_email: "" + +# Certificates will be renewed through an acme.sh managed cron job. By default +# acme.sh uses 60 days for each renewal attempt, but I've chosen to go with 30 +# by default to give 1 extra attempt in case something unexpected happens. +# +# Certificates that don't need to be renewed will be skipped by acme.sh, so +# it's all good. It's also worth mentioning this value cannot be > 60 days which +# is a limit enforced by acme.sh, this role does not double check the value. +acme_sh_renew_time_in_days: 30 + +# The base path where certificates will be copied into. If you're familiar with +# acme.sh, this is for the certificates generated with --install-cert. +# +# This is the final destination for your certificates and the user you've chosen +# will need write access to this path. This path will end up being having its +# owner:group set to the acme_sh_become_user's value. +acme_sh_copy_certs_to_path: "/etc/ssl/ansible" + +# At the end of the run, an Ansible debug message will print out a list of +# domains that have valid SSL certificates along with their expiration dates. +# You can disable this by setting it to False. +acme_sh_list_domains: True + +# When set to False, it will use the live Let's Encrypt servers, so please make +# sure everything works with staging True or you may find yourself rate limited. +# +# It is worth mentioning you'll need to force issue a new certificate when +# swiching between staging and live or vice versa. +acme_sh_default_staging: True + +# When set to True, this will regenerate a new certificate even if your list of +# domains didn't change. It's also used to set a new DNS provider and API keys. +# +# Be careful with this because you may get rate limited if on the live server. +# Only consider using this to update your DNS provider. You should set it back +# to False when you're done. +acme_sh_default_force_issue: False + +# When set to True, this will regenerate a new certificate for an existing list +# of certificates. This will not update your DNS provider or API keys. +# +# This could be useful to use if your certificates expired. You should set it +# back to False when you're done. +acme_sh_default_force_renew: False + +# When set to True, this will provide more detailed information to STDOUT. This +# could be useful if you're testing the role in staging mode. +acme_sh_default_debug: False + +# Which DNS provider should you use? +# A list of supported providers can be found at: +# https://github.com/Neilpang/acme.sh#7-automatic-dns-api-integration +# As for getting the name to use, you can find that at: +# https://github.com/Neilpang/acme.sh/tree/master/dnsapi +# +# It defaults to DigitalOcean. Make sure to include the dns_ part of the name, +# but leave off the .sh file extension. +acme_sh_default_dns_provider: "dns_dgon" + +# What are your DNS provider's API key(s)? +# The key names to use can be found at: +# https://github.com/Neilpang/acme.sh/tree/master/dnsapi +# +# The API key can be created on your DNS provider's website. Some providers +# require 1 key, while others require 2+. Just add them as key / value pairs here +# without the "export ". +# +# For example if you were using DigitalOcean you would enter: +# acme_sh_default_dns_provider_api_keys: +# "DO_API_KEY": "THE_API_SECRET_TOKEN_FROM_THE_DO_DASHBOARD" +acme_sh_default_dns_provider_api_keys: {} + +# How long should acme.sh sleep after attempting to set the TXT record to your +# DNS records? Some DNS providers do not update as fast as others. +# +# 120 is the default value from acme.sh itself but keep in mind if you use +# NameSilo, their DNS updates only propagate once per 15 minutes so you'll need +# to set this value to 900 or you run the risk of getting a DNS NXDOMAIN error. +# +# I recommend keeping it set to 120 or higher if your DNS provider requires it. +# +# Although as an aside, I used 10 when testing this role against DigitalOcean +# and it worked about 30 times in a row. Still, in production I would use 120 +# just to be safe because this 2 minute delay will only affect you on the first +# Ansible run. After that it will be updated in the background through a cron job. +acme_sh_default_dns_sleep: 120 + +# When issuing new certificates, you can choose to add additional flags that +# are not present here by default. Supply them just as you would on the command +# line, such as "--help". +acme_sh_default_extra_flags_issue: "" + +# When renewing certificates, you can choose to add additional flags that +# are not present here by default. Supply them just as you would on the command +# line, such as "--help". +acme_sh_default_extra_flags_renew: "" + +# When installing certificates, you can choose to add additional flags that +# are not present here by default. Supply them just as you would on the command +# line, such as "--help". +# +# Installing is different than issuing and we'll cover that later. +acme_sh_default_extra_flags_install_cert: "" + +# When a certificate is issued or renewed, acme.sh will attempt to run a command +# of your choosing. This could be to restart or reload your web server or proxy. +# +# Keep in mind the user you set in acme_sh_become_user needs access rights to +# sudo if you use sudo here, or if not, they need access rights to reload your +# web server / proxy. +# +# For a Docker example, check the example section of this README. +acme_sh_default_install_cert_reloadcmd: "sudo systemctl reload nginx" + +# If you need more fine grain control than the reloadcmd you can hook into the +# life cycle of issuing or renewing a certificate. By default the following 3 +# options do nothing unless you fill them out. They are not needed for everything +# to function as long as your reloadcmd works. +# +# When a certificate is issued or renewed, acme.sh will attempt to run a command +# before attempting to issue a certificate. This can only be applied while +# issuing a certificate but it will be saved and used for renewing as well. +# +# This will execute even if the certificate wasn't successfully issued / renewed. +acme_sh_default_issue_pre_hook: "" + +# When a certificate is issued or renewed, acme.sh will attempt to run a command +# after attempting to issue a certificate. This can only be applied while +# issuing a certificate but it will be saved and used for renewing as well. +# +# This will execute even if the certificate wasn't successfully issued / renewed. +acme_sh_default_issue_post_hook: "" + +# When a certificate is issued or renewed, acme.sh will attempt to run a command +# after a certificate is successfully renewed. This can only be applied while +# issuing a certificate but it will be saved and used for renewing as well. +# +# This will only execute if the certificate was successfully issued / renewed. +acme_sh_default_issue_renew_hook: "" + +# When set to True, certificates will be removed and unset from being renewed +# instead of being created and set for renewal. This will not uninstall acme.sh. +acme_sh_default_remove: False + +# This list contains a list of domains, along with key / value pairs to +# configure each set of domains individually. +# +# Here's an example with every available option documented, and a couple of real +# examples will also be included in the example section of this README: +acme_sh_domains: +# A list of 1 or more domains, you can use ["*.example.com" ,"example.com] for +# setting a wildcard + root domain certificate. Domains listed here will +# all belong to the same certificate. If you want separate certificate files +# then create a new "domains:" item in the list. +# +# The first domain in the list will end up being used as a base file name for +# the certificate name. In this case it would be "example.com.pem". +# - domains: ["example.com", "www.example.com] +# # Optionally override the default staging variable. This overall pattern lets +# # you situationally override the defaults listed above for each domain list. +# staging: False +# # Optionally force issue new certificates. +# force_issue: False +# # Optionally force renew certificates. +# force_renew: False +# # Optionally turn on debug mode. +# debug: True +# # Optionally override the default DNS provider. +# dns_provider: "dns_namesilo" +# # Optionally override the default DNS API keys. +# dns_provider_api_keys: +# "Namesilo_Key": "THE_API_SECRET_TOKEN_FROM_THE_NAMESILO_DASHBOARD" +# # Optionally override the default DNS sleep time. +# dns_sleep: 900 +# # Optionally add extra flags to any of these 3 actions: +# extra_flags_issue: "" +# extra_flags_renew: "" +# extra_flags_install_cert: "" +# # Optionally set a different reload command. +# install_cert_reloadcmd: "whoami" +# # Optionally run commands during different points in the cert issue process: +# extra_issue_pre_hook: "" +# extra_issue_post_hook: "" +# extra_issue_renew_hook: "" +# # Optionally remove and disable the certificate. +# remove: True + +# How long should the apt-cache last in seconds? +acme_sh_apt_cache_time: 86400 +``` + +## Example usage + +For the sake of this example let's assume you have a group called **app** and +you have a typical `site.yml` file. + +To use this role edit your `site.yml` file to look something like this: + +``` +--- + +- name: Configure app server(s) + hosts: "app" + become: True + + roles: + - { role: "nickjj.acme-sh", tags: ["acme-sh"] } +``` + +Here's a few examples. You can recreate this example on your end by opening or +creating `group_vars/app.yml` which is located relative to your `inventory` +directory and then making it look like this: + +``` +--- + +acme_sh_account_email: "you@example.com" + +# An example where a DNS provider has 2 keys for API access: +acme_sh_default_dns_provider: "dns_cf" +acme_sh_default_dns_provider_api_keys: + "CF_Key": "THE_API_SECRET_TOKEN_FROM_THE_CLOUDFLARE_DASHBOARD" + "CF_Email: "you@example.com" + +# Reloading nginx inside of a Docker container that is named "nginx". +# If you are running nginx in a Docker container then you'll also need to volume +# mount in your certificates, but I'm sure you knew that already! +acme_sh_default_install_cert_reloadcmd: "docker exec nginx nginx -s reload" + +# --- Here's a few different acme_sh_domains examples -------------------------- +# You would only need to supply one of these based on what you wanted to do! +# ------------------------------------------------------------------------------ + +# 1 certificate file for all of the domains. +acme_sh_domains: + - domains: ["example.com", "www.example.com", "admin.example.com"] + +# Produces this on your server: +# /etc/ssl/ansible/example.com.key (the private key) +# /etc/ssl/ansible/example.com.pem (the full chain certificate) + +# ------------------------------------------------------------------------------ + +# 2 certificate files using the same domains as above. +acme_sh_domains: + - domains: ["example.com", "www.example.com"] + - domains: ["admin.example.com"] + +# Produces this on your server: +# /etc/ssl/ansible/example.com.key (the private key) +# /etc/ssl/ansible/example.com.pem (the full chain certificate) +# /etc/ssl/ansible/admin.example.com.key (the private key) +# /etc/ssl/ansible/admin.example.com.pem (the full chain certificate) + +# ------------------------------------------------------------------------------ + +# 2 certificate files using the same example but the admin certificate will get +# removed and disabled. +acme_sh_domains: + - domains: ["example.com", "www.example.com"] + - domains: ["admin.example.com"] + remove: True + +# Produces this on your server: +# /etc/ssl/ansible/example.com.key (the private key) +# /etc/ssl/ansible/example.com.pem (the full chain certificate) + +# ------------------------------------------------------------------------------ + +# 2 certificate files using the same example but switching from staging to live +# on admin.example.com (but remember to remove force_issue after it runs once). +acme_sh_domains: + - domains: ["example.com", "www.example.com"] + - domains: ["admin.example.com"] + staging: False + force_issue: True + +# ------------------------------------------------------------------------------ + +# 2 certificate files using the same example but forcing a renew on +# admin.example.com (let's say a catastrophic error happened and the cert expired). +acme_sh_domains: + - domains: ["example.com", "www.example.com"] + - domains: ["admin.example.com"] + force_renew: True +``` + +*If you're looking for an Ansible role to create users, then check out my +[user role](https://github.com/nickjj/ansible-user)*. + +Now you would run `ansible-playbook -i inventory/hosts site.yml -t acme-sh`. + +## Installation + +`$ ansible-galaxy install nickjj.acme-sh` + +## Ansible Galaxy + +You can find it on the official +[Ansible Galaxy](https://galaxy.ansible.com/nickjj/acme-sh/) if you want to +rate it. + +## License + +MIT diff --git a/defaults/main.yml b/defaults/main.yml new file mode 100644 index 0000000..174a6de --- /dev/null +++ b/defaults/main.yml @@ -0,0 +1,46 @@ +--- + +acme_sh_become_user: "root" + +acme_sh_git_url: "https://github.com/Neilpang/acme.sh" +acme_sh_git_version: "master" +acme_sh_git_update: False +acme_sh_git_clone_dest: "/usr/local/src/acme.sh" + +acme_sh_upgrade: False +acme_sh_uninstall: False + +acme_sh_account_email: "" + +acme_sh_renew_time_in_days: 30 + +acme_sh_copy_certs_to_path: "/etc/ssl/ansible" + +acme_sh_list_domains: True + +acme_sh_default_staging: True + +acme_sh_default_force_issue: False +acme_sh_default_force_renew: False + +acme_sh_default_debug: False + +acme_sh_default_dns_provider: "dns_dgon" +acme_sh_default_dns_provider_api_keys: {} +acme_sh_default_dns_sleep: 120 + +acme_sh_default_extra_flags_issue: "" +acme_sh_default_extra_flags_renew: "" +acme_sh_default_extra_flags_install_cert: "" + +acme_sh_default_install_cert_reloadcmd: "sudo service nginx reload" + +acme_sh_default_issue_pre_hook: "" +acme_sh_default_issue_post_hook: "" +acme_sh_default_issue_renew_hook: "" + +acme_sh_default_remove: False + +acme_sh_domains: [] + +acme_sh_apt_cache_time: 86400 diff --git a/meta/main.yml b/meta/main.yml new file mode 100644 index 0000000..e20e62d --- /dev/null +++ b/meta/main.yml @@ -0,0 +1,27 @@ +--- + +galaxy_info: + role_name: "acme-sh" + author: "Nick Janetakis" + description: "Install and auto-renew SSL certificates with Let's Encrypt using acme.sh." + license: "license (MIT)" + min_ansible_version: 2.5 + + platforms: + - name: "Ubuntu" + versions: + - "xenial" + - "bionic" + - name: "Debian" + versions: + - "jessie" + - "stretch" + + galaxy_tags: + - "https" + - "networking" + - "security" + - "ssl" + - "system" + +dependencies: [] diff --git a/tasks/main.yml b/tasks/main.yml new file mode 100644 index 0000000..e161884 --- /dev/null +++ b/tasks/main.yml @@ -0,0 +1,233 @@ +--- + +- name: Install dependencies + apt: + name: "{{ item }}" + update_cache: True + cache_valid_time: "{{ acme_sh_apt_cache_time }}" + loop: ["cron", "git", "wget"] + when: not acme_sh_uninstall + +- name: Create git clone path + file: + path: "{{ acme_sh_git_clone_dest | dirname }}" + state: "directory" + owner: "{{ acme_sh_become_user }}" + group: "{{ acme_sh_become_user }}" + mode: "0755" + when: not acme_sh_uninstall + +- name: Git clone https://github.com/Neilpang/acme.sh + git: + repo: "{{ acme_sh_git_url }}" + version: "{{ acme_sh_git_version }}" + dest: "{{ acme_sh_git_clone_dest }}" + update: "{{ acme_sh_git_update }}" + when: not acme_sh_uninstall + become_user: "{{ acme_sh_become_user }}" + +- name: Install acme.sh + command: >- + ./acme.sh --install --log + --days {{ acme_sh_renew_time_in_days }} + {{ "--accountemail " + acme_sh_account_email if acme_sh_account_email else "" }} + args: + chdir: "{{ acme_sh_git_clone_dest }}" + creates: "~/.acme.sh/acme.sh" + when: not acme_sh_uninstall + become_user: "{{ acme_sh_become_user }}" + +- name: Determine if acme.sh is installed + stat: + path: "~/.acme.sh/acme.sh" + register: is_acme_sh_installed + become_user: "{{ acme_sh_become_user }}" + +- name: Upgrade acme.sh + command: ./acme.sh --upgrade + args: + chdir: "~/.acme.sh" + when: + - acme_sh_upgrade + - is_acme_sh_installed.stat.exists + - not acme_sh_uninstall + register: upgrade_result + changed_when: upgrade_result.rc == 0 and "Upgrade success" in upgrade_result.stdout + become_user: "{{ acme_sh_become_user }}" + +- name: Create certificate path + file: + path: "{{ acme_sh_copy_certs_to_path }}" + state: "directory" + owner: "{{ acme_sh_become_user }}" + group: "{{ acme_sh_become_user }}" + mode: "0755" + when: not acme_sh_uninstall + +- name: Uninstall acme.sh and disable all certificate renewals + command: ./acme.sh --uninstall + args: + chdir: "~/.acme.sh" + when: + - acme_sh_uninstall + - is_acme_sh_installed.stat.exists + become_user: "{{ acme_sh_become_user }}" + +- name: Remove acme.sh certificate(s) renewals from cron job + command: >- + ./acme.sh --remove -d {{ item.domains | first }} + {{ "--debug" if item.debug | default(acme_sh_default_debug) else "" }} + args: + chdir: "~/.acme.sh" + removes: "~/.acme.sh/{{ item.domains | first }}" + loop: "{{ acme_sh_domains }}" + when: + - acme_sh_domains and item.domains is defined and item.domains + - item.remove is defined and item.remove + - not acme_sh_uninstall + become_user: "{{ acme_sh_become_user }}" + register: remove_result + +- name: Remove acme.sh internal certificate files + file: + path: "~/.acme.sh/{{ item.domains | first }}" + state: "absent" + when: + - acme_sh_domains and item.domains is defined and item.domains + - item.remove is defined and item.remove + - not acme_sh_uninstall + loop: "{{ acme_sh_domains }}" + become_user: "{{ acme_sh_become_user }}" + +- name: Remove acme.sh installed certificate files + file: + path: "{{ acme_sh_copy_certs_to_path }}/{{ item.domains | first }}*" + state: "absent" + loop: "{{ acme_sh_domains }}" + when: + - acme_sh_domains and item.domains is defined and item.domains + - item.remove is defined and item.remove + - not acme_sh_uninstall + +- name: Remove acme.sh's cloned source code, installation path and log files + file: + path: "{{ item }}" + state: "absent" + loop: + - "{{ acme_sh_git_clone_dest }}" + - "~/.acme.sh" + when: + - acme_sh_uninstall + become_user: "{{ acme_sh_become_user }}" + +- name: Run custom acme.sh command + command: ./acme.sh {{ item.custom_command }} + args: + chdir: "~/.acme.sh" + environment: "{{ item.dns_provider_api_keys | default(acme_sh_default_dns_provider_api_keys) }}" + loop: "{{ acme_sh_domains }}" + when: + - acme_sh_domains and item.domains is defined and item.domains + - item.dns_provider | default(acme_sh_default_dns_provider) + - item.dns_provider_api_keys | default(acme_sh_default_dns_provider_api_keys) + - item.custom_command is defined and item.custom_command + - item.remove is undefined or not item.remove + - not acme_sh_uninstall + become_user: "{{ acme_sh_become_user }}" + +- name: Issue acme.sh certificate(s) (this will sleep for dns_sleep seconds) + command: >- + ./acme.sh --issue -d {{ item.domains | join(" -d ") }} + --dns {{ item.dns_provider | default(acme_sh_default_dns_provider) }} + --dnssleep {{ item.dns_sleep | default(acme_sh_default_dns_sleep) }} + {{ "--force" if item.force_issue | default(acme_sh_default_force_issue) else "" }} + {{ "--staging" if item.staging | default(acme_sh_default_staging) else "" }} + {{ "--debug" if item.debug | default(acme_sh_default_debug) else "" }} + {{ "--pre-hook " + '"' + item.issue_pre_hook | default(acme_sh_default_issue_pre_hook) + '"' if item.issue_pre_hook | default(acme_sh_default_issue_pre_hook) else "" }} + {{ "--post-hook " + '"' + item.issue_post_hook | default(acme_sh_default_issue_post_hook) + '"' if item.issue_post_hook | default(acme_sh_default_issue_post_hook) else "" }} + {{ "--renew-hook " + '"' + item.issue_renew_hook | default(acme_sh_default_issue_renew_hook) + '"' if item.issue_renew_hook | default(acme_sh_default_issue_renew_hook) else "" }} + {{ item.extra_flags_issue | default(acme_sh_default_extra_flags_issue) }} + args: + chdir: "~/.acme.sh" + environment: "{{ item.dns_provider_api_keys | default(acme_sh_default_dns_provider_api_keys) }}" + loop: "{{ acme_sh_domains }}" + when: + - acme_sh_domains and item.domains is defined and item.domains + - item.dns_provider | default(acme_sh_default_dns_provider) + - item.dns_provider_api_keys | default(acme_sh_default_dns_provider_api_keys) + - item.force_renew is undefined or not item.force_renew + - item.custom_command is undefined or not item.custom_command + - item.remove is undefined or not item.remove + - not acme_sh_uninstall + become_user: "{{ acme_sh_become_user }}" + register: issue_result + changed_when: issue_result.rc == 0 and "Cert success" in issue_result.stdout + failed_when: issue_result.rc != 0 and "Domains not changed" not in issue_result.stdout + +- name: Force renew acme.sh certificate(s) + command: >- + ./acme.sh --renew -d {{ item.domains | first }} --force + {{ "--debug" if item.debug | default(acme_sh_default_debug) else "" }} + {{ item.extra_flags_renew | default(acme_sh_default_extra_flags_renew) }} + args: + chdir: "~/.acme.sh" + loop: "{{ acme_sh_domains }}" + when: + - acme_sh_domains and item.domains is defined and item.domains + - item.force_issue is undefined or not item.force_issue + - item.force_renew is defined and item.force_renew + - item.remove is undefined or not item.remove + - not acme_sh_uninstall + become_user: "{{ acme_sh_become_user }}" + register: renew_result + failed_when: renew_result.rc != 0 and "Reload error for" not in renew_result.stderr + +- name: Ensure installed certificates have correct user / group ownership + file: + path: "{{ acme_sh_copy_certs_to_path }}/{{ item.domains | first }}*" + group: "{{ acme_sh_become_user }}" + owner: "{{ acme_sh_become_user }}" + loop: + - "{{ acme_sh_domains }}" + when: + - acme_sh_domains and item.domains is defined and item.domains + - item.custom_command is undefined or not item.custom_command + - item.remove is undefined or not item.remove + - not acme_sh_uninstall + +- name: Install acme.sh certificate(s) + command: >- + ./acme.sh --install-cert -d {{ item.domains | first }} + --key-file {{ acme_sh_copy_certs_to_path }}/{{ item.domains | first }}.key + --fullchain-file {{ acme_sh_copy_certs_to_path }}/{{ item.domains | first }}.pem + --reloadcmd "{{ item.install_cert_reloadcmd | default(acme_sh_default_install_cert_reloadcmd) }}" + {{ "--debug" if item.debug | default(acme_sh_default_debug) else "" }} + {{ item.extra_flags_install_cert | default(acme_sh_default_extra_flags_install_cert) }} + args: + chdir: "~/.acme.sh" + loop: "{{ acme_sh_domains }}" + loop_control: + index_var: domains_index + when: + - acme_sh_domains and item.domains is defined and item.domains + - item.custom_command is undefined or not item.custom_command + - item.remove is undefined or not item.remove + - not acme_sh_uninstall + become_user: "{{ acme_sh_become_user }}" + register: install_cert_result + changed_when: issue_result.results[domains_index].changed or renew_result.results[domains_index].changed + failed_when: install_cert_result.rc != 0 and "Reload error for" not in install_cert_result.stderr + +- name: Register acme.sh certificate information + command: ./acme.sh --list + args: + chdir: "~/.acme.sh" + when: acme_sh_list_domains and not acme_sh_uninstall + changed_when: False + register: list_domains + become_user: "{{ acme_sh_become_user }}" + +- name: List acme.sh certificate information + debug: + msg: "{{ list_domains.stdout_lines }}" + when: acme_sh_list_domains and not acme_sh_uninstall diff --git a/tests/test.yml b/tests/test.yml new file mode 100644 index 0000000..bac19e9 --- /dev/null +++ b/tests/test.yml @@ -0,0 +1,34 @@ +--- + +- hosts: "all" + become: True + + vars: + acme_sh_become_user: "test" + roles: + - "role_under_test" + + pre_tasks: + - name: Add test user + user: + name: "{{ acme_sh_become_user }}" + shell: "/bin/bash" + + post_tasks: + - name: Ensure acme.me was cloned + command: test -d /usr/local/src/acme.sh + register: result_cloned + changed_when: result_cloned.rc != 0 + + - name: Ensure acme.me was installed + command: ./acme.sh --version + args: + chdir: "~/.acme.sh" + register: result_installed + changed_when: result_installed.rc != 0 + become_user: "{{ acme_sh_become_user }}" + + - name: Ensure certificate installation path exists + command: test -d /etc/ssl/ansible + register: result_cert_installed_path + changed_when: result_cert_installed_path.rc != 0