Security Model
Purpose
Section titled “Purpose”This document defines initial security principles.
Trust Boundaries
Section titled “Trust Boundaries”Important trust boundaries:
- User browser to platform API.
- Customer API client to platform API.
- Netmore MQTT broker to
mqtt-ingestion-worker. mqtt-ingestion-workerto the durable raw stream.raw-storage-workerto the durable stream and PostgreSQL.flow-workerto the raw-stored stream, PostgreSQL, and sandbox request subject.script-sandbox-workerto untrusted custom code and its isolated QuickJS/WASM runtime.flow-action-workerto NATS, PostgreSQL, and the configured SMTP server.- Admin users to platform data.
- Tenant users to tenant data.
- A platform administrator’s browser through
platform-apito the temporary IOTA HTTPS source.
Authentication
Section titled “Authentication”V1 starts with local username/password login for interactive users.
The accepted local flow is defined by
ADR-0007: Argon2id
password hashes, 30-minute identity-only access JWTs, and rotating opaque
refresh tokens with a sliding seven-day inactivity deadline. Refresh values are
stored only as SHA-256 hashes. Browser access tokens stay in memory and refresh
tokens stay in a host-only HttpOnly cookie.
Every protected API request verifies current session, user, credential, role, and membership state in PostgreSQL. Disabling a user, changing a grant, revoking a session, or resetting a password therefore takes effect without waiting for access-token expiry.
The web-console follows the same browser contract in test and production. It
stores access tokens only in a private in-memory client reference, never renders
token contents, and never accesses Web Storage or IndexedDB. Refresh and logout
use the API-hosted HttpOnly cookie with browser credentials. The UI displays
roles and grants but makes no authorization decision. Its exact test origin is
allowed only in the test API configuration; Production allows only
https://app.iot-sens.schlossers.at as its web-console origin. Neither
environment accepts the other environment’s web-console origin.
The interactive API console makes the exact test and production documentation
origins part of the browser trust boundary. It sends requests directly to the
selected API origin with browser credentials, does not persist entered
authentication, and defaults to test. Because an allowed documentation origin
can execute authenticated API flows, documentation code and browser
dependencies require the same security review as other trusted frontends.
platform-api still authenticates, authorizes, scopes, and audits every
request; CORS permission alone grants no product access.
The authentication architecture must still support future OIDC-compatible identity providers and tenant-specific SSO, especially Microsoft Entra ID. The internal user, tenant, membership, and authorization model must not depend on a single authentication provider.
Long-term direction:
- OIDC for interactive login.
- Token-based API access.
- Revocable scoped tokens.
- Auditable authentication and token events.
Authorization
Section titled “Authorization”Public Tenant numbers and aggregate IDs are locators, not credentials. Numeric
Tenant collections resolve to the internal Tenant UUID before role evaluation.
Flat aggregate routes first resolve the owning Tenant with a fixed, typed
database locator and then authorize against that UUID. An inaccessible existing
aggregate and an unknown aggregate both return 404; authentication still runs
first and returns 401 when no valid token is present. No Tenant header or UI
session is accepted as authoritative scope.
Authorization is enforced by the platform API and workers.
Authorization must consider:
- Platform role.
- Tenant role.
- Organization access.
- Device access.
- Export permissions.
- Administrative permissions.
UI behavior must never be the only access control.
Global platform_admin assignments are separate from tenant memberships.
Tenant membership roles are tenant_admin, org_admin, and viewer. Multiple
organization grants form a union, and descendant access is explicit. A
cross-tenant or organization-out-of-scope identifier returns not-found
semantics to avoid confirming hidden resources.
Resource-inventory permissions are explicit: all tenant roles with inventory
access have device:read and device_type:read. Organization administrators
receive device:manage only within their grants. Tenant administrators receive
tenant-wide device and device-type management plus flow:read and
flow:manage. Tenant-scoped flow-library, version, release, activation,
parameter, Action, fixture, replay, and binding routes authorize the explicit
tenant from the request path or library query. Organization administrators and
viewers receive no flow access. Global custom blocks and templates remain
restricted to platform_admin. Platform administrators bypass the tenant role
matrix only after the target tenant is verified.
Raw-ingestion inspection is restricted to platform_admin because exact MQTT
bodies may contain sensitive customer and device data across tenant boundaries.
The list API omits payload bodies, the detail API loads one row lazily, both
responses are non-cacheable, and the same authorization is enforced by the API
even when the web-console navigation is hidden. Tenant filters use only trusted
registry assignments; the external Netmore customer is never an authorization
input.
Raw, Audit, and assignment-history cursors bind the fixed upper anchor and active query scope. Reusing a cursor with changed filters, Tenant, or Device fails validation. Repository filters apply Tenant and Organization visibility before any timeline item is returned; cursor contents never grant access.
The temporary IOTA snapshot importer is also restricted to platform_admin.
Its target tenant is explicit in the API path and is revalidated and locked at
write time. The source URL is HTTPS-only and limited to
commonapi-<slug>.xaas-a0a0.cloud/api; redirects, alternate origins, ports,
query strings, fragments, and URL credentials are rejected. The IOTA token is
sent only to the validated source origin. Existing cross-tenant DevEUIs are
reported but never moved. UI visibility does not replace API authorization.
Device visibility and assignment-history filtering happen inside tenant-aware SQL. Unassigned devices are hidden from organization roles. A move checks both source and destination grants, and assignment DTOs omit assigning-user IDs and every out-of-scope organization identifier.
Platform-role grants, revocations, and user deactivation share a PostgreSQL transaction advisory lock. The API blocks self-disable, self-revoke, and removal of the last active platform administrator. Accounts actually created by the admin CLI are additionally marked as protected break-glass identities and cannot be disabled, deleted, or stripped of the platform role through HTTP.
Tenant Isolation
Section titled “Tenant Isolation”Tenant isolation is mandatory.
All tenant-owned data must be tenant-scoped.
Cross-tenant data leakage is a security defect.
Secrets
Section titled “Secrets”Secrets must not be stored in source code.
Secrets must be provided through Kubernetes secrets or another documented secret mechanism.
IOTA migration credentials are deliberately request-scoped exceptions to
deployment secret injection: a platform administrator enters them for one
manual run, they remain only in browser and API memory, and the password is
cleared from the component state after the attempt. They are never persisted,
configured through .env, returned, or included in logs and audit metadata.
Netmore username, password, and full client ID are sensitive. NATS bootstrap, MQTT, Raw Storage, Flow Worker, Action Worker, Platform API and Sandbox identities are separate. The broker has no Ingress and each identity is limited to the subjects and durable consumers in its service contract. The sandbox can only subscribe to its execution subject and publish reply inboxes. Test and production intentionally share the rotated Netmore portal credential, so its rotation affects both environments at once; their stable client IDs remain different.
The receiver enforces TLS 1.2 or newer and certificate validation. No runtime setting can disable certificate checks. A credential disclosed in chat or logs must be revoked before live use.
External topic customer data is untrusted metadata. Only a globally unique device-registry DevEUI can derive tenant and device scope. Unknown and malformed rows stay tenantless unless a valid DevEUI safely resolves. This prevents a publisher-controlled customer segment from crossing a tenant boundary.
Logging
Section titled “Logging”Logs must not include:
- Passwords.
- Tokens.
- Raw credentials.
- App keys.
- Secrets.
- Unnecessary personal data.
- Sensitive tenant data.
- Raw MQTT payloads, topics, customer names, DevEUIs, or complete MQTT client IDs.
- IOTA URLs, usernames, passwords, access tokens, upstream bodies, or source DevEUIs from the snapshot import.
- Custom-script source, compiled artifacts, Flow execution input/output, parameter snapshots, recipient lists, rendered E-mail content, and device attribute values.
Audit Logging
Section titled “Audit Logging”Audit logs are required for:
- User changes.
- Role changes.
- Tenant changes.
- Organization access changes.
- Device assignment changes.
- Flow release, activation, rollback, and device-type assignment changes.
- Script/template release and deprecation, parameter and recipient changes, Action kill-switch changes, Replay requests and Dead Letter redrives.
- Export creation.
- Security-relevant admin actions.
Every known /v1 route is registered with mandatory audit metadata. The API
persists an incomplete request row before parsing the body or executing auth and
handler logic. Failure to start the audit returns 503 without invoking the
handler. Actor, session, and tenant scope are added only when safely known;
finalization stores the route template, response outcome, and duration. Entity
changes and their audit event commit in one transaction.
Resource-inventory and flow audit metadata contains only entity IDs, version numbers, and changed field names. It never contains request bodies, DevEUI search values, metadata/configuration values, module contents, block reasons, or credentials.
Authentication and request audits never contain passwords, tokens, raw login identifiers, request or response bodies, query values, cookies, authorization headers, IP addresses, or user-agent contents. Unknown identifiers are correlated only by a keyed HMAC fingerprint in the login throttle subsystem. Audit logs have a seven-calendar-year retention contract. Queries default to 24 hours and cannot span more than 31 days. Audit pages contain 25 metadata events and no exact total, so count disclosure cannot bypass the same scope filters.
Remaining decisions
Section titled “Remaining decisions”- MFA requirements.
- Service-account and personal-access-token models.
- OIDC provider mapping and tenant-specific SSO rollout details.
- Scheduled Kubernetes execution and monitoring for bounded auth/audit pruning.
Optional observability privacy boundary
Section titled “Optional observability privacy boundary”Sentry receives no request, user, tenant, organization, device, query, header, cookie, payload, or free-form message fields. PostHog receives neither e-mail, name, nor internal IDs: the API produces domain-separated HMAC-SHA256 user and tenant pseudonyms and the browser sends only allowlisted event properties after explicit consent. Revocation opts out immediately and resets local identity.
These pseudonyms remain personal data. EU region, DPA, legal basis, German consent copy, deletion workflow, and retention must be approved before enabling production. Customer on-premise deployments can omit both integrations without changing core behavior.