diff --git a/haproxy_manager.py b/haproxy_manager.py index 4ec5a14..504d43b 100644 --- a/haproxy_manager.py +++ b/haproxy_manager.py @@ -1863,6 +1863,12 @@ backend default-backend coraza_spoe_cfg = template_env.get_template( 'hap_coraza_spoe_engine.tpl' ).render() + # HAProxy also rejects this file without a trailing LF + # ("Missing LF on last line"). Belt-and-suspenders — even if the + # template ends with a newline, Jinja2 can trim it depending on + # how the file was authored. + if not coraza_spoe_cfg.endswith('\n'): + coraza_spoe_cfg += '\n' coraza_spoe_path = '/etc/haproxy/coraza-spoe.cfg' with open(coraza_spoe_path, 'w') as f: f.write(coraza_spoe_cfg) diff --git a/templates/hap_coraza_spoe_engine.tpl b/templates/hap_coraza_spoe_engine.tpl index d8809df..37a64a3 100644 --- a/templates/hap_coraza_spoe_engine.tpl +++ b/templates/hap_coraza_spoe_engine.tpl @@ -36,15 +36,7 @@ spoe-message coraza-check # `app=str(haproxy)` matches the application named "haproxy" in # coraza-spoa's config.yaml — that's how Coraza picks which ruleset # to apply. - args app=str(haproxy) \ - src-ip=src \ - src-port=src_port \ - dest-ip=dst \ - dest-port=dst_port \ - method=method \ - path=path \ - query=query \ - version=req.ver \ - headers=req.hdrs \ - body=req.body - event on-frontend-http-request + # NOTE: args must be on ONE line. HAProxy does not support backslash + # line continuations in spoe configs (verified the hard way 2026-05-12). + args app=str(haproxy) src-ip=src src-port=src_port dest-ip=dst dest-port=dst_port method=method path=path query=query version=req.ver headers=req.hdrs body=req.body + event on-frontend-http-request