nixos/komodo-periphery: v2.0.0+ support (#482922)

This commit is contained in:
Sandro
2026-07-18 18:11:36 +00:00
committed by GitHub
3 changed files with 430 additions and 99 deletions

View File

@@ -99,6 +99,8 @@
- `services.plantuml-server.packages.jetty` now supports `jetty_12`, it no longer supports `jetty_11`.
- `services.komodo-periphery` has been updated to support version 2.0.0. Some options have been renamed to match the new configuration structure; compatibility aliases are provided for the renamed options. The `passkeys` and `outbound.onboardingKey` options have been removed; use `passkeyFiles`, `auth.privateKey`/`auth.corePublicKeys`, or `outbound.onboardingKeyFile` instead. New outbound mode configuration is available under `outbound.*`.
## Other Notable Changes {#sec-release-26.11-notable-changes}
<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->

View File

@@ -8,41 +8,65 @@ let
cfg = config.services.komodo-periphery;
settingsFormat = pkgs.formats.toml { };
actualRepoDir = if cfg.repoDir != null then cfg.repoDir else "${cfg.rootDirectory}/repos";
actualStackDir = if cfg.stackDir != null then cfg.stackDir else "${cfg.rootDirectory}/stacks";
actualBuildDir = if cfg.buildDir != null then cfg.buildDir else "${cfg.rootDirectory}/builds";
genFinalSettings =
let
actualServerEnabled =
if cfg.inbound.serverEnabled != null then
cfg.inbound.serverEnabled
else
(cfg.outbound.coreAddress == "");
hasAnyPrivateKey = cfg.auth.privateKey != "";
baseSettings = {
port = cfg.port;
bind_ip = cfg.bindIp;
root_directory = cfg.rootDirectory;
repo_dir = "${cfg.rootDirectory}/repos";
stack_dir = "${cfg.rootDirectory}/stacks";
ssl_enabled = cfg.ssl.enable;
default_terminal_command = cfg.defaultTerminalCommand;
disable_terminals = cfg.disableTerminals;
disable_container_terminals = cfg.disableContainerTerminals;
stats_polling_rate = cfg.statsPollingRate;
container_stats_polling_rate = cfg.containerStatsPollingRate;
legacy_compose_cli = cfg.legacyComposeCli;
include_disk_mounts = cfg.includeDiskMounts;
exclude_disk_mounts = cfg.excludeDiskMounts;
# When a private key is explicitly configured, it's passed via env var.
# Otherwise, use the default file path so Periphery auto-generates a key.
private_key = if !hasAnyPrivateKey then "file:${cfg.rootDirectory}/keys/periphery.key" else "";
core_public_keys = [ ];
passkeys = [ ];
server_enabled = actualServerEnabled;
port = cfg.inbound.port;
bind_ip = cfg.inbound.bindIp;
allowed_ips = cfg.inbound.allowedIps;
ssl_enabled = cfg.inbound.ssl.enable;
}
// lib.optionalAttrs cfg.ssl.enable {
ssl_key_file = cfg.ssl.keyFile;
ssl_cert_file = cfg.ssl.certFile;
// {
core_address = cfg.outbound.coreAddress;
connect_as = cfg.outbound.connectAs;
onboarding_key = "";
}
// {
logging = {
level = cfg.logging.level;
stdio = cfg.logging.stdio;
opentelemetry_service_name = cfg.logging.opentelemetryServiceName;
opentelemetry_scope_name = cfg.logging.opentelemetryScopeName;
pretty = cfg.logging.pretty;
}
// lib.optionalAttrs (cfg.logging.otlpEndpoint != "") {
otlp_endpoint = cfg.logging.otlpEndpoint;
};
allowed_ips = cfg.allowedIps;
passkeys = cfg.passkeys;
disable_terminals = cfg.disableTerminals;
disable_container_exec = cfg.disableContainerExec;
stats_polling_rate = cfg.statsPollingRate;
container_stats_polling_rate = cfg.containerStatsPollingRate;
legacy_compose_cli = cfg.legacyComposeCli;
include_disk_mounts = cfg.includeDiskMounts;
exclude_disk_mounts = cfg.excludeDiskMounts;
pretty_startup_config = cfg.prettyStartupConfig;
}
// cfg.extraSettings;
in
lib.filterAttrsRecursive (_: v: v != null && v != { } && v != [ ]) baseSettings;
lib.filterAttrsRecursive (_: v: v != null && v != { } && v != [ ] && v != "") baseSettings;
configFile =
if cfg.configFile == null then
@@ -51,36 +75,98 @@ let
cfg.configFile;
in
{
imports = with lib; [
(mkRenamedOptionModule
[ "services" "komodo-periphery" "port" ]
[ "services" "komodo-periphery" "inbound" "port" ]
)
(mkRenamedOptionModule
[ "services" "komodo-periphery" "bindIp" ]
[ "services" "komodo-periphery" "inbound" "bindIp" ]
)
(mkRenamedOptionModule
[ "services" "komodo-periphery" "allowedIps" ]
[ "services" "komodo-periphery" "inbound" "allowedIps" ]
)
(mkRenamedOptionModule
[ "services" "komodo-periphery" "ssl" "enable" ]
[ "services" "komodo-periphery" "inbound" "ssl" "enable" ]
)
(mkRenamedOptionModule
[ "services" "komodo-periphery" "ssl" "keyFile" ]
[ "services" "komodo-periphery" "inbound" "ssl" "keyFile" ]
)
(mkRenamedOptionModule
[ "services" "komodo-periphery" "ssl" "certFile" ]
[ "services" "komodo-periphery" "inbound" "ssl" "certFile" ]
)
(mkRenamedOptionModule
[ "services" "komodo-periphery" "serverEnabled" ]
[ "services" "komodo-periphery" "inbound" "serverEnabled" ]
)
(mkRenamedOptionModule
[ "services" "komodo-periphery" "disableContainerExec" ]
[ "services" "komodo-periphery" "disableContainerTerminals" ]
)
(mkRemovedOptionModule [ "services" "komodo-periphery" "passkeys" ]
"services.komodo-periphery.passkeys has been removed. Use passkeyFiles for v1.X compatibility, or migrate to auth.privateKey and auth.corePublicKeys (v2.0+)."
)
(mkRemovedOptionModule [ "services" "komodo-periphery" "outbound" "onboardingKey" ]
"services.komodo-periphery.outbound.onboardingKey has been removed. Use outbound.onboardingKeyFile instead for better security."
)
];
options.services.komodo-periphery = {
enable = lib.mkEnableOption "Periphery, a multi-server Docker and Git deployment agent by Komodo";
package = lib.mkPackageOption pkgs "komodo" { };
configFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
dockerHost = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Path to the periphery configuration file. If null, a configuration file will be generated from the module options.";
description = ''
Docker host socket URI to use for container operations.
If null, uses the default Docker socket and automatically enables Docker.
Set this to use alternative container runtimes like Podman or remote Docker hosts.
'';
example = lib.literalExpression ''
pkgs.writeText "periphery.toml" '''
port = 8120
bind_ip = "[::]"
ssl_enabled = true
[logging]
level = "info"
'''
# Podman rootless
"unix:///run/user/1000/podman/podman.sock"
# Podman rootful
"unix:///run/podman/podman.sock"
# Remote Docker
"tcp://192.168.1.100:2375"
'';
};
port = lib.mkOption {
type = lib.types.port;
default = 8120;
description = "Port for the Periphery agent to listen on.";
};
configFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
Path to the Periphery configuration file.
bindIp = lib.mkOption {
type = lib.types.str;
default = "[::]";
description = "IP address to bind to.";
- If `null` (default), a configuration file will be automatically generated
from the module options (recommended for most users).
- If set to a path, that file will be used directly as the configuration file.
- You can also use `pkgs.writeText` to write configuration inline in your NixOS configuration.
When using a custom config file, all other module options (except `package`, `user`, `group`,
`environment`, and `environmentFile`) will be ignored.
'';
example = lib.literalExpression ''
# Option 1: Use an existing config file
"/etc/komodo/periphery.toml"
# Option 2: Write config inline using pkgs.writeText
pkgs.writeText "periphery.toml" '''
root_directory = "/var/lib/komodo-periphery"
logging.level = "info"
logging.stdio = "standard"
'''
'';
};
rootDirectory = lib.mkOption {
@@ -89,24 +175,25 @@ in
description = "Root directory for Komodo Periphery data.";
};
ssl = {
enable = lib.mkEnableOption "SSL/TLS support" // {
default = true;
};
repoDir = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
defaultText = lib.literalExpression ''"''${config.services.komodo-periphery.rootDirectory}/repos"'';
description = "Directory for Komodo Periphery to manage repos. If null, defaults to `\${rootDirectory}/repos`.";
};
keyFile = lib.mkOption {
type = lib.types.path;
default = "${cfg.rootDirectory}/ssl/key.pem";
defaultText = lib.literalExpression ''"''${config.services.komodo-periphery.rootDirectory}/ssl/key.pem"'';
description = "Path to SSL key file.";
};
stackDir = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
defaultText = lib.literalExpression ''"''${config.services.komodo-periphery.rootDirectory}/stacks"'';
description = "Directory for Komodo Periphery to manage stacks. If null, defaults to `\${rootDirectory}/stacks`.";
};
certFile = lib.mkOption {
type = lib.types.path;
default = "${cfg.rootDirectory}/ssl/cert.pem";
defaultText = lib.literalExpression ''"''${config.services.komodo-periphery.rootDirectory}/ssl/cert.pem"'';
description = "Path to SSL certificate file.";
};
buildDir = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
defaultText = lib.literalExpression ''"''${config.services.komodo-periphery.rootDirectory}/builds"'';
description = "Directory for Komodo Periphery to manage builds. If null, defaults to `\${rootDirectory}/builds`.";
};
logging = {
@@ -139,26 +226,41 @@ in
description = "OpenTelemetry OTLP endpoint for traces.";
example = "http://localhost:4317";
};
opentelemetryServiceName = lib.mkOption {
type = lib.types.str;
default = "Periphery";
description = "(v2.0+) OpenTelemetry service name attached to telemetry.";
};
opentelemetryScopeName = lib.mkOption {
type = lib.types.str;
default = "Komodo";
description = "(v2.0+) OpenTelemetry scope name attached to telemetry.";
};
pretty = lib.mkOption {
type = lib.types.bool;
default = false;
description = "(v2.0+) Enable human-readable logging (multi-line).";
};
};
allowedIps = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "IP addresses or subnets allowed to call the periphery API. Empty list allows all.";
example = [
"::ffff:12.34.56.78"
"10.0.10.0/24"
];
prettyStartupConfig = lib.mkOption {
type = lib.types.bool;
default = false;
description = "(v2.0+) Enable human-readable startup config log (multi-line).";
};
passkeys = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
passkeyFiles = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
Passkeys required to access the periphery API.
WARNING: These will be stored in the Nix store in plain text!
Path to file containing passkeys (v1.X compatibility).
This will be passed via `PERIPHERY_PASSKEYS_FILE` environment variable.
Consider migrating to the new v2.0+ authentication mechanism.
'';
example = [ "your-secure-passkey" ];
example = "/run/secrets/komodo-passkey";
};
extraSettings = lib.mkOption {
@@ -170,16 +272,23 @@ in
};
};
defaultTerminalCommand = lib.mkOption {
type = lib.types.str;
default = "bash";
description = "(v2.0+) Default terminal command used to initialize the shell.";
example = "zsh";
};
disableTerminals = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Disable remote shell access through Periphery.";
};
disableContainerExec = lib.mkOption {
disableContainerTerminals = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Disable remote container shell access through Periphery.";
description = "(v2.0+) Disable remote container shell access through Periphery.";
};
statsPollingRate = lib.mkOption {
@@ -222,6 +331,147 @@ in
];
};
auth = {
privateKey = lib.mkOption {
type = lib.types.str;
default = "";
description = ''
(v2.0+) Private key used with the Noise handshake.
Use `file:/path/to/file` prefix to load from a file. If the file doesn't exist,
Periphery will generate and write a new key to the path.
If empty, defaults to `file:''${rootDirectory}/keys/periphery.key`.
'';
example = lib.literalExpression ''
# Reference a secret file
"file:''${config.age.secrets.komodo-private-key.path}"
# Or direct path
"file:/run/secrets/komodo-private-key"
'';
};
corePublicKeys = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
(v2.0+) Accepted public keys to allow Core(s) to connect.
Accepts Spki base64 DER directly or can reference files using `file:/path/to/core.pub` prefix.
You can mix direct keys and file references.
For better security, use the `file:` prefix to reference secret files.
'';
example = lib.literalExpression ''
[
# Direct key value
"MCowBQYDK2VuAyEATZgrjGHeF0KJUe0+n77+qAWOg3YzEzXOmQWaRgO3OGQ="
# Reference secret files
"file:''${config.age.secrets.komodo-core1-pub.path}"
"file:''${config.age.secrets.komodo-core2-pub.path}"
]
'';
};
};
inbound = {
serverEnabled = lib.mkOption {
type = lib.types.nullOr lib.types.bool;
default = null;
description = ''
Enable the inbound connection server for Core -> Periphery connections.
If null, defaults to false when `outbound.coreAddress` is defined, otherwise true.
'';
};
port = lib.mkOption {
type = lib.types.port;
default = 8120;
description = "Port for the inbound Periphery server to listen on.";
};
bindIp = lib.mkOption {
type = lib.types.str;
default = "[::]";
description = ''
IP address the periphery server will bind to.
The default allows external IPv4 and IPv6 connections.
'';
};
allowedIps = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
Limit the IP addresses which can connect to Periphery.
Supports IPv4/IPv6 addresses and subnets.
Empty list allows all connections.
'';
example = [
"::ffff:12.34.56.78"
"10.0.10.0/24"
];
};
ssl = {
enable = lib.mkEnableOption "(v2.0+) SSL/TLS support for inbound connections" // {
default = true;
};
keyFile = lib.mkOption {
type = lib.types.path;
default = "${cfg.rootDirectory}/ssl/key.pem";
defaultText = lib.literalExpression ''"''${config.services.komodo-periphery.rootDirectory}/ssl/key.pem"'';
description = ''
Path to SSL key file.
If the file doesn't exist and SSL is enabled, self-signed keys will be generated using openssl.
'';
};
certFile = lib.mkOption {
type = lib.types.path;
default = "${cfg.rootDirectory}/ssl/cert.pem";
defaultText = lib.literalExpression ''"''${config.services.komodo-periphery.rootDirectory}/ssl/cert.pem"'';
description = ''
Path to SSL certificate file.
If the file doesn't exist and SSL is enabled, self-signed certificate will be generated using openssl.
'';
};
};
};
outbound = {
coreAddress = lib.mkOption {
type = lib.types.str;
default = "";
description = ''
(v2.0+) The address of Komodo Core. Must be reachable from this host.
If provided, Periphery will operate in outbound mode.
'';
example = "demo.komo.do";
};
connectAs = lib.mkOption {
type = lib.types.str;
default = "";
description = ''
(v2.0+) The Server this Periphery agent should connect as.
Must match an existing Server name or id.
'';
example = "server-name";
};
onboardingKeyFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
(v2.0+) Path to file containing the onboarding key.
This will be passed via `PERIPHERY_ONBOARDING_KEY_FILE` environment variable.
'';
example = lib.literalExpression ''"''${config.age.secrets.komodo-onboarding.path}"'';
};
};
user = lib.mkOption {
type = lib.types.str;
default = "komodo-periphery";
@@ -253,14 +503,14 @@ in
};
config = lib.mkIf cfg.enable {
virtualisation.docker.enable = true;
virtualisation.docker.enable = lib.mkDefault (cfg.dockerHost == null);
users.users.${cfg.user} = lib.mkIf (cfg.user == "komodo-periphery") {
isSystemUser = true;
group = cfg.group;
description = "Komodo Periphery service user";
home = cfg.rootDirectory;
extraGroups = [ "docker" ];
extraGroups = lib.optional (cfg.dockerHost == null) "docker";
};
users.groups.${cfg.group} = lib.mkIf (cfg.group == "komodo-periphery") { };
@@ -271,16 +521,26 @@ in
user = cfg.user;
group = cfg.group;
};
"${cfg.rootDirectory}/repos".d = {
"${actualRepoDir}".d = {
mode = "0755";
user = cfg.user;
group = cfg.group;
};
"${cfg.rootDirectory}/stacks".d = {
"${actualStackDir}".d = {
mode = "0755";
user = cfg.user;
group = cfg.group;
};
"${actualBuildDir}".d = {
mode = "0755";
user = cfg.user;
group = cfg.group;
};
"${cfg.rootDirectory}/keys".d = {
mode = "0700";
user = cfg.user;
group = cfg.group;
};
"${cfg.rootDirectory}/ssl".d = {
mode = "0700";
user = cfg.user;
@@ -292,35 +552,63 @@ in
description = "Komodo Periphery - Multi-server Docker and Git deployment agent";
after = [
"network-online.target"
"docker.service"
];
]
++ lib.optional (cfg.dockerHost == null) "docker.service";
wants = [
"network-online.target"
"docker.service"
];
]
++ lib.optional (cfg.dockerHost == null) "docker.service";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "simple";
User = cfg.user;
Group = cfg.group;
SupplementaryGroups = [ "docker" ];
SupplementaryGroups = lib.mkIf (cfg.dockerHost == null) [ "docker" ];
Restart = "on-failure";
RestartSec = "10s";
WorkingDirectory = cfg.rootDirectory;
ExecStart = lib.escapeShellArgs [
"${lib.getExe' cfg.package "periphery"}"
(lib.getExe' cfg.package "periphery")
"--config-path"
(if cfg.configFile != null then cfg.configFile else configFile)
configFile
];
Environment = lib.mapAttrsToList (name: value: "${name}=${value}") (
cfg.environment
// lib.optionalAttrs (!cfg.disableTerminals) {
PATH = "/run/current-system/sw/bin:/run/wrappers/bin";
lib.optionalAttrs (cfg.configFile == null) {
PERIPHERY_ROOT_DIRECTORY = cfg.rootDirectory;
PERIPHERY_REPO_DIR = actualRepoDir;
PERIPHERY_STACK_DIR = actualStackDir;
PERIPHERY_BUILD_DIR = actualBuildDir;
}
// lib.optionalAttrs (cfg.configFile == null && cfg.inbound.ssl.enable) {
PERIPHERY_SSL_KEY_FILE = cfg.inbound.ssl.keyFile;
PERIPHERY_SSL_CERT_FILE = cfg.inbound.ssl.certFile;
}
// lib.optionalAttrs (cfg.dockerHost != null) {
DOCKER_HOST = cfg.dockerHost;
}
// lib.optionalAttrs (cfg.passkeyFiles != null) {
PERIPHERY_PASSKEYS_FILE = cfg.passkeyFiles;
}
// lib.optionalAttrs (cfg.auth.privateKey != "") {
PERIPHERY_PRIVATE_KEY = cfg.auth.privateKey;
}
// lib.optionalAttrs (cfg.auth.corePublicKeys != [ ]) {
PERIPHERY_CORE_PUBLIC_KEYS = lib.concatStringsSep "," cfg.auth.corePublicKeys;
}
// lib.optionalAttrs (cfg.outbound.onboardingKeyFile != null) {
PERIPHERY_ONBOARDING_KEY_FILE = cfg.outbound.onboardingKeyFile;
}
// cfg.environment
);
ExecSearchPath = lib.mkIf (!cfg.disableTerminals) [
"/run/current-system/sw/bin"
"/run/wrappers/bin"
];
EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
StateDirectory = "komodo-periphery";
@@ -329,7 +617,7 @@ in
NoNewPrivileges = true;
PrivateTmp = true;
ProtectSystem = "full";
ProtectHome = true;
ProtectHome = "read-only";
};
};
};

View File

@@ -5,22 +5,63 @@
maintainers = with lib.maintainers; [ channinghe ];
};
nodes.machine =
{ config, pkgs, ... }:
{
virtualisation.docker.enable = true;
services.komodo-periphery = {
enable = true;
bindIp = "127.0.0.1";
port = 8120;
ssl.enable = false;
passkeys = [ "test-passkey" ];
nodes = {
periphery =
{ ... }:
{
virtualisation.docker.enable = true;
services.komodo-periphery = {
enable = true;
inbound.bindIp = "127.0.0.1";
inbound.port = 8120;
inbound.ssl.enable = false;
inbound.serverEnabled = true;
disableTerminals = true;
disableContainerTerminals = true;
statsPollingRate = "10-sec";
containerStatsPollingRate = "1-min";
logging.level = "debug";
extraSettings = {
secrets.TEST_SECRET = "test-value";
};
auth.privateKey = "file:/var/lib/komodo-periphery/keys/test.key";
auth.corePublicKeys = [ "MCowBQYDK2VuAyEATZgrjGHeF0KJUe0+n77+qAWOg3YzEzXOmQWaRgO3OGQ=" ];
};
};
};
peripheryOutbound =
{ ... }:
{
virtualisation.docker.enable = true;
services.komodo-periphery = {
enable = true;
inbound.ssl.enable = false;
logging.level = "debug";
outbound.coreAddress = "core.invalid";
outbound.connectAs = "test-server";
};
};
};
testScript = ''
machine.wait_for_unit("komodo-periphery.service")
machine.wait_for_open_port(8120)
machine.succeed("systemctl status komodo-periphery.service")
start_all()
with subtest("Inbound periphery starts and serves /version"):
periphery.wait_for_unit("komodo-periphery.service")
periphery.wait_for_open_port(8120)
periphery.succeed("curl -fsS --max-time 10 http://127.0.0.1:8120/version")
with subtest("Periphery creates managed directories"):
periphery.succeed("test -d /var/lib/komodo-periphery")
periphery.succeed("test -d /var/lib/komodo-periphery/repos")
periphery.succeed("test -d /var/lib/komodo-periphery/stacks")
periphery.succeed("test -d /var/lib/komodo-periphery/builds")
periphery.succeed("test -d /var/lib/komodo-periphery/keys")
periphery.succeed("test -d /var/lib/komodo-periphery/ssl")
with subtest("Outbound periphery stays active despite unreachable core"):
peripheryOutbound.wait_for_unit("komodo-periphery.service")
peripheryOutbound.sleep(15)
peripheryOutbound.succeed("systemctl is-active komodo-periphery")
'';
}