Modular services (#372170)

Changed files
+1136 -2
nixos
pkgs
by-name
gh
ni
nixos-render-docs
src
nixos_render_docs
+34
nixos/doc/manual/default.nix
···
inherit (pkgs) buildPackages runCommand docbook_xsl_ns;
inherit (pkgs.lib)
hasPrefix
removePrefix
flip
···
${testOptionsDoc.optionsJSON}/${common.outputPath}/options.json
sed -e '/@PYTHON_MACHINE_METHODS@/ {' -e 'r ${testDriverMachineDocstrings}/machine-methods.md' -e 'd' -e '}' \
-i ./development/writing-nixos-tests.section.md
'';
in
rec {
···
inherit (pkgs) buildPackages runCommand docbook_xsl_ns;
inherit (pkgs.lib)
+
evalModules
hasPrefix
removePrefix
flip
···
${testOptionsDoc.optionsJSON}/${common.outputPath}/options.json
sed -e '/@PYTHON_MACHINE_METHODS@/ {' -e 'r ${testDriverMachineDocstrings}/machine-methods.md' -e 'd' -e '}' \
-i ./development/writing-nixos-tests.section.md
+
substituteInPlace ./development/modular-services.md \
+
--replace-fail \
+
'@PORTABLE_SERVICE_OPTIONS@' \
+
${portableServiceOptions.optionsJSON}/${common.outputPath}/options.json
+
substituteInPlace ./development/modular-services.md \
+
--replace-fail \
+
'@SYSTEMD_SERVICE_OPTIONS@' \
+
${systemdServiceOptions.optionsJSON}/${common.outputPath}/options.json
'';
+
+
portableServiceOptions = buildPackages.nixosOptionsDoc {
+
inherit (evalModules { modules = [ ../../modules/system/service/portable/service.nix ]; }) options;
+
inherit revision warningsAreErrors;
+
transformOptions =
+
opt:
+
opt
+
// {
+
# Clean up declaration sites to not refer to the NixOS source tree.
+
declarations = map stripAnyPrefixes opt.declarations;
+
};
+
};
+
+
systemdServiceOptions = buildPackages.nixosOptionsDoc {
+
inherit (evalModules { modules = [ ../../modules/system/service/systemd/service.nix ]; }) options;
+
# TODO: filter out options that are not systemd-specific, maybe also change option prefix to just `service-opt-`?
+
inherit revision warningsAreErrors;
+
transformOptions =
+
opt:
+
opt
+
// {
+
# Clean up declaration sites to not refer to the NixOS source tree.
+
declarations = map stripAnyPrefixes opt.declarations;
+
};
+
};
in
rec {
+1
nixos/doc/manual/development/development.md
···
nixos-tests.chapter.md
developing-the-test-driver.chapter.md
testing-installer.chapter.md
```
···
nixos-tests.chapter.md
developing-the-test-driver.chapter.md
testing-installer.chapter.md
+
modular-services.md
```
+98
nixos/doc/manual/development/modular-services.md
···
···
+
+
# Modular Services {#modular-services}
+
+
Status: in development. This functionality is new in NixOS 25.11, and significant changes should be expected. We'd love to hear your feedback in <https://github.com/NixOS/nixpkgs/pull/372170>
+
+
Traditionally, NixOS services were defined using sets of options *in* modules, not *as* modules. This made them non-modular, resulting in problems with composability, reuse, and portability.
+
+
A configuration management framework is an application of `evalModules` with the `class` and `specialArgs` input attribute set to particular values.
+
NixOS is such a configuration management framework, and so are [Home Manager](https://github.com/nix-community/home-manager) and [`nix-darwin`](https://github.com/lnl7/nix-darwin).
+
+
The service management component of a configuration management framework is the set of module options that connects Nix expressions with the underlying service (or process) manager.
+
For NixOS this is the module wrapping [`systemd`](https://systemd.io/), on `nix-darwin` this is the module wrapping [`launchd`](https://en.wikipedia.org/wiki/Launchd).
+
+
A *modular service* is a [module] that defines values for a core set of options declared in the service management component of a configuration management framework, including which program to run.
+
Since it's a module, it can be composed with other modules via `imports` to extend its functionality.
+
+
NixOS provides two options into which such modules can be plugged:
+
+
- `system.services.<name>`
+
- an option for user services (TBD)
+
+
Crucially, these options have the type [`attrsOf`] [`submodule`].
+
The name of the service is the attribute name corresponding to `attrsOf`.
+
<!-- ^ This is how composition is *always* provided, instead of a difficult thing (but this is reference docs, not a changelog) -->
+
The `submodule` is pre-loaded with two modules:
+
- a generic module that is intended to be portable
+
- a module with systemd-specific options, whose values or defaults derive from the generic module's option values.
+
+
So note that the default value of `system.services.<name>` is not a complete service. It requires that the user provide a value, and this is typically done by importing a module. For example:
+
+
<!-- Not using typical example syntax, because reading this is *not* optional, and should it should not be folded closed. -->
+
```nix
+
{
+
system.services.my-service-instance = {
+
imports = [ pkgs.some-application.services.some-service-module ];
+
foo.settings = {
+
# ...
+
};
+
};
+
}
+
```
+
+
## Portability {#modular-service-portability}
+
+
It is possible to write service modules that are portable. This is done by either avoiding the `systemd` option tree, or by defining process-manager-specific definitions in an optional way:
+
+
```nix
+
{ config, options, lib, ... }: {
+
_class = "service";
+
config = {
+
process.argv = [ (lib.getExe config.foo.program) ];
+
} // lib.optionalAttrs (options?systemd) {
+
# ... systemd-specific definitions ...
+
};
+
}
+
```
+
+
This way, the module can be loaded into a configuration manager that does not use systemd, and the `systemd` definitions will be ignored.
+
Similarly, other configuration managers can declare their own options for services to customize.
+
+
## Composition and Ownership {#modular-service-composition}
+
+
Compared to traditional services, modular services are inherently more composable, by virtue of being modules and receiving a user-provided name when imported.
+
However, composition can not end there, because services need to be able to interact with each other.
+
This can be achieved in two ways:
+
1. Users can link services together by providing the necessary NixOS configuration.
+
2. Services can be compositions of other services.
+
+
These aren't mutually exclusive. In fact, it is a good practice when developing services to first write them as individual services, and then compose them into a higher-level composition. Each of these services is a valid modular service, including their composition.
+
+
## Migration {#modular-service-migration}
+
+
Many services could be migrated to the modular service system, but even when the modular service system is mature, it is not necessary to migrate all services.
+
For instance, many system-wide services are a mandatory part of a desktop system, and it doesn't make sense to have multiple instances of them.
+
Moving their logic into separate Nix files may still be beneficial for the efficient evaluation of configurations that don't use those services, but that is a rather minor benefit, unless modular services potentially become the standard way to define services.
+
+
<!-- TODO example of a single-instance service -->
+
+
## Portable Service Options {#modular-service-options-portable}
+
+
```{=include=} options
+
id-prefix: service-opt-
+
list-id: service-options
+
source: @PORTABLE_SERVICE_OPTIONS@
+
```
+
+
## Systemd-specific Service Options {#modular-service-options-systemd}
+
+
```{=include=} options
+
id-prefix: systemd-service-opt-
+
list-id: systemd-service-options
+
source: @SYSTEMD_SERVICE_OPTIONS@
+
```
+
+
[module]: https://nixos.org/manual/nixpkgs/stable/index.html#module-system
+
<!-- TODO: more anchors -->
+
[`attrsOf`]: #sec-option-types-composed
+
[`submodule`]: #sec-option-types-submodule
+18
nixos/doc/manual/redirects.json
···
"book-nixos-manual": [
"index.html#book-nixos-manual"
],
"module-services-anubis": [
"index.html#module-services-anubis"
],
···
"book-nixos-manual": [
"index.html#book-nixos-manual"
],
+
"modular-service-composition": [
+
"index.html#modular-service-composition"
+
],
+
"modular-service-migration": [
+
"index.html#modular-service-migration"
+
],
+
"modular-service-options-portable": [
+
"index.html#modular-service-options-portable"
+
],
+
"modular-service-options-systemd": [
+
"index.html#modular-service-options-systemd"
+
],
+
"modular-service-portability": [
+
"index.html#modular-service-portability"
+
],
+
"modular-services": [
+
"index.html#modular-services"
+
],
"module-services-anubis": [
"index.html#module-services-anubis"
],
+3 -1
nixos/modules/misc/assertions.nix
···
};
};
-
# impl of assertions is in <nixpkgs/nixos/modules/system/activation/top-level.nix>
}
···
};
};
+
# impl of assertions is in
+
# - <nixpkgs/nixos/modules/system/activation/top-level.nix>
+
# - <nixpkgs/nixos/modules/system/service/portable/lib.nix>
}
+2
nixos/modules/module-list.nix
···
./system/boot/uvesafb.nix
./system/boot/zram-as-tmp.nix
./system/etc/etc-activation.nix
./tasks/auto-upgrade.nix
./tasks/bcache.nix
./tasks/cpu-freq.nix
···
./system/boot/uvesafb.nix
./system/boot/zram-as-tmp.nix
./system/etc/etc-activation.nix
+
./system/service/systemd/system.nix
+
./system/service/systemd/user.nix
./tasks/auto-upgrade.nix
./tasks/bcache.nix
./tasks/cpu-freq.nix
+28
nixos/modules/system/service/README.md
···
···
+
+
# Modular Services
+
+
This directory defines a modular service infrastructure for NixOS.
+
See the [Modular Services chapter] in the manual [[source]](../../doc/manual/development/modular-services.md).
+
+
[Modular Services chapter]: https://nixos.org/manual/nixos/unstable/#modular-services
+
+
# Design decision log
+
+
- `system.services.<name>`. Alternatives considered
+
- `systemServices`: similar to does not allow importing a composition of services into `system`. Not sure if that's a good idea in the first place, but I've kept the possibility open.
+
- `services.abstract`: used in https://github.com/NixOS/nixpkgs/pull/267111, but too weird. Service modules should fit naturally into the configuration system.
+
Also "abstract" is wrong, because it has submodules - in other words, evalModules results, concrete services - not abstract at all.
+
- `services.modular`: only slightly better than `services.abstract`, but still weird
+
+
- No `daemon.*` options. https://github.com/NixOS/nixpkgs/pull/267111/files#r1723206521
+
+
- For now, do not add an `enable` option, because it's ambiguous. Does it disable at the Nix level (not generate anything) or at the systemd level (generate a service that is disabled)?
+
+
- Move all process options into a `process` option tree. Putting this at the root is messy, because we also have sub-services at that level. Those are rather distinct. Grouping them "by kind" should raise fewer questions.
+
+
- `modules/system/service/systemd/system.nix` has `system` twice. Not great, but
+
- they have different meanings
+
1. These are system-provided modules, provided by the configuration manager
+
2. `systemd/system` configures SystemD _system units_.
+
- This reserves `modules/service` for actual service modules, at least until those are lifted out of NixOS, potentially
+
+33
nixos/modules/system/service/portable/lib.nix
···
···
+
{ lib, ... }:
+
let
+
inherit (lib) concatLists mapAttrsToList showOption;
+
in
+
rec {
+
flattenMapServicesConfigToList =
+
f: loc: config:
+
f loc config
+
++ concatLists (
+
mapAttrsToList (
+
k: v:
+
flattenMapServicesConfigToList f (
+
loc
+
++ [
+
"services"
+
k
+
]
+
) v
+
) config.services
+
);
+
+
getWarnings = flattenMapServicesConfigToList (
+
loc: config: map (msg: "in ${showOption loc}: ${msg}") config.warnings
+
);
+
+
getAssertions = flattenMapServicesConfigToList (
+
loc: config:
+
map (ass: {
+
message = "in ${showOption loc}: ${ass.message}";
+
assertion = ass.assertion;
+
}) config.assertions
+
);
+
}
+48
nixos/modules/system/service/portable/service.nix
···
···
+
{
+
lib,
+
config,
+
options,
+
...
+
}:
+
let
+
inherit (lib) mkOption types;
+
pathOrStr = types.coercedTo types.path (x: "${x}") types.str;
+
in
+
{
+
# https://nixos.org/manual/nixos/unstable/#modular-services
+
_class = "service";
+
imports = [
+
../../../misc/assertions.nix
+
];
+
options = {
+
services = mkOption {
+
type = types.attrsOf (
+
types.submoduleWith {
+
modules = [
+
./service.nix
+
];
+
}
+
);
+
description = ''
+
A collection of [modular services](https://nixos.org/manual/nixos/unstable/#modular-services) that are configured in one go.
+
+
You could consider the sub-service relationship to be an ownership relation.
+
It **does not** automatically create any other relationship between services (e.g. systemd slices), unless perhaps such a behavior is explicitly defined and enabled in another option.
+
'';
+
default = { };
+
visible = "shallow";
+
};
+
process = {
+
argv = lib.mkOption {
+
type = types.listOf pathOrStr;
+
example = lib.literalExpression ''[ (lib.getExe config.package) "--nobackground" ]'';
+
description = ''
+
Command filename and arguments for starting this service.
+
This is a raw command-line that should not contain any shell escaping.
+
If expansion of environmental variables is required then use
+
a shell script or `importas` from `pkgs.execline`.
+
'';
+
};
+
};
+
};
+
}
+183
nixos/modules/system/service/portable/test.nix
···
···
+
# Run:
+
# nix-instantiate --eval nixos/modules/system/service/portable/test.nix
+
let
+
lib = import ../../../../../lib;
+
+
inherit (lib) mkOption types;
+
+
portable-lib = import ./lib.nix { inherit lib; };
+
+
dummyPkg =
+
name:
+
derivation {
+
system = "dummy";
+
name = name;
+
builder = "/bin/false";
+
};
+
+
exampleConfig = {
+
_file = "${__curPos.file}:${toString __curPos.line}";
+
services = {
+
service1 = {
+
process = {
+
argv = [
+
"/usr/bin/echo" # *giggles*
+
"hello"
+
];
+
};
+
assertions = [
+
{
+
assertion = false;
+
message = "you can't enable this for that reason";
+
}
+
];
+
warnings = [
+
"The `foo' service is deprecated and will go away soon!"
+
];
+
};
+
service2 = {
+
process = {
+
# No meta.mainProgram, because it's supposedly an executable script _file_,
+
# not a directory with a bin directory containing the main program.
+
argv = [
+
(dummyPkg "cowsay.sh")
+
"world"
+
];
+
};
+
};
+
service3 = {
+
process = {
+
argv = [ "/bin/false" ];
+
};
+
services.exclacow = {
+
process = {
+
argv = [
+
(lib.getExe (
+
dummyPkg "cowsay-ng"
+
// {
+
meta.mainProgram = "cowsay";
+
}
+
))
+
"!"
+
];
+
};
+
assertions = [
+
{
+
assertion = false;
+
message = "you can't enable this for such reason";
+
}
+
];
+
warnings = [
+
"The `bar' service is deprecated and will go away soon!"
+
];
+
};
+
};
+
};
+
};
+
+
exampleEval = lib.evalModules {
+
modules = [
+
{
+
options.services = mkOption {
+
type = types.attrsOf (
+
types.submoduleWith {
+
class = "service";
+
modules = [
+
./service.nix
+
];
+
}
+
);
+
};
+
}
+
exampleConfig
+
];
+
};
+
+
test =
+
assert
+
exampleEval.config == {
+
services = {
+
service1 = {
+
process = {
+
argv = [
+
"/usr/bin/echo"
+
"hello"
+
];
+
};
+
services = { };
+
assertions = [
+
{
+
assertion = false;
+
message = "you can't enable this for that reason";
+
}
+
];
+
warnings = [
+
"The `foo' service is deprecated and will go away soon!"
+
];
+
};
+
service2 = {
+
process = {
+
argv = [
+
"${dummyPkg "cowsay.sh"}"
+
"world"
+
];
+
};
+
services = { };
+
assertions = [ ];
+
warnings = [ ];
+
};
+
service3 = {
+
process = {
+
argv = [ "/bin/false" ];
+
};
+
services.exclacow = {
+
process = {
+
argv = [
+
"${dummyPkg "cowsay-ng"}/bin/cowsay"
+
"!"
+
];
+
};
+
services = { };
+
assertions = [
+
{
+
assertion = false;
+
message = "you can't enable this for such reason";
+
}
+
];
+
warnings = [ "The `bar' service is deprecated and will go away soon!" ];
+
};
+
assertions = [ ];
+
warnings = [ ];
+
};
+
};
+
};
+
+
assert
+
portable-lib.getWarnings [ "service1" ] exampleEval.config.services.service1 == [
+
"in service1: The `foo' service is deprecated and will go away soon!"
+
];
+
+
assert
+
portable-lib.getAssertions [ "service1" ] exampleEval.config.services.service1 == [
+
{
+
message = "in service1: you can't enable this for that reason";
+
assertion = false;
+
}
+
];
+
+
assert
+
portable-lib.getWarnings [ "service3" ] exampleEval.config.services.service3 == [
+
"in service3.services.exclacow: The `bar' service is deprecated and will go away soon!"
+
];
+
assert
+
portable-lib.getAssertions [ "service3" ] exampleEval.config.services.service3 == [
+
{
+
message = "in service3.services.exclacow: you can't enable this for such reason";
+
assertion = false;
+
}
+
];
+
+
"ok";
+
+
in
+
test
+121
nixos/modules/system/service/systemd/service.nix
···
···
+
{
+
lib,
+
config,
+
systemdPackage,
+
...
+
}:
+
let
+
inherit (lib)
+
concatMapStringsSep
+
isDerivation
+
isInt
+
isFloat
+
isPath
+
isString
+
mkOption
+
replaceStrings
+
types
+
;
+
inherit (builtins) toJSON;
+
+
# Local copy of systemd exec argument escaping function.
+
# TODO: This could perhaps be deduplicated, but it is unclear where it should go.
+
# Preferably, we don't create a hard dependency on NixOS here, so that this
+
# module can be reused in a non-NixOS context, such as mutaable services
+
# in /run/systemd/system.
+
+
# Quotes an argument for use in Exec* service lines.
+
# systemd accepts "-quoted strings with escape sequences, toJSON produces
+
# a subset of these.
+
# Additionally we escape % to disallow expansion of % specifiers. Any lone ;
+
# in the input will be turned it ";" and thus lose its special meaning.
+
# Every $ is escaped to $$, this makes it unnecessary to disable environment
+
# substitution for the directive.
+
escapeSystemdExecArg =
+
arg:
+
let
+
s =
+
if isPath arg then
+
"${arg}"
+
else if isString arg then
+
arg
+
else if isInt arg || isFloat arg || isDerivation arg then
+
toString arg
+
else
+
throw "escapeSystemdExecArg only allows strings, paths, numbers and derivations";
+
in
+
replaceStrings [ "%" "$" ] [ "%%" "$$" ] (toJSON s);
+
+
# Quotes a list of arguments into a single string for use in a Exec*
+
# line.
+
escapeSystemdExecArgs = concatMapStringsSep " " escapeSystemdExecArg;
+
+
in
+
{
+
imports = [
+
../portable/service.nix
+
(lib.mkAliasOptionModule [ "systemd" "service" ] [ "systemd" "services" "" ])
+
(lib.mkAliasOptionModule [ "systemd" "socket" ] [ "systemd" "sockets" "" ])
+
];
+
options = {
+
systemd.services = mkOption {
+
description = ''
+
This module configures systemd services, with the notable difference that their unit names will be prefixed with the abstract service name.
+
+
This option's value is not suitable for reading, but you can define a module here that interacts with just the unit configuration in the host system configuration.
+
+
Note that this option contains _deferred_ modules.
+
This means that the module has not been combined with the system configuration yet, no values can be read from this option.
+
What you can do instead is define a module that reads from the module arguments (such as `config`) that are available when the module is merged into the system configuration.
+
'';
+
type = types.lazyAttrsOf (
+
types.deferredModuleWith {
+
staticModules = [
+
# TODO: Add modules for the purpose of generating documentation?
+
];
+
}
+
);
+
default = { };
+
};
+
systemd.sockets = mkOption {
+
description = ''
+
Declares systemd socket units. Names will be prefixed by the service name / path.
+
+
See {option}`systemd.services`.
+
'';
+
type = types.lazyAttrsOf types.deferredModule;
+
default = { };
+
};
+
+
# Also import systemd logic into sub-services
+
# extends the portable `services` option
+
services = mkOption {
+
type = types.attrsOf (
+
types.submoduleWith {
+
class = "service";
+
modules = [
+
./service.nix
+
];
+
specialArgs = {
+
inherit systemdPackage;
+
};
+
}
+
);
+
};
+
};
+
config = {
+
# Note that this is the systemd.services option above, not the system one.
+
systemd.services."" = {
+
# TODO description;
+
wantedBy = lib.mkDefault [ "multi-user.target" ];
+
serviceConfig = {
+
Type = lib.mkDefault "simple";
+
Restart = lib.mkDefault "always";
+
RestartSec = lib.mkDefault "5";
+
ExecStart = [
+
(escapeSystemdExecArgs config.process.argv)
+
];
+
};
+
};
+
};
+
}
+90
nixos/modules/system/service/systemd/system.nix
···
···
+
{
+
lib,
+
config,
+
options,
+
pkgs,
+
...
+
}:
+
+
let
+
inherit (lib)
+
concatMapAttrs
+
mkOption
+
types
+
concatLists
+
mapAttrsToList
+
;
+
+
portable-lib = import ../portable/lib.nix { inherit lib; };
+
+
dash =
+
before: after:
+
if after == "" then
+
before
+
else if before == "" then
+
after
+
else
+
"${before}-${after}";
+
+
makeUnits =
+
unitType: prefix: service:
+
concatMapAttrs (unitName: unitModule: {
+
"${dash prefix unitName}" =
+
{ ... }:
+
{
+
imports = [ unitModule ];
+
};
+
}) service.systemd.${unitType}
+
// concatMapAttrs (
+
subServiceName: subService: makeUnits unitType (dash prefix subServiceName) subService
+
) service.services;
+
in
+
{
+
# First half of the magic: mix systemd logic into the otherwise abstract services
+
options = {
+
system.services = mkOption {
+
description = ''
+
A collection of NixOS [modular services](https://nixos.org/manual/nixos/unstable/#modular-services) that are configured as systemd services.
+
'';
+
type = types.attrsOf (
+
types.submoduleWith {
+
class = "service";
+
modules = [
+
./service.nix
+
];
+
specialArgs = {
+
# perhaps: features."systemd" = { };
+
inherit pkgs;
+
systemdPackage = config.systemd.package;
+
};
+
}
+
);
+
default = { };
+
visible = "shallow";
+
};
+
};
+
+
# Second half of the magic: siphon units that were defined in isolation to the system
+
config = {
+
+
assertions = concatLists (
+
mapAttrsToList (
+
name: cfg: portable-lib.getAssertions (options.system.services.loc ++ [ name ]) cfg
+
) config.system.services
+
);
+
+
warnings = concatLists (
+
mapAttrsToList (
+
name: cfg: portable-lib.getWarnings (options.system.services.loc ++ [ name ]) cfg
+
) config.system.services
+
);
+
+
systemd.services = concatMapAttrs (
+
serviceName: topLevelService: makeUnits "services" serviceName topLevelService
+
) config.system.services;
+
+
systemd.sockets = concatMapAttrs (
+
serviceName: topLevelService: makeUnits "sockets" serviceName topLevelService
+
) config.system.services;
+
};
+
}
+92
nixos/modules/system/service/systemd/test.nix
···
···
+
# Run:
+
# nix-build -A nixosTests.modularService
+
+
{
+
evalSystem,
+
runCommand,
+
hello,
+
...
+
}:
+
+
let
+
machine = evalSystem (
+
{ lib, ... }:
+
let
+
hello' = lib.getExe hello;
+
in
+
{
+
+
# Test input
+
+
system.services.foo = {
+
process = {
+
argv = [
+
hello'
+
"--greeting"
+
"hoi"
+
];
+
};
+
};
+
system.services.bar = {
+
process = {
+
argv = [
+
hello'
+
"--greeting"
+
"hoi"
+
];
+
};
+
systemd.service = {
+
serviceConfig.X-Bar = "lol crossbar whatever";
+
};
+
services.db = {
+
process = {
+
argv = [
+
hello'
+
"--greeting"
+
"Hi, I'm a database, would you believe it"
+
];
+
};
+
systemd.service = {
+
serviceConfig.RestartSec = "42";
+
};
+
};
+
};
+
+
# irrelevant stuff
+
system.stateVersion = "25.05";
+
fileSystems."/".device = "/test/dummy";
+
boot.loader.grub.enable = false;
+
}
+
);
+
+
inherit (machine.config.system.build) toplevel;
+
in
+
runCommand "test-modular-service-systemd-units"
+
{
+
passthru = {
+
inherit
+
machine
+
toplevel
+
;
+
};
+
}
+
''
+
echo ${toplevel}/etc/systemd/system/foo.service:
+
cat -n ${toplevel}/etc/systemd/system/foo.service
+
(
+
set -x
+
grep -F 'ExecStart="${hello}/bin/hello" "--greeting" "hoi"' ${toplevel}/etc/systemd/system/foo.service >/dev/null
+
+
grep -F 'ExecStart="${hello}/bin/hello" "--greeting" "hoi"' ${toplevel}/etc/systemd/system/bar.service >/dev/null
+
grep -F 'X-Bar=lol crossbar whatever' ${toplevel}/etc/systemd/system/bar.service >/dev/null
+
+
grep 'ExecStart="${hello}/bin/hello" "--greeting" ".*database.*"' ${toplevel}/etc/systemd/system/bar-db.service >/dev/null
+
grep -F 'RestartSec=42' ${toplevel}/etc/systemd/system/bar-db.service >/dev/null
+
+
[[ ! -e ${toplevel}/etc/systemd/system/foo.socket ]]
+
[[ ! -e ${toplevel}/etc/systemd/system/bar.socket ]]
+
[[ ! -e ${toplevel}/etc/systemd/system/bar-db.socket ]]
+
)
+
echo 🐬👍
+
touch $out
+
''
+3
nixos/modules/system/service/systemd/user.nix
···
···
+
# TBD, analogous to system.nix but for user units
+
{
+
}
+14
nixos/tests/all-tests.nix
···
featureFlags.minimalModules = { };
};
evalMinimalConfig = module: nixosLib.evalModules { modules = [ module ]; };
inherit
(rec {
···
gerrit = runTest ./gerrit.nix;
geth = runTest ./geth.nix;
ghostunnel = runTest ./ghostunnel.nix;
gitdaemon = runTest ./gitdaemon.nix;
gitea = handleTest ./gitea.nix { giteaPackage = pkgs.gitea; };
github-runner = runTest ./github-runner.nix;
···
mjolnir = runTest ./matrix/mjolnir.nix;
mobilizon = runTest ./mobilizon.nix;
mod_perl = runTest ./mod_perl.nix;
molly-brown = runTest ./molly-brown.nix;
mollysocket = runTest ./mollysocket.nix;
monado = runTest ./monado.nix;
···
featureFlags.minimalModules = { };
};
evalMinimalConfig = module: nixosLib.evalModules { modules = [ module ]; };
+
evalSystem =
+
module:
+
import ../lib/eval-config.nix {
+
system = null;
+
modules = [
+
../modules/misc/nixpkgs/read-only.nix
+
{ nixpkgs.pkgs = pkgs; }
+
module
+
];
+
};
inherit
(rec {
···
gerrit = runTest ./gerrit.nix;
geth = runTest ./geth.nix;
ghostunnel = runTest ./ghostunnel.nix;
+
ghostunnel-modular = runTest ./ghostunnel-modular.nix;
gitdaemon = runTest ./gitdaemon.nix;
gitea = handleTest ./gitea.nix { giteaPackage = pkgs.gitea; };
github-runner = runTest ./github-runner.nix;
···
mjolnir = runTest ./matrix/mjolnir.nix;
mobilizon = runTest ./mobilizon.nix;
mod_perl = runTest ./mod_perl.nix;
+
modularService = pkgs.callPackage ../modules/system/service/systemd/test.nix {
+
inherit evalSystem;
+
};
molly-brown = runTest ./molly-brown.nix;
mollysocket = runTest ./mollysocket.nix;
monado = runTest ./monado.nix;
+120
nixos/tests/ghostunnel-modular.nix
···
···
+
{ hostPkgs, lib, ... }:
+
{
+
_class = "nixosTest";
+
name = "ghostunnel";
+
nodes = {
+
backend =
+
{ pkgs, ... }:
+
{
+
services.nginx.enable = true;
+
services.nginx.virtualHosts."backend".root = pkgs.runCommand "webroot" { } ''
+
mkdir $out
+
echo hi >$out/hi.txt
+
'';
+
networking.firewall.allowedTCPPorts = [ 80 ];
+
};
+
service =
+
{ pkgs, ... }:
+
{
+
system.services."ghostunnel-plain-old" = {
+
imports = [ pkgs.ghostunnel.services.default ];
+
ghostunnel = {
+
listen = "0.0.0.0:443";
+
cert = "/root/service-cert.pem";
+
key = "/root/service-key.pem";
+
disableAuthentication = true;
+
target = "backend:80";
+
unsafeTarget = true;
+
};
+
};
+
system.services."ghostunnel-client-cert" = {
+
imports = [ pkgs.ghostunnel.services.default ];
+
ghostunnel = {
+
listen = "0.0.0.0:1443";
+
cert = "/root/service-cert.pem";
+
key = "/root/service-key.pem";
+
cacert = "/root/ca.pem";
+
target = "backend:80";
+
allowCN = [ "client" ];
+
unsafeTarget = true;
+
};
+
};
+
networking.firewall.allowedTCPPorts = [
+
443
+
1443
+
];
+
};
+
client =
+
{ pkgs, ... }:
+
{
+
environment.systemPackages = [
+
pkgs.curl
+
];
+
};
+
};
+
+
testScript = ''
+
+
# prepare certificates
+
+
def cmd(command):
+
print(f"+{command}")
+
r = os.system(command)
+
if r != 0:
+
raise Exception(f"Command {command} failed with exit code {r}")
+
+
# Create CA
+
cmd("${hostPkgs.openssl}/bin/openssl genrsa -out ca-key.pem 4096")
+
cmd("${hostPkgs.openssl}/bin/openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -subj '/C=NL/ST=Zuid-Holland/L=The Hague/O=Stevige Balken en Planken B.V./OU=OpSec/CN=Certificate Authority' -out ca.pem")
+
+
# Create service
+
cmd("${hostPkgs.openssl}/bin/openssl genrsa -out service-key.pem 4096")
+
cmd("${hostPkgs.openssl}/bin/openssl req -subj '/CN=service' -sha256 -new -key service-key.pem -out service.csr")
+
cmd("echo subjectAltName = DNS:service,IP:127.0.0.1 >> extfile.cnf")
+
cmd("echo extendedKeyUsage = serverAuth >> extfile.cnf")
+
cmd("${hostPkgs.openssl}/bin/openssl x509 -req -days 365 -sha256 -in service.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out service-cert.pem -extfile extfile.cnf")
+
+
# Create client
+
cmd("${hostPkgs.openssl}/bin/openssl genrsa -out client-key.pem 4096")
+
cmd("${hostPkgs.openssl}/bin/openssl req -subj '/CN=client' -new -key client-key.pem -out client.csr")
+
cmd("echo extendedKeyUsage = clientAuth > extfile-client.cnf")
+
cmd("${hostPkgs.openssl}/bin/openssl x509 -req -days 365 -sha256 -in client.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out client-cert.pem -extfile extfile-client.cnf")
+
+
cmd("ls -al")
+
+
start_all()
+
+
# Configuration
+
service.copy_from_host("ca.pem", "/root/ca.pem")
+
service.copy_from_host("service-cert.pem", "/root/service-cert.pem")
+
service.copy_from_host("service-key.pem", "/root/service-key.pem")
+
client.copy_from_host("ca.pem", "/root/ca.pem")
+
client.copy_from_host("service-cert.pem", "/root/service-cert.pem")
+
client.copy_from_host("client-cert.pem", "/root/client-cert.pem")
+
client.copy_from_host("client-key.pem", "/root/client-key.pem")
+
+
backend.wait_for_unit("nginx.service")
+
service.wait_for_unit("multi-user.target")
+
service.wait_for_unit("multi-user.target")
+
client.wait_for_unit("multi-user.target")
+
+
# Check assumptions before the real test
+
client.succeed("bash -c 'diff <(curl -v --no-progress-meter http://backend/hi.txt) <(echo hi)'")
+
+
# Plain old simple TLS can connect, ignoring cert
+
client.succeed("bash -c 'diff <(curl -v --no-progress-meter --insecure https://service/hi.txt) <(echo hi)'")
+
+
# Plain old simple TLS provides correct signature with its cert
+
client.succeed("bash -c 'diff <(curl -v --no-progress-meter --cacert /root/ca.pem https://service/hi.txt) <(echo hi)'")
+
+
# Client can authenticate with certificate
+
client.succeed("bash -c 'diff <(curl -v --no-progress-meter --cert /root/client-cert.pem --key /root/client-key.pem --cacert /root/ca.pem https://service:1443/hi.txt) <(echo hi)'")
+
+
# Client must authenticate with certificate
+
client.fail("bash -c 'diff <(curl -v --no-progress-meter --cacert /root/ca.pem https://service:1443/hi.txt) <(echo hi)'")
+
'';
+
+
meta.maintainers = with lib.maintainers; [
+
roberth
+
];
+
}
+6
pkgs/by-name/gh/ghostunnel/package.nix
···
fetchFromGitHub,
lib,
nixosTests,
apple-sdk_12,
darwinMinVersionHook,
}:
···
passthru.tests = {
nixos = nixosTests.ghostunnel;
podman = nixosTests.podman-tls-ghostunnel;
};
meta = {
···
fetchFromGitHub,
lib,
nixosTests,
+
ghostunnel,
apple-sdk_12,
darwinMinVersionHook,
}:
···
passthru.tests = {
nixos = nixosTests.ghostunnel;
podman = nixosTests.podman-tls-ghostunnel;
+
};
+
+
passthru.services.default = {
+
imports = [ ./service.nix ];
+
ghostunnel.package = ghostunnel; # FIXME: finalAttrs.finalPackage
};
meta = {
+241
pkgs/by-name/gh/ghostunnel/service.nix
···
···
+
{
+
lib,
+
config,
+
options,
+
pkgs,
+
...
+
}:
+
let
+
inherit (lib)
+
concatStringsSep
+
getExe
+
mkDefault
+
mkIf
+
mkOption
+
optional
+
types
+
;
+
cfg = config.ghostunnel;
+
+
in
+
{
+
# https://nixos.org/manual/nixos/unstable/#modular-services
+
_class = "service";
+
options = {
+
ghostunnel = {
+
package = mkOption {
+
description = "Package to use for ghostunnel";
+
type = types.package;
+
};
+
+
listen = mkOption {
+
description = ''
+
Address and port to listen on (can be HOST:PORT, unix:PATH).
+
'';
+
type = types.str;
+
};
+
+
target = mkOption {
+
description = ''
+
Address to forward connections to (can be HOST:PORT or unix:PATH).
+
'';
+
type = types.str;
+
};
+
+
keystore = mkOption {
+
description = ''
+
Path to keystore (combined PEM with cert/key, or PKCS12 keystore).
+
+
NB: storepass is not supported because it would expose credentials via `/proc/*/cmdline`.
+
+
Specify this or `cert` and `key`.
+
'';
+
type = types.nullOr types.str;
+
default = null;
+
};
+
+
cert = mkOption {
+
description = ''
+
Path to certificate (PEM with certificate chain).
+
+
Not required if `keystore` is set.
+
'';
+
type = types.nullOr types.str;
+
default = null;
+
};
+
+
key = mkOption {
+
description = ''
+
Path to certificate private key (PEM with private key).
+
+
Not required if `keystore` is set.
+
'';
+
type = types.nullOr types.str;
+
default = null;
+
};
+
+
cacert = mkOption {
+
description = ''
+
Path to CA bundle file (PEM/X509). Uses system trust store if `null`.
+
'';
+
type = types.nullOr types.str;
+
};
+
+
disableAuthentication = mkOption {
+
description = ''
+
Disable client authentication, no client certificate will be required.
+
'';
+
type = types.bool;
+
default = false;
+
};
+
+
allowAll = mkOption {
+
description = ''
+
If true, allow all clients, do not check client cert subject.
+
'';
+
type = types.bool;
+
default = false;
+
};
+
+
allowCN = mkOption {
+
description = ''
+
Allow client if common name appears in the list.
+
'';
+
type = types.listOf types.str;
+
default = [ ];
+
};
+
+
allowOU = mkOption {
+
description = ''
+
Allow client if organizational unit name appears in the list.
+
'';
+
type = types.listOf types.str;
+
default = [ ];
+
};
+
+
allowDNS = mkOption {
+
description = ''
+
Allow client if DNS subject alternative name appears in the list.
+
'';
+
type = types.listOf types.str;
+
default = [ ];
+
};
+
+
allowURI = mkOption {
+
description = ''
+
Allow client if URI subject alternative name appears in the list.
+
'';
+
type = types.listOf types.str;
+
default = [ ];
+
};
+
+
extraArguments = mkOption {
+
description = "Extra arguments to pass to `ghostunnel server`";
+
type = types.listOf types.str;
+
default = [ ];
+
};
+
+
unsafeTarget = mkOption {
+
description = ''
+
If set, does not limit target to localhost, 127.0.0.1, [::1], or UNIX sockets.
+
+
This is meant to protect against accidental unencrypted traffic on
+
untrusted networks.
+
'';
+
type = types.bool;
+
default = false;
+
};
+
};
+
};
+
+
config = {
+
assertions = [
+
{
+
message = ''
+
At least one access control flag is required.
+
Set at least one of:
+
- ${options.ghostunnel.disableAuthentication}
+
- ${options.ghostunnel.allowAll}
+
- ${options.ghostunnel.allowCN}
+
- ${options.ghostunnel.allowOU}
+
- ${options.ghostunnel.allowDNS}
+
- ${options.ghostunnel.allowURI}
+
'';
+
assertion =
+
cfg.disableAuthentication
+
|| cfg.allowAll
+
|| cfg.allowCN != [ ]
+
|| cfg.allowOU != [ ]
+
|| cfg.allowDNS != [ ]
+
|| cfg.allowURI != [ ];
+
}
+
];
+
+
ghostunnel = {
+
# Clients should not be authenticated with the public root certificates
+
# (afaict, it doesn't make sense), so we only provide that default when
+
# client cert auth is disabled.
+
cacert = mkIf cfg.disableAuthentication (mkDefault null);
+
};
+
+
# TODO assertions
+
+
process = {
+
argv =
+
# Use a shell if credentials need to be pulled from the environment.
+
optional
+
(builtins.any (v: v != null) [
+
cfg.keystore
+
cfg.cert
+
cfg.key
+
cfg.cacert
+
])
+
(
+
pkgs.writeScript "load-credentials" ''
+
#!${pkgs.runtimeShell}
+
exec $@ ${
+
concatStringsSep " " (
+
optional (cfg.keystore != null) "--keystore=$CREDENTIALS_DIRECTORY/keystore"
+
++ optional (cfg.cert != null) "--cert=$CREDENTIALS_DIRECTORY/cert"
+
++ optional (cfg.key != null) "--key=$CREDENTIALS_DIRECTORY/key"
+
++ optional (cfg.cacert != null) "--cacert=$CREDENTIALS_DIRECTORY/cacert"
+
)
+
}
+
''
+
)
+
++ [
+
(getExe cfg.package)
+
"server"
+
"--listen"
+
cfg.listen
+
"--target"
+
cfg.target
+
]
+
++ optional cfg.allowAll "--allow-all"
+
++ map (v: "--allow-cn=${v}") cfg.allowCN
+
++ map (v: "--allow-ou=${v}") cfg.allowOU
+
++ map (v: "--allow-dns=${v}") cfg.allowDNS
+
++ map (v: "--allow-uri=${v}") cfg.allowURI
+
++ optional cfg.disableAuthentication "--disable-authentication"
+
++ optional cfg.unsafeTarget "--unsafe-target"
+
++ cfg.extraArguments;
+
};
+
+
# refine the service
+
systemd.service = {
+
after = [ "network.target" ];
+
wants = [ "network.target" ];
+
wantedBy = [ "multi-user.target" ];
+
serviceConfig = {
+
Restart = "always";
+
AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
+
DynamicUser = true;
+
LoadCredential =
+
optional (cfg.keystore != null) "keystore:${cfg.keystore}"
+
++ optional (cfg.cert != null) "cert:${cfg.cert}"
+
++ optional (cfg.key != null) "key:${cfg.key}"
+
++ optional (cfg.cacert != null) "cacert:${cfg.cacert}";
+
};
+
};
+
};
+
}
+1 -1
pkgs/by-name/ni/nixos-render-docs/src/nixos_render_docs/redirects.py
···
- The first element of an identifier's redirects list must denote its current location.
"""
xref_targets = {}
-
ignored_identifier_patterns = ("opt-", "auto-generated-", "function-library-")
for id, target in initial_xref_targets.items():
# filter out automatically generated identifiers from module options and library documentation
if id.startswith(ignored_identifier_patterns):
···
- The first element of an identifier's redirects list must denote its current location.
"""
xref_targets = {}
+
ignored_identifier_patterns = ("opt-", "auto-generated-", "function-library-", "service-opt-", "systemd-service-opt")
for id, target in initial_xref_targets.items():
# filter out automatically generated identifiers from module options and library documentation
if id.startswith(ignored_identifier_patterns):