Thanks Rich! I am not really a network expert. Hence I debugged that further with the help of Claude - with some success! Some of the output that follows was generated with Claude…
The port bindings turned out to be a red herring, though a reasonable thing to suspect.
You’re right that host_port: 0 means “pick a random ephemeral port”, and docker port openhab confirmed it:
8443/tcp -> 0.0.0.0:32780
8080/tcp -> 0.0.0.0:32782
But those mappings are never used here. The container sits on two networks: the bridge (openhab_bridge) and a macvlan (openhab_network). The macvlan gives it its own LAN IP (192.168.178.210), which is what openhab resolves to — so I connect straight to the container and the published bridge ports play no part. Vestigial clutter, but harmless.
The real cause: two separate TLS problems
The confusing part was that curl -k https://openhab:8443/ returned a clean HTTP/2 200 while browsers and the Android app both failed. It turned out there were two independent issues, and curl happened to be immune to both.
1. The certificate has no subjectAltName (breaks Chrome)
openHAB’s auto-generated self-signed cert carries only a common name:
subject: CN=openhab.org; OU=None; O=None; L=None; C=None
No subjectAltName exists. Chrome has required a SAN since v58 and will not fall back to the CN, so there’s no name to match — it fails at the protocol level with ERR_SSL_PROTOCOL_ERROR and no click-through interstitial. curl -k skips validation entirely, which is why it never complained.
Deleting the keystore and letting openHAB regenerate does not help — it produces the same SAN-less cert.
Probably not a 5.2 regression. My original cert dated from June 2023 and had worked for years. openHAB has likely been generating SAN-less certs the whole time; what changed is browser tolerance. The trigger was most likely a Chrome update, and the timing with the 5.2 upgrade was coincidental.
Fix (example for my setup here) — generate a cert with a proper SAN:
docker exec openhab cp /openhab/userdata/etc/keystore /openhab/userdata/etc/keystore.bak
docker exec openhab keytool -delete -alias mykey \
-keystore /openhab/userdata/etc/keystore -storepass openhab
docker exec openhab keytool -genkeypair \
-alias mykey -keyalg EC -groupname secp256r1 \
-keystore /openhab/userdata/etc/keystore \
-storepass openhab -keypass openhab \
-dname "CN=openhab" -validity 3650 \
-ext "SAN=dns:openhab,dns:openhab.fritz.box,ip:192.168.178.210"
docker restart openhab
Adjust the SAN list to your own hostnames and IP. Verify:
openssl s_client -connect <ip>:8443 -showcerts </dev/null 2>/dev/null \
| openssl x509 -noout -text | grep -A2 "Alternative"
After having performed this, I can access OH through Chrome and port 8443 just fine now! But not yet through the OH Android app…
2. CRYPTO_POLICY=limited breaks TLS 1.3 (breaks the Android app)
With the cert fixed, Chrome was happy — but the openHAB Android app still failed:
javax.net.ssl.SSLProtocolException: Read error: Failure in SSL library, usually a protocol error
error:10000089:SSL routines:OPENSSL_internal:DECODE_ERROR
(external/boringssl/src/ssl/tls13_client.cc:1181)
That’s BoringSSL failing to decode the TLS 1.3 handshake — a protocol fault, not a trust fault. The same symptom is visible in openssl s_client, right at the end of an otherwise successful TLS 1.3 handshake:
error:0A00009F:SSL routines:tls_process_new_session_ticket:length mismatch
The server completes the handshake but then emits a NewSessionTicket that clients cannot decode. OpenSSL shrugs it off (so curl and s_client appear to “work”); BoringSSL — i.e. Android — rejects the connection outright. Forcing TLS 1.2 gives a completely clean handshake with no such error:
openssl s_client -connect openhab:8443 -tls1_2 # clean
openssl s_client -connect openhab:8443 # length mismatch on the ticket
The trigger is CRYPTO_POLICY. The openHAB Docker image defaults to limited — the Dockerfile deliberately rewrites crypto.policy=unlimited to limited in java.security, to comply with local laws that may restrict unlimited-strength cryptography.
Setting it to unlimited fixes the TLS 1.3 problem completely:
CRYPTO_POLICY=unlimited
Restart afterwards. TLS 1.3 then negotiates cleanly and the Android app connects.
Why this isn’t widely reported: every official example — the Docker Hub README, the openhab-docker README, the openhab.org Docker install docs, the Ansible playbook — sets CRYPTO_POLICY: "unlimited" explicitly. So most people never run with the default. My container had been created from the image defaults (Synology’s Container Manager captures the image env into its config), so I was on limited without ever having chosen it. If you’re in the same boat and use HTTPS, you will hit this.
I have not traced why a restricted crypto policy produces a malformed ticket rather than a clean failure or a graceful fallback — that’s for someone who knows the pax-web / Jetty / JDK internals. But the correlation is solid and reproducible: flip the variable, the behaviour flips. Arguably a restricted crypto policy should never cause the server to emit a structurally invalid handshake message, so I suspect there is a real bug somewhere in the stack.
Environment, for the record:
openhab/openhab:5.2.0 (Debian)
- Jetty 9.4.58.v20250814
- OpenJDK 21.0.11+10-1-deb13u2
tl;dr: not a Docker networking issue. Two TLS problems stacked on top of each other:
- openHAB’s self-signed cert has no
subjectAltName → modern Chrome rejects it outright. Regenerate the cert with an explicit SAN.
- The image default
CRYPTO_POLICY=limited causes the server to emit an undecodable TLS 1.3 NewSessionTicket → the Android app (BoringSSL) refuses to connect. Set CRYPTO_POLICY=unlimited.
curl -k was immune to both, which made the server look perfectly healthy when it wasn’t.