├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTORS ├── README.md ├── defaults └── main.yml ├── files └── .placeholder ├── handlers └── main.yml ├── meta └── main.yml ├── tasks ├── cert.yaml ├── client.yaml ├── main.yml └── test.yaml ├── templates └── .placeholder ├── tests └── test.yml └── vars └── main.yml /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | services: docker 3 | 4 | env: 5 | # ubuntu1604 fails due to apt error: No package matching 'build-essential' is available 6 | # - distro: ubuntu1604 7 | # init: /lib/systemd/systemd 8 | # run_opts: "--privileged --volume=/sys/fs/cgroup:/sys/fs/cgroup:ro" 9 | - distro: ubuntu1404 10 | init: /sbin/init 11 | run_opts: "" 12 | # TODO: add debian 13 | 14 | before_install: 15 | # Pull container. 16 | - 'docker pull geerlingguy/docker-${distro}-ansible:latest' 17 | 18 | script: 19 | - container_id=$(mktemp) 20 | # Run container in detached state. 21 | - 'docker run --detach --volume="${PWD}":/etc/ansible/roles/role_under_test:rw ${run_opts} geerlingguy/docker-${distro}-ansible:latest "${init}" > "${container_id}"' 22 | 23 | # Ansible syntax check. 24 | - 'docker exec --tty "$(cat ${container_id})" env TERM=xterm ansible-playbook /etc/ansible/roles/role_under_test/tests/test.yml --syntax-check' 25 | 26 | # Fix pip - see https://bugs.launchpad.net/ubuntu/+source/python-pip/+bug/1658844 27 | - 'docker exec "$(cat ${container_id})" apt-get update' 28 | - 'docker exec "$(cat ${container_id})" apt-get install -y python-pip' 29 | - 'docker exec "$(cat ${container_id})" python -m pip install -U pip' 30 | - 'docker exec "$(cat ${container_id})" pip install -U pip setuptools' 31 | 32 | # Test role. 33 | - 'docker exec "$(cat ${container_id})" ansible-playbook /etc/ansible/roles/role_under_test/tests/test.yml' 34 | 35 | # Test role idempotence. 36 | - idempotence=$(mktemp) 37 | - docker exec "$(cat ${container_id})" ansible-playbook /etc/ansible/roles/role_under_test/tests/test.yml | tee -a ${idempotence} 38 | - > 39 | tail ${idempotence} 40 | | grep -q 'changed=0.*failed=0' 41 | && (echo 'Idempotence test: pass' && exit 0) 42 | || (echo 'Idempotence test: fail' && exit 1) 43 | 44 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [unreleased] 2 | 3 | ### Added 4 | * Debian 9 "Stretch" is now supported. Instead of using the client from the source, on Debian 9 the Certbot client will be pulled via APT. Note! This means that the version pinning configuration variables in this role have no effect when deploying on Debian 9. Thanks to @bjoas for this addition. 5 | 6 | ## [0.6.1] - 2017-01-27 7 | 8 | ### Fixed 9 | * Fix regression relating to requesting `www.` domains introduced after 0.6.0. The regression caused a `www.` domain not to be requested even if explicitly setting `letsencrypt_request_www` to `true`. Note that this functionality used to work after the release of 0.6.0 AFAIK, but was possibly broken by a behaviour change in Ansible 2.1. 10 | 11 | ## [0.6.0] - 2016-11-19 12 | 13 | ### Important! 14 | * Required Ansible version is now 2.x - see https://github.com/jaywink/ansible-letsencrypt/issues/20 15 | 16 | ### Added 17 | - You can now specify all the parameters given to `certbot` by overriding the new variable `letsencrypt_certbot_default_args`. If you just want to add a parameter, use the old `letsencrypt_certbot_args` variable to add to the defaults already in place. Thanks for @robbyoconnor for this patch. 18 | 19 | ### Fixed 20 | * Cloning Certbot from GitHub was using `depth: 1` for a quicker clone. This was causing problems in changing the version of Certbot later. Fixed by removing the `depth` argument. Thanks @brennen for reporting this issue. 21 | 22 | ## [0.5.0] - 2016-10-10 23 | 24 | ### Backwards incompatible changes 25 | * Apache2 is no longer a dependency of this role and will not be installed. Thanks to @gronke for this patch. This also means `letsencrypt_pause_services` is an empty list by default. Make sure to add your webserver there so that it will be paused. A missing not installed service will not stop the role from executing so you can safely run this role before your main application role. 26 | 27 | ### Fixed 28 | * Settings `letsencrypt_force_renew` to `false` caused Certbot to fail in some situations. Now this is fixed by passing Certbot the flag `--keep-until-expiring`, in the case that forced renewal is not desired. If the certificate is not due for renewal, nothing will be done by Certbot but no error will be raised either. 29 | 30 | ### Changed 31 | * Certbot now runs with the `--non-interactive` flag, which should protect from Ansible hanging on unexpected prompts. **Note! This flag was added in Certbot 0.6.0** which is the lowest version this role can thus support. 32 | * Default version of Certbot installed is now v0.8.1, the latest release as of now. Master branch can have unexpected breakages. Due to this, the cli flag `--no-self-upgrade` was also added to stop Certbot from automatically updating itself. 33 | 34 | ## [0.4.1] - 2016-09-04 35 | 36 | ### Fixed 37 | * There was an error setting `letsencrypt_certbot_args` in 0.4.0. Thanks @gronke for a fast fix. 38 | 39 | ## [0.4.0] - 2016-09-03 40 | 41 | ### Added 42 | * Allow configuring the certbot version with a new variable `letsencrypt_certbot_version`. This defaults to master. Thanks @gronke for this patch! 43 | * Allow configuring what services are stopped when requesting a cert via new variable `letsencrypt_pause_services`. This is a list of items which by default includes `apache2`. You can set this variable empty to skip pausing services. Thanks @gronke for this patch! 44 | * Allow configuring the `--renew-by-default` command line flag to Certbot. By default this is enabled, switch it off by setting `letsencrypt_force_renew` to `false`. Thanks @gronke. 45 | * Additional Certbot command line args can now be passed in using the list variable `letsencrypt_certbot_args`. Thanks @gronke for the addition. 46 | 47 | ### Changed 48 | * Stability changed to "beta" to be less scary :) 49 | 50 | ## [0.3.0] - 2016-06-29 51 | 52 | ### Added 53 | 54 | * Allow specifying `letsencrypt_request_www` to disable requesting `www.` cert automatically. By default it is requested. 55 | 56 | ## [0.2.0] - 2016-05-14 57 | 58 | ### Added 59 | 60 | * Automatically add a `www.` subdomain to the certificate. 61 | 62 | ### Fixed 63 | 64 | * LetsEncrypt client is now Certbot. Adjusted this role to match the new renamed repository. 65 | 66 | ## [0.1.0] - 2016-05-02 67 | 68 | Initial release. 69 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | Jason Robinson (@jaywink) 2 | Robert O'Connor (@robbyoconnor) 3 | Stefan Grönke (@gronke) 4 | bjoas (@bjoas) 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/jaywink/ansible-letsencrypt.svg?branch=master)](https://travis-ci.org/jaywink/ansible-letsencrypt) 2 | [![Ansible Galaxy](https://img.shields.io/badge/ansible--galaxy-letsencrypt-blue.svg?style=flat-square)](https://galaxy.ansible.com/jaywink/letsencrypt) 3 | [![License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](https://tldrlegal.com/license/mit-license) 4 | 5 | ## Ansible LetsEncrypt 6 | 7 | A role to automate LetsEncrypt certificates. 8 | 9 | Stability: beta. 10 | 11 | Ansible version required: 2.x 12 | 13 | ### What does it do? 14 | 15 | This role will pull in the official [Certbot client](https://github.com/certbot/certbot), install it and issue or renew a certificate with your chosen domain. 16 | 17 | Functionality as follows: 18 | * Tested on Ubuntu 14.04 and Debian 8, Debian 9 19 | * One domain per role include only 20 | * Runs in `certonly` mode only 21 | 22 | PR's are welcome to include more functionality. 23 | 24 | ### Installation 25 | 26 | You can install the role directly from Galaxy as follows: 27 | 28 | ansible-galaxy install jaywink.letsencrypt 29 | 30 | ### Details 31 | 32 | #### Cerbot client location and version 33 | 34 | ##### Ubuntu 14.04, Debian 8 35 | 36 | * The client will be installed in `/opt/certbot` as root 37 | * Each run will pull in the Certbot client code from a proven release version. You can set a specific Certbot version using the variable `letsencrypt_certbot_version`. 38 | 39 | ##### Debian 9 40 | 41 | * The client will be installed via APT into the standard platform location according to the latest version in the repositories. 42 | 43 | #### Things to know 44 | 45 | * A list of services to be stopped before and (re-)started after obtaining a new certificate can be configured using the variable `letsencrypt_pause_services`. 46 | * `certonly` mode is used, which means no automatic web server installation 47 | * After cert issuing, you can find it in `/etc/letsencrypt/live/` 48 | * Tip, use this in your Apache2 config, for example, in your main role. Just make sure not to try and start Apache2 with the virtualhost active without the LetsEncrypt role running first! 49 | 50 | ``` 51 | SSLCertificateFile /etc/letsencrypt/live/{{ letsencrypt_domain }}/cert.pem 52 | SSLCertificateKeyFile /etc/letsencrypt/live/{{ letsencrypt_domain }}/privkey.pem 53 | SSLCertificateChainFile /etc/letsencrypt/live/{{ letsencrypt_domain }}/chain.pem 54 | ``` 55 | 56 | * Note! If this role fails in the cert request part, you might have stopped services - take care! 57 | * If the cert has been requested before, this role will automatically try to renew it, if possible. Disable this functionality by setting `letsencrypt_force_renew` to `false`. No renewal will be attempted in this case if cert is not due for renewal. 58 | * A `www.` subdomain will automatically be requested along with the certificate. 59 | * To disable this behaviour, set `letsencrypt_request_www` to `false` in your vars. 60 | 61 | ### Requirements 62 | 63 | Tested with the following: 64 | 65 | * Ubuntu 14.04 and Debian 8, Debian 9 66 | * Apache2 and Nginx 67 | * Ansible 2.x 68 | 69 | ### Role Variables 70 | 71 | #### Required 72 | 73 | * `letsencrypt_domain` - Domain the certificate is for. 74 | * `letsencrypt_email` - Your email as certificate owner. 75 | 76 | #### Optional 77 | 78 | * `letsencrypt_certbot_args` - Additional command line args to be passed to Certbot-- will be combined with `letsencrypt_certbot_default_args`. See [the Certbot docs](https://certbot.eff.org/docs/using.html) for arguments you may pass. 79 | * `letsencrypt_certbot_default_args` - Please see `defaults/main.yml` what the default arguments are. Also, you could add To override all the arguments to Certbot, for example to use another plugin, set them using this variable. 80 | * `letsencrypt_certbot_verbose` - Make Certbot output to console (default `true`). 81 | * `letsencrypt_certbot_version` - Set specific Certbot version, for example a git tag or branch. Note that the lowest version of Certbot we support is 0.6.0. Has no effect on Debian 9. 82 | * `letsencrypt_force_renew` - Whether to attempt renewal always, default to `true`. 83 | * `letsencrypt_pause_services` - List of services to stop/start while calling Certbot. 84 | * `letsencrypt_request_www` - Request `www.` automatically (default `true`). 85 | 86 | ### Example Playbook 87 | 88 | This role works best when included just before your main site role, for example. Or it can be used in an individual playbook, for example as below. 89 | 90 | This role should become root on the target host. 91 | 92 | --- 93 | - hosts: myhost 94 | become: yes 95 | become_user: root 96 | roles: 97 | - role: ansible-letsencrypt 98 | letsencrypt_email: email@example.com 99 | letsencrypt_domain: example.com 100 | letsencrypt_pause_services: 101 | - apache2 102 | 103 | ### License 104 | 105 | MIT 106 | 107 | ### Author Information 108 | 109 | Jason Robinson (@jaywink) - mail@jasonrobinson.me - https://jasonrobinson.me - https://twitter.com/jaywink 110 | 111 | Special thanks to Stefan Grönke (@gronke) for his work on expanding this role. 112 | 113 | See CONTRIBUTORS for a full list of contributors. 114 | -------------------------------------------------------------------------------- /defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This is the certificate admins email - make sure to set it to yours! 3 | letsencrypt_email: yourmail@example.com 4 | # The domain we're requesting/renewing for 5 | letsencrypt_domain: example.com 6 | # Always request www. also? 7 | letsencrypt_request_www: true 8 | # Version/Release tag or branch name of certbot to use 9 | letsencrypt_certbot_version: v0.8.1 10 | # Print Certbot output 11 | letsencrypt_certbot_verbose: true 12 | # Pause these services while updating the certificate 13 | letsencrypt_pause_services: [] 14 | # Force Certificate Reneval 15 | letsencrypt_force_renew: true 16 | # certbot custom arguments -- see: https://certbot.eff.org/docs/using.html 17 | letsencrypt_certbot_args: [] 18 | # default arguments passed to certbot 19 | letsencrypt_certbot_default_args: 20 | - certonly 21 | - --standalone 22 | - --expand 23 | - --text 24 | - -n 25 | - --no-self-upgrade 26 | - -m '{{letsencrypt_email}}' 27 | - --agree-tos 28 | 29 | # This is enabled when running tests - DO NOT TOUCH 30 | letsencrypt_test: false 31 | -------------------------------------------------------------------------------- /files/.placeholder: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaywink/ansible-letsencrypt/221827467f0b49171a97434123d685f44da98ef2/files/.placeholder -------------------------------------------------------------------------------- /handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # handlers file for letsencrypt 3 | -------------------------------------------------------------------------------- /meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | galaxy_info: 3 | author: Jason Robinson 4 | description: A role to automate LetsEncrypt certificates. 5 | license: MIT 6 | min_ansible_version: 2.0 7 | platforms: 8 | - name: Ubuntu 9 | versions: 10 | - trusty 11 | - name: Debian 12 | versions: 13 | - jessie 14 | - stretch 15 | categories: 16 | - cloud 17 | - web 18 | galaxy_tags: 19 | - letsencrypt 20 | dependencies: [] 21 | -------------------------------------------------------------------------------- /tasks/cert.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # prepare arguments 3 | - set_fact: _letsencrypt_certbot_args="{{ letsencrypt_certbot_args + ['--renew-by-default'] }}" 4 | when: letsencrypt_force_renew 5 | 6 | - set_fact: _letsencrypt_certbot_args="{{ letsencrypt_certbot_args + ['--keep-until-expiring'] }}" 7 | when: not letsencrypt_force_renew 8 | 9 | - set_fact: _letsencrypt_domains="--domains {{letsencrypt_domain}},www.{{letsencrypt_domain}}" 10 | when: letsencrypt_request_www 11 | 12 | - set_fact: _letsencrypt_domains="--domains {{letsencrypt_domain}}" 13 | when: not letsencrypt_request_www 14 | 15 | - set_fact: _letsencrypt_certbot_combined_args="{{_letsencrypt_certbot_args + letsencrypt_certbot_default_args + letsencrypt_certbot_args + [_letsencrypt_domains] }}" 16 | 17 | # stop service 18 | - name: Stopping Services 19 | service: name="{{item}}" state=stopped 20 | with_items: "{{ letsencrypt_pause_services }}" 21 | ignore_errors: yes 22 | register: _services_stopped 23 | 24 | # do the actual work 25 | - name: Obtain or renew cert for domain (source client) 26 | shell: ./certbot-auto {{_letsencrypt_certbot_combined_args | join(' ')}} 2>&1 27 | args: 28 | chdir: /opt/certbot 29 | executable: /bin/bash 30 | ignore_errors: true 31 | register: _certbot_command_source_client 32 | when: 33 | - not letsencrypt_test 34 | - not ansible_distribution_release == 'stretch' and not ansible_distribution_release == 'bionic' 35 | 36 | # do the actual work 37 | - name: Obtain or renew cert for domain (debian client) 38 | shell: certbot {{_letsencrypt_certbot_combined_args | join(' ')}} 2>&1 39 | args: 40 | executable: /bin/bash 41 | ignore_errors: true 42 | register: _certbot_command_debian_client 43 | when: 44 | - not letsencrypt_test 45 | - ansible_distribution_release == 'stretch' or ansible_distribution_release == 'bionic' 46 | 47 | # combine results 48 | # because of rather unpleasant ansible feature, see https://github.com/ansible/ansible/issues/4297 49 | - set_fact: 50 | _certbot_command: "{{ _certbot_command_debian_client if ansible_distribution_release == 'stretch' or ansible_distribution_release == 'bionic' else _certbot_command_source_client }}" 51 | 52 | # analyze output 53 | - set_fact: _signing_successful='{{ certbot_success_message in _certbot_command.stdout }}' 54 | when: not letsencrypt_test 55 | - set_fact: _signing_skipped='{{ (certbot_skip_renewal_message in _certbot_command.stdout) and not letsencrypt_force_renew }}' 56 | when: not letsencrypt_test 57 | - debug: msg="{{ (_certbot_command.stdout_lines if _certbot_command.stdout_lines is defined else _certbot_command.stderr_lines) | pprint }}" 58 | when: not letsencrypt_test and (letsencrypt_certbot_verbose or ((not _signing_successful) and not _signing_skipped)) 59 | 60 | - name: Starting paused Services 61 | service: name="{{item.item}}" state=started 62 | when: (item.state is defined and item.state == "stopped") 63 | with_items: "{{ _services_stopped.results|default([]) }}" 64 | 65 | - fail: msg="Error signing the certificate" 66 | when: not letsencrypt_test and not _signing_successful and not _signing_skipped 67 | -------------------------------------------------------------------------------- /tasks/client.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # clone client from source 3 | - name: Operating system dependencies 4 | apt: name={{ item }} state=present 5 | with_items: 6 | - build-essential 7 | - libssl-dev 8 | - libffi-dev 9 | - python-dev 10 | - git 11 | - python-pip 12 | - python-virtualenv 13 | - dialog 14 | - libaugeas0 15 | - ca-certificates 16 | when: not ansible_distribution_release == 'stretch' and not ansible_distribution_release == 'bionic' 17 | - name: Python cryptography module 18 | pip: name=cryptography 19 | when: not ansible_distribution_release == 'stretch' and not ansible_distribution_release == 'bionic' 20 | - name: Letsencrypt Python client 21 | git: 22 | dest: /opt/certbot 23 | clone: yes 24 | update: yes 25 | repo: https://github.com/certbot/certbot 26 | force: yes 27 | version: '{{letsencrypt_certbot_version}}' 28 | when: not ansible_distribution_release == 'stretch' and not ansible_distribution_release == 'bionic' 29 | 30 | # install client from package repo (available in debian stretch) 31 | - name: Install Certbot 32 | apt: name={{ item }} update_cache=yes state=latest 33 | with_items: 34 | - certbot 35 | when: ansible_distribution_release == 'stretch' or ansible_distribution_release == 'bionic' 36 | -------------------------------------------------------------------------------- /tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - include: client.yaml 3 | - include: cert.yaml 4 | 5 | - include: test.yaml 6 | when: letsencrypt_test 7 | -------------------------------------------------------------------------------- /tasks/test.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - debug: msg="{{ _letsencrypt_certbot_combined_args | pprint }}" 3 | 4 | - fail: msg="Expected args to certbot were not as expected" 5 | when: letsencrypt_test_expected_args != _letsencrypt_certbot_combined_args 6 | 7 | # When testing, run certbot-auto with an invalid argument just to make it build the environment.. 8 | - shell: ./certbot-auto --no-self-upgrade invalid-test-arg 2>&1 9 | args: 10 | chdir: /opt/certbot 11 | executable: /bin/bash 12 | ignore_errors: true 13 | register: _certbot_test_command 14 | changed_when: False 15 | 16 | - set_fact: _certbot_test_successful='{{ certbot_test_success_message in _certbot_test_command.stdout }}' 17 | 18 | - fail: msg="Invalid test response from certbot-auto" 19 | when: not _certbot_test_command 20 | -------------------------------------------------------------------------------- /templates/.placeholder: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaywink/ansible-letsencrypt/221827467f0b49171a97434123d685f44da98ef2/templates/.placeholder -------------------------------------------------------------------------------- /tests/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | 4 | vars: 5 | letsencrypt_email: user@example.com 6 | letsencrypt_domain: example.com 7 | letsencrypt_pause_services: 8 | - apache2 9 | letsencrypt_test: true 10 | letsencrypt_test_expected_args: 11 | - --renew-by-default 12 | - certonly 13 | - --standalone 14 | - --expand 15 | - --text 16 | - -n 17 | - --no-self-upgrade 18 | - -m 'user@example.com' 19 | - --agree-tos 20 | - --domains example.com,www.example.com 21 | 22 | roles: 23 | - role_under_test 24 | -------------------------------------------------------------------------------- /vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # vars file for letsencrypt 3 | 4 | certbot_skip_renewal_message: "Certificate not yet due for renewal; no action taken" 5 | certbot_success_message: "Congratulations!" 6 | certbot_test_success_message: "letsencrypt: error: unrecognized arguments: invalid-test-arg" 7 | --------------------------------------------------------------------------------