From 24a02d386c3d5a34aa7c4b3ced568f66e23b8301 Mon Sep 17 00:00:00 2001 From: h7x4 Date: Wed, 12 Jul 2023 23:34:23 +0200 Subject: [PATCH] tsuki/hedgedoc: misc: - Experiment with reducing the number of options in the module - Use UNIX socket behind nginx - "Upstream" systemd hardening to module --- hosts/tsuki/services/hedgedoc/default.nix | 34 +- hosts/tsuki/services/hedgedoc/hedgedoc.nix | 1075 -------------------- hosts/tsuki/services/hedgedoc/module.nix | 412 ++++++++ 3 files changed, 419 insertions(+), 1102 deletions(-) delete mode 100644 hosts/tsuki/services/hedgedoc/hedgedoc.nix create mode 100644 hosts/tsuki/services/hedgedoc/module.nix diff --git a/hosts/tsuki/services/hedgedoc/default.nix b/hosts/tsuki/services/hedgedoc/default.nix index 777356b..4922900 100644 --- a/hosts/tsuki/services/hedgedoc/default.nix +++ b/hosts/tsuki/services/hedgedoc/default.nix @@ -1,7 +1,7 @@ { pkgs, lib, config, options, ... }: let cfg = config.services.hedgedoc; in { - imports = [ ./hedgedoc.nix ]; + imports = [ ./module.nix ]; disabledModules = [ "services/web-apps/hedgedoc.nix" ]; config = { @@ -10,9 +10,10 @@ in { restartUnits = [ "hedgedoc.service" ]; }; + users.groups.hedgedoc.members = [ "nginx" ]; + services.hedgedoc = { enable = true; - workDir = "${config.machineVars.dataDrives.default}/var/hedgedoc"; environmentFile = config.sops.secrets."hedgedoc/env".path; settings = { domain = "docs.nani.wtf"; @@ -21,6 +22,8 @@ in { allowAnonymousEdits = true; protocolUseSSL = true; + path = "/run/hedgedoc/hedgedoc.sock"; + db = { username = "hedgedoc"; # TODO: set a password @@ -59,35 +62,12 @@ in { }]; }; - systemd.services.hedgedoc = { + systemd.services.hedgedoc = rec { requires = [ "postgresql.service" "kanidm.service" ]; - serviceConfig = { - CapabilityBoundingSet = ""; - LockPersonality = true; - NoNewPrivileges = true; - PrivateDevices = true; - PrivateMounts = true; - PrivateTmp = true; - PrivateUsers = true; - ProtectClock = true; - ProtectHome = true; - ProtectHostname = true; - ProtectKernelLogs = true; - ProtectKernelModules = true; - ProtectKernelTunables = true; - ProtectProc = "invisible"; - ProtectSystem = "strict"; - ReadWritePaths = [ cfg.workDir ]; - RemoveIPC = true; - RestrictSUIDSGID = true; - UMask = "0007"; - RestrictAddressFamilies = [ "AF_UNIX AF_INET AF_INET6" ]; - SystemCallArchitectures = "native"; - SystemCallFilter = "~@clock @cpu-emulation @debug @keyring @module @mount @obsolete @raw-io @reboot @setuid @swap"; - }; + after = requires; }; }; } diff --git a/hosts/tsuki/services/hedgedoc/hedgedoc.nix b/hosts/tsuki/services/hedgedoc/hedgedoc.nix deleted file mode 100644 index e2014a9..0000000 --- a/hosts/tsuki/services/hedgedoc/hedgedoc.nix +++ /dev/null @@ -1,1075 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; - -let - 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 versionAtLeast config.system.stateVersion "21.03" - then "hedgedoc" - else "codimd"; - - settingsFormat = pkgs.formats.json {}; - - prettyJSON = conf: - pkgs.runCommandLocal "hedgedoc-config.json" { - nativeBuildInputs = [ pkgs.jq ]; - } '' - jq '{production:del(.[]|nulls)|del(.[][]?|nulls)}' \ - < ${settingsFormat.generate "hedgedoc-ugly.json" cfg.settings} \ - > $out - ''; -in -{ - imports = [ - (mkRenamedOptionModule [ "services" "codimd" ] [ "services" "hedgedoc" ]) - (mkRenamedOptionModule - [ "services" "hedgedoc" "configuration" ] [ "services" "hedgedoc" "settings" ]) - ]; - - options.services.hedgedoc = { - enable = mkEnableOption (lib.mdDoc "the HedgeDoc Markdown Editor"); - - groups = mkOption { - type = types.listOf types.str; - default = []; - description = lib.mdDoc '' - Groups to which the service user should be added. - ''; - }; - - workDir = mkOption { - type = types.path; - default = "/var/lib/${name}"; - description = lib.mdDoc '' - Working directory for the HedgeDoc service. - ''; - }; - - settings = let options = { - debug = mkEnableOption (lib.mdDoc "debug mode"); - domain = mkOption { - type = types.nullOr types.str; - default = null; - example = "hedgedoc.org"; - description = lib.mdDoc '' - Domain name for the HedgeDoc instance. - ''; - }; - urlPath = mkOption { - type = types.nullOr types.str; - default = null; - example = "/url/path/to/hedgedoc"; - description = lib.mdDoc '' - Path under which HedgeDoc is accessible. - ''; - }; - host = mkOption { - type = types.str; - default = "localhost"; - description = lib.mdDoc '' - Address to listen on. - ''; - }; - port = mkOption { - type = types.port; - default = 3000; - example = 80; - description = lib.mdDoc '' - Port to listen on. - ''; - }; - path = mkOption { - type = types.nullOr types.str; - default = null; - example = "/run/hedgedoc.sock"; - description = lib.mdDoc '' - Specify where a UNIX domain socket should be placed. - ''; - }; - allowOrigin = mkOption { - type = types.listOf types.str; - default = []; - example = [ "localhost" "hedgedoc.org" ]; - description = lib.mdDoc '' - List of domains to whitelist. - ''; - }; - useSSL = mkOption { - type = types.bool; - default = false; - description = lib.mdDoc '' - Enable to use SSL server. This will also enable - {option}`protocolUseSSL`. - ''; - }; - hsts = { - enable = mkOption { - type = types.bool; - default = true; - description = lib.mdDoc '' - Whether to enable HSTS if HTTPS is also enabled. - ''; - }; - maxAgeSeconds = mkOption { - type = types.int; - default = 31536000; - description = lib.mdDoc '' - Max duration for clients to keep the HSTS status. - ''; - }; - includeSubdomains = mkOption { - type = types.bool; - default = true; - description = lib.mdDoc '' - Whether to include subdomains in HSTS. - ''; - }; - preload = mkOption { - type = types.bool; - default = true; - description = lib.mdDoc '' - Whether to allow preloading of the site's HSTS status. - ''; - }; - }; - csp = mkOption { - type = types.nullOr types.attrs; - default = null; - example = literalExpression '' - { - enable = true; - directives = { - scriptSrc = "trustworthy.scripts.example.com"; - }; - upgradeInsecureRequest = "auto"; - addDefaults = true; - } - ''; - description = lib.mdDoc '' - Specify the Content Security Policy which is passed to Helmet. - For configuration details see . - ''; - }; - protocolUseSSL = mkOption { - type = types.bool; - default = false; - description = lib.mdDoc '' - Enable to use TLS for resource paths. - This only applies when {option}`domain` is set. - ''; - }; - urlAddPort = mkOption { - type = types.bool; - default = false; - description = lib.mdDoc '' - Enable to add the port to callback URLs. - This only applies when {option}`domain` is set - and only for ports other than 80 and 443. - ''; - }; - useCDN = mkOption { - type = types.bool; - default = false; - description = lib.mdDoc '' - Whether to use CDN resources or not. - ''; - }; - allowAnonymous = mkOption { - type = types.bool; - default = true; - description = lib.mdDoc '' - Whether to allow anonymous usage. - ''; - }; - allowAnonymousEdits = mkOption { - type = types.bool; - default = false; - description = lib.mdDoc '' - Whether to allow guests to edit existing notes with the `freely` permission, - when {option}`allowAnonymous` is enabled. - ''; - }; - allowFreeURL = mkOption { - type = types.bool; - default = false; - description = lib.mdDoc '' - Whether to allow note creation by accessing a nonexistent note URL. - ''; - }; - requireFreeURLAuthentication = mkOption { - type = types.bool; - default = false; - description = lib.mdDoc '' - Whether to require authentication for FreeURL mode style note creation. - ''; - }; - defaultPermission = mkOption { - type = types.enum [ "freely" "editable" "limited" "locked" "private" ]; - default = "editable"; - description = lib.mdDoc '' - Default permissions for notes. - This only applies for signed-in users. - ''; - }; - dbURL = mkOption { - type = types.nullOr types.str; - default = null; - example = '' - postgres://user:pass@host:5432/dbname - ''; - description = lib.mdDoc '' - Specify which database to use. - HedgeDoc supports mysql, postgres, sqlite and mssql. - See [ - https://sequelize.readthedocs.io/en/v3/](https://sequelize.readthedocs.io/en/v3/) for more information. - Note: This option overrides {option}`db`. - ''; - }; - db = mkOption { - type = types.attrs; - default = {}; - example = literalExpression '' - { - dialect = "sqlite"; - storage = "/var/lib/${name}/db.${name}.sqlite"; - } - ''; - description = lib.mdDoc '' - Specify the configuration for sequelize. - HedgeDoc supports mysql, postgres, sqlite and mssql. - See [ - https://sequelize.readthedocs.io/en/v3/](https://sequelize.readthedocs.io/en/v3/) for more information. - Note: This option overrides {option}`db`. - ''; - }; - sslKeyPath= mkOption { - type = types.nullOr types.str; - default = null; - example = "/var/lib/hedgedoc/hedgedoc.key"; - description = lib.mdDoc '' - Path to the SSL key. Needed when {option}`useSSL` is enabled. - ''; - }; - sslCertPath = mkOption { - type = types.nullOr types.str; - default = null; - example = "/var/lib/hedgedoc/hedgedoc.crt"; - description = lib.mdDoc '' - Path to the SSL cert. Needed when {option}`useSSL` is enabled. - ''; - }; - sslCAPath = mkOption { - type = types.listOf types.str; - default = []; - example = [ "/var/lib/hedgedoc/ca.crt" ]; - description = lib.mdDoc '' - SSL ca chain. Needed when {option}`useSSL` is enabled. - ''; - }; - dhParamPath = mkOption { - type = types.nullOr types.str; - default = null; - example = "/var/lib/hedgedoc/dhparam.pem"; - description = lib.mdDoc '' - Path to the SSL dh params. Needed when {option}`useSSL` is enabled. - ''; - }; - tmpPath = mkOption { - type = types.str; - default = "/tmp"; - description = lib.mdDoc '' - Path to the temp directory HedgeDoc should use. - Note that {option}`serviceConfig.PrivateTmp` is enabled for - the HedgeDoc systemd service by default. - (Non-canonical paths are relative to HedgeDoc's base directory) - ''; - }; - defaultNotePath = mkOption { - type = types.nullOr types.str; - default = "${cfg.package}/public/default.md"; - defaultText = literalExpression "\"\${cfg.package}/public/default.md\""; - description = lib.mdDoc '' - Path to the default Note file. - (Non-canonical paths are relative to HedgeDoc's base directory) - ''; - }; - docsPath = mkOption { - type = types.nullOr types.str; - default = "${cfg.package}/public/docs"; - defaultText = literalExpression "\"\${cfg.package}/public/docs\""; - description = lib.mdDoc '' - Path to the docs directory. - (Non-canonical paths are relative to HedgeDoc's base directory) - ''; - }; - indexPath = mkOption { - type = types.nullOr types.str; - default = "${cfg.package}/public/views/index.ejs"; - defaultText = literalExpression "\"\${cfg.package}/public/views/index.ejs\""; - description = lib.mdDoc '' - Path to the index template file. - (Non-canonical paths are relative to HedgeDoc's base directory) - ''; - }; - hackmdPath = mkOption { - type = types.nullOr types.str; - default = "${cfg.package}/public/views/hackmd.ejs"; - defaultText = literalExpression "\"\${cfg.package}/public/views/hackmd.ejs\""; - description = lib.mdDoc '' - Path to the hackmd template file. - (Non-canonical paths are relative to HedgeDoc's base directory) - ''; - }; - errorPath = mkOption { - type = types.nullOr types.str; - default = "${cfg.package}/public/views/error.ejs"; - defaultText = literalExpression "\"\${cfg.package}/public/views/error.ejs\""; - description = lib.mdDoc '' - Path to the error template file. - (Non-canonical paths are relative to HedgeDoc's base directory) - ''; - }; - prettyPath = mkOption { - type = types.nullOr types.str; - default = "${cfg.package}/public/views/pretty.ejs"; - defaultText = literalExpression "\"\${cfg.package}/public/views/pretty.ejs\""; - description = lib.mdDoc '' - Path to the pretty template file. - (Non-canonical paths are relative to HedgeDoc's base directory) - ''; - }; - slidePath = mkOption { - type = types.nullOr types.str; - default = "${cfg.package}/public/views/slide.hbs"; - defaultText = literalExpression "\"\${cfg.package}/public/views/slide.hbs\""; - description = lib.mdDoc '' - Path to the slide template file. - (Non-canonical paths are relative to HedgeDoc's base directory) - ''; - }; - uploadsPath = mkOption { - type = types.str; - default = "${cfg.workDir}/uploads"; - defaultText = literalExpression "\"\${cfg.workDir}/uploads\""; - description = lib.mdDoc '' - Path under which uploaded files are saved. - ''; - }; - sessionName = mkOption { - type = types.str; - default = "connect.sid"; - description = lib.mdDoc '' - Specify the name of the session cookie. - ''; - }; - sessionSecret = mkOption { - type = types.nullOr types.str; - default = null; - description = lib.mdDoc '' - Specify the secret used to sign the session cookie. - If unset, one will be generated on startup. - ''; - }; - sessionLife = mkOption { - type = types.int; - default = 1209600000; - description = lib.mdDoc '' - Session life time in milliseconds. - ''; - }; - heartbeatInterval = mkOption { - type = types.int; - default = 5000; - description = lib.mdDoc '' - Specify the socket.io heartbeat interval. - ''; - }; - heartbeatTimeout = mkOption { - type = types.int; - default = 10000; - description = lib.mdDoc '' - Specify the socket.io heartbeat timeout. - ''; - }; - documentMaxLength = mkOption { - type = types.int; - default = 100000; - description = lib.mdDoc '' - Specify the maximum document length. - ''; - }; - email = mkOption { - type = types.bool; - default = true; - description = lib.mdDoc '' - Whether to enable email sign-in. - ''; - }; - allowEmailRegister = mkOption { - type = types.bool; - default = true; - description = lib.mdDoc '' - Whether to enable email registration. - ''; - }; - allowGravatar = mkOption { - type = types.bool; - default = true; - description = lib.mdDoc '' - Whether to use gravatar as profile picture source. - ''; - }; - imageUploadType = mkOption { - type = types.enum [ "imgur" "s3" "minio" "filesystem" ]; - default = "filesystem"; - description = lib.mdDoc '' - Specify where to upload images. - ''; - }; - minio = mkOption { - type = types.nullOr (types.submodule { - options = { - accessKey = mkOption { - type = types.str; - description = lib.mdDoc '' - Minio access key. - ''; - }; - secretKey = mkOption { - type = types.str; - description = lib.mdDoc '' - Minio secret key. - ''; - }; - endPoint = mkOption { - type = types.str; - description = lib.mdDoc '' - Minio endpoint. - ''; - }; - port = mkOption { - type = types.port; - default = 9000; - description = lib.mdDoc '' - Minio listen port. - ''; - }; - secure = mkOption { - type = types.bool; - default = true; - description = lib.mdDoc '' - Whether to use HTTPS for Minio. - ''; - }; - }; - }); - default = null; - description = lib.mdDoc "Configure the minio third-party integration."; - }; - s3 = mkOption { - type = types.nullOr (types.submodule { - options = { - accessKeyId = mkOption { - type = types.str; - description = lib.mdDoc '' - AWS access key id. - ''; - }; - secretAccessKey = mkOption { - type = types.str; - description = lib.mdDoc '' - AWS access key. - ''; - }; - region = mkOption { - type = types.str; - description = lib.mdDoc '' - AWS S3 region. - ''; - }; - }; - }); - default = null; - description = lib.mdDoc "Configure the s3 third-party integration."; - }; - s3bucket = mkOption { - type = types.nullOr types.str; - default = null; - description = lib.mdDoc '' - Specify the bucket name for upload types `s3` and `minio`. - ''; - }; - allowPDFExport = mkOption { - type = types.bool; - default = true; - description = lib.mdDoc '' - Whether to enable PDF exports. - ''; - }; - imgur.clientId = mkOption { - type = types.nullOr types.str; - default = null; - description = lib.mdDoc '' - Imgur API client ID. - ''; - }; - azure = mkOption { - type = types.nullOr (types.submodule { - options = { - connectionString = mkOption { - type = types.str; - description = lib.mdDoc '' - Azure Blob Storage connection string. - ''; - }; - container = mkOption { - type = types.str; - description = lib.mdDoc '' - Azure Blob Storage container name. - It will be created if non-existent. - ''; - }; - }; - }); - default = null; - description = lib.mdDoc "Configure the azure third-party integration."; - }; - oauth2 = mkOption { - type = types.nullOr (types.submodule { - options = { - authorizationURL = mkOption { - type = types.str; - description = lib.mdDoc '' - Specify the OAuth authorization URL. - ''; - }; - tokenURL = mkOption { - type = types.str; - description = lib.mdDoc '' - Specify the OAuth token URL. - ''; - }; - baseURL = mkOption { - type = with types; nullOr str; - default = null; - description = lib.mdDoc '' - Specify the OAuth base URL. - ''; - }; - userProfileURL = mkOption { - type = with types; nullOr str; - default = null; - description = lib.mdDoc '' - Specify the OAuth userprofile URL. - ''; - }; - userProfileUsernameAttr = mkOption { - type = with types; nullOr str; - default = null; - description = lib.mdDoc '' - Specify the name of the attribute for the username from the claim. - ''; - }; - userProfileDisplayNameAttr = mkOption { - type = with types; nullOr str; - default = null; - description = lib.mdDoc '' - Specify the name of the attribute for the display name from the claim. - ''; - }; - userProfileEmailAttr = mkOption { - type = with types; nullOr str; - default = null; - description = lib.mdDoc '' - Specify the name of the attribute for the email from the claim. - ''; - }; - scope = mkOption { - type = with types; nullOr str; - default = null; - description = lib.mdDoc '' - Specify the OAuth scope. - ''; - }; - providerName = mkOption { - type = with types; nullOr str; - default = null; - description = lib.mdDoc '' - Specify the name to be displayed for this strategy. - ''; - }; - rolesClaim = mkOption { - type = with types; nullOr str; - default = null; - description = lib.mdDoc '' - Specify the role claim name. - ''; - }; - accessRole = mkOption { - type = with types; nullOr str; - default = null; - description = lib.mdDoc '' - Specify role which should be included in the ID token roles claim to grant access - ''; - }; - clientID = mkOption { - type = types.str; - description = lib.mdDoc '' - Specify the OAuth client ID. - ''; - }; - clientSecret = mkOption { - type = with types; nullOr str; - default = null; - description = lib.mdDoc '' - Specify the OAuth client secret. - ''; - }; - }; - }); - default = null; - description = lib.mdDoc "Configure the OAuth integration."; - }; - facebook = mkOption { - type = types.nullOr (types.submodule { - options = { - clientID = mkOption { - type = types.str; - description = lib.mdDoc '' - Facebook API client ID. - ''; - }; - clientSecret = mkOption { - type = types.str; - description = lib.mdDoc '' - Facebook API client secret. - ''; - }; - }; - }); - default = null; - description = lib.mdDoc "Configure the facebook third-party integration"; - }; - twitter = mkOption { - type = types.nullOr (types.submodule { - options = { - consumerKey = mkOption { - type = types.str; - description = lib.mdDoc '' - Twitter API consumer key. - ''; - }; - consumerSecret = mkOption { - type = types.str; - description = lib.mdDoc '' - Twitter API consumer secret. - ''; - }; - }; - }); - default = null; - description = lib.mdDoc "Configure the Twitter third-party integration."; - }; - github = mkOption { - type = types.nullOr (types.submodule { - options = { - clientID = mkOption { - type = types.str; - description = lib.mdDoc '' - GitHub API client ID. - ''; - }; - clientSecret = mkOption { - type = types.str; - description = lib.mdDoc '' - Github API client secret. - ''; - }; - }; - }); - default = null; - description = lib.mdDoc "Configure the GitHub third-party integration."; - }; - gitlab = mkOption { - type = types.nullOr (types.submodule { - options = { - baseURL = mkOption { - type = types.str; - default = ""; - description = lib.mdDoc '' - GitLab API authentication endpoint. - Only needed for other endpoints than gitlab.com. - ''; - }; - clientID = mkOption { - type = types.str; - description = lib.mdDoc '' - GitLab API client ID. - ''; - }; - clientSecret = mkOption { - type = types.str; - description = lib.mdDoc '' - GitLab API client secret. - ''; - }; - scope = mkOption { - type = types.enum [ "api" "read_user" ]; - default = "api"; - description = lib.mdDoc '' - GitLab API requested scope. - GitLab snippet import/export requires api scope. - ''; - }; - }; - }); - default = null; - description = lib.mdDoc "Configure the GitLab third-party integration."; - }; - mattermost = mkOption { - type = types.nullOr (types.submodule { - options = { - baseURL = mkOption { - type = types.str; - description = lib.mdDoc '' - Mattermost authentication endpoint. - ''; - }; - clientID = mkOption { - type = types.str; - description = lib.mdDoc '' - Mattermost API client ID. - ''; - }; - clientSecret = mkOption { - type = types.str; - description = lib.mdDoc '' - Mattermost API client secret. - ''; - }; - }; - }); - default = null; - description = lib.mdDoc "Configure the Mattermost third-party integration."; - }; - dropbox = mkOption { - type = types.nullOr (types.submodule { - options = { - clientID = mkOption { - type = types.str; - description = lib.mdDoc '' - Dropbox API client ID. - ''; - }; - clientSecret = mkOption { - type = types.str; - description = lib.mdDoc '' - Dropbox API client secret. - ''; - }; - appKey = mkOption { - type = types.str; - description = lib.mdDoc '' - Dropbox app key. - ''; - }; - }; - }); - default = null; - description = lib.mdDoc "Configure the Dropbox third-party integration."; - }; - google = mkOption { - type = types.nullOr (types.submodule { - options = { - clientID = mkOption { - type = types.str; - description = lib.mdDoc '' - Google API client ID. - ''; - }; - clientSecret = mkOption { - type = types.str; - description = lib.mdDoc '' - Google API client secret. - ''; - }; - }; - }); - default = null; - description = lib.mdDoc "Configure the Google third-party integration."; - }; - ldap = mkOption { - type = types.nullOr (types.submodule { - options = { - providerName = mkOption { - type = types.str; - default = ""; - description = lib.mdDoc '' - Optional name to be displayed at login form, indicating the LDAP provider. - ''; - }; - url = mkOption { - type = types.str; - example = "ldap://localhost"; - description = lib.mdDoc '' - URL of LDAP server. - ''; - }; - bindDn = mkOption { - type = types.str; - description = lib.mdDoc '' - Bind DN for LDAP access. - ''; - }; - bindCredentials = mkOption { - type = types.str; - description = lib.mdDoc '' - Bind credentials for LDAP access. - ''; - }; - searchBase = mkOption { - type = types.str; - example = "o=users,dc=example,dc=com"; - description = lib.mdDoc '' - LDAP directory to begin search from. - ''; - }; - searchFilter = mkOption { - type = types.str; - example = "(uid={{username}})"; - description = lib.mdDoc '' - LDAP filter to search with. - ''; - }; - searchAttributes = mkOption { - type = types.nullOr (types.listOf types.str); - default = null; - example = [ "displayName" "mail" ]; - description = lib.mdDoc '' - LDAP attributes to search with. - ''; - }; - userNameField = mkOption { - type = types.str; - default = ""; - description = lib.mdDoc '' - LDAP field which is used as the username on HedgeDoc. - By default {option}`useridField` is used. - ''; - }; - useridField = mkOption { - type = types.str; - example = "uid"; - description = lib.mdDoc '' - LDAP field which is a unique identifier for users on HedgeDoc. - ''; - }; - tlsca = mkOption { - type = types.str; - default = "/etc/ssl/certs/ca-certificates.crt"; - example = "server-cert.pem,root.pem"; - description = lib.mdDoc '' - Root CA for LDAP TLS in PEM format. - ''; - }; - }; - }); - default = null; - description = lib.mdDoc "Configure the LDAP integration."; - }; - saml = mkOption { - type = types.nullOr (types.submodule { - options = { - idpSsoUrl = mkOption { - type = types.str; - example = "https://idp.example.com/sso"; - description = lib.mdDoc '' - IdP authentication endpoint. - ''; - }; - idpCert = mkOption { - type = types.path; - example = "/path/to/cert.pem"; - description = lib.mdDoc '' - Path to IdP certificate file in PEM format. - ''; - }; - issuer = mkOption { - type = types.str; - default = ""; - description = lib.mdDoc '' - Optional identity of the service provider. - This defaults to the server URL. - ''; - }; - identifierFormat = mkOption { - type = types.str; - default = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"; - description = lib.mdDoc '' - Optional name identifier format. - ''; - }; - groupAttribute = mkOption { - type = types.str; - default = ""; - example = "memberOf"; - description = lib.mdDoc '' - Optional attribute name for group list. - ''; - }; - externalGroups = mkOption { - type = types.listOf types.str; - default = []; - example = [ "Temporary-staff" "External-users" ]; - description = lib.mdDoc '' - Excluded group names. - ''; - }; - requiredGroups = mkOption { - type = types.listOf types.str; - default = []; - example = [ "Hedgedoc-Users" ]; - description = lib.mdDoc '' - Required group names. - ''; - }; - providerName = mkOption { - type = types.str; - default = ""; - example = "My institution"; - description = lib.mdDoc '' - Optional name to be displayed at login form indicating the SAML provider. - ''; - }; - attribute = { - id = mkOption { - type = types.str; - default = ""; - description = lib.mdDoc '' - Attribute map for `id`. - Defaults to `NameID` of SAML response. - ''; - }; - username = mkOption { - type = types.str; - default = ""; - description = lib.mdDoc '' - Attribute map for `username`. - Defaults to `NameID` of SAML response. - ''; - }; - email = mkOption { - type = types.str; - default = ""; - description = lib.mdDoc '' - Attribute map for `email`. - Defaults to `NameID` of SAML response if - {option}`identifierFormat` has - the default value. - ''; - }; - }; - }; - }); - default = null; - description = lib.mdDoc "Configure the SAML integration."; - }; - }; in lib.mkOption { - type = lib.types.submodule { - freeformType = settingsFormat.type; - inherit options; - }; - description = lib.mdDoc '' - HedgeDoc configuration, see - - for documentation. - ''; - }; - - environmentFile = mkOption { - type = with types; nullOr path; - default = null; - example = "/var/lib/hedgedoc/hedgedoc.env"; - description = lib.mdDoc '' - 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. - ''; - }; - - package = mkOption { - type = types.package; - default = pkgs.hedgedoc; - defaultText = literalExpression "pkgs.hedgedoc"; - description = lib.mdDoc '' - Package that provides HedgeDoc. - ''; - }; - - }; - - config = mkIf cfg.enable { - assertions = [ - { assertion = cfg.settings.db == {} -> ( - cfg.settings.dbURL != "" && cfg.settings.dbURL != null - ); - message = "Database configuration for HedgeDoc missing."; } - ]; - users.groups.${name} = {}; - users.users.${name} = { - description = "HedgeDoc service user"; - group = name; - extraGroups = cfg.groups; - home = cfg.workDir; - createHome = true; - isSystemUser = true; - }; - - systemd.services.hedgedoc = { - description = "HedgeDoc Service"; - wantedBy = [ "multi-user.target" ]; - after = [ "networking.target" ]; - preStart = '' - ${pkgs.envsubst}/bin/envsubst \ - -o ${cfg.workDir}/config.json \ - -i ${prettyJSON cfg.settings} - mkdir -p ${cfg.settings.uploadsPath} - ''; - serviceConfig = { - WorkingDirectory = cfg.workDir; - StateDirectory = [ cfg.workDir cfg.settings.uploadsPath ]; - ExecStart = "${cfg.package}/bin/hedgedoc"; - EnvironmentFile = mkIf (cfg.environmentFile != null) [ cfg.environmentFile ]; - Environment = [ - "CMD_CONFIG_FILE=${cfg.workDir}/config.json" - "NODE_ENV=production" - ]; - Restart = "always"; - User = name; - PrivateTmp = true; - }; - }; - }; -} diff --git a/hosts/tsuki/services/hedgedoc/module.nix b/hosts/tsuki/services/hedgedoc/module.nix new file mode 100644 index 0000000..3e56f56 --- /dev/null +++ b/hosts/tsuki/services/hedgedoc/module.nix @@ -0,0 +1,412 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + 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 versionAtLeast config.system.stateVersion "21.03" + then "hedgedoc" + else "codimd"; + + settingsFormat = pkgs.formats.json {}; + + prettyJSON = conf: + pkgs.runCommandLocal "hedgedoc-config.json" { + nativeBuildInputs = [ pkgs.jq ]; + } '' + jq '{production:del(.[]|nulls)|del(.[][]?|nulls)}' \ + < ${settingsFormat.generate "hedgedoc-ugly.json" cfg.settings} \ + > $out + ''; +in +{ + imports = [ + (mkRenamedOptionModule [ "services" "codimd" ] [ "services" "hedgedoc" ]) + (mkRenamedOptionModule + [ "services" "hedgedoc" "configuration" ] [ "services" "hedgedoc" "settings" ]) + (mkRenamedOptionModule + [ "services" "hedgedoc" "groups" ] [ "users" "users" name "extraGroups" ]) + (mkRemovedOptionModule [ "services" "hedgedoc" "workDir" ] '' + TODO: write paragraph + '') + ]; + + options.services.hedgedoc = { + enable = mkEnableOption (lib.mdDoc "the HedgeDoc Markdown Editor"); + enableUnixSocket = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Sets up a socket at `/run/hedgedoc/hedgedoc.sock`. + + This socket will be part of the `hedgedoc` group, + so if you want a webserver like nginx to be able to + communicate with this socket, you will have to add + the user of the webserver (nginx in this case) to + `users.groups.hedgedoc.members` + ''; + }; + + settings = let options = { + # host = mkOption { + # type = types.str; + # default = "localhost"; + # description = lib.mdDoc '' + # Address to listen on. + # ''; + # }; + # port = mkOption { + # type = types.port; + # default = 3000; + # example = 80; + # description = lib.mdDoc '' + # Port to listen on. + # ''; + # }; + # allowOrigin = mkOption { + # type = types.listOf types.str; + # default = []; + # example = [ "localhost" "hedgedoc.org" ]; + # description = lib.mdDoc '' + # List of domains to whitelist. + # ''; + # }; + # useSSL = mkOption { + # type = types.bool; + # default = false; + # description = lib.mdDoc '' + # Enable to use SSL server. This will also enable + # {option}`protocolUseSSL`. + # ''; + # }; + # dbURL = mkOption { + # type = types.nullOr types.str; + # default = null; + # example = '' + # postgres://user:pass@host:5432/dbname + # ''; + # description = lib.mdDoc '' + # Specify which database to use. + # HedgeDoc supports mysql, postgres, sqlite and mssql. + # See [ + # https://sequelize.readthedocs.io/en/v3/](https://sequelize.readthedocs.io/en/v3/) for more information. + # Note: This option overrides {option}`db`. + # ''; + # }; + # db = mkOption { + # type = types.attrs; + # default = {}; + # example = literalExpression '' + # { + # dialect = "sqlite"; + # storage = "/var/lib/${name}/db.${name}.sqlite"; + # } + # ''; + # description = lib.mdDoc '' + # Specify the configuration for sequelize. + # HedgeDoc supports mysql, postgres, sqlite and mssql. + # See [ + # https://sequelize.readthedocs.io/en/v3/](https://sequelize.readthedocs.io/en/v3/) for more information. + # Note: This option overrides {option}`db`. + # ''; + # }; + # sslKeyPath= mkOption { + # type = types.nullOr types.str; + # default = null; + # example = "/var/lib/hedgedoc/hedgedoc.key"; + # description = lib.mdDoc '' + # Path to the SSL key. Needed when {option}`useSSL` is enabled. + # ''; + # }; + # sslCertPath = mkOption { + # type = types.nullOr types.str; + # default = null; + # example = "/var/lib/hedgedoc/hedgedoc.crt"; + # description = lib.mdDoc '' + # Path to the SSL cert. Needed when {option}`useSSL` is enabled. + # ''; + # }; + # sslCAPath = mkOption { + # type = types.listOf types.str; + # default = []; + # example = [ "/var/lib/hedgedoc/ca.crt" ]; + # description = lib.mdDoc '' + # SSL ca chain. Needed when {option}`useSSL` is enabled. + # ''; + # }; + # dhParamPath = mkOption { + # type = types.nullOr types.str; + # default = null; + # example = "/var/lib/hedgedoc/dhparam.pem"; + # description = lib.mdDoc '' + # Path to the SSL dh params. Needed when {option}`useSSL` is enabled. + # ''; + # }; + # tmpPath = mkOption { + # type = types.str; + # default = "/tmp"; + # description = lib.mdDoc '' + # Path to the temp directory HedgeDoc should use. + # Note that {option}`serviceConfig.PrivateTmp` is enabled for + # the HedgeDoc systemd service by default. + # (Non-canonical paths are relative to HedgeDoc's base directory) + # ''; + # }; + # defaultNotePath = mkOption { + # type = types.nullOr types.str; + # default = "${cfg.package}/public/default.md"; + # defaultText = literalExpression "\"\${cfg.package}/public/default.md\""; + # description = lib.mdDoc '' + # Path to the default Note file. + # (Non-canonical paths are relative to HedgeDoc's base directory) + # ''; + # }; + # docsPath = mkOption { + # type = types.nullOr types.str; + # default = "${cfg.package}/public/docs"; + # defaultText = literalExpression "\"\${cfg.package}/public/docs\""; + # description = lib.mdDoc '' + # Path to the docs directory. + # (Non-canonical paths are relative to HedgeDoc's base directory) + # ''; + # }; + # indexPath = mkOption { + # type = types.nullOr types.str; + # default = "${cfg.package}/public/views/index.ejs"; + # defaultText = literalExpression "\"\${cfg.package}/public/views/index.ejs\""; + # description = lib.mdDoc '' + # Path to the index template file. + # (Non-canonical paths are relative to HedgeDoc's base directory) + # ''; + # }; + # hackmdPath = mkOption { + # type = types.nullOr types.str; + # default = "${cfg.package}/public/views/hackmd.ejs"; + # defaultText = literalExpression "\"\${cfg.package}/public/views/hackmd.ejs\""; + # description = lib.mdDoc '' + # Path to the hackmd template file. + # (Non-canonical paths are relative to HedgeDoc's base directory) + # ''; + # }; + # errorPath = mkOption { + # type = types.nullOr types.str; + # default = "${cfg.package}/public/views/error.ejs"; + # defaultText = literalExpression "\"\${cfg.package}/public/views/error.ejs\""; + # description = lib.mdDoc '' + # Path to the error template file. + # (Non-canonical paths are relative to HedgeDoc's base directory) + # ''; + # }; + # prettyPath = mkOption { + # type = types.nullOr types.str; + # default = "${cfg.package}/public/views/pretty.ejs"; + # defaultText = literalExpression "\"\${cfg.package}/public/views/pretty.ejs\""; + # description = lib.mdDoc '' + # Path to the pretty template file. + # (Non-canonical paths are relative to HedgeDoc's base directory) + # ''; + # }; + # slidePath = mkOption { + # type = types.nullOr types.str; + # default = "${cfg.package}/public/views/slide.hbs"; + # defaultText = literalExpression "\"\${cfg.package}/public/views/slide.hbs\""; + # description = lib.mdDoc '' + # Path to the slide template file. + # (Non-canonical paths are relative to HedgeDoc's base directory) + # ''; + # }; + # uploadsPath = mkOption { + # type = types.str; + # default = "${cfg.workDir}/uploads"; + # defaultText = literalExpression "\"\${cfg.workDir}/uploads\""; + # description = lib.mdDoc '' + # Path under which uploaded files are saved. + # ''; + # }; + # ldap = mkOption { + # type = types.nullOr (types.submodule { + # options = { + # providerName = mkOption { + # type = types.str; + # default = ""; + # description = lib.mdDoc '' + # Optional name to be displayed at login form, indicating the LDAP provider. + # ''; + # }; + # url = mkOption { + # type = types.str; + # example = "ldap://localhost"; + # description = lib.mdDoc '' + # URL of LDAP server. + # ''; + # }; + # bindDn = mkOption { + # type = types.str; + # description = lib.mdDoc '' + # Bind DN for LDAP access. + # ''; + # }; + # bindCredentials = mkOption { + # type = types.str; + # description = lib.mdDoc '' + # Bind credentials for LDAP access. + # ''; + # }; + # searchBase = mkOption { + # type = types.str; + # example = "o=users,dc=example,dc=com"; + # description = lib.mdDoc '' + # LDAP directory to begin search from. + # ''; + # }; + # searchFilter = mkOption { + # type = types.str; + # example = "(uid={{username}})"; + # description = lib.mdDoc '' + # LDAP filter to search with. + # ''; + # }; + # searchAttributes = mkOption { + # type = types.nullOr (types.listOf types.str); + # default = null; + # example = [ "displayName" "mail" ]; + # description = lib.mdDoc '' + # LDAP attributes to search with. + # ''; + # }; + # userNameField = mkOption { + # type = types.str; + # default = ""; + # description = lib.mdDoc '' + # LDAP field which is used as the username on HedgeDoc. + # By default {option}`useridField` is used. + # ''; + # }; + # useridField = mkOption { + # type = types.str; + # example = "uid"; + # description = lib.mdDoc '' + # LDAP field which is a unique identifier for users on HedgeDoc. + # ''; + # }; + # tlsca = mkOption { + # type = types.str; + # default = "/etc/ssl/certs/ca-certificates.crt"; + # example = "server-cert.pem,root.pem"; + # description = lib.mdDoc '' + # Root CA for LDAP TLS in PEM format. + # ''; + # }; + # }; + # }); + # default = null; + # description = lib.mdDoc "Configure the LDAP integration."; + # }; + }; in lib.mkOption { + type = lib.types.submodule { + freeformType = settingsFormat.type; + inherit options; + }; + description = lib.mdDoc '' + HedgeDoc configuration, see + + for documentation. + ''; + }; + + environmentFile = mkOption { + type = with types; nullOr path; + default = null; + example = "/var/lib/hedgedoc/hedgedoc.env"; + description = lib.mdDoc '' + 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. + ''; + }; + + package = mkPackageOption pkgs "hedgedoc" { }; + }; + + config = mkIf cfg.enable { + users.groups."hedgedoc" = { }; + users.users.${name} = { + description = "HedgeDoc service user"; + group = name; + isSystemUser = true; + }; + + systemd.services.hedgedoc = { + description = "HedgeDoc Service"; + wantedBy = [ "multi-user.target" ]; + after = [ "networking.target" ]; + preStart = '' + ${pkgs.envsubst}/bin/envsubst \ + -o /var/lib/${name}/config.json \ + -i ${prettyJSON cfg.settings} + ''; + serviceConfig = { + RuntimeDirectory = [ name ]; + # WorkingDirectory = "/var/lib/${}"; + # StateDirectory = [ cfg.workDir cfg.settings.uploadsPath ]; + ReadWriteDirectories = mkIf (cfg.settings ? "uploadsPath") [ cfg.settings.uploadsPath ]; + StateDirectory = [ name ]; + ExecStart = "${cfg.package}/bin/hedgedoc"; + EnvironmentFile = mkIf (cfg.environmentFile != null) [ cfg.environmentFile ]; + Environment = [ + "CMD_CONFIG_FILE=/var/lib/${name}/config.json" + "NODE_ENV=production" + ]; + Restart = "always"; + # DynamicUser = true; + User = name; + Group = name; + + # Hardening + CapabilityBoundingSet = ""; + LockPersonality = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateMounts = true; + PrivateTmp = true; + PrivateUsers = true; + ProtectClock = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "strict"; + RemoveIPC = true; + RestrictSUIDSGID = true; + UMask = "0007"; + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ] ++ (lib.optional (cfg.settings.path != null) "AF_UNIX"); + SystemCallArchitectures = "native"; + SystemCallFilter = "~@clock @cpu-emulation @debug @keyring @module @mount @obsolete @raw-io @reboot @setuid @swap"; + }; + }; + }; +}