Skip to content

HAProxy ssl_sni_matches/host_matches frontend ACLs are stored via the API but never rendered into haproxy.cfg, breaking SNI/Host-based routing #948

Open

Description

@antoniodargenio

Is your feature request related to existing pfSense functionality that is missing from the API? Please describe.
Yes — Services > HAProxy > Settings > Frontend > <edit> > Access Control Lists, expression types "SSL SNI matches" / "Host matches" (and presumably the sibling ssl_sni_contains/ssl_sni_starts_with/ssl_sni_ends_with/ssl_sni_regex/host_* variants, only the two above were directly tested). The REST API exposes the identical field (ha_acls[].expression on /api/v2/services/haproxy/frontend, and the dedicated /api/v2/services/haproxy/frontend/acl sub-resource) and accepts/stores these values without error — but this isn't a missing field, it's a field that's accepted and echoed back correctly by GET yet doesn't actually work.

Is your feature request related to a problem? Please describe.
I'm always frustrated when I configure a TCP-mode SNI-passthrough frontend (the standard pfSense-HAProxy recipe for routing multiple HTTPS backends off one WAN IP without terminating TLS on pfSense) entirely through the REST API, and it silently fails to actually work. Repro:

  1. POST /api/v2/services/haproxy/frontend — TCP-mode frontend with a default backend.
  2. POST /api/v2/services/haproxy/frontend/acl — {"parent_id": <id>, "name": "my_acl", "expression": "ssl_sni_matches", "value": "example.com"}.
  3. POST /api/v2/services/haproxy/frontend/action — {"parent_id": <id>, "action": "use_backend", "acl": "my_acl", "backend": "other_backend"}.
  4. POST /api/v2/services/haproxy/apply — returns 200/{"applied": false}, no error surfaced to the caller.
  5. HAProxy actually fails to reload. The only place the failure shows up is /var/log/system.log:
    [ALERT] : config : parsing [.../haproxy.cfg:N] : error detected while parsing switching rule : no such ACL : 'my_acl'.
    [ALERT] : config : Fatal errors found in configuration file
    
    The generated config contains the use_backend ... if my_acl switching line but never the acl my_acl ... declaration it depends on — so the ACL is silently dropped somewhere between stored config and rendered haproxy.cfg, only for these expression types. A source_ip-expression ACL added the identical way (same frontend, same nested/sub-resource creation paths tried both ways) rendered and reloaded correctly, so this isn't a general "ACLs aren't wired up" issue.

Describe the solution you'd like
Fix whatever translates a stored ACL's expression enum value into the corresponding acl <name> <fetch> line in haproxy.cfg so it covers ssl_sni_matches/ssl_sni_contains/ssl_sni_starts_with/ssl_sni_ends_with/ssl_sni_regex and the host_* family the same way it already correctly covers source_ip and (presumably) other working expression types.

Describe alternatives you've considered

  • expression: "custom" with the raw HAProxy fetch syntax as value (e.g. "req.ssl_sni -i example.com" for SNI, "hdr(host) -i example.com" for Host) — this renders correctly and is the workaround I'm using, but it means the friendlier typed expression options are unusable for the single most common HAProxy-on-pfSense use case (SNI/Host-based backend routing).

Additional context

  • Versions: pfSense 2.9.0-RELEASE (FreeBSD 16.0-CURRENT), pfSense-pkg-RESTAPI 2.10_2, pfSense-pkg-haproxy 0.65.7 (haproxy 3.2.19).
  • Reproduced on a stock install with only pfSense-pkg-haproxy installed via POST /api/v2/system/package {"name": "pfSense-pkg-haproxy"}, no other HAProxy config present beforehand.
  • Tried both ACL-creation paths (inline ha_acls/a_actionitems arrays in the frontend's own POST/PATCH body, and the dedicated /frontend/acl + /frontend/action sub-resource endpoints) — both produce the same broken render, and GET on the frontend correctly echoes the ACL back with the right parent_id either way, confirming it's stored correctly and the bug is specifically in the config-file renderer.
  • Happy to provide full request/response payloads and system.log excerpts on request.

Activity

  1. jaredhendrickson13 commented on Oct 20, 2026

    @jaredhendrickson13
    Member

    I'm having some trouble reproducing this. Could you provide the full frontend configuration you're using when you encounter this? Feel free to scrub any sensitive or environment specific values.

    This is strange behavior because the API itself actually doesn't render the haproxy.cfg file, it just calls the pfSense function that is responsible for rendering the HAProxy configuration from the pfSense configuration. It's the same function that triggers when you click on the "Apply changes" button in the UI. The full frontend configuration will help narrow it down.

    Thanks!

  2. antoniodargenio commented on Oct 20, 2026

    @antoniodargenio
    Author

    Thanks for looking into this! I re-ran it on a throwaway frontend today and found the cause.

    Repro (frontend bound to 127.0.0.1:18443, backends prod_https/deb1_https already exist):

    POST /services/haproxy/frontend      {"name":"zz_repro948","status":"active","type":"tcp","backend_serverpool":"prod_https","a_extaddr":[{"extaddr":"custom","extaddr_custom":"127.0.0.1","extaddr_port":"18443","extaddr_ssl":false}],"advanced":"tcp-request inspect-delay 5s\ntcp-request content accept if { req.ssl_hello_type 1 }"}
    POST /services/haproxy/frontend/acl  {"parent_id":4,"name":"is_demo_www","expression":"ssl_sni_matches","value":"demo-www.example.com","casesensitive":false,"not":false}
    POST /services/haproxy/frontend/action {"parent_id":4,"action":"use_backend","acl":"is_demo_www","backend":"deb1_https"}
    POST /services/haproxy/apply
    

    Rendered section (/var/etc/haproxy_test/haproxy.cfg), acl line missing:

    frontend zz_repro948
    	bind 127.0.0.1:18443 name 127.0.0.1:18443
    	mode tcp
    	...
    	use_backend deb1_https_ipvANY  if  is_demo_www
    	default_backend prod_https_ipvANY
    

    Log: error detected while parsing switching rule : no such ACL : 'is_demo_www'.

    Cause: in haproxy.inc the ssl_sni_* entries of $a_acltypes have 'mode' => 'https', and the renderer skips an ACL when $acltype['mode'] != strtolower($frontendtype) (line ~1067). In the package, a TCP-mode SSL frontend has internal type https ("ssl / https (TCP mode)"), but the API's type field takes tcp, and that value is what gets stored. So tcp never matches https and all ssl_sni_* ACLs are silently filtered. source_ip has no mode, so it is unaffected, which matches my first report.

    Confirmed: PATCH {"id":4,"type":"https"} is accepted by the API, and then the same ACL renders (acl is_demo_www req.ssl_sni -i demo-www.example.com) and HAProxy reloads fine. So the model's type choices (tcp/http) probably need to map to the package's https/http values, or the type enum should expose https. I'm not sure which is intended, since tcp also seems to be what the GUI would show as "ssl/https (TCP mode)" — does the GUI store https for that option on your side?

    Versions: pfSense 2.9.0, pfSense-pkg-RESTAPI 2.10_2, pfSense-pkg-haproxy 0.65.7 (HAProxy 3.2.19).

  3. jaredhendrickson13 commented on Oct 23, 2026

    @jaredhendrickson13
    Member

    Yeah the UI's descriptive values are a little confusing compared to values they store internally in the XML config, which is what the API uses.

    • http / https(offloading) in the UI = http internally
    • ssl / https(TCP mode) in the UI = https internally
    • tcp in the UI = tcp internally

    I think a good adjustment here would be to add some extra validation to the ACL's expression field to ensure the type is support for the frontend's configured type. I can get that worked into the next patch.

    Thanks!

  4. antoniodargenio commented on Oct 23, 2026

    @antoniodargenio
    Author

    Thanks for the clear explanation of the UI vs. internal values, that settles my question, and adding validation of the ACL expression against the frontend type sounds like exactly the right fix. For what it's worth, we're using type: https for TCP-mode SSL frontends meanwhile and it works well. Great, responsive maintenance on this project, thank you!

  5. linked a pull request that will close this issuev2.10.3 Fixes & Features #959on Oct 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementIssues or PRs that enhance existing features

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions