Skip to content

Commit fe98c20

Browse files
author
Miguel Medinilla
committed
Add ALTCHA and decision context management
1 parent 9fb2f90 commit fe98c20

File tree

5 files changed

+356
-4
lines changed

5 files changed

+356
-4
lines changed

README.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,21 @@ required repositories configured.
7171
| `haproxy_decision_listeners`, `haproxy_decision_frontends`, `haproxy_decision_backends` | `[]` | Optional lists of sections appended to the generated configuration. |
7272
| `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`. |
7373
| `haproxy_decision_decision_policy` | `{}` | Mapping rendered into the policy file via `to_nice_yaml`. Mirror the structure described in the decision-spoa documentation. |
74+
| `haproxy_decision_manage_decision_context` | `false` | Controls whether the optional `context.yml` is rendered (co-located with `policy.yml`). Set alongside `haproxy_decision_decision_context`. |
75+
| `haproxy_decision_decision_context` | `{}` | Dictionary rendered to `context.yml` to drive Decision’s trusted-session tagging. Mirrors the structure described in the upstream README. |
76+
| `haproxy_decision_manage_decision_secret` | `false` | Creates `{{ haproxy_decision_decision_secret_dir }}` and manages the HMAC secret referenced by `context.yml` (default `secrets/edge_hmac.key`). Provide either `_secret_src` (role file) or `_secret_content`. |
77+
| `haproxy_decision_decision_secret_generate` | `true` | When no `_secret_src`/`_secret_content` is supplied, generate a random base64 secret (length controlled by `_secret_generate_bytes`) the first time the role runs. |
78+
| `haproxy_decision_decision_configcheck_enabled` | `true` | Runs `decision-configcheck` (default `/usr/local/bin/decision-configcheck -root {{ haproxy_decision_decision_policy_dir }}`) after updating policy/context files to catch syntax errors early. Override `_configcheck_bin`/`_configcheck_args` to customize the command or disable by setting this to `false`. |
7479
| `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. |
7580
| `haproxy_decision_manage_spoa_configs` | `true` | Controls whether the role writes SPOE configuration snippets. |
7681
| `haproxy_decision_manage_spoa_env` | `true` | Controls whether `/etc/default/*` files are managed for SPOAs. |
7782
| `haproxy_decision_manage_spoa_services` | `true` | Enable or disable service/timer management for SPOAs. |
83+
| `haproxy_decision_cookie_guard_altcha_page_template` | `""` | Optional template rendered to `/altcha` (set to a role path such as `files/altcha_challenge.html.lf.j2` when you need to override the package-provided file). Leave blank to keep the file managed by the cookie-guard-spoa package. |
84+
| `haproxy_decision_cookie_guard_altcha_page_dest` | `{{ haproxy_decision_config_dir }}/altcha_challenge.html.lf` | Location of the challenge page when you opt-in to managing it via the role. |
85+
| `haproxy_decision_cookie_guard_manage_altcha_assets` | `false` | When true, the role stages `altcha.min.js` under `{{ haproxy_decision_cookie_guard_altcha_assets_dir }}/<version>/`, writes a `VERSION` file, and refreshes the `active` symlink. Leave `false` to rely on the cookie-guard-spoa package installing/updating the assets. |
86+
| `haproxy_decision_cookie_guard_altcha_page_owner` / `_group` / `_mode` | see defaults | Ownership and permissions applied to the managed ALTCHA HTML page. |
87+
| `haproxy_decision_cookie_guard_altcha_assets_dir` / `_version` / `_asset_src` | see defaults | Controls where ALTCHA JS assets are installed and the label/source to copy when `manage_altcha_assets` is enabled. Keep `_asset_src` empty when relying on packages. |
88+
| `haproxy_decision_cookie_guard_altcha_assets_owner` / `_group` / `_mode` | see defaults | Ownership and permissions enforced on the ALTCHA asset tree plus `VERSION`. |
7889
| `haproxy_decision_coraza_spoa_relax_systemd` | `false` | When `true` the role installs a systemd drop-in that removes the `BindReadOnlyPaths=-/etc/ld.so.cache` restriction from the `coraza-spoa` service. |
7990
| `haproxy_decision_release_url_template` | `https://github.com/{repo}/releases/download/{version}/{asset}` | Base template used to compose download URLs for GitHub releases. |
8091
| `haproxy_decision_haproxy_url_template` | `haproxy_decision_release_url_template` | Template applied to HAProxy downloads. Package entries may override it per release. |
@@ -135,6 +146,90 @@ Each `listener`, `frontend`, and `backend` entry can optionally supply a single
135146
rendered with Ansible’s template lookup and appended after the static `lines`,
136147
which lets you reuse complex fragments while keeping simple cases inline.
137148

149+
## Cookie-guard ALTCHA flow
150+
151+
Enabling `haproxy_decision_spoas.cookie_guard.enabled` now deploys everything
152+
required to run the built-in ALTCHA challenge provided by
153+
[`cookie-guard-spoa`](https://github.com/artefactual-labs/cookie-guard-spoa):
154+
155+
- The cookie-guard-spoa package already installs `/etc/haproxy/altcha_challenge.html.lf`
156+
plus the ALTCHA assets under `/etc/haproxy/assets/altcha/`. The role leaves
157+
these files untouched by default. Provide
158+
`haproxy_decision_cookie_guard_altcha_page_template` or set
159+
`haproxy_decision_cookie_guard_manage_altcha_assets: true` only when you need
160+
to override them (for example, to ship a custom HTML page or bundle a specific
161+
JS release under version control).
162+
- Default CLI flags include `-cookie-secure`, `-altcha-assets`, and
163+
`-altcha-page` so the agent knows where to find these files.
164+
- The managed SPOE template now emits both the TCP backend that HAProxy uses for
165+
SPOE frames and an HTTP backend (`cookie_guard_http_backend` by default) that
166+
points to the agent’s metrics port.
167+
168+
Expose the endpoints by adding a simple ACL to any frontend that should serve
169+
ALTCHA traffic:
170+
171+
```
172+
acl altcha_routes path_beg -i /altcha /altcha- /assets/altcha/
173+
use_backend cookie_guard_http_backend if altcha_routes
174+
```
175+
176+
When HAProxy decides a client needs a challenge (for example, when
177+
`var(txn.cookieguard.valid) -m str 1` fails), redirect them to `/altcha` and
178+
preserve the original path so the page can return once the hb_v2 cookie is set:
179+
180+
```
181+
http-request redirect code 302 location /altcha?url=%[req.uri] if chal_target !cookie_ok
182+
```
183+
184+
You can override any of the `_altcha_*` variables to bring your own HTML, take
185+
over asset management, or point the generated HTTP backend at a different
186+
listener. Adjust `haproxy_decision_spoas.cookie_guard.http_backend` if you
187+
prefer a different backend name or need to disable the section entirely. Keep
188+
`haproxy_decision_cookie_guard_manage_altcha_assets: false` (default) when the
189+
cookie-guard-spoa package provides `/etc/haproxy/assets/altcha` for you.
190+
191+
## Decision context + secrets
192+
193+
When running `decision-spoa` you often need to ship a policy bundle that now
194+
consists of:
195+
196+
```
197+
/etc/decision-policy /
198+
policy.yml # request rules
199+
context.yml # response allowlist and trusted-session tags
200+
secrets/
201+
edge_hmac.key # HMAC secret referenced by context.yml
202+
```
203+
204+
Enable `haproxy_decision_manage_decision_policy` to create the base directory
205+
and render `policy.yml`. To manage the other files:
206+
207+
- `haproxy_decision_manage_decision_context: true` renders
208+
`{{ haproxy_decision_decision_context_path }}` from the
209+
`haproxy_decision_decision_context` mapping via
210+
`templates/spoa/context.yml.j2`. Match the schema in the upstream
211+
[Trusted context](https://github.com/artefactual-labs/decision-spoa#trusted-context-contextyml)
212+
section (response allowlist + tags, hashing mode, secret path).
213+
- `haproxy_decision_manage_decision_secret: true` ensures
214+
`{{ haproxy_decision_decision_secret_dir }}` exists and writes the secret to
215+
`{{ haproxy_decision_decision_secret_path }}`. Provide either
216+
`haproxy_decision_decision_secret_src` (path relative to this role’s `files/`
217+
directory or the Ansible control machine) or
218+
`haproxy_decision_decision_secret_content` (inline string). Leave both empty to
219+
let the role generate a base64 secret once (length controlled by
220+
`haproxy_decision_decision_secret_generate_bytes`). The task runs with `no_log`
221+
enabled by default.
222+
- Set `haproxy_decision_decision_configcheck_enabled: true` (default) to invoke
223+
`decision-configcheck` after rendering the files. This mirrors running
224+
`decision-configcheck -root /etc/decision-policy` manually and fails fast when
225+
either YAML file is invalid.
226+
227+
All three tasks notify the `decision-spoa` service so changes take effect
228+
immediately (or at the next handler run, depending on your play). When the
229+
secret path is relative (default `secrets/edge_hmac.key`) the role keeps it under
230+
`haproxy_decision_decision_policy_dir`, mirroring the layout described in the
231+
Decision README.
232+
138233
## Certificate management
139234

140235
Enable `haproxy_decision_manage_certificates` to have the role assemble the `.pem`

defaults/main.yml

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,20 @@ haproxy_decision_backends: []
9797
haproxy_decision_spoe_configs_dir: "{{ haproxy_decision_config_dir }}/spoe.d"
9898
haproxy_decision_spoe_include_glob: "{{ haproxy_decision_spoe_configs_dir }}/*.cfg"
9999
haproxy_decision_spoe_include_configs: false
100+
haproxy_decision_cookie_guard_altcha_page_template: ""
101+
haproxy_decision_cookie_guard_altcha_page_dest: "{{ haproxy_decision_config_dir }}/altcha_challenge.html.lf"
102+
haproxy_decision_cookie_guard_altcha_page_owner: "{{ haproxy_decision_config_owner }}"
103+
haproxy_decision_cookie_guard_altcha_page_group: "{{ haproxy_decision_config_group }}"
104+
haproxy_decision_cookie_guard_altcha_page_mode: "0640"
105+
haproxy_decision_cookie_guard_manage_altcha_assets: false
106+
haproxy_decision_cookie_guard_altcha_assets_dir: "{{ haproxy_decision_config_dir }}/assets/altcha"
107+
haproxy_decision_cookie_guard_altcha_assets_dir_mode: "0755"
108+
haproxy_decision_cookie_guard_altcha_assets_version: ""
109+
haproxy_decision_cookie_guard_altcha_asset_src: ""
110+
haproxy_decision_cookie_guard_altcha_assets_owner: "{{ haproxy_decision_config_owner }}"
111+
haproxy_decision_cookie_guard_altcha_assets_group: "{{ haproxy_decision_config_group }}"
112+
haproxy_decision_cookie_guard_altcha_assets_mode: "0644"
113+
haproxy_decision_cookie_guard_altcha_active_symlink: "active"
100114
haproxy_decision_spoa_config_owner: root
101115
haproxy_decision_spoa_config_group: haproxy
102116
haproxy_decision_spoa_config_mode: "0640"
@@ -127,7 +141,39 @@ haproxy_decision_decision_policy_file_mode: "0640"
127141
haproxy_decision_decision_policy_owner: haproxy
128142
haproxy_decision_decision_policy_group: haproxy
129143
haproxy_decision_decision_policy_file: policy.yml
144+
haproxy_decision_decision_policy_path: >-
145+
{{ haproxy_decision_decision_policy_file
146+
if (haproxy_decision_decision_policy_file | string).startswith('/')
147+
else haproxy_decision_decision_policy_dir ~ '/' ~ haproxy_decision_decision_policy_file }}
130148
haproxy_decision_decision_policy: {}
149+
haproxy_decision_manage_decision_context: false
150+
haproxy_decision_decision_context_file: context.yml
151+
haproxy_decision_decision_context_path: >-
152+
{{ haproxy_decision_decision_context_file
153+
if (haproxy_decision_decision_context_file | string).startswith('/')
154+
else haproxy_decision_decision_policy_dir ~ '/' ~ haproxy_decision_decision_context_file }}
155+
haproxy_decision_decision_context: {}
156+
haproxy_decision_manage_decision_secret: false
157+
haproxy_decision_decision_secret_dir: "{{ haproxy_decision_decision_policy_dir }}/secrets"
158+
haproxy_decision_decision_secret_dir_mode: "0750"
159+
haproxy_decision_decision_secret_file: edge_hmac.key
160+
haproxy_decision_decision_secret_path: >-
161+
{{ haproxy_decision_decision_secret_file
162+
if (haproxy_decision_decision_secret_file | string).startswith('/')
163+
else haproxy_decision_decision_secret_dir ~ '/' ~ haproxy_decision_decision_secret_file }}
164+
haproxy_decision_decision_secret_owner: "{{ haproxy_decision_decision_policy_owner }}"
165+
haproxy_decision_decision_secret_group: "{{ haproxy_decision_decision_policy_group }}"
166+
haproxy_decision_decision_secret_mode: "0640"
167+
haproxy_decision_decision_secret_content: ""
168+
haproxy_decision_decision_secret_src: ""
169+
haproxy_decision_decision_secret_no_log: true
170+
haproxy_decision_decision_secret_generate: true
171+
haproxy_decision_decision_secret_generate_bytes: 48
172+
haproxy_decision_decision_configcheck_enabled: true
173+
haproxy_decision_decision_configcheck_bin: /usr/local/bin/decision-configcheck
174+
haproxy_decision_decision_configcheck_args:
175+
- "-root"
176+
- "{{ haproxy_decision_decision_policy_dir }}"
131177

132178
haproxy_decision_spoas:
133179
decision:
@@ -195,7 +241,11 @@ haproxy_decision_spoas:
195241
- "-metrics 127.0.0.1:9904"
196242
- "-secret /etc/cookie-guard-spoa/secret.key"
197243
- "-ttl 1h"
198-
- "-expected-len 168"
244+
- "-skew 30s"
245+
- "-altcha-assets {{ haproxy_decision_cookie_guard_altcha_assets_dir }}"
246+
- "-altcha-page {{ haproxy_decision_cookie_guard_altcha_page_dest }}"
247+
- "-altcha-expires 2m"
248+
- "-cookie-secure"
199249
config_template: spoa/cookie-guard-spoa.cfg.j2
200250
config_path: "{{ haproxy_decision_spoe_configs_dir }}/cookie-guard-spoa.cfg"
201251
backend:
@@ -207,3 +257,16 @@ haproxy_decision_spoas:
207257
address: 127.0.0.1
208258
port: 9903
209259
options: "check"
260+
http_backend:
261+
enabled: true
262+
name: cookie_guard_http_backend
263+
mode: http
264+
lines:
265+
- "option forwarded"
266+
- "option forwardfor"
267+
- "http-request set-header X-Forwarded-For %[src]"
268+
servers:
269+
- name: cookie_guard_http
270+
address: 127.0.0.1
271+
port: 9904
272+
options: "check"

0 commit comments

Comments
 (0)