In order to simplify using certificates provided by ACME, add the option `services.murmur.tls.useACMEHost`. If set, the options keyPath, certPath and caPath are pre-configured appropriately. Also, add the appriopriate ACME systemd service to Murmur's dependencies if useACMEHost is set. Signed-off-by: Felix Singer <felixsinger@posteo.net>
479 lines
16 KiB
Nix
479 lines
16 KiB
Nix
{
|
||
config,
|
||
lib,
|
||
pkgs,
|
||
...
|
||
}:
|
||
|
||
let
|
||
cfg = config.services.murmur;
|
||
acmeHostDir = config.security.acme.certs."${cfg.tls.useACMEHost}".directory;
|
||
|
||
forking = cfg.logToFile;
|
||
configFile = pkgs.writeText "murmurd.ini" ''
|
||
database=${cfg.stateDir}/murmur.sqlite
|
||
dbDriver=QSQLITE
|
||
|
||
autobanAttempts=${toString cfg.autobanAttempts}
|
||
autobanTimeframe=${toString cfg.autobanTimeframe}
|
||
autobanTime=${toString cfg.autobanTime}
|
||
|
||
logfile=${lib.optionalString cfg.logToFile "/var/log/murmur/murmurd.log"}
|
||
${lib.optionalString forking "pidfile=/run/murmur/murmurd.pid"}
|
||
|
||
welcometext="${cfg.welcometext}"
|
||
port=${toString cfg.port}
|
||
|
||
${lib.optionalString (cfg.hostName != "") "host=${cfg.hostName}"}
|
||
${lib.optionalString (cfg.password != "") "serverpassword=${cfg.password}"}
|
||
|
||
bandwidth=${toString cfg.bandwidth}
|
||
users=${toString cfg.users}
|
||
|
||
textmessagelength=${toString cfg.textMsgLength}
|
||
imagemessagelength=${toString cfg.imgMsgLength}
|
||
allowhtml=${lib.boolToString cfg.allowHtml}
|
||
logdays=${toString cfg.logDays}
|
||
bonjour=${lib.boolToString cfg.bonjour}
|
||
sendversion=${lib.boolToString cfg.sendVersion}
|
||
|
||
${lib.optionalString (cfg.registerName != "") "registerName=${cfg.registerName}"}
|
||
${lib.optionalString (cfg.registerPassword != "") "registerPassword=${cfg.registerPassword}"}
|
||
${lib.optionalString (cfg.registerUrl != "") "registerUrl=${cfg.registerUrl}"}
|
||
${lib.optionalString (cfg.registerHostname != "") "registerHostname=${cfg.registerHostname}"}
|
||
|
||
certrequired=${lib.boolToString cfg.clientCertRequired}
|
||
${lib.optionalString (cfg.tls.certPath != null) "sslCert=${cfg.tls.certPath}"}
|
||
${lib.optionalString (cfg.tls.keyPath != null) "sslKey=${cfg.tls.keyPath}"}
|
||
${lib.optionalString (cfg.tls.caPath != null) "sslCA=${cfg.tls.caPath}"}
|
||
|
||
${lib.optionalString (cfg.dbus != null) "dbus=${cfg.dbus}"}
|
||
|
||
${cfg.extraConfig}
|
||
'';
|
||
in
|
||
{
|
||
|
||
imports = [
|
||
(lib.mkRemovedOptionModule [
|
||
"services"
|
||
"murmur"
|
||
"logFile"
|
||
] "This option has been superseded by services.murmur.logToFile")
|
||
(lib.mkRenamedOptionModule [ "services" "murmur" "sslCa" ] [ "services" "murmur" "tls" "caPath" ])
|
||
(lib.mkRenamedOptionModule [ "services" "murmur" "sslKey" ] [ "services" "murmur" "tls" "keyPath" ])
|
||
(lib.mkRenamedOptionModule
|
||
[ "services" "murmur" "sslCert" ]
|
||
[ "services" "murmur" "tls" "certPath" ]
|
||
)
|
||
];
|
||
|
||
options = {
|
||
services.murmur = {
|
||
enable = lib.mkEnableOption "Mumble server";
|
||
|
||
openFirewall = lib.mkEnableOption "opening ports in the firewall for the Mumble server";
|
||
|
||
user = lib.mkOption {
|
||
type = lib.types.str;
|
||
default = "murmur";
|
||
description = ''
|
||
The name of an existing user to use to run the service.
|
||
If not specified, the default user will be created.
|
||
'';
|
||
};
|
||
|
||
group = lib.mkOption {
|
||
type = lib.types.str;
|
||
default = "murmur";
|
||
description = ''
|
||
The name of an existing group to use to run the service.
|
||
If not specified, the default group will be created.
|
||
'';
|
||
};
|
||
|
||
stateDir = lib.mkOption {
|
||
type = lib.types.path;
|
||
default = "/var/lib/murmur";
|
||
description = ''
|
||
Directory to store data for the server.
|
||
'';
|
||
};
|
||
|
||
autobanAttempts = lib.mkOption {
|
||
type = lib.types.int;
|
||
default = 10;
|
||
description = ''
|
||
Number of attempts a client is allowed to make in
|
||
`autobanTimeframe` seconds, before being
|
||
banned for `autobanTime`.
|
||
'';
|
||
};
|
||
|
||
autobanTimeframe = lib.mkOption {
|
||
type = lib.types.int;
|
||
default = 120;
|
||
description = ''
|
||
Timeframe in which a client can connect without being banned
|
||
for repeated attempts (in seconds).
|
||
'';
|
||
};
|
||
|
||
autobanTime = lib.mkOption {
|
||
type = lib.types.int;
|
||
default = 300;
|
||
description = "The amount of time an IP ban lasts (in seconds).";
|
||
};
|
||
|
||
logToFile = lib.mkEnableOption "logging to a file instead of journald, which is stored in /var/log/murmur";
|
||
|
||
welcometext = lib.mkOption {
|
||
type = lib.types.str;
|
||
default = "";
|
||
description = "Welcome message for connected clients.";
|
||
};
|
||
|
||
port = lib.mkOption {
|
||
type = lib.types.port;
|
||
default = 64738;
|
||
description = "Ports to bind to (UDP and TCP).";
|
||
};
|
||
|
||
hostName = lib.mkOption {
|
||
type = lib.types.str;
|
||
default = "";
|
||
description = "Host to bind to. Defaults binding on all addresses.";
|
||
};
|
||
|
||
package = lib.mkPackageOption pkgs "murmur" { };
|
||
|
||
password = lib.mkOption {
|
||
type = lib.types.str;
|
||
default = "";
|
||
description = "Required password to join server, if specified.";
|
||
};
|
||
|
||
bandwidth = lib.mkOption {
|
||
type = lib.types.int;
|
||
default = 72000;
|
||
description = ''
|
||
Maximum bandwidth (in bits per second) that clients may send
|
||
speech at.
|
||
'';
|
||
};
|
||
|
||
users = lib.mkOption {
|
||
type = lib.types.int;
|
||
default = 100;
|
||
description = "Maximum number of concurrent clients allowed.";
|
||
};
|
||
|
||
textMsgLength = lib.mkOption {
|
||
type = lib.types.int;
|
||
default = 5000;
|
||
description = "Max length of text messages. Set 0 for no limit.";
|
||
};
|
||
|
||
imgMsgLength = lib.mkOption {
|
||
type = lib.types.int;
|
||
default = 131072;
|
||
description = "Max length of image messages. Set 0 for no limit.";
|
||
};
|
||
|
||
allowHtml = lib.mkOption {
|
||
type = lib.types.bool;
|
||
default = true;
|
||
description = ''
|
||
Allow HTML in client messages, comments, and channel
|
||
descriptions.
|
||
'';
|
||
};
|
||
|
||
logDays = lib.mkOption {
|
||
type = lib.types.int;
|
||
default = 31;
|
||
description = ''
|
||
How long to store RPC logs for in the database. Set 0 to
|
||
keep logs forever, or -1 to disable DB logging.
|
||
'';
|
||
};
|
||
|
||
bonjour = lib.mkEnableOption "Bonjour auto-discovery, which allows clients over your LAN to automatically discover Mumble servers";
|
||
|
||
sendVersion = lib.mkOption {
|
||
type = lib.types.bool;
|
||
default = true;
|
||
description = "Send Murmur version in UDP response.";
|
||
};
|
||
|
||
registerName = lib.mkOption {
|
||
type = lib.types.str;
|
||
default = "";
|
||
description = ''
|
||
Public server registration name, and also the name of the
|
||
Root channel. Even if you don't publicly register your
|
||
server, you probably still want to set this.
|
||
'';
|
||
};
|
||
|
||
registerPassword = lib.mkOption {
|
||
type = lib.types.str;
|
||
default = "";
|
||
description = ''
|
||
Public server registry password, used authenticate your
|
||
server to the registry to prevent impersonation; required for
|
||
subsequent registry updates.
|
||
'';
|
||
};
|
||
|
||
registerUrl = lib.mkOption {
|
||
type = lib.types.str;
|
||
default = "";
|
||
description = "URL website for your server.";
|
||
};
|
||
|
||
registerHostname = lib.mkOption {
|
||
type = lib.types.str;
|
||
default = "";
|
||
description = ''
|
||
DNS hostname where your server can be reached. This is only
|
||
needed if you want your server to be accessed by its
|
||
hostname and not IP - but the name *must* resolve on the
|
||
internet properly.
|
||
'';
|
||
};
|
||
|
||
clientCertRequired = lib.mkEnableOption "requiring clients to authenticate via certificates";
|
||
|
||
tls = {
|
||
certPath = lib.mkOption {
|
||
type = lib.types.nullOr lib.types.path;
|
||
default = if (cfg.tls.useACMEHost != null) then "${acmeHostDir}/cert.pem" else null;
|
||
defaultText = lib.literalMD "If {option}`services.murmur.tls.useACMEHost` is set, defaults to what's provided by the ACME module.";
|
||
description = "Path to your TLS certificate.";
|
||
};
|
||
|
||
keyPath = lib.mkOption {
|
||
type = lib.types.nullOr lib.types.path;
|
||
default = if (cfg.tls.useACMEHost != null) then "${acmeHostDir}/key.pem" else null;
|
||
defaultText = lib.literalMD "If {option}`services.murmur.tls.useACMEHost` is set, defaults to what's provided by the ACME module.";
|
||
description = "Path to your TLS key.";
|
||
};
|
||
|
||
caPath = lib.mkOption {
|
||
type = lib.types.nullOr lib.types.path;
|
||
default = if (cfg.tls.useACMEHost != null) then "${acmeHostDir}/chain.pem" else null;
|
||
defaultText = lib.literalMD "If {option}`services.murmur.tls.useACMEHost` is set, defaults to what's provided by the ACME module.";
|
||
description = "Path to your TLS CA certificate.";
|
||
};
|
||
|
||
useACMEHost = lib.mkOption {
|
||
type = lib.types.nullOr lib.types.str;
|
||
default = null;
|
||
example = "mumble.example.com";
|
||
description = ''
|
||
Host of an existing Let's Encrypt certificate to use for TLS.
|
||
Make sure that the certificate directory is readable by the
|
||
`murmur` user or group. *Note that this option does not
|
||
create any certificates and it doesn't add subdomains to
|
||
existing ones – you will need to create them manually using
|
||
{option}`security.acme.certs`.*
|
||
'';
|
||
};
|
||
};
|
||
|
||
extraConfig = lib.mkOption {
|
||
type = lib.types.lines;
|
||
default = "";
|
||
description = "Extra configuration to put into murmur.ini.";
|
||
};
|
||
|
||
environmentFile = lib.mkOption {
|
||
type = lib.types.nullOr lib.types.path;
|
||
default = null;
|
||
example = lib.literalExpression ''"''${config.services.murmur.stateDir}/murmurd.env"'';
|
||
description = ''
|
||
Environment file as defined in {manpage}`systemd.exec(5)`.
|
||
|
||
Secrets may be passed to the service without adding them to the world-readable
|
||
Nix store, by specifying placeholder variables as the option value in Nix and
|
||
setting these variables accordingly in the environment file.
|
||
|
||
```
|
||
# snippet of murmur-related config
|
||
services.murmur.password = "$MURMURD_PASSWORD";
|
||
```
|
||
|
||
```
|
||
# content of the environment file
|
||
MURMURD_PASSWORD=verysecretpassword
|
||
```
|
||
|
||
Note that this file needs to be available on the host on which
|
||
`murmur` is running.
|
||
'';
|
||
};
|
||
|
||
dbus = lib.mkOption {
|
||
type = lib.types.enum [
|
||
null
|
||
"session"
|
||
"system"
|
||
];
|
||
default = null;
|
||
description = "Enable D-Bus remote control. Set to the bus you want Murmur to connect to.";
|
||
};
|
||
};
|
||
};
|
||
|
||
config = lib.mkIf cfg.enable {
|
||
users.users.murmur = lib.mkIf (cfg.user == "murmur") {
|
||
description = "Murmur Service user";
|
||
home = cfg.stateDir;
|
||
createHome = true;
|
||
uid = config.ids.uids.murmur;
|
||
group = cfg.group;
|
||
};
|
||
users.groups.murmur = lib.mkIf (cfg.group == "murmur") {
|
||
gid = config.ids.gids.murmur;
|
||
};
|
||
|
||
networking.firewall = lib.mkIf cfg.openFirewall {
|
||
allowedTCPPorts = [ cfg.port ];
|
||
allowedUDPPorts = [ cfg.port ];
|
||
};
|
||
|
||
security.acme.certs = lib.mkIf (cfg.tls.useACMEHost != null) {
|
||
"${cfg.tls.useACMEHost}".reloadServices = [ "murmur.service" ];
|
||
};
|
||
|
||
systemd.services.murmur = {
|
||
description = "Murmur Chat Service";
|
||
wantedBy = [ "multi-user.target" ];
|
||
after = [
|
||
"network.target"
|
||
]
|
||
++ lib.optional (cfg.tls.useACMEHost != null) "acme-${cfg.tls.useACMEHost}.service";
|
||
wants = lib.mkIf (cfg.tls.useACMEHost != null) [ "acme-${cfg.tls.useACMEHost}.service" ];
|
||
preStart = ''
|
||
${pkgs.envsubst}/bin/envsubst \
|
||
-o /run/murmur/murmurd.ini \
|
||
-i ${configFile}
|
||
'';
|
||
|
||
serviceConfig = {
|
||
# murmurd doesn't fork when logging to the console.
|
||
Type = if forking then "forking" else "simple";
|
||
PIDFile = lib.mkIf forking "/run/murmur/murmurd.pid";
|
||
EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
|
||
ExecStart = "${cfg.package}/bin/mumble-server -ini /run/murmur/murmurd.ini";
|
||
Restart = "always";
|
||
LogsDirectory = lib.mkIf cfg.logToFile "murmur";
|
||
LogsDirectoryMode = "0750";
|
||
RuntimeDirectory = "murmur";
|
||
RuntimeDirectoryMode = "0700";
|
||
User = cfg.user;
|
||
Group = cfg.group;
|
||
|
||
# service hardening
|
||
AmbientCapabilities = "CAP_NET_BIND_SERVICE";
|
||
CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
|
||
LockPersonality = true;
|
||
MemoryDenyWriteExecute = true;
|
||
MountAPIVFS = true;
|
||
NoNewPrivileges = true;
|
||
PrivateDevices = true;
|
||
PrivateMounts = true;
|
||
PrivateTmp = true;
|
||
PrivateUsers = true;
|
||
ProtectClock = true;
|
||
ProtectControlGroups = "strict";
|
||
ProtectHome = true;
|
||
ProtectHostname = true;
|
||
ProtectKernelLogs = true;
|
||
ProtectKernelModules = true;
|
||
ProtectKernelTunables = true;
|
||
ProtectProc = "invisible";
|
||
ProtectSystem = "strict";
|
||
ReadWritePaths = [
|
||
cfg.stateDir
|
||
];
|
||
RestrictAddressFamilies = [
|
||
"AF_INET"
|
||
"AF_INET6"
|
||
];
|
||
RestrictNamespaces = true;
|
||
RestrictSUIDSGID = true;
|
||
RestrictRealtime = true;
|
||
SystemCallArchitectures = "native";
|
||
SystemCallFilter = "@system-service";
|
||
UMask = 27;
|
||
};
|
||
};
|
||
|
||
# currently not included in upstream package, addition requested at
|
||
# https://github.com/mumble-voip/mumble/issues/6078
|
||
services.dbus.packages = lib.mkIf (cfg.dbus == "system") [
|
||
(pkgs.writeTextFile {
|
||
name = "murmur-dbus-policy";
|
||
text = ''
|
||
<!DOCTYPE busconfig PUBLIC
|
||
"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
|
||
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
|
||
<busconfig>
|
||
<policy user="${cfg.user}">
|
||
<allow own="net.sourceforge.mumble.murmur"/>
|
||
</policy>
|
||
|
||
<policy context="default">
|
||
<allow send_destination="net.sourceforge.mumble.murmur"/>
|
||
<allow receive_sender="net.sourceforge.mumble.murmur"/>
|
||
</policy>
|
||
</busconfig>
|
||
'';
|
||
destination = "/share/dbus-1/system.d/murmur.conf";
|
||
})
|
||
];
|
||
|
||
security.apparmor.policies."bin.mumble-server".profile = ''
|
||
abi <abi/4.0>,
|
||
include <tunables/global>
|
||
|
||
profile ${cfg.package}/bin/{mumble-server,.mumble-server-wrapped} {
|
||
include <abstractions/base>
|
||
include <abstractions/nameservice>
|
||
include <abstractions/ssl_certs>
|
||
include "${pkgs.apparmorRulesFromClosure { name = "mumble-server"; } cfg.package}"
|
||
${cfg.package}/bin/.mumble-server-wrapped pix,
|
||
|
||
${config.environment.etc."os-release".source} r,
|
||
${config.environment.etc."lsb-release".source} r,
|
||
owner ${cfg.stateDir}/murmur.sqlite rwk,
|
||
owner ${cfg.stateDir}/murmur.sqlite-journal rw,
|
||
owner ${cfg.stateDir}/ r,
|
||
/run/murmur/murmurd.pid r,
|
||
/run/murmur/murmurd.ini r,
|
||
${configFile} r,
|
||
${lib.optionalString cfg.logToFile ''
|
||
/var/log/murmur/murmurd.log rw,
|
||
''}
|
||
${lib.optionalString (cfg.tls.certPath != null) ''
|
||
${cfg.tls.certPath} r,
|
||
''}
|
||
${lib.optionalString (cfg.tls.keyPath != null) ''
|
||
${cfg.tls.keyPath} r,
|
||
''}
|
||
${lib.optionalString (cfg.tls.caPath != null) ''
|
||
${cfg.tls.caPath} r,
|
||
''}
|
||
${lib.optionalString (cfg.dbus != null) ''
|
||
dbus bus=${cfg.dbus},
|
||
''}
|
||
include if exists <local/bin.mumble-server>
|
||
}
|
||
'';
|
||
};
|
||
|
||
meta.maintainers = with lib.maintainers; [ felixsinger ];
|
||
}
|