switch to emqx
check / flake-check (push) Successful in 25s
deploy / deploy (push) Successful in 48s

This commit is contained in:
Lars Nolden
2026-07-02 15:19:59 +02:00
parent 08d9fc594e
commit 58155dc737
11 changed files with 366 additions and 512 deletions
+42 -88
View File
@@ -17,75 +17,30 @@ let
];
ingesterKeys = [ "ingester_postgres_password" ];
postgresAdminKeys = [ "postgres_admin_password" ];
hivemqKeys = [ "hivemq_ingester_password" "hivemq_admin_password" "hivemq_device_password" ];
emqxKeys = [
"emqx_ingester_password"
"emqx_admin_password"
"emqx_device_password"
];
# HiveMQ file-RBAC credentials.xml. Only the passwords are secret; the role/
# topic policy is structural. Devices publish under `acrios/<IMSI>/<metric>`
# (set by the sender firmware's mqtt_topic_base), so that is the namespace the
# ingester consumes and the device role is scoped to.
# EMQX authn bootstrap CSV (bootstrap_type = plain). Seeds the broker's
# built-in auth database on first start. Broker users:
# ingester — the Go ingester; SUBSCRIBE the whole device tree (see the ACL
# in gebos-emqx.nix).
# admin — operational break-glass; is_superuser bypasses the ACL.
# bender — shared device user for the whole fleet (Option A). Tenant
# isolation is enforced downstream by the ingester's IMSI→tenant
# registry, NOT at the broker. TODO(Option B): per-device users
# (username == IMSI) — see the ACL comment in gebos-emqx.nix.
#
# Roles:
# ingest — the Go ingester; SUBSCRIBE the whole device tree.
# device — physical senders; full access under acrios/ (publish telemetry
# + the LWT/status and any downlink topics the firmware uses).
# superuser — operational break-glass.
#
# Option A (current): one shared `device` user (`bender`) for the whole fleet.
# Tenant isolation is enforced downstream by the ingester's IMSI→tenant
# registry lookup, NOT at the broker — any device could publish under any IMSI.
#
# TODO(Option B / per-device auth): give each sender its own RBAC user with
# username == IMSI and scope its role to `acrios/${{username}}/#`, so the
# broker enforces that a device can only publish under its own IMSI (real
# anti-spoofing + per-device revocation). Requires setting mqtt_user = <IMSI>
# on each device and a per-device password provisioning flow (generate → sops
# → push to device). file-RBAC hot-reloads, so added users need no restart.
hivemqCredentialsXml = ingesterPassword: adminPassword: devicePassword: ''
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<file-rbac>
<users>
<user>
<name>ingester</name>
<password>${ingesterPassword}</password>
<roles><id>ingest</id></roles>
</user>
<user>
<name>admin</name>
<password>${adminPassword}</password>
<roles><id>superuser</id></roles>
</user>
<user>
<name>bender</name>
<password>${devicePassword}</password>
<roles><id>device</id></roles>
</user>
</users>
<roles>
<role>
<id>ingest</id>
<permissions>
<permission>
<topic>acrios/#</topic>
<activity>SUBSCRIBE</activity>
</permission>
</permissions>
</role>
<role>
<id>device</id>
<permissions>
<!-- No <activity> = PUBLISH and SUBSCRIBE; covers telemetry,
the LWT/status topic, and any downlink under acrios/. -->
<permission><topic>acrios/#</topic></permission>
</permissions>
</role>
<role>
<id>superuser</id>
<permissions>
<permission><topic>#</topic></permission>
</permissions>
</role>
</roles>
</file-rbac>
# Plaintext passwords are acceptable here because the file is sops-encrypted
# at rest and rendered 0400 into a tmpfs at runtime; passwords must not
# contain commas (CSV).
emqxUsersCsv = ingesterPassword: adminPassword: devicePassword: ''
user_id,password,is_superuser
ingester,${ingesterPassword},false
admin,${adminPassword},true
bender,${devicePassword},false
'';
# Render an env file body from a list of (key, envVarName) pairs.
@@ -112,7 +67,7 @@ let
lib.optionals cfg.supabase supabaseKeys
++ lib.optionals cfg.ingester ingesterKeys
++ lib.optionals cfg.postgresAdmin postgresAdminKeys
++ lib.optionals cfg.hivemq hivemqKeys;
++ lib.optionals cfg.emqx emqxKeys;
in
{
options.services.gebos.secrets = {
@@ -126,24 +81,24 @@ in
postgresAdmin = lib.mkEnableOption ''
Postgres bootstrap admin password applied via a one-shot ALTER ROLE unit.'';
hivemq = lib.mkEnableOption ''
HiveMQ file-RBAC credentials.xml (ingester + admin MQTT users), rendered
0400/gebos-hivemq for the broker's file-RBAC extension.'';
emqx = lib.mkEnableOption ''
EMQX broker secrets: the authn bootstrap users.csv (ingester + admin +
device MQTT users).'';
hivemqCredentialsFile = lib.mkOption {
emqxBootstrapUsersFile = lib.mkOption {
type = lib.types.path;
readOnly = true;
description = ''
Path of the rendered HiveMQ file-RBAC credentials.xml consumed by the
gebos-hivemq broker. Resolves to the sops-templated secret when sops
material is present and the hivemq toggle is on, else to a baked-in
Path of the rendered EMQX authn bootstrap users.csv consumed by the
gebos-emqx broker. Resolves to the sops-templated secret when sops
material is present and the emqx toggle is on, else to a baked-in
dev-defaults file (so first-boot eval and local VMs work).
'';
default =
if hasSecrets && cfg.hivemq
then config.sops.templates."hivemq-credentials".path
else "${pkgs.writeText "hivemq-credentials-dev.xml"
(hivemqCredentialsXml "dev-ingester" "dev-admin" "dev-device")}";
if hasSecrets && cfg.emqx
then config.sops.templates."emqx-users.csv".path
else "${pkgs.writeText "emqx-users-dev.csv"
(emqxUsersCsv "dev-ingester" "dev-admin" "dev-device")}";
};
envFile = lib.mkOption {
@@ -182,16 +137,15 @@ in
owner = "root";
};
# Rendered as a standalone file (not env) — the file-RBAC extension reads
# credentials.xml directly. Owned by the broker user so preStart can link
# it into the extension's conf/ dir.
templates."hivemq-credentials" = lib.mkIf cfg.hivemq {
content = hivemqCredentialsXml
config.sops.placeholder.hivemq_ingester_password
config.sops.placeholder.hivemq_admin_password
config.sops.placeholder.hivemq_device_password;
# Rendered as a standalone file — the gebos-emqx preStart copies it into
# the broker state dir, chowned to the container's emqx uid.
templates."emqx-users.csv" = lib.mkIf cfg.emqx {
content = emqxUsersCsv
config.sops.placeholder.emqx_ingester_password
config.sops.placeholder.emqx_admin_password
config.sops.placeholder.emqx_device_password;
mode = "0400";
owner = "gebos-hivemq";
owner = "root";
};
};
};