Skip to content

Commit

Permalink
Handle a greater variety of system states in renew-certs.py
Browse files Browse the repository at this point in the history
Accommodate more potential starting states, unexpected combinations of
existing vs. missing Let's Encrypt files, and the potential that users
could run only the `wordpress` role after changes that really require
running the `letsencrypt` role. Where possible, run tasks to accommodate
these situations. Otherwise print helpful error messages in the more
likely error situations.
  • Loading branch information
fullyint committed May 6, 2017
1 parent a038820 commit b46823d
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 47 deletions.
2 changes: 2 additions & 0 deletions roles/letsencrypt/tasks/certificates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
src: renew-certs.py
dest: "{{ acme_tiny_data_directory }}/renew-certs.py"
mode: 0700
tags: [wordpress, wordpress-setup, wordpress-setup-nginx, nginx-includes]

- name: Generate the certificates
command: ./renew-certs.py
Expand All @@ -45,3 +46,4 @@
register: generate_certs
changed_when: generate_certs.stdout is defined and 'Created' in generate_certs.stdout
notify: reload nginx
tags: [wordpress, wordpress-setup, wordpress-setup-nginx, nginx-includes]
93 changes: 64 additions & 29 deletions roles/letsencrypt/templates/renew-certs.py
Original file line number Diff line number Diff line change
@@ -1,53 +1,88 @@
#!/usr/bin/env python

from __future__ import print_function

import os
import sys
import time

from hashlib import sha1
from subprocess import CalledProcessError, check_output, STDOUT

failed = False
letsencrypt_cert_ids = {{ letsencrypt_cert_ids }}

for site in {{ sites_using_letsencrypt }}:
cert_path = os.path.join('{{ letsencrypt_certs_dir }}', site + '-' + letsencrypt_cert_ids[site] + '.cert')
bundled_cert_path = os.path.join('{{ letsencrypt_certs_dir }}', site + '-bundled.cert')
csr_path = os.path.join('{{ acme_tiny_data_directory }}', 'csrs', '{}-{}.csr'.format(site, letsencrypt_cert_ids[site]))
cert_path = os.path.join('{{ letsencrypt_certs_dir }}', '{}-{}.cert'.format(site, letsencrypt_cert_ids[site]))
bundled_cert_path = os.path.join('{{ letsencrypt_certs_dir }}', '{}-bundled.cert'.format(site))

if os.access(cert_path, os.F_OK):
stat = os.stat(cert_path)
print 'Certificate file ' + cert_path + ' already exists'
# Generate or update root cert if needed
if not os.access(csr_path, os.F_OK):
failed = True
print('The required CSR file {} does not exist. This could happen if you changed site_hosts and have '
'not yet rerun the letsencrypt role. Create the CSR file by running the Trellis server.yml playbook with '
'`--tags letsencrypt`'.format(csr_path), file=sys.stderr)
continue

if time.time() - stat.st_mtime < {{ letsencrypt_min_renewal_age }} * 86400 and os.access(bundled_cert_path, os.F_OK):
print ' The certificate is younger than {{ letsencrypt_min_renewal_age }} days. Not creating a new certificate.\n'
continue
elif os.access(cert_path, os.F_OK) and time.time() - os.stat(cert_path).st_mtime < {{ letsencrypt_min_renewal_age }} * 86400:
print('Certificate file {} already exists and is younger than {{ letsencrypt_min_renewal_age }} days. '
'Not creating a new certificate.'.format(cert_path))

else:
cmd = ('/usr/bin/env python {{ acme_tiny_software_directory }}/acme_tiny.py '
'--quiet '
'--ca {{ letsencrypt_ca }} '
'--account-key {{ letsencrypt_account_key }} '
'--csr {} '
'--acme-dir {{ acme_tiny_challenges_directory }}'
).format(csr_path)

print 'Generating certificate for ' + site
try:
cert = check_output(cmd, stderr=STDOUT, shell=True)
except CalledProcessError as e:
failed = True
print('Error while generating certificate for {}\n{}'.format(site, e.output), file=sys.stderr)
continue
else:
with open(cert_path, 'w') as cert_file:
cert_file.write(cert)

cmd = ('/usr/bin/env python {{ acme_tiny_software_directory }}/acme_tiny.py '
'--quiet '
'--ca {{ letsencrypt_ca }} '
'--account-key {{ letsencrypt_account_key }} '
'--csr {{ acme_tiny_data_directory }}/csrs/{0}-{1}.csr '
'--acme-dir {{ acme_tiny_challenges_directory }}'
).format(site, letsencrypt_cert_ids[site])
print('Created certificate {}'.format(cert_file))

try:
cert = check_output(cmd, stderr=STDOUT, shell=True)
except CalledProcessError as e:
# Ensure intermediate cert is available for creating bundled cert
if not os.access('{{ letsencrypt_intermediate_cert_path }}', os.F_OK):
failed = True
print 'Error while generating certificate for ' + site
print e.output
else:
with open(cert_path, 'w') as cert_file:
cert_file.write(cert)
print('The required intermediate cert file {{ letsencrypt_intermediate_cert_path }} does not exist. '
'This could happen if you have not yet run the letsencrypt role with the latest `letsencrypt_intermediate_cert_path` value. '
'Try running the Trellis server.yml playbook with `--tags letsencrypt`', file=sys.stderr)
continue

# Retrieve binary content for root cert, intermediate cert, and bundled cert
with open(cert_path, 'rb') as cert_file:
cert = cert_file.read()

with open('{{ letsencrypt_intermediate_cert_path }}', 'rb') as intermediate_cert_file:
intermediate_cert = intermediate_cert_file.read()

new_bundled_needed = True
if os.access(bundled_cert_path, os.F_OK):
with open(bundled_cert_path, 'rb') as bundled_cert_file:
bundled_cert = bundled_cert_file.read()

with open('{{ letsencrypt_intermediate_cert_path }}') as intermediate_cert_file:
intermediate_cert = intermediate_cert_file.read()
# Compare sha1 hashes of new vs. existing bundled content
new = sha1()
new.update(cert + intermediate_cert)
existing = sha1()
existing.update(bundled_cert)
new_bundled_needed = new.hexdigest() != existing.hexdigest()

with open(bundled_cert_path, 'w') as bundled_file:
bundled_file.write(''.join([cert, intermediate_cert]))
# Generate or update bundled cert if needed
if new_bundled_needed:
with open(bundled_cert_path, 'wb') as bundled_cert_file:
bundled_cert_file.write(cert + intermediate_cert)

print 'Created certificate for ' + site
print('Created bundled certificate {}'.format(bundled_cert_path))

if failed:
sys.exit(1)
18 changes: 0 additions & 18 deletions roles/wordpress-setup/tasks/nginx.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,6 @@
with_dict: "{{ wordpress_sites }}"
when: ssl_enabled and item.value.ssl.key is defined

- name: Find Let's Encrypt bundled certs with current ID in filename
stat:
path: "{{ letsencrypt_certs_dir }}/{{ item.key }}-{{ letsencrypt_cert_ids[item.key] }}-bundled.cert"
with_dict: "{{ wordpress_sites }}"
register: bundled_certs_with_id
when:
- site_uses_letsencrypt
- generate_certs is not defined or not generate_certs | changed
tags: nginx-includes

- name: Rsync Let's Encrypt bundled certs into filename without ID
command: rsync -ac --info=NAME {{ item.stat.path }} {{ letsencrypt_certs_dir }}/{{ item.item.key }}-bundled.cert
with_items: "{{ bundled_certs_with_id.results }}"
register: sync_bundled_cert
changed_when: sync_bundled_cert.stdout == item.stat.path | basename
when: not item | skipped and item.stat.exists
tags: nginx-includes

- include: "{{ playbook_dir }}/roles/common/tasks/disable_challenge_sites.yml"

- name: Create Nginx conf for challenges location
Expand Down

0 comments on commit b46823d

Please sign in to comment.