Tenant-Stack Setup¶
Each customer (tenant) operates an independent instance of the Tenant-Stack. It provides all device-facing services and optionally connects to a shared Provider-Stack via the JOIN workflow to establish a chain of trust and cross-tenant telemetry forwarding.
Services¶
| Service | Container prefix | Role |
|---|---|---|
| Caddy | {TENANT_ID}-caddy |
Reverse proxy, single entry point on :8888 |
| Keycloak | {TENANT_ID}-keycloak |
Tenant realm, OIDC SSO for all services |
| ThingsBoard | {TENANT_ID}-thingsboard |
Device registry, MQTT, rule engine, dashboards |
| hawkBit | {TENANT_ID}-hawkbit |
OTA campaigns and artefact storage |
| step-ca | {TENANT_ID}-step-ca |
Issuing Sub-CA (optionally signed by Provider Root CA) |
| WireGuard | {TENANT_ID}-wireguard |
Device VPN server |
| Terminal Proxy | {TENANT_ID}-terminal-proxy |
Browser terminal via WireGuard |
| TimescaleDB | {TENANT_ID}-tb-db |
Device telemetry and ThingsBoard time-series storage |
| pgAdmin | {TENANT_ID}-pgadmin |
Database management UI |
| Grafana | {TENANT_ID}-grafana |
Tenant dashboards |
| IoT Bridge API | {TENANT_ID}-iot-bridge-api |
Device enrollment, cert issuance, WG provisioning |
Port Map¶
| Service | Default Port | Protocol | Notes |
|---|---|---|---|
| Caddy (entry point) | 8888 | HTTP/HTTPS | Path-based routing for all services |
| ThingsBoard UI | 9090 | HTTP | Direct port (SPA sub-path limitation) |
| ThingsBoard MQTT TLS | 8883 | MQTTS / mTLS | Direct, device connections |
| step-ca | 9000 | HTTPS | Direct, device cert enrollment |
| WireGuard | 51820 | UDP | Direct, device VPN |
| Keycloak | /auth/ via Caddy |
HTTP | |
| Grafana | /grafana/ via Caddy |
HTTP | |
| hawkBit | /hawkbit/ via Caddy |
HTTP | |
| IoT Bridge API | /api/ via Caddy |
HTTP | |
| Terminal Proxy | /terminal/ via Caddy |
WebSocket | |
| PKI (step-ca) | /pki/ via Caddy |
HTTPS upstream |
Multiple tenant instances
When running more than one tenant on the same host, each instance must use a
different CADDY_PORT, WG_PORT and WG_INTERNAL_SUBNET. Example:
| Tenant | CADDY_PORT | WG_PORT | WG_INTERNAL_SUBNET |
|---|---|---|---|
| tenant1 | 8888 | 51820 | 10.8.1.0 |
| tenant2 | 8889 | 51821 | 10.8.2.0 |
Prerequisites¶
- Docker Engine ≥ 24 and Docker Compose v2
- (Optional) A running Provider-Stack for Sub-CA signing and telemetry forwarding
- Port
51820/udpopen in the firewall for WireGuard - Port
8883/tcpopen for device MQTT TLS connections
Quick Start (standalone mode)¶
In standalone mode the Tenant Sub-CA starts as a self-signed Root CA. The JOIN workflow with the Provider-Stack is optional and can be completed later.
Edit .env and change every line marked # [CHANGE ME]. At minimum set:
Generate random secrets:
# Helper: generate a 32-char random hex string
rnd() { openssl rand -hex 16; }
sed -i \
-e "s/KC_DB_PASSWORD=changeme/KC_DB_PASSWORD=$(rnd)/" \
-e "s/KC_ADMIN_PASSWORD=changeme/KC_ADMIN_PASSWORD=$(rnd)/" \
-e "s/GRAFANA_OIDC_SECRET=changeme/GRAFANA_OIDC_SECRET=$(rnd)/" \
-e "s/TB_OIDC_SECRET=changeme/TB_OIDC_SECRET=$(rnd)/" \
-e "s/HB_OIDC_SECRET=changeme/HB_OIDC_SECRET=$(rnd)/" \
-e "s/BRIDGE_OIDC_SECRET=changeme/BRIDGE_OIDC_SECRET=$(rnd)/" \
-e "s/PORTAL_OIDC_SECRET=changeme/PORTAL_OIDC_SECRET=$(rnd)/" \
-e "s/TSDB_PASSWORD=changeme/TSDB_PASSWORD=$(rnd)/" \
-e "s/TSDB_TELEGRAF_PASSWORD=changeme/TSDB_TELEGRAF_PASSWORD=$(rnd)/" \
-e "s/TSDB_GRAFANA_PASSWORD=changeme/TSDB_GRAFANA_PASSWORD=$(rnd)/" \
-e "s/PGADMIN_PASSWORD=changeme/PGADMIN_PASSWORD=$(rnd)/" \
.env
Start the stack:
Wait for all services to be healthy:
Then initialise the step-ca provisioner (run once):
The script prints the STEP_CA_FINGERPRINT — copy it into .env:
Restart IoT Bridge API to pick it up:
The landing page is now available at http://localhost:8888.
Keycloak Realm Setup¶
The Tenant Keycloak realm is imported automatically on first start from
keycloak/realms/realm-tenant.json.tpl. Three bootstrap users are created:
| User | Role | Initial password env var |
|---|---|---|
admin |
cdm-admin |
TENANT_ADMIN_PASSWORD |
operator |
cdm-operator |
TENANT_OPERATOR_PASSWORD |
viewer |
cdm-viewer |
TENANT_VIEWER_PASSWORD |
Temporary passwords
All bootstrap user passwords are marked "temporary": true. Users must
choose a new password on first login through the Account Portal at
/auth/realms/{TENANT_ID}/account/.
ThingsBoard Provisioning (optional)¶
ThingsBoard needs a tenant account created via its System Admin API. Use the provisioning profile:
# Start only the provision helper (runs once and exits)
docker compose --profile provision up thingsboard-provision
JOIN Workflow (connecting to a Provider-Stack)¶
The JOIN workflow links the Tenant Sub-CA to the Provider Root CA and optionally forwards device telemetry to the Provider RabbitMQ.
Step 1 – Get the Provider Root CA fingerprint:
# On the machine running the Provider-Stack:
docker compose exec provider-step-ca step ca fingerprint
Step 2 – Set Provider vars in tenant-stack/.env:
STEP_CA_PROVIDER_URL=https://<provider-host>:9000
STEP_CA_PROVIDER_FINGERPRINT=<fingerprint from step 1>
STEP_CA_PROVIDER_ADMIN_PROVISIONER=cdm-admin@cdm.local
STEP_CA_PROVIDER_ADMIN_PASSWORD=<provider step-ca admin password>
Step 3 – Run the Sub-CA signing script:
docker compose restart step-ca # restart to pick up new env vars
docker compose exec ${TENANT_ID:-tenant1}-step-ca /usr/local/bin/init-sub-ca.sh
The Tenant Issuing CA certificate is now signed by the Provider Root CA. All device certificates issued by this Tenant Sub-CA will be trusted by any service that trusts the Provider Root CA chain.
Enabling mTLS for MQTT¶
Once the Sub-CA is signed and device certs are available:
- Set the following in
.env: - Issue a server certificate for ThingsBoard MQTT:
- Restart ThingsBoard:
docker compose restart thingsboard