251 lines
7.9 KiB
Nix
251 lines
7.9 KiB
Nix
{ 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 ];
|
|
};
|
|
};
|
|
}
|