Centralize Tensamin packages modules and release automation
Some checks failed
action.yml / Centralize Tensamin packages modules and release automation (push) Failing after 0s
Canary release / release (push) Failing after 21s

This commit is contained in:
Alois 2026-10-04 19:19:56 +02:00
commit 123205c97d
Signed by: alois
SSH key fingerprint: SHA256:GBzT2DXvAuGV9XIV5W3WrzVpjU54FThmxHXdbz95J24
28 changed files with 32292 additions and 10 deletions

106
modules/README.md Normal file
View file

@ -0,0 +1,106 @@
# NixOS modules
`import ./default.nix { inherit self; }` returns `default`, `iota`, `omikron`,
`omega`, and `client`. `default` imports all four component modules. The flake's
existing conditional import accepts this interface directly.
These new files must be included in the consuming Git checkout for Git-backed
flake evaluation. Verification used `path:` to include the untracked modules.
All options live under `tensamin`, with no `services.tensamin` wrapper or legacy
`services.iota`, `services.omikron`, or `services.omega` aliases.
## Interfaces
All components have `enable`, `package`, `bindAddress`, `port`, and
`openFirewall`. Services are disabled by default. Package defaults resolve via
`self.packages.${pkgs.stdenv.hostPlatform.system}`.
| Component | Package | Bind address | Port | Open firewall |
| --- | --- | --- | --- | --- |
| `tensamin.iota` | `iota-daemon` | `0.0.0.0` | 1984 | true |
| `tensamin.omikron` | `omikron` | `0.0.0.0` | 443 | true |
| `tensamin.omega` | `omega` | `0.0.0.0` | 443 | true |
| `tensamin.client` | `client-web` | `127.0.0.1` | 8080 | false |
Iota, Omikron, and Omega open TCP and UDP. Client opens only TCP when requested.
### Iota
- `stateDir`, `cacheDir`, `runtimeDir`, `logDir` default to `/var/lib/iota`,
`/var/cache/iota`, `/run/iota`, `/var/log/iota`.
- `assetDir` defaults to the central `iota` package's pinned source
`static/web` directory. This is upstream's shipped asset directory, currently
containing its 404 page, not the client application. Override it to serve
other assets.
- `webMode` is `network` by default, or `loopback` or `disabled`. Upstream
requires TLS for both enabled modes. Set `bindAddress` explicitly for loopback.
- `certFile` and `keyFile` are nullable runtime path strings, supplied together.
An enabled listener requires them unless `settingsFile` supplies complete TLS
configuration.
- `omegaApiUrl` defaults to `https://omega.tensamin.net`.
- `environmentFiles` is a list of runtime path strings, defaulting to `[]`.
- `settings` contains YAML-compatible operator configuration, defaulting to `{}`.
Module listener and asset options take precedence. Startup rewrites the mutable
`stateDir/config.yaml`, preserving omitted `iota_id`, `omikron_host`,
`omikron_port`, and `omikron_id`. Explicit values, including null, override them.
Other daemon/operator edits to this file are replaced at the next startup.
- `settingsFile` is a nullable runtime path string. It replaces generated
settings, with the same preservation of omitted discovery fields. Its listener
and TLS settings must agree with the module's firewall and capability options.
Relative paths resolve against `stateDir`, not the source file's directory.
The systemd service and socket are named `iota`. IPC uses
`${runtimeDir}/iota.sock`, mode `0660`, owned by `iota:iota`. Add authorized
operators to the `iota` group. Daemon identities remain under
`${stateDir}/identity`; the module does not reseed them. Exit code 75 forces a
restart. Config paths, deployment mode, supervisor, and all mutable directories
are passed through the upstream `IOTA_*` environment contract.
### Omikron and Omega
- `stateDir` defaults to `/var/lib/omikron` or `/var/lib/omega` and is the working
directory of the matching systemd service and service user.
- Set either `acmeCertDir`, containing `fullchain.pem` and `key.pem`, or both
`certFile` and `keyFile`. All are nullable runtime path strings. Startup copies
the certificate to `certs/cert.pem` and converts the key to unencrypted PKCS8
at `certs/key.pem`, owned by the service user with mode `0600`.
- `identityFile` and `publicIdentityFile` are nullable runtime path strings,
supplied together. Startup copies them to `omikron.mk` and `omikron.mpkb`, or
`omega.mk` and `omega.mpkb`. Null retains existing state or lets upstream
generate an identity. Supplied identities are reapplied on every startup.
- `environment` is an attribute set of non-secret string settings, default `{}`.
Module-generated listener and Omikron discovery variables take precedence.
- `environmentFiles` is a list of runtime environment paths, default `[]`.
Systemd loads these after the declared environment, so they can override it.
Keep listener variables consistent with firewall options. Omega requires
`DB_URL`; it uses file-based identities, not `PRIVATE_KEY`/`PUBLIC_KEY`.
Omikron also requires positive `id` and `omegaTrustFile`, the runtime Omega public
key bundle copied to `omega.mpkb`. `omegaHost` defaults to `tensamin.net` and
`omegaPort` to the upstream default 9187. Set it to the deployed Omega listener
port, whose module default is 443. These become `ID`, `OMEGA_HOST`, `OMEGA_PORT`,
`RHO_PORT`, and `BIND_ADDRESS`. Omega uses `PORT` and `BIND_ADDRESS`.
Trust and identity installation runs for both manual TLS and ACME. Configure
certificate issuance separately and restart the corresponding service after
renewal so it recopies certificates. Environment and secret path strings do not
copy secret contents into the Nix store.
### Client
`hostName` defaults to `localhost`. The module enables nginx and adds that
virtual host with an explicit HTTP listener at `bindAddress:port`, using
`client-web`'s output root and SPA fallback to `/index.html`. Configure public
TLS/proxy routing separately. It does not configure Anubis, guest accounts, or
install the Electron client.
## Verification
The combined module was evaluated with actual central package outputs in
minimal x86_64-linux and aarch64-linux NixOS container configurations. All
assertions passed and `system.build.toplevel.drvPath` evaluated. Checks included
both server TLS branches, all services disabled, Iota disabled-listener mode,
a custom Iota runtime directory, and a custom nginx listener. Missing Omikron
TLS produces the intended assertion. Evaluation does not build or start the
applications.

51
modules/client.nix Normal file
View file

@ -0,0 +1,51 @@
{ self }:
{
config,
lib,
pkgs,
...
}:
let
cfg = config.tensamin.client;
inherit (lib) mkOption types;
in
{
options.tensamin.client = {
enable = lib.mkEnableOption "the static Tensamin web client";
package = mkOption {
type = types.package;
default = self.packages.${pkgs.stdenv.hostPlatform.system}.client-web;
description = "Static client package with index.html at its output root.";
};
hostName = mkOption {
type = types.str;
default = "localhost";
};
bindAddress = mkOption {
type = types.str;
default = "127.0.0.1";
};
port = mkOption {
type = types.port;
default = 8080;
};
openFirewall = mkOption {
type = types.bool;
default = false;
};
};
config = lib.mkIf cfg.enable {
services.nginx.enable = true;
services.nginx.virtualHosts.${cfg.hostName} = {
listen = [
{
addr = cfg.bindAddress;
inherit (cfg) port;
}
];
root = cfg.package;
locations."/".tryFiles = "$uri $uri/ /index.html";
};
networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ cfg.port ];
};
}

21
modules/default.nix Normal file
View file

@ -0,0 +1,21 @@
{ self }:
let
components = {
iota = import ./iota.nix { inherit self; };
omikron = import ./server.nix {
inherit self;
name = "omikron";
};
omega = import ./server.nix {
inherit self;
name = "omega";
};
client = import ./client.nix { inherit self; };
};
in
components
// {
default = {
imports = builtins.attrValues components;
};
}

251
modules/iota.nix Normal file
View file

@ -0,0 +1,251 @@
{ self }:
{
config,
lib,
pkgs,
...
}:
let
cfg = config.tensamin.iota;
inherit (lib) mkOption types;
format = pkgs.formats.yaml { };
settings = lib.recursiveUpdate cfg.settings {
port = cfg.port;
web = {
mode = cfg.webMode;
bind = cfg.bindAddress;
port = cfg.port;
required = cfg.webMode != "disabled";
asset_dir = cfg.assetDir;
}
// lib.optionalAttrs (cfg.certFile != null) {
certificate = "${cfg.stateDir}/tls/cert.pem";
key = "${cfg.stateDir}/tls/key.pem";
};
};
sourceConfig =
if cfg.settingsFile != null then cfg.settingsFile else format.generate "iota-config.yaml" settings;
configFile = "${cfg.stateDir}/config.yaml";
python = pkgs.python3.withPackages (ps: [ ps.pyyaml ]);
mergeConfig = pkgs.writeText "iota-merge-config.py" ''
import os
import sys
import yaml
source, destination = sys.argv[1:]
with open(source) as stream:
settings = yaml.safe_load(stream) or {}
if os.path.exists(destination):
with open(destination) as stream:
previous = yaml.safe_load(stream) or {}
# Retain discovery state unless the operator explicitly supplies it.
for field in ["iota_id", "omikron_host", "omikron_port", "omikron_id"]:
if field not in settings and field in previous:
settings[field] = previous[field]
temporary = destination + ".new"
with open(temporary, "w") as stream:
yaml.safe_dump(settings, stream, sort_keys=False)
os.chmod(temporary, 0o640)
os.replace(temporary, destination)
'';
setup = pkgs.writeShellScript "iota-setup" ''
set -eu
umask 077
${python}/bin/python ${mergeConfig} ${lib.escapeShellArg sourceConfig} ${lib.escapeShellArg configFile}
chown iota:iota ${lib.escapeShellArg configFile}
${lib.optionalString (cfg.certFile != null) ''
install -d -m 0700 -o iota -g iota ${lib.escapeShellArg "${cfg.stateDir}/tls"}
install -m 0644 -o iota -g iota ${lib.escapeShellArg cfg.certFile} ${lib.escapeShellArg "${cfg.stateDir}/tls/cert.pem"}
install -m 0600 -o iota -g iota ${lib.escapeShellArg cfg.keyFile} ${lib.escapeShellArg "${cfg.stateDir}/tls/key.pem"}
''}
'';
in
{
options.tensamin.iota = {
enable = lib.mkEnableOption "the Tensamin Iota daemon";
package = mkOption {
type = types.package;
default = self.packages.${pkgs.stdenv.hostPlatform.system}.iota-daemon;
};
stateDir = mkOption {
type = types.str;
default = "/var/lib/iota";
};
cacheDir = mkOption {
type = types.str;
default = "/var/cache/iota";
};
runtimeDir = mkOption {
type = types.str;
default = "/run/iota";
};
logDir = mkOption {
type = types.str;
default = "/var/log/iota";
};
assetDir = mkOption {
type = types.str;
default = "${self.packages.${pkgs.stdenv.hostPlatform.system}.iota.passthru.source}/static/web";
description = "Static Iota assets. Defaults to the pinned source's shipped web directory.";
};
bindAddress = mkOption {
type = types.str;
default = "0.0.0.0";
};
port = mkOption {
type = types.port;
default = 1984;
};
webMode = mkOption {
type = types.enum [
"disabled"
"loopback"
"network"
];
default = "network";
description = "Iota listener mode. Both loopback and network modes require TLS upstream.";
};
certFile = mkOption {
type = types.nullOr types.str;
default = null;
description = "Runtime TLS certificate path.";
};
keyFile = mkOption {
type = types.nullOr types.str;
default = null;
description = "Runtime TLS private key path.";
};
omegaApiUrl = mkOption {
type = types.str;
default = "https://omega.tensamin.net";
};
openFirewall = mkOption {
type = types.bool;
default = true;
};
environmentFiles = mkOption {
type = types.listOf types.str;
default = [ ];
};
settings = mkOption {
type = format.type;
default = { };
description = "Operator settings, refreshed at startup. Listener options take precedence; omitted discovery fields retain daemon values.";
};
settingsFile = mkOption {
type = types.nullOr types.str;
default = null;
description = "Complete runtime YAML configuration, replacing generated settings. Its listener and TLS settings must agree with module options.";
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = (cfg.certFile == null) == (cfg.keyFile == null);
message = "tensamin.iota: certFile and keyFile must be supplied together.";
}
{
assertion = cfg.settingsFile != null || cfg.webMode == "disabled" || cfg.certFile != null;
message = "tensamin.iota: an enabled listener requires certFile and keyFile, or a complete settingsFile with TLS.";
}
{
assertion = lib.all (path: lib.hasPrefix "/" path) [
cfg.stateDir
cfg.cacheDir
cfg.runtimeDir
cfg.logDir
cfg.assetDir
];
message = "tensamin.iota: directory paths must be absolute.";
}
];
users.users.iota = {
isSystemUser = true;
group = "iota";
home = cfg.stateDir;
};
users.groups.iota = { };
systemd.tmpfiles.rules = map (path: "d ${path} 0750 iota iota -") [
cfg.stateDir
cfg.cacheDir
cfg.runtimeDir
cfg.logDir
];
systemd.sockets.iota = {
description = "Tensamin Iota IPC socket";
wantedBy = [ "sockets.target" ];
socketConfig = {
ListenStream = "${cfg.runtimeDir}/iota.sock";
SocketMode = "0660";
SocketUser = "iota";
SocketGroup = "iota";
DirectoryMode = "0750";
Backlog = 5;
RemoveOnStop = true;
};
};
systemd.services.iota = {
description = "Tensamin Iota daemon";
wantedBy = [ "multi-user.target" ];
after = [
"network.target"
"iota.socket"
];
requires = [ "iota.socket" ];
environment = {
OMEGA_API_URL = cfg.omegaApiUrl;
IOTA_SOCKET = "${cfg.runtimeDir}/iota.sock";
IOTA_CONFIG_FILE = configFile;
IOTA_CONFIG_DIR = cfg.stateDir;
IOTA_STATE_DIR = cfg.stateDir;
IOTA_CACHE_DIR = cfg.cacheDir;
IOTA_RUNTIME_DIR = cfg.runtimeDir;
IOTA_LOG_DIR = cfg.logDir;
IOTA_ASSET_DIR = cfg.assetDir;
IOTA_DEPLOYMENT_MODE = "system_socket_activated";
IOTA_SUPERVISOR = "systemd";
};
serviceConfig = {
Type = "simple";
User = "iota";
Group = "iota";
WorkingDirectory = cfg.stateDir;
ExecStart = "${cfg.package}/bin/iota-daemon";
ExecStartPre = [ "+${setup}" ];
EnvironmentFile = cfg.environmentFiles;
Restart = "on-failure";
RestartSec = "5s";
RestartPreventExitStatus = "0";
RestartForceExitStatus = "75";
TimeoutStopSec = "10s";
KillMode = "mixed";
KillSignal = "SIGTERM";
UMask = "0077";
AmbientCapabilities = lib.optional (cfg.port < 1024) "CAP_NET_BIND_SERVICE";
CapabilityBoundingSet = lib.optional (cfg.port < 1024) "CAP_NET_BIND_SERVICE";
ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = true;
NoNewPrivileges = true;
ReadWritePaths = [
cfg.stateDir
cfg.cacheDir
cfg.runtimeDir
cfg.logDir
];
ReadOnlyPaths = [ cfg.assetDir ];
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
LockPersonality = true;
MemoryDenyWriteExecute = true;
};
};
networking.firewall = lib.mkIf (cfg.openFirewall && cfg.webMode != "disabled") {
allowedTCPPorts = [ cfg.port ];
allowedUDPPorts = [ cfg.port ];
};
};
}

199
modules/server.nix Normal file
View file

@ -0,0 +1,199 @@
{ self, name }:
{
config,
lib,
pkgs,
...
}:
let
cfg = config.tensamin.${name};
isOmikron = name == "omikron";
inherit (lib) mkOption types;
cert = if cfg.acmeCertDir != null then "${cfg.acmeCertDir}/fullchain.pem" else cfg.certFile;
key = if cfg.acmeCertDir != null then "${cfg.acmeCertDir}/key.pem" else cfg.keyFile;
setup = pkgs.writeShellScript "${name}-setup" ''
set -eu
umask 077
install -d -m 0750 -o ${name} -g ${name} ${lib.escapeShellArg cfg.stateDir}
cd ${lib.escapeShellArg cfg.stateDir}
install -d -m 0700 -o ${name} -g ${name} certs
install -m 0644 -o ${name} -g ${name} ${
lib.escapeShellArg (if cert == null then "" else cert)
} certs/cert.pem
${pkgs.openssl}/bin/openssl pkcs8 -topk8 -nocrypt \
-in ${lib.escapeShellArg (if key == null then "" else key)} -out certs/key.pem
chown ${name}:${name} certs/key.pem
chmod 0600 certs/key.pem
${lib.optionalString isOmikron ''
install -m 0644 -o ${name} -g ${name} ${lib.escapeShellArg cfg.omegaTrustFile} omega.mpkb
''}
${lib.optionalString (cfg.identityFile != null) ''
install -m 0600 -o ${name} -g ${name} ${lib.escapeShellArg cfg.identityFile} ${name}.mk
''}
${lib.optionalString (cfg.publicIdentityFile != null) ''
install -m 0644 -o ${name} -g ${name} ${lib.escapeShellArg cfg.publicIdentityFile} ${name}.mpkb
''}
'';
in
{
options.tensamin.${name} = {
enable = lib.mkEnableOption "Tensamin ${name}";
package = mkOption {
type = types.package;
default = self.packages.${pkgs.stdenv.hostPlatform.system}.${name};
description = "Central ${name} package, or an explicit replacement.";
};
stateDir = mkOption {
type = types.str;
default = "/var/lib/${name}";
description = "Persistent working directory, including identities and certificates.";
};
bindAddress = mkOption {
type = types.str;
default = "0.0.0.0";
};
port = mkOption {
type = types.port;
default = 443;
};
openFirewall = mkOption {
type = types.bool;
default = true;
};
certFile = mkOption {
type = types.nullOr types.str;
default = null;
description = "Runtime TLS certificate path. Set together with keyFile.";
};
keyFile = mkOption {
type = types.nullOr types.str;
default = null;
description = "Runtime TLS private key path. Never copied into the Nix store.";
};
acmeCertDir = mkOption {
type = types.nullOr types.str;
default = null;
description = "Runtime directory containing fullchain.pem and key.pem. Restart the service after renewal.";
};
identityFile = mkOption {
type = types.nullOr types.str;
default = null;
description = "Existing private keyring to install at startup, or null to retain or generate state.";
};
publicIdentityFile = mkOption {
type = types.nullOr types.str;
default = null;
description = "Matching public key bundle to install at startup.";
};
environment = mkOption {
type = types.attrsOf types.str;
default = { };
description = "Additional non-secret runtime settings. Listener options take precedence.";
};
environmentFiles = mkOption {
type = types.listOf types.str;
default = [ ];
description =
if isOmikron then
"Runtime environment files, including optional LiveKit credentials."
else
"Runtime environment files. Supply DB_URL here; identities are file-based.";
};
}
// lib.optionalAttrs isOmikron {
id = mkOption {
type = types.ints.positive;
description = "Omikron ID assigned by Omega.";
};
omegaHost = mkOption {
type = types.str;
default = "tensamin.net";
};
omegaPort = mkOption {
type = types.port;
default = 9187;
};
omegaTrustFile = mkOption {
type = types.str;
description = "Runtime path to Omega's trusted public key bundle.";
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion =
(cfg.acmeCertDir != null && cfg.certFile == null && cfg.keyFile == null)
|| (cfg.acmeCertDir == null && cfg.certFile != null && cfg.keyFile != null);
message = "tensamin.${name}: set either acmeCertDir or both certFile and keyFile.";
}
{
assertion = (cfg.identityFile == null) == (cfg.publicIdentityFile == null);
message = "tensamin.${name}: identityFile and publicIdentityFile must be supplied together.";
}
{
assertion = lib.hasPrefix "/" cfg.stateDir;
message = "tensamin.${name}.stateDir must be absolute.";
}
];
users.users.${name} = {
isSystemUser = true;
group = name;
home = cfg.stateDir;
};
users.groups.${name} = { };
systemd.tmpfiles.rules = [ "d ${cfg.stateDir} 0750 ${name} ${name} -" ];
systemd.services.${name} = {
description = "Tensamin ${name}";
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
environment =
cfg.environment
// {
BIND_ADDRESS = cfg.bindAddress;
}
// (
if isOmikron then
{
RHO_PORT = toString cfg.port;
OMEGA_HOST = cfg.omegaHost;
OMEGA_PORT = toString cfg.omegaPort;
ID = toString cfg.id;
}
else
{ PORT = toString cfg.port; }
);
serviceConfig = {
Type = "simple";
User = name;
Group = name;
WorkingDirectory = cfg.stateDir;
ExecStart = "${cfg.package}/bin/${name}";
ExecStartPre = [ "+${setup}" ];
EnvironmentFile = cfg.environmentFiles;
Restart = "always";
RestartSec = "5s";
UMask = "0077";
AmbientCapabilities = lib.optional (cfg.port < 1024) "CAP_NET_BIND_SERVICE";
CapabilityBoundingSet = lib.optional (cfg.port < 1024) "CAP_NET_BIND_SERVICE";
ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = true;
NoNewPrivileges = true;
ReadWritePaths = [ cfg.stateDir ];
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
LockPersonality = true;
MemoryDenyWriteExecute = true;
};
};
networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [ cfg.port ];
allowedUDPPorts = [ cfg.port ];
};
};
}