Skip to content
Merged
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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,17 @@ Notable changes to Cloudfall. The format follows
`spec.steps[].arguments` only holds arguments that ran whole; the trace
still keeps the start of the raw arguments. `agent-investigation.schema.json`
bounds both finding fields with `maxLength` (#63)
- `domains` deploys a TLS route on Debian 12. The vhost wrote `http2 on;`,
which Nginx only knows from 1.25.1, so on Debian's 1.22.1 `nginx -t`
failed with `unknown directive "http2"`. The role now reads the installed
version with `nginx -v` (failing the converge when it cannot) and writes
`listen 443 ssl http2;` below 1.25.1; the rendered vhost on 1.25.1 and
later is unchanged. A vhost that `nginx -t` rejects no longer stays on
disk and enabled for the next reload (a certbot hook, a reboot) to take
every site on the host down: the role restores the previous file, or
removes a new file and the `sites-enabled` link this run created, then
fails with Nginx's error and without reloading. Nginx is now started only
after the configuration test passes (#66)

### Security

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
---
# A render goes live only behind `nginx -t`. What was on disk before is
# recorded first, so a rejected render is rolled back before the play fails:
# otherwise the invalid vhost stays enabled and the next reload (a certbot
# hook, a reboot) takes every site on the host down with it. The rescue ends
# in a failure, so with force_handlers off the queued reload never runs.
- name: Inspect the current virtual host
ansible.builtin.stat:
path: "{{ cloudfall_nginx_site_domain.proxy.configurationPath }}"
get_checksum: false
register: cloudfall_nginx_site_previous_file

- name: Keep the current virtual host for rollback
ansible.builtin.slurp:
src: "{{ cloudfall_nginx_site_domain.proxy.configurationPath }}"
register: cloudfall_nginx_site_previous_content
when: cloudfall_nginx_site_previous_file.stat.exists

- name: Inspect the current virtual host link
ansible.builtin.stat:
path: "{{ cloudfall_nginx_site_link }}"
get_checksum: false
register: cloudfall_nginx_site_previous_link
when: cloudfall_nginx_site_link | length > 0

- name: Apply the virtual host behind the configuration test
block:
- name: Render the managed virtual host
ansible.builtin.template:
src: domain-site.conf.j2
dest: "{{ cloudfall_nginx_site_domain.proxy.configurationPath }}"
owner: root
group: root
mode: "0644"
notify: Reload Nginx

- name: Enable the managed virtual host
ansible.builtin.file:
src: "{{ cloudfall_nginx_site_domain.proxy.configurationPath }}"
dest: "{{ cloudfall_nginx_site_link }}"
state: link
notify: Reload Nginx
when: cloudfall_nginx_site_link | length > 0

- name: Validate the Nginx configuration
ansible.builtin.command:
argv:
- /usr/sbin/nginx
- -t
changed_when: false
when: not ansible_check_mode
rescue:
- name: Restore the previous virtual host
ansible.builtin.copy:
content: >-
{{ cloudfall_nginx_site_previous_content.content | b64decode }}
dest: "{{ cloudfall_nginx_site_domain.proxy.configurationPath }}"
owner: "{{ cloudfall_nginx_site_previous_file.stat.pw_name }}"
group: "{{ cloudfall_nginx_site_previous_file.stat.gr_name }}"
mode: "{{ cloudfall_nginx_site_previous_file.stat.mode }}"
when:
- not ansible_check_mode
- cloudfall_nginx_site_previous_file.stat.exists

- name: Remove the rejected new virtual host
ansible.builtin.file:
path: "{{ cloudfall_nginx_site_domain.proxy.configurationPath }}"
state: absent
when:
- not ansible_check_mode
- not cloudfall_nginx_site_previous_file.stat.exists

- name: Remove the link this run created
ansible.builtin.file:
path: "{{ cloudfall_nginx_site_link }}"
state: absent
when:
- not ansible_check_mode
- cloudfall_nginx_site_link | length > 0
- not cloudfall_nginx_site_previous_link.stat.exists

- name: Fail after rolling back the rejected virtual host
ansible.builtin.fail:
msg: >-
{{ cloudfall_nginx_site_domain.proxy.configurationPath }} was
rejected and rolled back, and Nginx was not reloaded: {{
ansible_failed_result.stderr | default('', true)
or ansible_failed_result.msg | default('')
}}
105 changes: 60 additions & 45 deletions engine/ansible/roles/cloudfall_nginx_site/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,47 @@
update_cache: true
cache_valid_time: 3600

# The TLS server block spells HTTP/2 the way the installed Nginx parses it:
# `http2 on;` exists only from 1.25.1, and Debian 12 ships 1.22.1. Nginx
# prints its version on stderr. The probe also runs in check mode, since the
# template needs the version there too.
- name: Read the installed Nginx version
ansible.builtin.command:
argv:
- /usr/sbin/nginx
- -v
register: cloudfall_nginx_site_version_probe
changed_when: false
failed_when: false
check_mode: false

- name: Require a readable Nginx version
ansible.builtin.assert:
that:
- cloudfall_nginx_site_version_probe.rc == 0
- >-
cloudfall_nginx_site_version_probe.stderr is search(
'^nginx version: nginx/[0-9]+[.][0-9]+[.][0-9]+', multiline=True
)
fail_msg: >-
Cannot read the installed Nginx version from `nginx -v` (exit {{
cloudfall_nginx_site_version_probe.rc
}}): {{
cloudfall_nginx_site_version_probe.stderr | default('', true)
or cloudfall_nginx_site_version_probe.msg | default('')
}}

- name: Record the installed Nginx version
ansible.builtin.set_fact:
cloudfall_nginx_site_version: >-
{{
cloudfall_nginx_site_version_probe.stderr
| regex_findall(
'^nginx version: nginx/([0-9]+[.][0-9]+[.][0-9]+)', multiline=True
)
| first
}}

- name: Create the ACME challenge webroot
ansible.builtin.file:
path: "{{ cloudfall_nginx_site_acme_webroot }}"
Expand Down Expand Up @@ -50,43 +91,31 @@
and cloudfall_nginx_site_certificate.stat.exists
}}

- name: Render the managed virtual host
ansible.builtin.template:
src: domain-site.conf.j2
dest: "{{ cloudfall_nginx_site_domain.proxy.configurationPath }}"
owner: root
group: root
mode: "0644"
notify: Reload Nginx

- name: Enable the managed virtual host
ansible.builtin.file:
src: "{{ cloudfall_nginx_site_domain.proxy.configurationPath }}"
dest: >-
/etc/nginx/sites-enabled/{{
cloudfall_nginx_site_domain.proxy.configurationPath | basename
# Only a vhost under sites-available is linked into sites-enabled; an empty
# link path means the declared path is read by Nginx where it is.
- name: Locate the managed virtual host link
ansible.builtin.set_fact:
cloudfall_nginx_site_link: >-
{{
'/etc/nginx/sites-enabled/'
~ (cloudfall_nginx_site_domain.proxy.configurationPath | basename)
if cloudfall_nginx_site_domain.proxy.configurationPath.startswith(
'/etc/nginx/sites-available/'
)
else ''
}}
state: link
notify: Reload Nginx
when: >-
cloudfall_nginx_site_domain.proxy.configurationPath.startswith(
'/etc/nginx/sites-available/'
)

- name: Apply the virtual host before issuance
ansible.builtin.include_tasks: apply_virtual_host.yml

# Nginx is started only after the configuration test passed, so a rejected
# vhost never reaches a starting server.
- name: Enable Nginx
ansible.builtin.systemd_service:
name: nginx.service
enabled: true
state: started

- name: Validate the Nginx configuration before issuance
ansible.builtin.command:
argv:
- /usr/sbin/nginx
- -t
changed_when: false
when: not ansible_check_mode

- name: Serve the ACME challenge location before issuance
ansible.builtin.meta: flush_handlers

Expand Down Expand Up @@ -133,14 +162,8 @@
and cloudfall_nginx_site_certificate.stat.exists
}}

- name: Render the managed virtual host with issued material
ansible.builtin.template:
src: domain-site.conf.j2
dest: "{{ cloudfall_nginx_site_domain.proxy.configurationPath }}"
owner: root
group: root
mode: "0644"
notify: Reload Nginx
- name: Apply the virtual host with issued material
ansible.builtin.include_tasks: apply_virtual_host.yml

- name: Enable the certificate renewal timer
ansible.builtin.systemd_service:
Expand All @@ -149,14 +172,6 @@
state: started
when: cloudfall_nginx_site_domain.tls.mode == "required"

- name: Validate the complete Nginx configuration
ansible.builtin.command:
argv:
- /usr/sbin/nginx
- -t
changed_when: false
when: not ansible_check_mode

- name: Apply the validated virtual host
ansible.builtin.meta: flush_handlers

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,14 @@ server {
{% if cloudfall_nginx_site_tls_active %}

server {
{% if cloudfall_nginx_site_version is version('1.25.1', '>=') %}
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
{% else %}
listen 443 ssl http2;
listen [::]:443 ssl http2;
{% endif %}
server_name {{ names }};

ssl_certificate /etc/letsencrypt/live/{{ cloudfall_nginx_site_domain.primaryName }}/fullchain.pem;
Expand Down
Loading
Loading