From b63447f4fe3481ee0b02818cd2af434915d35bb4 Mon Sep 17 00:00:00 2001 From: Panos Kalogeropoulos Date: Sat, 18 Oct 2025 17:40:33 +0200 Subject: [PATCH 1/7] Update documentation to include changes needed to locally run the central instance and edge gateway Docker profiles for development --- .../gateway-tunnelling-setup.md | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) diff --git a/docs/developer-guide/gateway-tunnelling-setup.md b/docs/developer-guide/gateway-tunnelling-setup.md index 7413a953..f9aecac1 100644 --- a/docs/developer-guide/gateway-tunnelling-setup.md +++ b/docs/developer-guide/gateway-tunnelling-setup.md @@ -39,3 +39,157 @@ This guide describes the steps necessary to setup the gateway tunnelling functio * Set TCP port range in sish service (to allow raw TCP tunnelling) * Allow inbound access to port `2222` and to the TCP port range exposed on the instance * Generate or select existing SSH private key and add this to the deployment image and set SISH variable: `--private-keys-directory` + +# Gateway Tunnelling Development Setup + +To run the manager locally as an edge gateway, to test the gateway tunnelling functionality, two different docker compose profiles need to be running: +* The central instance profile (e.g. `docker-compose.central.yml`) needs to be running to provide the sish server functionality, with the correctly configured environment variables +* The proxy development profile needs to be running to provide the correctly setup proxying functionality, with the correctly configured environment variables + +You need to setup the SSH keys as described in the "Edge Instance Setup" section above. + +For the **central instance** profile: + +Run the main `docker-compose.yml` file with `OR_HOSTNAME=localhost`, and add the following: +* In the proxy service: + * SISH_PORT: 8090 + * SISH_HOST: sish +* In the manager service: + * Remove the manager port exports for metrics etc., and add ``8008:8008`` to allow attaching the debugger from the IDE + * Optionally, set the manager to be built from context ``./manager/build/install/manager``, so that code changes are reflected during Docker image rebuild (after running `./gradlew clean installDist`) + * Add `OR_JAVA_OPTS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:8008"` to allow remote debugging from the IDE + * `OR_METRICS_ENABLED: false` + * `OR_GATEWAY_TUNNEL_SSH_HOSTNAME: "localhost"` + * `OR_GATEWAY_TUNNEL_SSH_PORT: 2222` + * `OR_GATEWAY_TUNNEL_TCP_START: 9000` + * `OR_GATEWAY_TUNNEL_HOSTNAME: "localhost"` + * `OR_GATEWAY_TUNNEL_AUTO_CLOSE_MINUTES: 2` +* Add the ``sish`` service, as found in `deploy.yml`, and modify: + * Add volume ``./deployment:/deployment`` so that you can map the SSH keys as instructed above + +For the **proxy development profile**: + +* Modify the proxy service: + * Instead of extending from `deploy.yml`, instead use the default proxy image: + * `image: openremote/proxy:latest` + * Set the ports of the proxy service, so that they dont clash with the central instance's proxy ports: + * `- "444:443"` + * `- "808:80"` + * Set the sish environment variables: + * `SISH_HOST: sish` + * `SISH_PORT: 8090` + +The above setup should make the **`org.openremote.test.gateway.GatewayTest#Gateway Tunneling Edge Gateway Integration test`** pass when run from the IDE or via Gradle. + + +Here's a Git diff you can apply to your local `docker-compose.central.yml` and `docker-compose.proxy.dev.yml` files to get the above setup: + + +
+ Click to see the patch + +```diff +Index: profile/dev-proxy.yml +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/profile/dev-proxy.yml b/profile/dev-proxy.yml +--- a/profile/dev-proxy.yml (revision 2b8c46b824dd3a48657fd71defd6699dac65b98e) ++++ b/profile/dev-proxy.yml (date 1760799651300) +@@ -16,9 +16,16 @@ + services: + + proxy: +- extends: +- file: deploy.yml +- service: proxy ++# extends: ++# file: deploy.yml ++# service: proxy ++ image: openremote/proxy:latest ++ depends_on: ++ keycloak: ++ condition: service_healthy ++ ports: ++ - "444:443" ++ - "808:80" + environment: + MANAGER_HOST: 'host.docker.internal' + # Uncomment to use sish in development +Index: docker-compose.yml +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/docker-compose.yml b/docker-compose.yml +--- a/docker-compose.yml (revision 2b8c46b824dd3a48657fd71defd6699dac65b98e) ++++ b/docker-compose.yml (date 1760798570417) +@@ -33,7 +33,8 @@ + DOMAINNAMES: ${OR_ADDITIONAL_HOSTNAMES:-} + # USE A CUSTOM PROXY CONFIG - COPY FROM https://raw.githubusercontent.com/openremote/proxy/main/haproxy.cfg + #HAPROXY_CONFIG: '/data/proxy/haproxy.cfg' +- ++ SISH_PORT: 8090 ++ SISH_HOST: sish + postgresql: + restart: always + image: openremote/postgresql:${POSTGRESQL_VERSION:-latest} +@@ -57,16 +58,19 @@ + + + manager: +-# privileged: true ++ # privileged: true + restart: always + image: openremote/manager:${MANAGER_VERSION:-latest} ++# build: ++# context: ./manager/build/install/manager + depends_on: + keycloak: + condition: service_healthy + ports: +- - "127.0.0.1:8405:8405" # Localhost metrics access +- - "${PRIVATE_IP:-127.0.0.1}:8405:8405" # Allows to also expose metrics on a given IP address, ++# - "127.0.0.1:8405:8405" # Localhost metrics access ++# - "${PRIVATE_IP:-127.0.0.1}:8405:8405" # Allows to also expose metrics on a given IP address, + # intended to be IP of the interface of the private subnet of the EC2 VM ++ - "8008:8008" + environment: + OR_SETUP_TYPE: + OR_ADMIN_PASSWORD: +@@ -77,11 +81,19 @@ + OR_EMAIL_X_HEADERS: + OR_EMAIL_FROM: + OR_EMAIL_ADMIN: +- OR_METRICS_ENABLED: ${OR_METRICS_ENABLED:-true} ++ OR_METRICS_ENABLED: ${OR_METRICS_ENABLED:-false} + OR_HOSTNAME: ${OR_HOSTNAME:-localhost} + OR_ADDITIONAL_HOSTNAMES: + OR_SSL_PORT: ${OR_SSL_PORT:--1} + OR_DEV_MODE: ${OR_DEV_MODE:-false} ++ OR_JAVA_OPTS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:8008" ++ ++ ++ OR_GATEWAY_TUNNEL_SSH_HOSTNAME: "localhost" ++ OR_GATEWAY_TUNNEL_SSH_PORT: 2222 ++ OR_GATEWAY_TUNNEL_TCP_START: 9000 ++ OR_GATEWAY_TUNNEL_HOSTNAME: "localhost" ++ OR_GATEWAY_TUNNEL_AUTO_CLOSE_MINUTES: 2 + + # The following variables will configure the demo + OR_FORECAST_SOLAR_API_KEY: +@@ -90,3 +102,10 @@ + OR_SETUP_IMPORT_DEMO_AGENT_VELBUS: + volumes: + - manager-data:/storage ++ ++ sish: ++ extends: ++ file: profile/deploy.yml ++ service: sish ++ volumes: ++ - ./deployment:/deployment +``` + +
\ No newline at end of file From 97c6b0ea0079f0017e8645356f8be396de09a7ab Mon Sep 17 00:00:00 2001 From: Panos Kalogeropoulos Date: Tue, 28 Oct 2025 16:10:35 +0200 Subject: [PATCH 2/7] Update documentation to include changes needed to locally run the central instance and edge gateway Docker profiles for development --- .../gateway-tunnelling-setup.md | 133 +----------------- 1 file changed, 4 insertions(+), 129 deletions(-) diff --git a/docs/developer-guide/gateway-tunnelling-setup.md b/docs/developer-guide/gateway-tunnelling-setup.md index f9aecac1..9e69965e 100644 --- a/docs/developer-guide/gateway-tunnelling-setup.md +++ b/docs/developer-guide/gateway-tunnelling-setup.md @@ -44,7 +44,7 @@ This guide describes the steps necessary to setup the gateway tunnelling functio To run the manager locally as an edge gateway, to test the gateway tunnelling functionality, two different docker compose profiles need to be running: * The central instance profile (e.g. `docker-compose.central.yml`) needs to be running to provide the sish server functionality, with the correctly configured environment variables -* The proxy development profile needs to be running to provide the correctly setup proxying functionality, with the correctly configured environment variables +* The testing (unproxied) development profile needs to be running to allow the manager to run properly in the IDE. You need to setup the SSH keys as described in the "Edge Instance Setup" section above. @@ -55,7 +55,7 @@ Run the main `docker-compose.yml` file with `OR_HOSTNAME=localhost`, and add the * SISH_PORT: 8090 * SISH_HOST: sish * In the manager service: - * Remove the manager port exports for metrics etc., and add ``8008:8008`` to allow attaching the debugger from the IDE + * Add ``8008:8008`` to allow attaching the debugger from the IDE * Optionally, set the manager to be built from context ``./manager/build/install/manager``, so that code changes are reflected during Docker image rebuild (after running `./gradlew clean installDist`) * Add `OR_JAVA_OPTS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:8008"` to allow remote debugging from the IDE * `OR_METRICS_ENABLED: false` @@ -65,131 +65,6 @@ Run the main `docker-compose.yml` file with `OR_HOSTNAME=localhost`, and add the * `OR_GATEWAY_TUNNEL_HOSTNAME: "localhost"` * `OR_GATEWAY_TUNNEL_AUTO_CLOSE_MINUTES: 2` * Add the ``sish`` service, as found in `deploy.yml`, and modify: - * Add volume ``./deployment:/deployment`` so that you can map the SSH keys as instructed above + * Add volume ``./deployment:/deployment`` so that you can map the SSH keys that were generated above -For the **proxy development profile**: - -* Modify the proxy service: - * Instead of extending from `deploy.yml`, instead use the default proxy image: - * `image: openremote/proxy:latest` - * Set the ports of the proxy service, so that they dont clash with the central instance's proxy ports: - * `- "444:443"` - * `- "808:80"` - * Set the sish environment variables: - * `SISH_HOST: sish` - * `SISH_PORT: 8090` - -The above setup should make the **`org.openremote.test.gateway.GatewayTest#Gateway Tunneling Edge Gateway Integration test`** pass when run from the IDE or via Gradle. - - -Here's a Git diff you can apply to your local `docker-compose.central.yml` and `docker-compose.proxy.dev.yml` files to get the above setup: - - -
- Click to see the patch - -```diff -Index: profile/dev-proxy.yml -IDEA additional info: -Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP -<+>UTF-8 -=================================================================== -diff --git a/profile/dev-proxy.yml b/profile/dev-proxy.yml ---- a/profile/dev-proxy.yml (revision 2b8c46b824dd3a48657fd71defd6699dac65b98e) -+++ b/profile/dev-proxy.yml (date 1760799651300) -@@ -16,9 +16,16 @@ - services: - - proxy: -- extends: -- file: deploy.yml -- service: proxy -+# extends: -+# file: deploy.yml -+# service: proxy -+ image: openremote/proxy:latest -+ depends_on: -+ keycloak: -+ condition: service_healthy -+ ports: -+ - "444:443" -+ - "808:80" - environment: - MANAGER_HOST: 'host.docker.internal' - # Uncomment to use sish in development -Index: docker-compose.yml -IDEA additional info: -Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP -<+>UTF-8 -=================================================================== -diff --git a/docker-compose.yml b/docker-compose.yml ---- a/docker-compose.yml (revision 2b8c46b824dd3a48657fd71defd6699dac65b98e) -+++ b/docker-compose.yml (date 1760798570417) -@@ -33,7 +33,8 @@ - DOMAINNAMES: ${OR_ADDITIONAL_HOSTNAMES:-} - # USE A CUSTOM PROXY CONFIG - COPY FROM https://raw.githubusercontent.com/openremote/proxy/main/haproxy.cfg - #HAPROXY_CONFIG: '/data/proxy/haproxy.cfg' -- -+ SISH_PORT: 8090 -+ SISH_HOST: sish - postgresql: - restart: always - image: openremote/postgresql:${POSTGRESQL_VERSION:-latest} -@@ -57,16 +58,19 @@ - - - manager: --# privileged: true -+ # privileged: true - restart: always - image: openremote/manager:${MANAGER_VERSION:-latest} -+# build: -+# context: ./manager/build/install/manager - depends_on: - keycloak: - condition: service_healthy - ports: -- - "127.0.0.1:8405:8405" # Localhost metrics access -- - "${PRIVATE_IP:-127.0.0.1}:8405:8405" # Allows to also expose metrics on a given IP address, -+# - "127.0.0.1:8405:8405" # Localhost metrics access -+# - "${PRIVATE_IP:-127.0.0.1}:8405:8405" # Allows to also expose metrics on a given IP address, - # intended to be IP of the interface of the private subnet of the EC2 VM -+ - "8008:8008" - environment: - OR_SETUP_TYPE: - OR_ADMIN_PASSWORD: -@@ -77,11 +81,19 @@ - OR_EMAIL_X_HEADERS: - OR_EMAIL_FROM: - OR_EMAIL_ADMIN: -- OR_METRICS_ENABLED: ${OR_METRICS_ENABLED:-true} -+ OR_METRICS_ENABLED: ${OR_METRICS_ENABLED:-false} - OR_HOSTNAME: ${OR_HOSTNAME:-localhost} - OR_ADDITIONAL_HOSTNAMES: - OR_SSL_PORT: ${OR_SSL_PORT:--1} - OR_DEV_MODE: ${OR_DEV_MODE:-false} -+ OR_JAVA_OPTS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:8008" -+ -+ -+ OR_GATEWAY_TUNNEL_SSH_HOSTNAME: "localhost" -+ OR_GATEWAY_TUNNEL_SSH_PORT: 2222 -+ OR_GATEWAY_TUNNEL_TCP_START: 9000 -+ OR_GATEWAY_TUNNEL_HOSTNAME: "localhost" -+ OR_GATEWAY_TUNNEL_AUTO_CLOSE_MINUTES: 2 - - # The following variables will configure the demo - OR_FORECAST_SOLAR_API_KEY: -@@ -90,3 +102,10 @@ - OR_SETUP_IMPORT_DEMO_AGENT_VELBUS: - volumes: - - manager-data:/storage -+ -+ sish: -+ extends: -+ file: profile/deploy.yml -+ service: sish -+ volumes: -+ - ./deployment:/deployment -``` - -
\ No newline at end of file +The above setup should make the **`org.openremote.test.gateway.GatewayTest#Gateway Tunneling Edge Gateway Integration test`** pass when run from the IDE or via Gradle. \ No newline at end of file From bcfdcb3c9693f9e387d712430b0bb58cec2120f0 Mon Sep 17 00:00:00 2001 From: Panos Kalogeropoulos Date: Tue, 25 Nov 2025 14:55:41 +0100 Subject: [PATCH 3/7] Copilot corrections --- docs/developer-guide/gateway-tunnelling-setup.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/developer-guide/gateway-tunnelling-setup.md b/docs/developer-guide/gateway-tunnelling-setup.md index 9e69965e..566947fb 100644 --- a/docs/developer-guide/gateway-tunnelling-setup.md +++ b/docs/developer-guide/gateway-tunnelling-setup.md @@ -55,8 +55,8 @@ Run the main `docker-compose.yml` file with `OR_HOSTNAME=localhost`, and add the * SISH_PORT: 8090 * SISH_HOST: sish * In the manager service: - * Add ``8008:8008`` to allow attaching the debugger from the IDE - * Optionally, set the manager to be built from context ``./manager/build/install/manager``, so that code changes are reflected during Docker image rebuild (after running `./gradlew clean installDist`) + * Add `8008:8008` to allow attaching the debugger from the IDE + * Optionally, set the manager to be built from context `./manager/build/install/manager`, so that code changes are reflected during Docker image rebuild (after running `./gradlew clean installDist`) * Add `OR_JAVA_OPTS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:8008"` to allow remote debugging from the IDE * `OR_METRICS_ENABLED: false` * `OR_GATEWAY_TUNNEL_SSH_HOSTNAME: "localhost"` @@ -64,7 +64,7 @@ Run the main `docker-compose.yml` file with `OR_HOSTNAME=localhost`, and add the * `OR_GATEWAY_TUNNEL_TCP_START: 9000` * `OR_GATEWAY_TUNNEL_HOSTNAME: "localhost"` * `OR_GATEWAY_TUNNEL_AUTO_CLOSE_MINUTES: 2` -* Add the ``sish`` service, as found in `deploy.yml`, and modify: - * Add volume ``./deployment:/deployment`` so that you can map the SSH keys that were generated above +* Add the `sish` service, as found in `deploy.yml`, and modify: + * Add volume `./deployment:/deployment` so that you can map the SSH keys that were generated above -The above setup should make the **`org.openremote.test.gateway.GatewayTest#Gateway Tunneling Edge Gateway Integration test`** pass when run from the IDE or via Gradle. \ No newline at end of file +The above setup should make the **`org.openremote.test.gateway.GatewayTest#Gateway Tunnelling Edge Gateway Integration test`** pass when run from the IDE or via Gradle. \ No newline at end of file From b5c14537fe9fa65659bb275b860bcc5b45ad4c84 Mon Sep 17 00:00:00 2001 From: Panos Kalogeropoulos Date: Tue, 25 Nov 2025 15:05:35 +0100 Subject: [PATCH 4/7] Add `/etc/hosts` file edit description --- docs/developer-guide/gateway-tunnelling-setup.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/developer-guide/gateway-tunnelling-setup.md b/docs/developer-guide/gateway-tunnelling-setup.md index 566947fb..75c81e89 100644 --- a/docs/developer-guide/gateway-tunnelling-setup.md +++ b/docs/developer-guide/gateway-tunnelling-setup.md @@ -67,4 +67,10 @@ Run the main `docker-compose.yml` file with `OR_HOSTNAME=localhost`, and add the * Add the `sish` service, as found in `deploy.yml`, and modify: * Add volume `./deployment:/deployment` so that you can map the SSH keys that were generated above +The routing of requests from the central instance to the gateway looks like this: Central Instance --> Sish --> Gateway Proxy --> Keycloak/Manager + +For the "Sish --> Gateway Proxy" requests to be routed correctly, we need to edit the local `/etc/hosts` file to route the . to localhost, like this: +``` +127.0.0.1 gw-5fj1sxvwwfp7wvgqgve91n.localhost +``` The above setup should make the **`org.openremote.test.gateway.GatewayTest#Gateway Tunnelling Edge Gateway Integration test`** pass when run from the IDE or via Gradle. \ No newline at end of file From ac307e5ae2bbf7c8c2f2fa798f75cea85ec4fe9d Mon Sep 17 00:00:00 2001 From: Panos Kalogeropoulos Date: Tue, 25 Nov 2025 15:58:30 +0100 Subject: [PATCH 5/7] Fixes --- .../gateways-and-devices/auto-provisioning.md | 333 +++++++++++++++++- 1 file changed, 325 insertions(+), 8 deletions(-) diff --git a/docs/user-guide/gateways-and-devices/auto-provisioning.md b/docs/user-guide/gateways-and-devices/auto-provisioning.md index 17b59521..ed480462 100644 --- a/docs/user-guide/gateways-and-devices/auto-provisioning.md +++ b/docs/user-guide/gateways-and-devices/auto-provisioning.md @@ -17,11 +17,33 @@ When the auto provisioning is configured, an authorised device will first create There are two basic mechanisms for client provisioning. -### X.509 Client Certificate +### X.509 Client Certificate (Message-Based) -Supports the industry standard X.509 certificate authentication mechanism where each client has a unique client certificate that is signed by a CA certificate that must also be registered within OpenRemote, the certificate must contain a unique ID within the CN attribute of the certificate. OpenRemote then verifies the certificate and CN attribute presented during provisioning. +Supports the industry standard X.509 certificate authentication mechanism where each client has a unique client certificate that is signed by a CA certificate that must also be registered within OpenRemote. The certificate must contain a unique ID within the CN attribute of the certificate. OpenRemote then verifies the certificate and CN attribute presented during provisioning. -This is the most secure authentication mechanism but adds complexity to the manufacturing/flashing process. +In this mode, the client connects to the standard MQTT broker (default port 1883) and sends the certificate as part of the provisioning message payload. + +This is a secure authentication mechanism but adds complexity to the manufacturing/flashing process. + +### mTLS (Mutual TLS) Client Certificate + +Supports automatic provisioning using mTLS (Mutual TLS) at the transport layer. This is the **recommended approach** for production deployments as it provides the highest security by enforcing client authentication at the TLS handshake level before any MQTT communication occurs. + +In this mode, clients connect to a dedicated mTLS MQTT broker endpoint (default port 8884) and present their client certificate during the TLS handshake. OpenRemote extracts the realm from the certificate's `OU` (Organizational Unit) field and the unique device ID from the `CN` (Common Name) field. + +**Key differences from message-based X.509:** +* Client authentication happens at the TLS layer (before MQTT connection) +* No need to send certificate in the provisioning message +* Realm is extracted from the certificate's `OU` field +* Device ID is extracted from the certificate's `CN` field +* More secure as unauthenticated clients cannot connect at all +* Simpler provisioning message format + +**Certificate Requirements for mTLS:** +* Client certificate must be signed by a CA registered in OpenRemote +* Certificate subject must include: `CN=,OU=` +* Certificate must have the `clientAuth` Extended Key Usage +* Example subject: `CN=device123,OU=master` ### Symmetric Key (HMAC-SHA256) @@ -47,13 +69,23 @@ The following illustrates the connect process (through [MQTT topics](../manager- **NOTE THAT THE 'WHITELIST/BLACKLIST FAILURE', IS NOT YET IMPLEMENTED. THIS FUNCTION ENHANCES SECURITY AS ONLY SPECIFIED DEVICES CAN CONNECT (WHITELIST) OR CAN BE EXCLUDED (BLACKLIST)** -#### X.509 Client Certificate Validation +#### X.509 Client Certificate Validation (Message-Based) 1. Find X.509 realm config whose CA cert subject matches the client cert issuer 2. Check client certificate has been signed by the CA -3. Extract client certificate subject ‘CN’ value +3. Extract client certificate subject 'CN' value 4. Check it matches UNIQUE_ID +#### mTLS Client Certificate Validation + +1. Client presents certificate during TLS handshake +2. Server validates certificate is signed by a trusted CA +3. Extract realm from certificate subject 'OU' value +4. Extract unique device ID from certificate subject 'CN' value +5. Find matching provisioning config for the realm +6. Verify realm in certificate matches provisioning config realm +7. Authenticate or auto-provision service user based on extracted credentials + #### Symmetric Key Validation **NOT YET IMPLEMENTED** @@ -63,7 +95,7 @@ The following illustrates the connect process (through [MQTT topics](../manager- ### Message Schema -### X.509 Provisioning Request Message +#### X.509 Provisioning Request Message (Message-Based) The provisioning message format for X.509 is as follows: ```json @@ -75,6 +107,19 @@ The provisioning message format for X.509 is as follows: The cert field should be in PEM format and must contain the certificate chain up to and including the CA certificate registered within OpenRemote. +#### mTLS Provisioning Request Message + +The provisioning message format for mTLS is much simpler since authentication has already occurred at the TLS layer: + +```json + { + "type": "mtls", + "req": null + } +``` + +No certificate needs to be sent in the payload as the client certificate was already validated during the TLS handshake. The realm and device ID are extracted from the certificate's `OU` and `CN` fields respectively. + #### Symmetric Key Provisioning Request Message **NOT YET IMPLEMENTED** @@ -125,11 +170,12 @@ The code field should be the base64 encoded HMAC specific to this client. Client certificate generation is done using standard tooling e.g. openssl: -1. A unique client private key and X.509 certificate should be generated with the client’s unique ID stored in the CN attribute of the certificate. +1. A unique client private key and X.509 certificate should be generated with the client's unique ID stored in the CN attribute of the certificate. +1. **For mTLS**: The certificate must also include the realm name in the OU (Organizational Unit) field of the certificate subject (e.g., `CN=device123,OU=master`). 1. The certificate should then be signed by an intermediate CA (can be self-signed or signed by a CA) 1. The intermediate CA certificate is then uploaded into OpenRemote within a Realm config instance -When the client publishes its certificate to OpenRemote it must be in the PEM format. Client certificate generation can take place within the manufacturing environment without any external dependencies. +When the client publishes its certificate to OpenRemote (message-based X.509 only) it must be in the PEM format. For mTLS, the certificate is presented during the TLS handshake. Client certificate generation can take place within the manufacturing environment without any external dependencies. :::note @@ -147,11 +193,31 @@ Generate CSR for device (inc. key): ```shell openssl req -nodes --newkey rsa:4096 -keyout deviceN.key -subj "/C=NL/ST=North Brabant/O=OpenRemote/CN=deviceN" -out deviceN.csr ``` + +**For mTLS**, include the OU field with the realm name: +```shell +openssl req -nodes --newkey rsa:2048 -keyout device123.key -subj "/CN=device123,OU=master" -out device123.csr +``` + Generate signed cert for device: ```shell openssl x509 -req -in deviceN.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out deviceN.pem -days 500 -sha256 ``` +**For mTLS**, add the clientAuth Extended Key Usage: +```shell +# Create an extensions file +cat > device-ext.cnf < Auto Provisioning (top right menu); the menu item is only present for superusers. +### mTLS MQTT Broker Configuration + +To enable the mTLS MQTT broker endpoint, you need to configure the following environment variables: + +* `OR_MQTT_MTLS_SERVER_LISTEN_HOST` - Host/IP address for the mTLS MQTT broker to listen on (default: `0.0.0.0`) +* `OR_MQTT_MTLS_SERVER_LISTEN_PORT` - Port for the mTLS MQTT broker (default: `8884`) +* `OR_MQTT_MTLS_KEYSTORE_PATH` - Path to the server keystore file in PKCS#12 format (default: `keystores/server_keystore.p12`) +* `OR_MQTT_MTLS_KEYSTORE_PASSWORD` - Password for the server keystore (default: `secret`) +* `OR_MQTT_MTLS_TRUSTSTORE_PATH` - Path to the server truststore file containing trusted CA certificates (default: `keystores/server_truststore.p12`) +* `OR_MQTT_MTLS_TRUSTSTORE_PASSWORD` - Password for the server truststore (default: `secret`) + +The mTLS endpoint is automatically configured when the OpenRemote Manager starts. Clients connecting to this endpoint must present a valid client certificate signed by a CA that is registered in the truststore. + +:::tip +For production deployments, ensure: +1. The server keystore contains a valid TLS certificate with appropriate SANs (Subject Alternative Names) +2. The truststore contains only the CA certificates you trust for client authentication +3. Strong passwords are used for both keystores +4. Keystores are stored securely with appropriate file permissions +::: + ### Provisioning Configuration Provisioning Configurations are used to allow configuration of the above via the `Manager UI`; a realm can contain any number of `Provisioning Configuration` items. @@ -263,3 +350,233 @@ awk 'NF {sub(/\r/, ""); printf "%s\n",$0;}' * [Git GitHub, Self Signed Certificate with Custom Root CA](https://gist.github.com/fntlnz/cf14feb5a46b2eda428e000157447309) * [Whitepaper Device Manufacturing and Provisioning with X.509](https://d1.awsstatic.com/whitepapers/device-manufacturing-provisioning.pdf) + +## mTLS Autoprovisioning - End-to-End Example + +This section provides a complete example of setting up mTLS-based automatic device provisioning. + +### Step 1: Generate Certificates + +First, generate a root CA, server certificates, and client certificates: + +```bash +# Generate Root CA +openssl genrsa -out rootca.key 4096 +openssl req -x509 -new -nodes -key rootca.key -sha256 -days 3650 \ + -out rootca.crt -subj "/CN=OpenRemote Root CA" + +# Generate Server Key and CSR +openssl genrsa -out server.key 2048 +openssl req -new -key server.key -out server.csr \ + -subj "/CN=localhost" + +# Create server extensions file with SANs +cat > server-ext.cnf < client-ext.cnf < Date: Tue, 25 Nov 2025 15:58:46 +0100 Subject: [PATCH 6/7] Fixes --- docs/developer-guide/gateway-tunnelling-setup.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/gateway-tunnelling-setup.md b/docs/developer-guide/gateway-tunnelling-setup.md index 75c81e89..b7d73659 100644 --- a/docs/developer-guide/gateway-tunnelling-setup.md +++ b/docs/developer-guide/gateway-tunnelling-setup.md @@ -69,7 +69,7 @@ Run the main `docker-compose.yml` file with `OR_HOSTNAME=localhost`, and add the The routing of requests from the central instance to the gateway looks like this: Central Instance --> Sish --> Gateway Proxy --> Keycloak/Manager -For the "Sish --> Gateway Proxy" requests to be routed correctly, we need to edit the local `/etc/hosts` file to route the . to localhost, like this: +For the "Sish --> Gateway Proxy" requests to be routed correctly, we need to edit the local `/etc/hosts` file to route the `.` to localhost, like this: ``` 127.0.0.1 gw-5fj1sxvwwfp7wvgqgve91n.localhost ``` From d2faca29ef156e7556ee3fab662bfa5556c1af2d Mon Sep 17 00:00:00 2001 From: Panos Kalogeropoulos Date: Tue, 25 Nov 2025 16:00:33 +0100 Subject: [PATCH 7/7] Fixes --- .../gateways-and-devices/auto-provisioning.md | 333 +----------------- 1 file changed, 8 insertions(+), 325 deletions(-) diff --git a/docs/user-guide/gateways-and-devices/auto-provisioning.md b/docs/user-guide/gateways-and-devices/auto-provisioning.md index ed480462..17b59521 100644 --- a/docs/user-guide/gateways-and-devices/auto-provisioning.md +++ b/docs/user-guide/gateways-and-devices/auto-provisioning.md @@ -17,33 +17,11 @@ When the auto provisioning is configured, an authorised device will first create There are two basic mechanisms for client provisioning. -### X.509 Client Certificate (Message-Based) +### X.509 Client Certificate -Supports the industry standard X.509 certificate authentication mechanism where each client has a unique client certificate that is signed by a CA certificate that must also be registered within OpenRemote. The certificate must contain a unique ID within the CN attribute of the certificate. OpenRemote then verifies the certificate and CN attribute presented during provisioning. +Supports the industry standard X.509 certificate authentication mechanism where each client has a unique client certificate that is signed by a CA certificate that must also be registered within OpenRemote, the certificate must contain a unique ID within the CN attribute of the certificate. OpenRemote then verifies the certificate and CN attribute presented during provisioning. -In this mode, the client connects to the standard MQTT broker (default port 1883) and sends the certificate as part of the provisioning message payload. - -This is a secure authentication mechanism but adds complexity to the manufacturing/flashing process. - -### mTLS (Mutual TLS) Client Certificate - -Supports automatic provisioning using mTLS (Mutual TLS) at the transport layer. This is the **recommended approach** for production deployments as it provides the highest security by enforcing client authentication at the TLS handshake level before any MQTT communication occurs. - -In this mode, clients connect to a dedicated mTLS MQTT broker endpoint (default port 8884) and present their client certificate during the TLS handshake. OpenRemote extracts the realm from the certificate's `OU` (Organizational Unit) field and the unique device ID from the `CN` (Common Name) field. - -**Key differences from message-based X.509:** -* Client authentication happens at the TLS layer (before MQTT connection) -* No need to send certificate in the provisioning message -* Realm is extracted from the certificate's `OU` field -* Device ID is extracted from the certificate's `CN` field -* More secure as unauthenticated clients cannot connect at all -* Simpler provisioning message format - -**Certificate Requirements for mTLS:** -* Client certificate must be signed by a CA registered in OpenRemote -* Certificate subject must include: `CN=,OU=` -* Certificate must have the `clientAuth` Extended Key Usage -* Example subject: `CN=device123,OU=master` +This is the most secure authentication mechanism but adds complexity to the manufacturing/flashing process. ### Symmetric Key (HMAC-SHA256) @@ -69,23 +47,13 @@ The following illustrates the connect process (through [MQTT topics](../manager- **NOTE THAT THE 'WHITELIST/BLACKLIST FAILURE', IS NOT YET IMPLEMENTED. THIS FUNCTION ENHANCES SECURITY AS ONLY SPECIFIED DEVICES CAN CONNECT (WHITELIST) OR CAN BE EXCLUDED (BLACKLIST)** -#### X.509 Client Certificate Validation (Message-Based) +#### X.509 Client Certificate Validation 1. Find X.509 realm config whose CA cert subject matches the client cert issuer 2. Check client certificate has been signed by the CA -3. Extract client certificate subject 'CN' value +3. Extract client certificate subject ‘CN’ value 4. Check it matches UNIQUE_ID -#### mTLS Client Certificate Validation - -1. Client presents certificate during TLS handshake -2. Server validates certificate is signed by a trusted CA -3. Extract realm from certificate subject 'OU' value -4. Extract unique device ID from certificate subject 'CN' value -5. Find matching provisioning config for the realm -6. Verify realm in certificate matches provisioning config realm -7. Authenticate or auto-provision service user based on extracted credentials - #### Symmetric Key Validation **NOT YET IMPLEMENTED** @@ -95,7 +63,7 @@ The following illustrates the connect process (through [MQTT topics](../manager- ### Message Schema -#### X.509 Provisioning Request Message (Message-Based) +### X.509 Provisioning Request Message The provisioning message format for X.509 is as follows: ```json @@ -107,19 +75,6 @@ The provisioning message format for X.509 is as follows: The cert field should be in PEM format and must contain the certificate chain up to and including the CA certificate registered within OpenRemote. -#### mTLS Provisioning Request Message - -The provisioning message format for mTLS is much simpler since authentication has already occurred at the TLS layer: - -```json - { - "type": "mtls", - "req": null - } -``` - -No certificate needs to be sent in the payload as the client certificate was already validated during the TLS handshake. The realm and device ID are extracted from the certificate's `OU` and `CN` fields respectively. - #### Symmetric Key Provisioning Request Message **NOT YET IMPLEMENTED** @@ -170,12 +125,11 @@ The code field should be the base64 encoded HMAC specific to this client. Client certificate generation is done using standard tooling e.g. openssl: -1. A unique client private key and X.509 certificate should be generated with the client's unique ID stored in the CN attribute of the certificate. -1. **For mTLS**: The certificate must also include the realm name in the OU (Organizational Unit) field of the certificate subject (e.g., `CN=device123,OU=master`). +1. A unique client private key and X.509 certificate should be generated with the client’s unique ID stored in the CN attribute of the certificate. 1. The certificate should then be signed by an intermediate CA (can be self-signed or signed by a CA) 1. The intermediate CA certificate is then uploaded into OpenRemote within a Realm config instance -When the client publishes its certificate to OpenRemote (message-based X.509 only) it must be in the PEM format. For mTLS, the certificate is presented during the TLS handshake. Client certificate generation can take place within the manufacturing environment without any external dependencies. +When the client publishes its certificate to OpenRemote it must be in the PEM format. Client certificate generation can take place within the manufacturing environment without any external dependencies. :::note @@ -193,31 +147,11 @@ Generate CSR for device (inc. key): ```shell openssl req -nodes --newkey rsa:4096 -keyout deviceN.key -subj "/C=NL/ST=North Brabant/O=OpenRemote/CN=deviceN" -out deviceN.csr ``` - -**For mTLS**, include the OU field with the realm name: -```shell -openssl req -nodes --newkey rsa:2048 -keyout device123.key -subj "/CN=device123,OU=master" -out device123.csr -``` - Generate signed cert for device: ```shell openssl x509 -req -in deviceN.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out deviceN.pem -days 500 -sha256 ``` -**For mTLS**, add the clientAuth Extended Key Usage: -```shell -# Create an extensions file -cat > device-ext.cnf < Auto Provisioning (top right menu); the menu item is only present for superusers. -### mTLS MQTT Broker Configuration - -To enable the mTLS MQTT broker endpoint, you need to configure the following environment variables: - -* `OR_MQTT_MTLS_SERVER_LISTEN_HOST` - Host/IP address for the mTLS MQTT broker to listen on (default: `0.0.0.0`) -* `OR_MQTT_MTLS_SERVER_LISTEN_PORT` - Port for the mTLS MQTT broker (default: `8884`) -* `OR_MQTT_MTLS_KEYSTORE_PATH` - Path to the server keystore file in PKCS#12 format (default: `keystores/server_keystore.p12`) -* `OR_MQTT_MTLS_KEYSTORE_PASSWORD` - Password for the server keystore (default: `secret`) -* `OR_MQTT_MTLS_TRUSTSTORE_PATH` - Path to the server truststore file containing trusted CA certificates (default: `keystores/server_truststore.p12`) -* `OR_MQTT_MTLS_TRUSTSTORE_PASSWORD` - Password for the server truststore (default: `secret`) - -The mTLS endpoint is automatically configured when the OpenRemote Manager starts. Clients connecting to this endpoint must present a valid client certificate signed by a CA that is registered in the truststore. - -:::tip -For production deployments, ensure: -1. The server keystore contains a valid TLS certificate with appropriate SANs (Subject Alternative Names) -2. The truststore contains only the CA certificates you trust for client authentication -3. Strong passwords are used for both keystores -4. Keystores are stored securely with appropriate file permissions -::: - ### Provisioning Configuration Provisioning Configurations are used to allow configuration of the above via the `Manager UI`; a realm can contain any number of `Provisioning Configuration` items. @@ -350,233 +263,3 @@ awk 'NF {sub(/\r/, ""); printf "%s\n",$0;}' * [Git GitHub, Self Signed Certificate with Custom Root CA](https://gist.github.com/fntlnz/cf14feb5a46b2eda428e000157447309) * [Whitepaper Device Manufacturing and Provisioning with X.509](https://d1.awsstatic.com/whitepapers/device-manufacturing-provisioning.pdf) - -## mTLS Autoprovisioning - End-to-End Example - -This section provides a complete example of setting up mTLS-based automatic device provisioning. - -### Step 1: Generate Certificates - -First, generate a root CA, server certificates, and client certificates: - -```bash -# Generate Root CA -openssl genrsa -out rootca.key 4096 -openssl req -x509 -new -nodes -key rootca.key -sha256 -days 3650 \ - -out rootca.crt -subj "/CN=OpenRemote Root CA" - -# Generate Server Key and CSR -openssl genrsa -out server.key 2048 -openssl req -new -key server.key -out server.csr \ - -subj "/CN=localhost" - -# Create server extensions file with SANs -cat > server-ext.cnf < client-ext.cnf <