Centralize Tensamin packages modules and release automation
This commit is contained in:
parent
423ee32a37
commit
123205c97d
28 changed files with 32292 additions and 10 deletions
106
modules/README.md
Normal file
106
modules/README.md
Normal 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
51
modules/client.nix
Normal 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
21
modules/default.nix
Normal 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
251
modules/iota.nix
Normal 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
199
modules/server.nix
Normal 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 ];
|
||||
};
|
||||
};
|
||||
}
|
||||
Loading…
Reference in a new issue