{ 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"; }; }; }; }