{ config, lib, pkgs, ... }:
let
inherit (lib) mkOption types literalExpression;
cfg = config.services.hedgedoc;
# 21.03 will not be an official release - it was instead 21.05. This
# versionAtLeast statement remains set to 21.03 for backwards compatibility.
# See https://github.com/NixOS/nixpkgs/pull/108899 and
# https://github.com/NixOS/rfcs/blob/master/rfcs/0080-nixos-release-schedule.md.
name = if lib.versionAtLeast config.system.stateVersion "21.03" then
"hedgedoc"
else
"codimd";
settingsFormat = pkgs.formats.json { };
in
{
meta.maintainers = with lib.maintainers; [ SuperSandro2000 h7x4 ];
imports = [
(lib.mkRenamedOptionModule [ "services" "codimd" ] [ "services" "hedgedoc" ])
(lib.mkRenamedOptionModule [ "services" "hedgedoc" "configuration" ] [ "services" "hedgedoc" "settings" ])
(lib.mkRenamedOptionModule [ "services" "hedgedoc" "groups" ] [ "users" "users" "hedgedoc" "extraGroups" ])
(lib.mkRemovedOptionModule [ "services" "hedgedoc" "workDir" ] ''
This option has been removed in favor of systemd managing the state directory.
If you have set this option without specifying `services.hedgedoc.settings.uploadsPath`,
please move these files to `/var/lib/hedgedoc/uploads`, or set the option to point
at the correct location.
'')
];
options.services.hedgedoc = {
package = lib.mkPackageOption pkgs "hedgedoc" { };
enable = lib.mkEnableOption "the HedgeDoc Markdown Editor";
settings = mkOption {
type = types.submodule {
freeformType = settingsFormat.type;
options = {
domain = mkOption {
type = with types; nullOr str;
default = null;
example = "hedgedoc.org";
description = ''
Domain to use for website.
This is useful if you are trying to run hedgedoc behind
a reverse proxy.
'';
};
urlPath = mkOption {
type = with types; nullOr str;
default = null;
example = "hedgedoc";
description = ''
URL path for the website.
This is useful if you are hosting hedgedoc on a path like
`www.example.com/hedgedoc`
'';
};
host = mkOption {
type = with types; nullOr str;
default = "localhost";
description = ''
Address to listen on.
'';
};
port = mkOption {
type = types.port;
default = 3000;
example = 80;
description = ''
Port to listen on.
'';
};
path = mkOption {
type = with types; nullOr path;
default = null;
example = "/run/hedgedoc/hedgedoc.sock";
description = ''
Path to UNIX domain socket to listen on
::: {.note}
If specified, {option}`host` and {option}`port` will be ignored.
:::
'';
};
protocolUseSSL = mkOption {
type = types.bool;
default = false;
example = true;
description = ''
Use `https://` for all links.
This is useful if you are trying to run hedgedoc behind
a reverse proxy.
::: {.note}
Only applied if {option}`domain` is set.
:::
'';
};
allowOrigin = mkOption {
type = with types; listOf str;
default = with cfg.settings; [ host ] ++ lib.optionals (domain != null) [ domain ];
defaultText = literalExpression ''
with config.services.hedgedoc.settings; [ host ] ++ lib.optionals (domain != null) [ domain ]
'';
example = [ "localhost" "hedgedoc.org" ];
description = ''
List of domains to whitelist.
'';
};
db = mkOption {
type = types.attrs;
default = {
dialect = "sqlite";
storage = "/var/lib/${name}/db.sqlite";
};
defaultText = literalExpression ''
{
dialect = "sqlite";
storage = "/var/lib/hedgedoc/db.sqlite";
}
'';
example = literalExpression ''
db = {
username = "hedgedoc";
database = "hedgedoc";
host = "localhost:5432";
# or via socket
# host = "/run/postgresql";
dialect = "postgresql";
};
'';
description = ''
Specify the configuration for sequelize.
HedgeDoc supports `mysql`, `postgres`, `sqlite` and `mssql`.
See
for more information.
::: {.note}
The relevant parts will be overriden if you set {option}`dbURL`.
:::
'';
};
useSSL = mkOption {
type = types.bool;
default = false;
description = ''
Enable to use SSL server.
::: {.note}
This will also enable {option}`protocolUseSSL`.
It will also require you to set the following:
- {option}`sslKeyPath`
- {option}`sslCertPath`
- {option}`sslCAPath`
- {option}`dhParamPath`
:::
'';
};
uploadsPath = mkOption {
type = types.path;
default = "/var/lib/${name}/uploads";
defaultText = "/var/lib/hedgedoc/uploads";
description = ''
Directory for storing uploaded images.
'';
};
# Declared because we change the default to false.
allowGravatar = mkOption {
type = types.bool;
default = false;
example = true;
description = ''
Whether to enable [Libravatar](https://wiki.libravatar.org/) as
profile picture source on your instance.
Despite the naming of the setting, Hedgedoc replaced Gravatar
with Libravatar in [CodiMD 1.4.0](https://hedgedoc.org/releases/1.4.0/)
'';
};
};
};
description = ''
HedgeDoc configuration, see
for documentation.
'';
};
environmentFile = mkOption {
type = with types; nullOr path;
default = null;
example = "/var/lib/hedgedoc/hedgedoc.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 HedgeDoc-related config
services.hedgedoc.settings.dbURL = "postgres://hedgedoc:\''${DB_PASSWORD}@db-host:5432/hedgedocdb";
services.hedgedoc.settings.minio.secretKey = "$MINIO_SECRET_KEY";
```
```
# content of the environment file
DB_PASSWORD=verysecretdbpassword
MINIO_SECRET_KEY=verysecretminiokey
```
Note that this file needs to be available on the host on which
`HedgeDoc` is running.
'';
};
};
config = lib.mkIf cfg.enable {
users.groups.${name} = { };
users.users.${name} = {
description = "HedgeDoc service user";
group = name;
isSystemUser = true;
};
services.hedgedoc.settings = {
defaultNotePath = lib.mkDefault "${cfg.package}/share/hedgedoc/public/default.md";
docsPath = lib.mkDefault "${cfg.package}/share/hedgedoc/public/docs";
viewPath = lib.mkDefault "${cfg.package}/share/hedgedoc/public/views";
};
systemd.services.hedgedoc = {
description = "HedgeDoc Service";
documentation = [ "https://docs.hedgedoc.org/" ];
wantedBy = [ "multi-user.target" ];
after = [ "networking.target" ];
preStart =
let
configFile = settingsFormat.generate "hedgedoc-config.json" {
production = cfg.settings;
};
in
''
${pkgs.envsubst}/bin/envsubst \
-o /run/${name}/config.json \
-i ${configFile}
${pkgs.coreutils}/bin/mkdir -p ${cfg.settings.uploadsPath}
'';
serviceConfig = {
User = name;
Group = name;
Restart = "always";
ExecStart = lib.getExe cfg.package;
RuntimeDirectory = [ name ];
StateDirectory = [ name ];
WorkingDirectory = "/run/${name}";
ReadWritePaths = [
"-${cfg.settings.uploadsPath}"
] ++ lib.optionals (cfg.settings.db ? "storage") [ "-${cfg.settings.db.storage}" ];
EnvironmentFile = lib.mkIf (cfg.environmentFile != null) [ cfg.environmentFile ];
Environment = [
"CMD_CONFIG_FILE=/run/${name}/config.json"
"NODE_ENV=production"
];
# Hardening
AmbientCapabilities = "";
CapabilityBoundingSet = "";
LockPersonality = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateMounts = true;
PrivateTmp = true;
PrivateUsers = true;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
RemoveIPC = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
# Required for connecting to database sockets,
# and listening to unix socket at `cfg.settings.path`
"AF_UNIX"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SocketBindAllow = lib.mkIf (cfg.settings.path == null) cfg.settings.port;
SocketBindDeny = "any";
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged @obsolete"
"@pkey"
];
UMask = "0007";
};
};
};
}