The Invernaderos API uses username/password authentication with ACL-based authorization for MQTT clients. Credentials are stored in PostgreSQL and validated by the EMQX broker.
Authentication data is stored in the metadata.mqtt_users and metadata.mqtt_acl tables.
Stores MQTT user credentials and device metadata (MqttUsers.kt:41-106):
Schema
CREATE TABLE metadata.mqtt_users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), username VARCHAR(100) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, salt VARCHAR(255) NOT NULL, device_type VARCHAR(50), -- SENSOR, ACTUATOR, GATEWAY, API greenhouse_id BIGINT REFERENCES metadata.greenhouses(id), tenant_id BIGINT REFERENCES metadata.tenants(id), is_active BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), last_connected_at TIMESTAMPTZ);CREATE INDEX idx_mqtt_users_username ON metadata.mqtt_users(username);CREATE INDEX idx_mqtt_users_greenhouse ON metadata.mqtt_users(greenhouse_id);CREATE INDEX idx_mqtt_users_tenant ON metadata.mqtt_users(tenant_id);CREATE INDEX idx_mqtt_users_device_type ON metadata.mqtt_users(device_type);CREATE INDEX idx_mqtt_users_active ON metadata.mqtt_users(is_active);
Field Descriptions:
Field
Type
Description
id
UUID
Unique user identifier
username
VARCHAR(100)
MQTT username (unique)
password_hash
VARCHAR(255)
Hashed password (bcrypt, pbkdf2)
salt
VARCHAR(255)
Salt for password hashing
device_type
VARCHAR(50)
Device category (see below)
greenhouse_id
BIGINT
Associated greenhouse (nullable)
tenant_id
BIGINT
Multi-tenant isolation
is_active
BOOLEAN
Enable/disable user
last_connected_at
TIMESTAMPTZ
Last MQTT connection timestamp
Device Types (MqttUsers.kt:99-104):
companion object { object DeviceType { const val SENSOR = "SENSOR" const val ACTUATOR = "ACTUATOR" const val GATEWAY = "GATEWAY" const val API = "API" }}
Defines publish/subscribe permissions per user (MqttAcl.kt:29-94):
Schema
CREATE TABLE metadata.mqtt_acl ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), username VARCHAR(100) NOT NULL, permission VARCHAR(20) NOT NULL, -- allow, deny action VARCHAR(20) NOT NULL, -- publish, subscribe, pubsub topic VARCHAR(255) NOT NULL, -- Topic pattern with wildcards qos INT, -- NULL = any QoS created_at TIMESTAMPTZ NOT NULL DEFAULT NOW());CREATE INDEX idx_mqtt_acl_username ON metadata.mqtt_acl(username);CREATE INDEX idx_mqtt_acl_topic ON metadata.mqtt_acl(topic);CREATE INDEX idx_mqtt_acl_username_topic ON metadata.mqtt_acl(username, topic);
Field Descriptions:
Field
Type
Description
username
VARCHAR(100)
MQTT username (matches mqtt_users.username)
permission
VARCHAR(20)
allow or deny
action
VARCHAR(20)
publish, subscribe, or pubsub
topic
VARCHAR(255)
Topic pattern with wildcards (#, +)
qos
INT
QoS level (0, 1, 2) or NULL for any
Permission Constants (MqttAcl.kt:83-92):
companion object { object Permission { const val ALLOW = "allow" const val DENY = "deny" } object Action { const val PUBLISH = "publish" const val SUBSCRIBE = "subscribe" const val PUBSUB = "pubsub" }}
# Using bcrypt (recommended)htpasswd -bnBC 10 "" your-password | tr -d ':'# Using Pythonpython3 -c "import bcrypt; print(bcrypt.hashpw(b'your-password', bcrypt.gensalt()).decode())"
2
Insert User
INSERT INTO metadata.mqtt_users ( username, password_hash, salt, device_type, tenant_id, is_active) VALUES ( 'sensor-greenhouse-01', '$2b$10$abcdef1234567890abcdef1234567890abcdef1234567890abcd', 'random-salt-value', 'SENSOR', (SELECT id FROM metadata.tenants WHERE name = 'SARA'), true);
Prevent abuse with connection/message rate limits:
EMQX Limits
listeners: tcp: external: max_connections: 1024 max_conn_rate: 1000 # Connections per secondzone: external: publish_limit: "100/s" # Max 100 messages per second per client subscribe_limit: "10/s"
-- Find users with wildcard publish permissionsSELECT username, topic, permission, actionFROM metadata.mqtt_aclWHERE topic LIKE '%#%' AND action IN ('publish', 'pubsub') AND permission = 'allow';-- Find users with cross-tenant accessSELECT u.username, u.tenant_id, a.topicFROM metadata.mqtt_users uJOIN metadata.mqtt_acl a ON u.username = a.usernameWHERE a.topic LIKE 'GREENHOUSE/%' AND a.topic NOT LIKE CONCAT('GREENHOUSE/', (SELECT name FROM metadata.tenants WHERE id = u.tenant_id), '%');-- Count ACL rules per userSELECT username, COUNT(*) AS rule_countFROM metadata.mqtt_aclGROUP BY usernameORDER BY rule_count DESC;