Problem
When a group name contains the special character &, the group cannot be synchronized by the ox-connector to Open-Xchange. This causes the ox-connector consumer to stop processing further messages, leading to a broken synchronization state in the environment.
The issue occurs because the Open-Xchange SOAP provisioning API rejects illegal characters during object creation, based on a configurable validation regex.
Symptoms
- Groups containing
&in the name are not created in Open-Xchange. - The
ox-connectorstops processing NATS messages. - Error messages appear in the
ox-connectorpod logs.
Logs from the ox-connector pod
2026-02-25 11:04:52,617 INFO [consumer.handle_message:245] Received message with topic: groups/group, sequence_number: 52, num_delivered: 1
2026-02-25 11:04:52,618 INFO [__init__.get_group_objs:175] Group OX Test & Group will be OX Group
2026-02-25 11:04:52,619 INFO [__init__.get_group_objs:206] Object('groups/group', 'cn=ox test & group,cn=groups,dc=swp-ldap,dc=internal') will be processed with context 1
2026-02-25 11:04:52,619 INFO [groups.create_group:104] Creating Object('groups/group', 'cn=ox test & group,cn=groups,dc=swp-ldap,dc=internal')
2026-02-25 11:04:52,984 ERROR [consumer.create:343] Failed to handle creation
Traceback (most recent call last):
File "/consumer.py", line 318, in create
run(obj)
File "/usr/local/lib/python3.11/dist-packages/univention/ox/provisioning/__init__.py", line 121, in run
create_group(new_obj)
File "/usr/local/lib/python3.11/dist-packages/univention/ox/provisioning/groups.py", line 123, in create_group
group.create()
...
File "/usr/local/lib/python3.11/dist-packages/zeep/wsdl/bindings/soap.py", line 329, in process_error
raise Fault(
zeep.exceptions.Fault: Illegal chars: "&"; exceptionId 192484105-4
2026-02-25 11:04:52,988 WARNING [consumer.create:344] Illegal chars: "&"; exceptionId 192484105-4
Root cause exception from the OX SOAP backend
zeep.exceptions.Fault: Illegal chars: "&"
Root Cause
The Open-Xchange SOAP provisioning API validates the group name field (the technical identifier) against a configurable regular expression. By default, this regex rejects special characters such as &.
Important distinction:
name→ technical identifier, strict regex validation (rejects&).displayname→ human-readable label, allows&and other special characters.
This can be reproduced directly on the OX backend CLI:
./creategroup -c 1 -A oxadmin -P <password> -n 'j&b' -d 'j&b'
→ Illegal chars: "&"; exceptionId -319055231-121
(rejected on -n, accepted on -d)
Processing flow when the issue occurs
- The group object is received via the NATS provisioning stream.
- The
ox-connectortries to create the group via SOAP. - The SOAP call fails due to invalid character validation.
- The message remains in the NATS stream and blocks further processing.
Reference: Univention Bugzilla #59077
Investigation
Verify the LDAP group exists
kubectl exec -n $NAMESPACE ums-ldap-server-primary-0 -- ldapsearch -x \
-D "$(kubectl get -n $NAMESPACE configmaps ums-ldap-server-primary -o json | jq -r '.data.ADMIN_DN')" \
-w "$(kubectl get -n $NAMESPACE secrets ums-ldap-server-admin -o json | jq -r '.data.password' | base64 -d)" \
-b "$(kubectl get -n $NAMESPACE configmaps ums-ldap-server-primary -o json | jq -r '.data.LDAP_BASEDN')" \
cn="OX Test & Group"
See also: How can I use ldapsearch in Nubus openDesk?
Expected result: the LDAP object exists, confirming that the group was created in Nubus but failed to propagate to OX:
dn: cn=OX Test & Group,cn=groups,dc=swp-ldap,dc=internal
cn: OX Test & Group
gidNumber: 5024
isOxGroup: OK
oxContextIDNum: 1
uniqueMember: uid=test.ox.02,cn=users,dc=swp-ldap,dc=internal
uniqueMember: uid=test.ox.01,cn=users,dc=swp-ldap,dc=internal
objectClass: oxGroup
objectClass: univentionGroup
...
Solution
Permanent Fix (Recommended): Allow & in OX Group Names
Open-Xchange validates group name values against a configurable regular expression in /opt/open-xchange/etc/Group.properties. By default, this regex does not include &, which causes the SOAP fault Illegal chars: "&".
The displayname field is not subject to this validation — only name (the technical identifier) is affected.
Step 1: Check the current configuration
Inspect the active Group.properties inside the OX middleware pod to see which validation properties are currently set:
kubectl exec -n <ox-namespace> <core-mw-pod> -- \
cat /opt/open-xchange/etc/Group.properties | grep -i CHECK_GROUP_UID
Example output (default configuration in opendesk):
kubectl exec -n $NAMESPACE open-xchange-core-mw-groupware-6b95f48d94-zdmj2 -- \
cat /opt/open-xchange/etc/Group.properties | grep -i CHECK_GROUP_UID
Defaulted container "core-mw" out of: core-mw, init-middleware (init)
CHECK_GROUP_UID_FOR_NOT_ALLOWED_CHARS = false
CHECK_GROUP_UID_REGEXP = [ $@%\\.+a-zA-Z0-9_-]
Important observation: Despite
CHECK_GROUP_UID_FOR_NOT_ALLOWED_CHARS = false, theCHECK_GROUP_UID_REGEXPis still enforced by the OX backend. The boolean switch alone is not sufficient — the regex itself must be extended to permit&.
Step 2: Understand the two properties
| Property | Default | Effect |
|---|---|---|
CHECK_GROUP_UID_FOR_NOT_ALLOWED_CHARS |
false |
Boolean toggle; in practice insufficient to disable validation |
CHECK_GROUP_UID_REGEXP |
[ $@%\.+a-zA-Z0-9_-] |
Character class applied to the group name; must include & to allow it |
Step 3: Extend the regex in the opendesk Helm values
Edit helmfile/apps/open-xchange/values-openxchange.yaml.gotmpl and add / adjust the CHECK_GROUP_UID_REGEXP entry:
propertiesFiles:
/opt/open-xchange/etc/Group.properties:
CHECK_GROUP_UID_FOR_NOT_ALLOWED_CHARS: "false"
CHECK_GROUP_UID_REGEXP: "[ $@%\\.+a-zA-Z0-9_&-]"
Variations:
- Minimal (only add
&):CHECK_GROUP_UID_REGEXP: "[ $@%\\.+a-zA-Z0-9_&-]" - Including German umlauts and additional common characters:
CHECK_GROUP_UID_REGEXP: "[ $@%\\.+a-zA-Z0-9_&äöüÄÖÜß-]"
Step 4: Apply and restart
helmfile apply -e dev -n ${NAMESPACE}
kubectl rollout restart deployment/open-xchange-core-mw-groupware \
-n ${NAMESPACE}
Step 5: Verify the fix
5.1 Confirm the new property is active inside the pod:
kubectl exec -n ${NAMESPACE} <core-mw-pod> -- \
grep CHECK_GROUP_UID /opt/open-xchange/etc/Group.properties
Expected output:
CHECK_GROUP_UID_FOR_NOT_ALLOWED_CHARS = false
CHECK_GROUP_UID_REGEXP = [ $@%\.+a-zA-Z0-9_&-]
5.2 Test directly via the OX CLI (bypasses ox-connector / SOAP):
kubectl exec -n ${NAMESPACE} <core-mw-pod> -it -- \
/opt/open-xchange/sbin/creategroup \
-c 1 -A oxadmin -P <password> \
-n 'test&group' -d 'test & group'
Expected: success message with group ID, no Illegal chars exception.
5.3 End-to-end test via UMC and ox-connector:
- Create a group containing
&in UMC. - Check
ox-connectorpod logs: the message must be processed and acknowledged withoutzeep.exceptions.Fault. - If a previously stuck message exists in the NATS stream, follow the Recovery section to remove it.
Caveats and notes
- Position of
-in the character class matters: keep it at the end (or escape it). Otherwise it will be interpreted as a range operator and silently break the regex. &is an XML/SOAP special character. The fact that the OX server returns the fault confirms the SOAP request transports correctly — the issue is purely backend-side validation, which is resolved by this configuration change.- Only add characters you actually need. Including
<,>,",'etc. may affect downstream consumers (search, URL handling, client UIs). - Supportability: Confirm with Open-Xchange / Univention support whether this customization is covered by your subscription before applying it to production.
- Other object types: The same pattern likely applies to Users (
CHECK_USER_UID_REGEXPinUser.properties) and Resources (CHECK_RESOURCE_UID_REGEXPinResource.properties). Extend analogously if needed.
Workaround: Rename the Affected Group
If adjusting the OX configuration is not feasible (policy, supportability concerns, or limited access), the simplest mitigation is to rename the group through UMC.
Steps
- Open UMC.
- Edit the affected group.
- Replace the
&character in the group name withand, or remove it entirely. - Save the changes.
Example:
OX Test & Group → OX Test and Group
OX Test & Group → OX Test Group
This prevents SOAP provisioning failures during synchronization. Note that this only avoids the issue for future syncs — if the connector is already stuck on a blocking message, additionally follow the Recovery section below.
Recovery: Remove Stuck Messages from the NATS Stream
If the ox-connector is already stuck and not processing messages, the blocking NATS message must be removed before the connector can resume.
Decision point: If you do not want to enable the NATS debug container or redeploy, you can skip directly to Step 3 (NATS debug session) using
kubectl debugonly.
Step 1: Enable the NATS debug container
The natsBox debug container of the bundled NATS is not deployed by default. Enable it via Helm values as described in the official documentation: Provisioning Service.
nubusProvisioning:
nats:
natsBox:
enabled: true
Optional: edit the values file using yq
Install yq (see yq on GitHub):
snap install yq
Change into the opendesk deployment directory and update the values file:
cd ~/git/opendesk/opendesk-latest/opendesk/
cat helmfile/environments/dev/values.yaml.gotmpl \
| yq ".nubusProvisioning.nats.natsBox.enabled=true" \
> helmfile/environments/dev/values.yaml
Verify the configuration:
cat helmfile/environments/dev/values.yaml
Expected output (excerpt):
replicas:
umsPortalServer: 2
certificate:
issuerRef:
name: "letsencrypt-prod-http"
jitsi:
enabled: false
nubusProvisioning:
nats:
natsBox:
enabled: true
Step 2: Redeploy the environment
helmfile apply -e dev -n ${NAMESPACE}
Step 3: NATS debug session
Access the NATS debug shell as described in the official docs.
Retrieve credentials:
NATS_USER=admin
NATS_PASSWORD=$(kubectl get secrets \
-n ${NAMESPACE} \
ums-provisioning-nats-admin \
-o "jsonpath={.data.password}" | base64 -d)
Start the debug container:
kubectl debug \
-n ${NAMESPACE} \
-it ums-provisioning-nats-0 \
--env NATS_USER="${NATS_USER}" \
--env NATS_PASSWORD="${NATS_PASSWORD}" \
--image=docker.io/natsio/nats-box:0.18.1-nonroot \
-- /bin/sh
Create a NATS context:
nats context add nubus \
--server nats://127.0.0.1:4222 \
--user "${NATS_USER}" \
--password "${NATS_PASSWORD}"
Expected output:
NATS Configuration Context "nubus"
Server URLs: nats://127.0.0.1:4222
Username: admin
Password: *****************************************
Path: /nsc/.config/nats/context/nubus.json
WARNING: Shell environment overrides in place using NATS_USER, NATS_PASSWORD
Step 4: Identify the problematic message sequence
List streams:
nats stream ls
Example output:
╭─────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Streams │
├─────────────────────────┬─────────────┬─────────────────────┬──────────┬─────────┬──────────────┤
│ Name │ Description │ Created │ Messages │ Size │ Last Message │
├─────────────────────────┼─────────────┼─────────────────────┼──────────┼─────────┼──────────────┤
│ stream:ox-connector │ │ 2026-02-24 19:40:33 │ 2 │ 2.8 KiB │ 12m10s │
│ ... │
╰─────────────────────────┴─────────────┴─────────────────────┴──────────┴─────────┴──────────────╯
The relevant stream is stream:ox-connector. Inspect it:
nats stream info stream:ox-connector
Look at the State section at the end of the output:
State:
Messages: 2
Bytes: 2.8 KiB
First Sequence: 52 @ 2026-02-25 11:04:52
Last Sequence: 53 @ 2026-02-25 11:15:48
Active Consumers: 1
Number of Subjects: 1
The first sequence number is typically the blocking one. The exact number can also be read from the
ox-connectorlogs:INFO [consumer.handle_message:245] Received message with topic: groups/group, sequence_number: 52, num_delivered: 1
Step 5: Remove only the problematic message
Do not remove the entire stream with
rm. Usermm(stream rmm— securely removes an individual message) instead.
nats stream rmm stream:ox-connector
Interactive prompt:
? Message Sequence to remove 52
? Really remove message 52 from Stream stream:ox-connector Yes
Step 6: Restart the ox-connector pod
In k9s, simply kill the pod (<ctrl-k>); it will restart and continue processing the remaining messages. Alternatively:
kubectl rollout restart deployment/ox-connector -n ${NAMESPACE}
Verification
The pod should run healthy again and only the unblocked sequence remains in the stream:
nats stream info stream:ox-connector
...
State:
Messages: 1
Bytes: 1.4 KiB
First Sequence: 53 @ 2026-02-25 11:15:48
Last Sequence: 53 @ 2026-02-25 11:15:48
Successful processing logs:
2026-02-25 11:30:42,769 INFO [consumer.start_listening_for_changes:219] Listening for changes in topics: {'oxmail/accessprofile', 'groups/group', ...}
2026-02-25 11:30:43,216 INFO [consumer.handle_message:245] Received message with topic: groups/group, sequence_number: 53, num_delivered: 1
2026-02-25 11:30:43,632 INFO [consumer.handle_message:296] Committing database entries
2026-02-25 11:30:43,646 INFO [api.acknowledge_message_with_retries:155] Message 53 was acknowledged.
Important Notes
- Always verify sequence numbers before removing NATS messages.
- Use
rmminstead ofrmto avoid deleting the entire stream. - Pods may need to be restarted after configuration changes.
- The permanent fix (regex extension) is preferred over the rename workaround for environments where group names with
&are expected by business policy.
Conclusion
This issue is caused by the OX SOAP provisioning API’s regex-based validation of group name values. The default regex ([ $@%\.+a-zA-Z0-9_-]) does not include &, leading to SOAP faults that block the ox-connector NATS consumer.
- Permanent fix: Extend
CHECK_GROUP_UID_REGEXPinGroup.propertiesvia the opendesk Helm values to include&. - Workaround: Rename affected groups to remove
&. - Recovery: Remove stuck NATS messages with
nats stream rmmand restart the connector pod.