Skip to content

Commit

Permalink
14 support no webserver option (#15)
Browse files Browse the repository at this point in the history
* Update readme

* Let certbot be installed by itself without touching any web server config

* Let role survive check mode

* Update readme
  • Loading branch information
dale-c-anderson authored Sep 16, 2022
1 parent f1b63dc commit 6188830
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 94 deletions.
73 changes: 54 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
# Ansible role: letsencrypt

For use on shared hosting servers. The role:
- Installs certbot from LetsEncrypt (snapd version),
- Makes a `/.well-known/acme-challenge` virtual directory available to all virtual hosts on the server (including the default site), so all sites can regsiter and renew LE SSL certificates,
- Overwrites the default site config (after backing up the original), so it can be served with a valid LetsEncrypt certificate instead of the default snakeoil certificate.
This role was designed to work with [acromedia.nginx](https://github.com/AcroMedia/ansible-role-nginx) and [acromedia.nginx](https://github.com/AcroMedia/ansible-role-virtual-host), but it is possible to use it only install certbot if you set your vars as in the example bare-bones playbook below.

As an added bonus, after this role is installed, you won't need to create new virtual hosts to register new LetsEncrypt certificates. Since the default site acts as a catch-all, as long as DNS points at your server, you can register a certificate for that name.
Assuming the above is true, the default behaviour of the role is to:
- Install certbot from LetsEncrypt (snapd version),
- Make a `/.well-known/acme-challenge` virtual directory, which is automatically available to all the virtual hosts on the server (including the default site), so all sites can register and renew LE SSL certificates without extra configuration,
- Overwrite the default site config (after backing up the original), so it can be served with a valid LetsEncrypt certificate instead of the default snakeoil certificate.

After this role is installed using its default configuration, you won't need to create any (additional) virtual hosts to register new LetsEncrypt certificates. Since the default site is already a catch-all, as long as DNS points at your server for the name you need a certificate for, you can register a certificate for it using certbot's `webroot` method.

## Requirements

###### In all cases:
- Snapd + core must already be working
- Working DNS: LetsEncrypt uses DNS validation. Any certificate name you're registering must resolve to the machine you're registering the cert from.

###### When `letsencrypt_webserver` is not `none`:
- (NGINX or Apache 2) on (Ubuntu >= 16.04 or CentOS/RedHat >= 7)
- Working DNS: The cert name you're registering must resolve to the machine you're registering the cert from
- A working fully qualified host name: If `hostname -f` on the machine doesn't correctly resolve to the machine from the outside world, you need to either fix it, or override it with one that does resolve with `default_site_fqdn` from your playbook instead.

###### When: `letsencrypt_create_default_server_cert` is `true` (the default):
- A working fully qualified host name, OR, that you specify `letsencrypt_default_site_fqdn` as a dns name that does correctly. If `hostname -f` on the machine doesn't correctly resolve to the machine from the outside world, you need to either fix the hostname, or override it with one that does resolve by specifying a valid `default_site_fqdn` from your playbook instead.

## Role Variables

- **letsencrypt_webserver**

The brand of web server that's installed on the server. Defaults to `nginx`. Can be either `nginx` or `apache`.
The brand of web server that's installed on the server. Defaults to `nginx`. Can be one of: `nginx`, `apache`, or `none`.

- **http_port**

Expand All @@ -30,22 +37,50 @@ As an added bonus, after this role is installed, you won't need to create new vi

- **letsencrypt_renew_cron_minute**, **letsencrypt_renew_cron_hour**, **letsencrypt_renew_cron_day**

Control what time the server attempts LE certificate renewal. These default to `5`, `7`, and `*`, respectively (ie. 7:05 AM daily, local server time).
These variable names are a inaccurate now, but they're still used to control what time the server ~~attempts LE certificate renewal~~ reloads your web service. These default to `5`, `7`, and `*`, respectively (ie. 7:05 AM daily, local server time). The snap version of letsencrypt renews certs automatically without the need for a cron job, but your web server will won't pick up the new versions of those certificates without being reloaded.

- **letsencrypt_create_default_server_cert**

## Dependencies
Boolean (`true`) by default. The role will attempt to create a certificate for `$(hostname -f)` unless you set this to `false`.

- [acromedia.nginx](https://github.com/AcroMedia/ansible-role-nginx)
- **letsencrypt_webroot**

String, defaults to `/var/www/letsencrypt`. The physical directory for certbot to create acme challenge files in. Each virtual host's `/.well-known/acme-challenge` location maps to `{{ letsencrypt_webroot }}/.well-known/acme-challenge`.

## Dependencies

## Example Playbook
* [acromedia.nginx](https://github.com/AcroMedia/ansible-role-nginx) when `letsencrypt_webserver: nginx` (the default), or
* none, when `letsencrypt_webserver: none`.

## Example Playbooks

#### Normal scenario
```yaml
- hosts: simple_web_servers
roles:
- role: acromedia.nginx
- role: acromedia.letsencrypt
vars:
letsencrypt_default_site_fqdn: host-id.example.com
letsencrypt_webserver: nginx
http_port: 80
https_port: 443
- role: acromedia.virtual-host
# ... etc etc etc

```

#### **Only** install certbot

```yaml
- hosts: some_other_specialized_group
roles:
- role: acromedia.letsencrypt
vars:
letsencrypt_webserver: none
letsencrypt_create_default_server_cert: false
```
- hosts: servers
roles:
- role: acromedia.letsencrypt
vars:
letsencrypt_webserver: nginx
http_port: 80
https_port: 443
## License
Expand Down
9 changes: 7 additions & 2 deletions defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,13 @@ apache_auth: |
apache_dhparams: SSLOpenSSLConfCmd DHParameters /usr/local/ssl/private/dhparams.pem
apache_ssl_line: SSLEngine on

# In some edge cases, ansible_fqdn may doesn't return what we expect, so this lets us override it.
default_site_fqdn: "{{ ansible_fqdn }}" # in most cases, this is the same as $(hostname -f)
# The role tries to create a certificate for $(hostname -f) by default.
letsencrypt_create_default_server_cert: true

# If `ansible_fqdn` doesn't resolve to working DNS name, override it with this
letsencrypt_default_site_fqdn: "{{ ansible_fqdn }}" # in most cases, this is the same as $(hostname -f)
default_site_fqdn: "{{ letsencrypt_default_site_fqdn }}" # legacy var name



http_port: 80
Expand Down
10 changes: 10 additions & 0 deletions handlers/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,21 @@
service:
name: "{{ apache2_name }}"
state: restarted

- name: reload nginx
service:
name: nginx
state: reloaded

- name: restart nginx
service:
name: nginx
state: restarted

- name: restart none
debug:
msg: This handler is a no-op; letsencrypt_webserver is set to 'none'.

- name: reload none
debug:
msg: This handler is a no-op; letsencrypt_webserver is set to 'none'.
154 changes: 81 additions & 73 deletions tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,39 +10,42 @@
- name: Run {{ letsencrypt_webserver }}.yml playbook
include: "{{ letsencrypt_webserver }}.yml"

- name: Create the acme-challenge dir
file:
path: "{{ letsencrypt_webroot }}/.well-known/acme-challenge"
state: directory
notify: "restart {{ letsencrypt_webserver }}"
- block:
- name: Create the acme-challenge dir
file:
path: "{{ letsencrypt_webroot }}/.well-known/acme-challenge"
state: directory
notify: "restart {{ letsencrypt_webserver }}"

- name: Be able to ping the challenge dir
lineinfile:
dest: "{{ letsencrypt_webroot }}/.well-known/acme-challenge/ping"
state: present
line: pong
create: yes

- name: Be able to ping the challenge dir
lineinfile:
dest: "{{ letsencrypt_webroot }}/.well-known/acme-challenge/ping"
state: present
line: pong
create: yes
when: letsencrypt_webserver != 'none'

- block:
- name: Remove obsolete PPA version of letsencrypt
apt:
name:
- letsencrypt
- certbot
state: absent
purge: false # Do NOT remove config files
update_cache: false # We're not installing anything new. There's no need to refresh this.

- name: Remove obsolete Certbot PPA repo
apt_repository:
repo: ppa:certbot/certbot
state: absent
update_cache: false # We're not installing anything new. There's no need to refresh this.

- name: Purge obsolete certbot virtual environment
file:
state: absent
path: /opt/eff.org/certbot
- name: Remove obsolete PPA version of letsencrypt
apt:
name:
- letsencrypt
- certbot
state: absent
purge: false # Do NOT remove config files
update_cache: false # We're not installing anything new. There's no need to refresh this.

- name: Remove obsolete Certbot PPA repo
apt_repository:
repo: ppa:certbot/certbot
state: absent
update_cache: false # We're not installing anything new. There's no need to refresh this.

- name: Purge obsolete certbot virtual environment
file:
state: absent
path: /opt/eff.org/certbot

when: ansible_distribution == 'Ubuntu'

Expand All @@ -67,49 +70,54 @@
state: absent
name: "Update certbot-auto once a month"

- name: Create a destination for dhparams
file:
path: "/usr/local/ssl/private"
state: directory
mode: "2750"

- name: Create dhparams.pem (can take several minutes)
shell: umask 077 && openssl dhparam -out /usr/local/ssl/private/dhparams.pem 2048
args:
creates: /usr/local/ssl/private/dhparams.pem

- name: Stat the default site SSL cert
shell: "test -e /etc/letsencrypt/live/{{ default_site_fqdn }}"
register: default_cert_result
changed_when: "default_cert_result.rc != 0"
ignore_errors: true

- name: Compose the certbot command string for the default site cert
set_fact:
certbot_command_string: >-
/snap/bin/certbot certonly
--non-interactive
--agree-tos
--email {{ default_mail_recipient }}
--webroot
--webroot-path {{ letsencrypt_webroot }}
--domains {{ default_site_fqdn }}
- name: Create a cert for the default site (can take some time)
shell: >
{{ certbot_command_string }} --dry-run && {{ certbot_command_string }}
register: certbot_result
when: default_cert_result is defined
and default_cert_result.rc is defined
and default_cert_result.rc != 0
environment:
DEBIAN_FRONTEND: noninteractive

- name: Re-stat default site SSL cert
shell: "test -e /etc/letsencrypt/live/{{ default_site_fqdn }}"
register: default_cert_retest
ignore_errors: true
changed_when: default_cert_retest.rc != default_cert_result.rc
- block:
- name: Create a destination for dhparams
file:
path: "/usr/local/ssl/private"
state: directory
mode: "2750"

- name: Create dhparams.pem (can take several minutes)
shell: umask 077 && openssl dhparam -out /usr/local/ssl/private/dhparams.pem 2048
args:
creates: /usr/local/ssl/private/dhparams.pem
when: letsencrypt_webserver != 'none'

- block:
- name: Stat the default site SSL cert
shell: "test -e /etc/letsencrypt/live/{{ default_site_fqdn }}"
register: default_cert_result
changed_when: "default_cert_result.rc != 0"
ignore_errors: true

- name: Compose the certbot command string for the default site cert
set_fact:
certbot_command_string: >-
/snap/bin/certbot certonly
--non-interactive
--agree-tos
--email {{ default_mail_recipient }}
--webroot
--webroot-path {{ letsencrypt_webroot }}
--domains {{ default_site_fqdn }}
- name: Create a cert for the default site (can take some time)
shell: >
{{ certbot_command_string }} --dry-run && {{ certbot_command_string }}
register: certbot_result
when: default_cert_result is defined
and default_cert_result.rc is defined
and default_cert_result.rc != 0
environment:
DEBIAN_FRONTEND: noninteractive

- name: Re-stat default site SSL cert
shell: "test -e /etc/letsencrypt/live/{{ default_site_fqdn }}"
register: default_cert_retest
ignore_errors: true
changed_when: default_cert_retest.rc != default_cert_result.rc

when: letsencrypt_create_default_server_cert | default(true)

- name: Run {{ letsencrypt_webserver }}_default_ssl.yml playbook
include: "{{ letsencrypt_webserver }}_default_ssl.yml"
1 change: 1 addition & 0 deletions tasks/nginx.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
src: "{{ nginx_vhost_conf_dir }}/{{ nginx_default_vhost_filename }}"
notify: reload nginx
when: ansible_os_family == "Debian"
ignore_errors: "{{ ansible_check_mode }}"

- name: Reload nginx if necessary
meta: flush_handlers
Expand Down
2 changes: 2 additions & 0 deletions tasks/none.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
# This playbook is a no-op when a web server isn't actually needed.
2 changes: 2 additions & 0 deletions tasks/none_default_ssl.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
# This playbook is a no-op when a web server isn't actually needed.

0 comments on commit 6188830

Please sign in to comment.