Skip to content

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

Description

@miruoy

[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

Activity

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions