Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

simp_le client running as unprivileged user #18

Closed
wants to merge 9 commits into from
Closed
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
1 change: 1 addition & 0 deletions CONTRIBUTORS
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
Jason Robinson (@jaywink)
Robert O'Connor (@robbyoconnor)
Stefan Grönke (@gronke)
34 changes: 21 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,18 @@ Stability: beta.

### What does it do?

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.
This role will pull in the [simp_le](https://github.com/kuba/simp_le) that is recommended from LetsEncrypt for automation. After setting up the client the role will try to obtain a certificate for the provided domain.

Functionality as follows:
* Tested on Ubuntu 14.04 and Debian 8
* One domain per role include only
* Runs in `certonly` mode only
* Runs the client as unprivileged user
* Certificates are only renewed when they expire within a definable threshold

PR's are welcome to include more functionality.

#### More detail

* The client will be installed in `/opt/certbot` as root
* 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`.
* The client will be installed in `/opt/letsencrypt` as root
* 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`.
* `certonly` mode is used, which means no automatic web server installation
* After cert issuing, you can find it in `/etc/letsencrypt/live/<domainname>`
Expand All @@ -33,14 +32,13 @@ PR's are welcome to include more functionality.
SSLCertificateChainFile /etc/letsencrypt/live/{{ hostname }}/chain.pem
```

* Note! If this role fails in the cert request part, you might have stopped services - take care!
* 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.
* A `www.` subdomain will automatically be requested along with the certificate.
* To disable this behaviour, set `letsencrypt_request_www` to `false` in your vars.

### Requirements

Tested with the following:
Tested with

* Ubuntu 14.04 and Debian 8
* Apache2 and Nginx
Expand All @@ -52,28 +50,38 @@ Tested with the following:

* `letsencrypt_domain` - Domain the certificate is for.
* `letsencrypt_email` - Your email as certificate owner.
* `letsencrypt_tos_sha256` - Provide the SHA256 hash of the latest Terms-of-Service you agreed to
Copy link
Owner

@jaywink jaywink Sep 28, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it's in the defaults, no need to mention it here - nice section related to it in the readme which could also have this variable name.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry. The plan was to remove it from the default, so that the user needs to confirm the terms first. I think it's a good idea to use the hash of the document for that. It's important to leave a hint in the Readme and provide proper error messages when the hash is invalid or missing.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd still like this default to be enabled in the defaults. The rationale is that the role should make things as easy as possible to use. Requiring the user to add extra variables to their role that could be provided as defaults IMHO is going backwards. The current role already has --agree-terms etc. The readme can clearly state which ToS the user will be accepting by using the role, and note that the user can change which ToS is being accepted via the variables.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LetsEncrypt TOS handling can be discussed in a separate issue. To keep the previous behavior of --agree-terms being enabled by default, we can just set the default to the hash of the current terms of service document and make a new release whenever this document was updated.


#### Optional

* `letsencrypt_certbot_args` - Additional command line args to Certbot.
* `letsencrypt_certbot_verbose` - Make Certbot output to console (default `true`).
* `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.
* `letsencrypt_args` - Additional command line args to simp_le LetsEncrypt client.
* `letsencrypt_force_renew` - Whether to attempt renewal always, default to `true`.
* `letsencrypt_pause_services` - List of services to stop/start while calling Certbot.
* `letsencrypt_request_www` - Request `www.` automatically (default `true`).

### Terms of Service

On the [Let's Encrypt: Policy and Legal Repository](https://letsencrypt.org/repository/) page the latest "Terms of Service" PDF document can be downloaded. Make sure you agree to this document before you calculate the checksum and provide it as role input variable.

```bash
export TOS_DOCUMENT=LE-SA-v1.1.1-August-1-2016.pdf
wget https://letsencrypt.org/documents/$TOS_DOCUMENT
shasum -a 256 $TOS_DOCUMENT
```

At writing time the latest tos_sha256 is `6373439b9f29d67a5cd4d18cbc7f264809342dbf21cb2ba2fc7588df987a6221`

### Example Playbook

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.

This role should become root on the target host.
If not manually specified the role will create system user and group "letsencrypt" that executes the client

---
- hosts: myhost
become: yes
become_user: root
roles:
- role: ansible-letsencrypt
letsencrypt_user: "letsencrypt"
letsencrypt_email: email@example.com
letsencrypt_domain: example.com
letsencrypt_pause_services:
Expand Down
40 changes: 35 additions & 5 deletions defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,45 @@
letsencrypt_email: yourmail@example.com
# The domain we're requesting/renewing for
letsencrypt_domain: example.com
# LetsEncrypt Terms of Service SHA256
letsencrypt_tos_sha256: "6373439b9f29d67a5cd4d18cbc7f264809342dbf21cb2ba2fc7588df987a6221"
# staging mode to create a fake certificate
letsencrypt_staging: false
# Always request www. also?
letsencrypt_request_www: true
# Version/Release tag or branch name of certbot to use
letsencrypt_certbot_version: v0.8.1
# Print Certbot output
letsencrypt_certbot_verbose: true
letsencrypt_verbose: true
# Pause these services while updating the certificate
letsencrypt_pause_services: []
# Force Certificate Reneval
letsencrypt_force_renew: true
# Additional certbot arguments
letsencrypt_certbot_args: []
# Certificate expiration threshold in seconds (default is 30 days - 1/3 of total certificate lifecycle)
letsencrypt_expiration_threshold: "{{ (90 * 24 * 60 * 60) if letsencrypt_force_renew else (90 * 24 * 60 * 60) / 3 }}"
# User that runs certbot and generates the certificates
letsencrypt_user: letsencrypt
letsencrypt_group: "{{ letsencrypt_user }}"
letsencrypt_home_dir: "/opt/letsencrypt"
# output directory for ACME challenges
letsencrypt_webroot_path: "{{ letsencrypt_home_dir }}/htdocs"
# export directory
letsencrypt_export_dir: "/etc/letsencrypt/live/{{letsencrypt_domain}}"
# python virtualenv directory
letsencrypt_virtualenv_dir: "{{ letsencrypt_home_dir }}/simp_le/venv"
# LetsEncrypt account
letsencrypt_account_file: "{{ '/etc/letsencrypt/accounts/simp_le-' + ('staging' if letsencrypt_staging else 'production') + '.json' }}"
# ACME challenge server
letsencrypt_challenge_url: "https://{{ 'acme-staging' if letsencrypt_staging else 'acme-v01' }}.api.letsencrypt.org/directory"
# simp_le custom arguments -- see: https://github.com/kuba/simp_le
letsencrypt_args: []
# default arguments passed to simp_le
letsencrypt_default_args:
- --email "{{ letsencrypt_email }}"
- -f account_key.json
- -f fullchain.pem
- -f key.pem
- -f cert.pem
- -f chain.pem
- --default_root "{{ letsencrypt_webroot_path }}"
- --server "{{ letsencrypt_challenge_url }}"
- --tos_sha256 "{{ letsencrypt_tos_sha256 }}"
- --valid_min {{ letsencrypt_expiration_threshold | int }}
16 changes: 14 additions & 2 deletions meta/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,24 @@ galaxy_info:
- name: Ubuntu
versions:
- trusty
- utopic
- vivid
- wily
- xenial
- name: Debian
versions:
- jessie
- wheezy
- name: CentOS
versions:
- 7.2
categories:
- cloud
- web
- cloud
- web
galaxy_tags:
- letsencrypt
- encryption
- crypto
- ssl
- tls
dependencies: []
65 changes: 52 additions & 13 deletions tasks/cert.yaml
Original file line number Diff line number Diff line change
@@ -1,38 +1,77 @@
---

- set_fact: _letsencrypt_certbot_args="{{letsencrypt_certbot_args + ['--renew-by-default']}}"
when: letsencrypt_force_renew == true

- set_fact: _letsencrypt_certbot_args="{{letsencrypt_certbot_args + ['--keep-until-expiring']}}"
when: letsencrypt_force_renew != true

- set_fact: _letsencrypt_domains="{{letsencrypt_domain}},www.{{letsencrypt_domain}}"
- set_fact: _letsencrypt_domains="{{ [letsencrypt_domain] }}"
- set_fact: _letsencrypt_domains="{{ _letsencrypt_domains + [{{ 'www.' + letsencrypt_domain }}] }}"
when: letsencrypt_request_www

- set_fact: _letsencrypt_combined_args="{{ letsencrypt_default_args + ['-d ' + (_letsencrypt_domains | join(' -d '))] + letsencrypt_args }}"

- name: Stopping Services
service: name="{{item}}" state=stopped
with_items: "{{ letsencrypt_pause_services }}"
ignore_errors: yes
register: _services_stopped

- name: Start SimpleHTTPServer for ACME Challenges
service:
name: letsencrypt-simplehttpd
state: started

- name: fullchain.pem, cert.pem and chain.pem are linked
file:
src: "{{ letsencrypt_export_dir }}/{{ item }}"
dest: "{{ letsencrypt_home_dir }}/simp_le/{{ item }}"
state: link
owner: "{{ letsencrypt_user }}"
group: "{{ letsencrypt_group }}"
force: yes
with_items:
- "fullchain.pem"
- "cert.pem"
- "chain.pem"

- name: privkey.pem is linked
file:
src: "{{ letsencrypt_export_dir }}/privkey.pem"
dest: "{{ letsencrypt_home_dir }}/simp_le/key.pem"
state: link
owner: "{{ letsencrypt_user }}"
group: "{{ letsencrypt_group }}"
force: yes

- name: LetsEncrypt account key is linked
file:
src: "{{ letsencrypt_account_file }}"
dest: "{{ letsencrypt_home_dir }}/simp_le/account_key.json"
state: link
owner: "{{ letsencrypt_user }}"
group: "{{ letsencrypt_group }}"
force: yes

- name: Obtain or renew cert for domain
shell: ./certbot-auto certonly --text -n --no-self-upgrade -m {{ letsencrypt_email }} --domains {{ _letsencrypt_domains | default(letsencrypt_domain) }} --agree-tos --standalone --expand {{_letsencrypt_certbot_args | join(' ')}} 2>&1
shell: PATH="{{ letsencrypt_virtualenv_dir }}/bin" "{{ letsencrypt_virtualenv_dir }}/bin/python" ./simp_le.py {{_letsencrypt_combined_args | join(' ')}} 2>&1
args:
chdir: /opt/certbot
chdir: "{{ letsencrypt_home_dir }}/simp_le"
executable: /bin/bash
become: yes
become_user: "{{ letsencrypt_user }}"
ignore_errors: true
register: _certbot_command

- set_fact: _signing_successful='{{ certbot_success_message in _certbot_command.stdout }}'
- set_fact: _signing_skipped='{{ (letsencrypt_force_renew != true) and (certbot_skip_renewal_message in _certbot_command.stdout) }}'

- set_fact: _signing_skipped='{{ (certbot_skip_renewal_message in _certbot_command.stdout) and not letsencrypt_force_renew }}'
- debug: msg="{{ (_certbot_command.stdout_lines if _certbot_command.stdout_lines is defined else _certbot_command.stderr_lines) | pprint }}"
when: letsencrypt_certbot_verbose or ((_signing_successful == false) and (_signing_skipped == false))
when: letsencrypt_verbose or not (_signing_successful and _signing_skipped)

- name: Stop SimpleHTTPServer after running certbot
service:
name: letsencrypt-simplehttpd
state: stopped

- name: Starting paused Services
service: name="{{item.item}}" state=started
when: (item.state is defined and item.state == "stopped")
with_items: "{{ _services_stopped.results }}"

- fail: msg="Error signing the certificate"
when: (_signing_successful == false) and (_signing_skipped == false)
when: (not _signing_successful) and not _signing_skipped
Loading