Skip to content

Commit

Permalink
simp_le client running as unprivileged user
Browse files Browse the repository at this point in the history
  • Loading branch information
gronke committed Sep 26, 2016
1 parent a8712bf commit 7fae374
Show file tree
Hide file tree
Showing 11 changed files with 306 additions and 44 deletions.
30 changes: 20 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ 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.

Expand All @@ -33,14 +33,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 +51,39 @@ 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

#### Optional

* `letsencrypt_certbot_args` - Additional command line args to Certbot.
* `letsencrypt_args` - Additional command line args to simp_le LetsEncrypt client.
* `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_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: "le-client-user"
letsencrypt_email: email@example.com
letsencrypt_domain: example.com
letsencrypt_pause_services:
Expand Down
24 changes: 21 additions & 3 deletions defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,33 @@
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
# Pause these services while updating the certificate
letsencrypt_pause_services: []
# Force Certificate Reneval
letsencrypt_force_renew: true
# 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 }}"
# Additional certbot arguments
letsencrypt_certbot_args: []
letsencrypt_args: []
# 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"
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: []
49 changes: 41 additions & 8 deletions tasks/cert.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
---

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

- 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_domains + [{{ 'www.' + letsencrypt_domain }}] }}"
when: letsencrypt_request_www

- name: Stopping Services
Expand All @@ -15,11 +13,41 @@
ignore_errors: yes
register: _services_stopped

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

- name: fullchain.pem is linked
file:
src: "{{ letsencrypt_export_dir }}/{{ item[0] }}"
dest: "{{ letsencrypt_home_dir }}/simp_le/{{ item[1] }}"
state: link
owner: "{{ letsencrypt_user }}"
group: "{{ letsencrypt_group }}"
force: yes
with_items:
- [ "fullchain.pem", "fullchain.pem" ]
- [ "privkey.pem", "key.pem" ]
- [ "cert.pem", "cert.pem" ]
- [ "chain.pem", "chain.pem" ]

- 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 --email {{ letsencrypt_email }} -f account_key.json -f fullchain.pem -f key.pem -f cert.pem -f chain.pem -d {{ _letsencrypt_domains | join(' -d ') }} --default_root "{{ letsencrypt_webroot_path }}" --server "{{ letsencrypt_challenge_url }}" --tos_sha256 "{{ letsencrypt_tos_sha256 }}" --valid_min {{ letsencrypt_expiration_threshold }} 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

Expand All @@ -29,6 +57,11 @@
- 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))

- 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")
Expand Down
167 changes: 148 additions & 19 deletions tasks/client.yaml
Original file line number Diff line number Diff line change
@@ -1,25 +1,154 @@
---
- name: Operating system dependencies
apt: name={{ item }} state=present

# dependencies

- name: Dependencies are installed with Aptitude
apt:
name: "{{ item }}"
state: present
with_items: "{{ _letsencrypt_apt_packages }}"
when: _letsencrypt_apt_packages is defined

- name: Dependencies are installed with RPM
apt:
name: "{{ item }}"
state: present
with_items: "{{ _letsencrypt_rpm_packages }}"
when: _letsencrypt_rpm_packages is defined


# user and group configuration

- name: letsencrypt_group exits
group:
name: "{{ letsencrypt_group }}"
system: yes
state: present

- name: letsencrypt_user exists
user:
name: "{{ letsencrypt_user }}"
group: "{{ letsencrypt_group }}"
system: yes
home: "{{ letsencrypt_home_dir }}"
move_home: no
createhome: yes
append: yes

- name: letsencrypt_home_dir is writable for letsencrypt_user
file:
path: "{{ letsencrypt_home_dir }}"
state: directory
mode: 0770


# file system access

- name: letsencrypt user is able to write to output directories
file:
path: "{{ item }}"
recurse: yes
owner: "{{ letsencrypt_user }}"
with_items:
- build-essential
- libssl-dev
- libffi-dev
- python-dev
- git
- python-pip
- python-virtualenv
- dialog
- libaugeas0
- ca-certificates
- name: Python cryptography module
pip: name=cryptography
- name: Letsencrypt Python client
- /var/lib/letsencrypt
- /var/log/letsencrypt
- /etc/letsencrypt

- name: letsencrypt account directory exists and is writable
file:
path: "{{ letsencrypt_account_file | dirname }}"
owner: root
group: "{{ letsencrypt_group }}"
mode: 0770
state: directory

- name: letsencrypt_webroot_path exists and is writable by letsencrypt_user
file:
path: "{{ letsencrypt_webroot_path }}"
state: directory
owner: "{{ letsencrypt_user }}"
group: "{{ letsencrypt_group }}"
mode: 0775
recurse: yes


# python webserver systemd wrapper

- name: webserver systemd service is installed
template:
src: "systemd/letsencrypt-simplehttpd.service.j2"
dest: "/etc/systemd/system/letsencrypt-simplehttpd.service"
owner: root
group: root
mode: 0755
register: _systemd_service_template

# Ansible < 2.2 and >= 2.2
- name: systemd daemons are reloaded via systemctl command
command: systemctl daemon-reload
when: _systemd_service_template.changed

# # Ansible >= 2.2 only
# - name: systemd daemons are reloaded after changing the service
# systemd:
# name: letsencrypt-simplehttpd
# daemon_reload: yes
# enabled: false
# when: _systemd_service_template.changed

- name: export directory exists
file:
path: "{{ letsencrypt_export_dir }}"
state: directory
owner: "{{ letsencrypt_user }}"
group: "root"
mode: 0775
recurse: yes
force: yes


# virtualenv and python dependencies

- name: virtualenv environment exists
command: virtualenv --no-site-packages "{{ letsencrypt_virtualenv_dir }}"
args:
creates: "{{ letsencrypt_virtualenv_dir }}"
become: yes
become_user: "{{ letsencrypt_user }}"

- name: Python modules are installed to virtualenv
pip:
name: "{{ item }}"
virtualenv: "{{ letsencrypt_virtualenv_dir }}"
virtualenv_site_packages: no
with_items:
- 'cryptography'
- 'pyOpenSSL'
- 'pytz'
- 'requests'
become: yes
become_user: "{{ letsencrypt_user }}"

- name: ACME Python module 0.6 is installed
pip:
name: "acme"
version: "0.6"
virtualenv: "{{ letsencrypt_virtualenv_dir }}"
virtualenv_site_packages: no
become: yes
become_user: "{{ letsencrypt_user }}"


# simp_le client

- name: simp_le client is installed
git:
dest: /opt/certbot
repo: "https://github.com/kuba/simp_le.git"
dest: "{{ letsencrypt_home_dir }}/simp_le"
depth: 1
clone: yes
update: yes
depth: 1
repo: https://github.com/certbot/certbot
force: yes
version: '{{letsencrypt_certbot_version}}'
become: yes
become_user: "{{ letsencrypt_user }}"

Loading

0 comments on commit 7fae374

Please sign in to comment.