opnsense / opnsense/plugins

net/haproxy: Template generation fails when model serializes list fields as dict instead of string

Open
#5,690 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
PHP
Stars
1.2k
Forks
863
Avg merge
2d 6h
Merged PRs (30d)
10

Description

[HAProxy] Template generation crashes when model serializes list fields as dict instead of string

Important notices

Describe the bug
Applying any HAProxy frontend configuration fails with
Configuration exception — Template generation failed for internal service "haproxy". See backend log for details.
(and a red "HAProxy configtest found critical errors" banner in the GUI).

Root cause: the HAProxy Jinja2 templates (haproxy.conf, sslCerts.yaml)
assume several model fields are CSV strings, but on os-haproxy 5.1 /
OPNsense 26.7.3_8 the model serializes those same fields as dicts
(OrderedDict). The template then calls .split(",") / .name on a dict
and aborts rendering — so HAProxy can never be (re)started from the
plugin-generated config.

Last known working version: unknown — the trigger depends on how the
model serializes list-typed fields; on setups where it still emits
strings the bug stays hidden.

To Reproduce

  1. OPNsense 26.7.3_8, install os-haproxy 5.1, enable the plugin.
  2. Go to Services → HAProxy → Frontend (https://<firewall>/ui/haproxy/frontend).
  3. Click Add, set bind to 0.0.0.0:443, enable SSL, select a certificate, set a default backend, and add an action (use_backend on an ACL) plus the matching ACL.
  4. Click Apply (or trigger it via configctl template reload OPNsense/HAProxy).
  5. See error: red banner + configd.log shows a chain of Jinja
    UndefinedError / TypeError while rendering
    OPNsense/HAProxy/haproxy.conf and OPNsense/HAProxy/sslCerts.yaml.

Expected behavior
Apply should render a valid haproxy.conf and (re)start HAProxy
without manual template patching, regardless of whether the model
serializes list fields as string or dict.

Screenshots
n/a

Relevant log files
configd.log (excerpt, plugin version 5.1 / OPNsense 26.7.3_8):

error generating template OPNsense/HAProxy : ...
  File ".../OPNsense/HAProxy/haproxy.conf", line 2468, in top-level template code
    {% for bind in frontend.bind.split(",") %}
jinja2.exceptions.UndefinedError: 'collections.OrderedDict object' has no attribute 'split'

error generating template OPNsense/HAProxy : ...
  File ".../OPNsense/HAProxy/sslCerts.yaml", line 37, in top-level template code
    {% for cert in frontend.get('ssl_certificates', '').split(',') + ... %}
TypeError: can only concatenate list (not "odict_keys") to list

Confirmed failing spots (all in
/usr/local/opnsense/service/templates/OPNsense/HAProxy/):

  • haproxy.conf ~2468: frontend.bind.split(",")bind is
    {"0.0.0.0:443": {"value": "0.0.0.0:443", "selected": 1}}
  • haproxy.conf AclsAndActions macro (~88, ~96, ~1757):
    linkedData.split(","), action_data.linkedAcls.split(","),
    action_data.name — dicts, not strings/objects
  • haproxy.conf ~2517: option {{frontend.connectionBehaviour}}
    renders a bare option (empty) when the field is empty → hard
    HAProxy syntax error
  • haproxy.conf ~2811: backend.linkedServers.split(",") — dict →
    no server lines emitted → HTTP 502 on every route
  • sslCerts.yaml ~37: frontend.get('ssl_certificates','').split(',')
    — dict → no certlist generated → frontend has no listener

Upstream confirmation
The exact same patterns are present in current master of the plugin:

  • net/haproxy/src/opnsense/service/templates/OPNsense/HAProxy/haproxy.conf
    ({% for action in linkedData.split(",") %},
    action_data.linkedAcls.split(","),
    backend.linkedServers.split(","))
  • net/haproxy/src/opnsense/service/templates/OPNsense/HAProxy/sslCerts.yaml
    (frontend.get('ssl_certificates', '').split(','))

So the templates assume string-typed list fields. The bug only surfaces
when the model serializes those fields as dict (os-haproxy 5.1 /
OPNsense 26.7.3_8). On setups where the model still emits strings the
failure is invisible — which is likely why prior reports were
auto-triaged as low-priority.

Workaround (tested locally — not a PR)

  1. template_helpers.py — make getUUID() return a dict subclass that
    supports both obj['key'] and obj.key:
    class _DotDict(dict):
        def __getattr__(self, name):
            if name in self: return self[name]
            raise AttributeError(name)
        def __getitem__(self, key):
            v = dict.__getitem__(self, key)
            if isinstance(v, dict) and not isinstance(v, _DotDict): return _DotDict(v)
            return v
    
    and return _DotDict(self._template_in_data['__uuid__'].get(uuid, {}))
    inside getUUID().
  2. haproxy.conf — guard the .split(",") calls so they accept a dict:
    {% for x in (field.keys() if field is mapping else field.split(",")) %}
    (applied to frontend.bind, backend.linkedServers,
    action_data.linkedAcls, frontend_data.bind).
  3. haproxy.conf — guard connectionBehaviour:
    {% if frontend.connectionBehaviour|default("") != "" %} option {{frontend.connectionBehaviour}}{% endif %}
  4. sslCerts.yaml — replace the .split(',') on ssl_certificates /
    ssl_default_certificate with a for-loop that iterates dict keys when
    the value is a mapping.

After these, configctl template reload OPNsense/HAProxy returns OK
and HAProxy starts from the plugin-generated config with all backends up.

Suggested proper fix
Add a small Jinja filter (e.g. tolist) in the template engine /
helpers that normalizes both shapes — string CSV and dict — into a list,
and use it wherever the templates currently call .split(",") on a model
field. That makes the templates robust regardless of how the model
serializes list-typed fields.

Additional context
This report was drafted with assistance from an AI tool (Hermes / Nous
Research, model tencent/hy3) based on direct inspection of the failing
template render and a comparison with the plugin master branch, as
required by CONTRIBUTING.md.

Environment

  • OPNsense 26.7.3_8 (amd64)
  • os-haproxy plugin: 5.1
  • haproxy: 3.2.22
  • Python: 3.13.15
  • Jinja2: 3.1.6
  • FreeBSD: 15.1-RELEASE-p3

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the failing configctl template reload OPNsense/HAProxy command and inspect net/haproxy/src/opnsense/service/templates/OPNsense/HAProxy/haproxy.conf and sslCerts.yaml, along with template_helpers.py. Trace the listed fields in both string and dict forms, then verify that frontend, certificate, ACL/action, and backend rendering succeeds and HAProxy starts without syntax errors.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend, networking
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.