Skip to content

Commit 5bb8668

Browse files
author
Miguel Medinilla
committed
Manage decision policy rendering
1 parent 6d5ddbe commit 5bb8668

File tree

5 files changed

+168
-3
lines changed

5 files changed

+168
-3
lines changed

README.md

Lines changed: 105 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ required repositories configured.
6969
| `haproxy_decision_certbot_hook_reload_command` | `systemctl reload haproxy` | Command executed by the hook after regenerating PEM bundles. |
7070
| `haproxy_decision_global_settings` / `haproxy_decision_defaults_settings` | see defaults | Lists of directives written to the `global` and `defaults` sections. |
7171
| `haproxy_decision_listeners`, `haproxy_decision_frontends`, `haproxy_decision_backends` | `[]` | Optional lists of sections appended to the generated configuration. |
72+
| `haproxy_decision_manage_decision_policy` | `false` | When `true` and the `decision` SPOA is enabled, the role creates `/etc/decision-policy` (override with `haproxy_decision_decision_policy_dir`) and renders a managed `policy.yml`. |
73+
| `haproxy_decision_decision_policy` | `{}` | Mapping rendered into the policy file via `to_nice_yaml`. Mirror the structure described in the decision-spoa documentation. |
7274
| `haproxy_decision_spoas` | see defaults | Dictionary describing each SPOA daemon. Set `enabled: true` to activate one, adjust service/backend data, and rely on `haproxy_decision_spoa_releases` for download metadata when installing from GitHub releases. |
7375
| `haproxy_decision_manage_spoa_configs` | `true` | Controls whether the role writes SPOE configuration snippets. |
7476
| `haproxy_decision_manage_spoa_env` | `true` | Controls whether `/etc/default/*` files are managed for SPOAs. |
@@ -133,6 +135,102 @@ Each `listener`, `frontend`, and `backend` entry can optionally supply a single
133135
rendered with Ansible’s template lookup and appended after the static `lines`,
134136
which lets you reuse complex fragments while keeping simple cases inline.
135137

138+
## Certificate management
139+
140+
Enable `haproxy_decision_manage_certificates` to have the role assemble the `.pem`
141+
bundles that HAProxy expects under `haproxy_decision_certificate_dir`. Certificates
142+
can come from Certbot or any other CA—point each entry at either a combined PEM or
143+
the separate `fullchain` and `privkey` files exposed on the target host:
144+
145+
```yaml
146+
haproxy_decision_manage_certificates: true
147+
haproxy_decision_certificates:
148+
- name: apps
149+
domains:
150+
- apps.example.org
151+
fullchain_path: /etc/letsencrypt/live/apps.example.org/fullchain.pem
152+
privkey_path: /etc/letsencrypt/live/apps.example.org/privkey.pem
153+
```
154+
155+
When the referenced files are missing (for example, before Certbot provisions a
156+
fresh certificate), the role drops a short-lived self-signed bundle so HAProxy can
157+
start and continue proxying ACME HTTP-01 traffic. Tune this bootstrap behaviour
158+
with `haproxy_decision_certificate_bootstrap_enabled`, `*_valid_days`, and the
159+
other `haproxy_decision_certificate_bootstrap_*` variables.
160+
161+
Set `haproxy_decision_manage_certbot_hook: true` to have the role install a
162+
Certbot deploy hook that rebuilds any managed PEM bundles sourced from
163+
`/etc/letsencrypt/live/<domain>/` and then executes
164+
`haproxy_decision_certbot_hook_reload_command` (defaults to reloading the
165+
HAProxy service). This keeps certificates fresh immediately after every renewal
166+
without waiting for the next configuration run.
167+
168+
### Using the role with Certbot
169+
170+
The [geerlingguy.certbot](https://github.com/geerlingguy/ansible-role-certbot)
171+
role stores issued material under `/etc/letsencrypt/live/<domain>/` and lets you
172+
describe each request via `certbot_certs`. To keep HAProxy running while
173+
performing HTTP-01 challenges, configure a dedicated backend that proxies ACME
174+
requests to Certbot’s standalone listener on `127.0.0.1:8009`:
175+
176+
```yaml
177+
- hosts: loadbalancers
178+
become: true
179+
vars:
180+
haproxy_decision_manage_certificates: true
181+
haproxy_decision_certificates:
182+
- name: apps
183+
domains: ["apps.example.org"]
184+
fullchain_path: /etc/letsencrypt/live/apps.example.org/fullchain.pem
185+
privkey_path: /etc/letsencrypt/live/apps.example.org/privkey.pem
186+
haproxy_decision_frontends:
187+
- name: public_http
188+
lines:
189+
- "bind *:80"
190+
- "mode http"
191+
- "acl is_certbot path_beg -i /.well-known/acme-challenge"
192+
- "use_backend certbot if is_certbot"
193+
- "http-request redirect scheme https unless { ssl_fc } || is_certbot"
194+
- "default_backend app_servers"
195+
haproxy_decision_backends:
196+
- name: certbot
197+
lines:
198+
- "mode http"
199+
- "server certbot_local 127.0.0.1:8009"
200+
- name: app_servers
201+
lines:
202+
- "mode http"
203+
- "server app1 10.0.0.10:8080 check"
204+
certbot_certs:
205+
- domains: ["apps.example.org"]
206+
certbot_create_method: standalone
207+
certbot_create_standalone_stop_services: []
208+
certbot_create_command: >-
209+
{{ certbot_script }} certonly --{{ certbot_create_method }}
210+
{{ '--test-cert' if certbot_testmode else '' }}
211+
--noninteractive --agree-tos
212+
--email {{ cert_item.email | default(certbot_admin_email) }}
213+
--http-01-port 8009
214+
-d {{ cert_item.domains | join(',') }}
215+
roles:
216+
- role: artefactual.ansible-haproxy-decision
217+
- role: geerlingguy.certbot
218+
tasks:
219+
- name: Refresh HAProxy certificate bundles after Certbot runs
220+
ansible.builtin.import_role:
221+
name: artefactual.ansible-haproxy-decision
222+
tasks_from: certificates.yml
223+
```
224+
225+
The first role run installs HAProxy and, if necessary, seeds it with a bootstrap
226+
certificate. Certbot (proxied through HAProxy on `/.well-known/acme-challenge`)
227+
then retrieves trusted material, and the final task rebuilds the HAProxy bundles
228+
from Certbot’s live files—which triggers a graceful reload through the built-in
229+
handler whenever the certificate changes. On future renewals you can rerun just
230+
the certificate logic with `ansible-playbook … --tags haproxy-decision-certificates`
231+
or invoke it from a Certbot deploy hook so HAProxy picks up the new bundle
232+
immediately.
233+
136234
## SPOA customisation
137235

138236
Each SPOA definition accepts overrides that feed directly into the templates:
@@ -145,6 +243,9 @@ Each SPOA definition accepts overrides that feed directly into the templates:
145243
you need to source binaries from somewhere other than the defaults.
146244
- Supply additional messages or groups for the Cookie Guard SPOA using the
147245
`messages` or `group_definitions` structures.
246+
- Manage the Decision policy tree by setting `haproxy_decision_manage_decision_policy: true`
247+
and filling `haproxy_decision_decision_policy` with a mapping that matches
248+
the YAML schema documented upstream.
148249

149250
Example release override:
150251

@@ -159,9 +260,10 @@ haproxy_decision_spoa_releases:
159260

160261
Legacy overrides that supply `haproxy_decision_spoas.<name>.release.assets`
161262
still work, but migrating to the central `haproxy_decision_spoa_releases`
162-
structure keeps package metadata in a single place.
163-
```
164-
263+
structure keeps package metadata in a single place. Consult the upstream
264+
[`decision-spoa` README](https://github.com/artefactual-labs/decision-spoa#policy-configuration)
265+
for the canonical policy layout and examples; mirror that structure when
266+
populating `haproxy_decision_decision_policy`.
165267
If a more drastic change is required, point `config_template` or `env_template`
166268
to a custom template shipped alongside your playbook.
167269

defaults/main.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,15 @@ haproxy_decision_download_delay: 10
120120

121121
haproxy_decision_coraza_spoa_relax_systemd: false
122122

123+
haproxy_decision_manage_decision_policy: false
124+
haproxy_decision_decision_policy_dir: /etc/decision-policy
125+
haproxy_decision_decision_policy_dir_mode: "0750"
126+
haproxy_decision_decision_policy_file_mode: "0640"
127+
haproxy_decision_decision_policy_owner: haproxy
128+
haproxy_decision_decision_policy_group: haproxy
129+
haproxy_decision_decision_policy_file: policy.yml
130+
haproxy_decision_decision_policy: {}
131+
123132
haproxy_decision_spoas:
124133
decision:
125134
enabled: false

tasks/spoa-item.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,3 +264,35 @@
264264
- haproxy-decision
265265
- haproxy-decision-spoa
266266
- haproxy-decision-config
267+
268+
- name: Ensure decision policy directory exists
269+
ansible.builtin.file:
270+
path: "{{ haproxy_decision_decision_policy_dir }}"
271+
state: directory
272+
owner: "{{ haproxy_decision_decision_policy_owner }}"
273+
group: "{{ haproxy_decision_decision_policy_group }}"
274+
mode: "{{ haproxy_decision_decision_policy_dir_mode }}"
275+
when:
276+
- haproxy_decision_manage_decision_policy | bool
277+
- spoa_item.key == 'decision'
278+
tags:
279+
- haproxy-decision
280+
- haproxy-decision-spoa
281+
- haproxy-decision-config
282+
283+
- name: Render decision policy configuration
284+
ansible.builtin.template:
285+
src: spoa/decision-policy.yml.j2
286+
dest: "{{ haproxy_decision_decision_policy_dir }}/{{ haproxy_decision_decision_policy_file }}"
287+
owner: "{{ haproxy_decision_decision_policy_owner }}"
288+
group: "{{ haproxy_decision_decision_policy_group }}"
289+
mode: "{{ haproxy_decision_decision_policy_file_mode }}"
290+
notify: "restart {{ spoa_item.value.service }}"
291+
when:
292+
- haproxy_decision_manage_decision_policy | bool
293+
- spoa_item.key == 'decision'
294+
- haproxy_decision_decision_policy is defined
295+
tags:
296+
- haproxy-decision
297+
- haproxy-decision-spoa
298+
- haproxy-decision-config
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Managed by artefactual.ansible-haproxy-decision
2+
{{ haproxy_decision_decision_policy | default({}) | to_nice_yaml(indent=2, sort_keys=False) }}

tests/e2e/site.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,19 @@
66
haproxy_decision_manage_repo: false
77
haproxy_decision_coraza_spoa_relax_systemd: true
88
haproxy_decision_rhel_disable_gpg_check: true
9+
haproxy_decision_manage_decision_policy: true
10+
haproxy_decision_decision_policy:
11+
defaults:
12+
global:
13+
use_varnish: true
14+
use_coraza: true
15+
use_challenge: true
16+
rules:
17+
- name: skip-varnish-canada
18+
match:
19+
country: [CA]
20+
return:
21+
use_varnish: false
922
haproxy_decision_spoa_releases:
1023
decision:
1124
rh_package_url: https://github.com/artefactual-labs/decision-spoa/releases/download/v0.1.0/decision-spoa_0.1.0_amd64.rpm
@@ -227,6 +240,13 @@
227240
state: started
228241
enabled: true
229242

243+
- name: Validate decision policy syntax
244+
ansible.builtin.command:
245+
cmd: decision-configcheck --root {{ haproxy_decision_decision_policy_dir }} {{ haproxy_decision_decision_policy_dir }}/{{ haproxy_decision_decision_policy_file }}
246+
environment:
247+
PATH: "/usr/local/bin:{{ ansible_env.PATH | default('/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin') }}"
248+
changed_when: false
249+
230250
handlers:
231251
- name: restart nginx
232252
ansible.builtin.systemd:

0 commit comments

Comments
 (0)