diff --git a/nixbld-etc-nixos/configuration.nix b/nixbld-etc-nixos/configuration.nix index 93daa0bf..22f31bf4 100644 --- a/nixbld-etc-nixos/configuration.nix +++ b/nixbld-etc-nixos/configuration.nix @@ -17,6 +17,7 @@ in ./hardware-configuration.nix ./backup-module.nix ./github-backup-module.nix + ./rt.nix ]; boot.loader.grub.enable = true; @@ -685,6 +686,14 @@ in useACMEHost = "nixbld.m-labs.hk"; root = "/var/www/perso"; }; + "rt.m-labs.hk" = { + forceSSL = true; + useACMEHost = "nixbld.m-labs.hk"; + locations."/".proxyPass = "http://127.0.0.1:4201"; + extraConfig = '' + client_max_body_size 100M; + ''; + }; "nmigen.org" = { addSSL = true; useACMEHost = "nixbld.m-labs.hk"; @@ -737,5 +746,15 @@ in }; services.jitsi-videobridge.openFirewall = true; + services.rt = { + enable = true; + organization = "M-Labs"; + domain = "rt.m-labs.hk"; + rtName = "Helpdesk"; + ownerEmail = "sb" + "@m-labs.hk"; + commentAddress = "helpdesk" + "@m-labs.hk"; + correspondAddress = "helpdesk" + "@m-labs.hk"; + }; + system.stateVersion = "21.05"; } diff --git a/nixbld-etc-nixos/rt.nix b/nixbld-etc-nixos/rt.nix new file mode 100644 index 00000000..43204b1b --- /dev/null +++ b/nixbld-etc-nixos/rt.nix @@ -0,0 +1,311 @@ +# based on https://gist.github.com/ajs124/ff04ab14435908d914cf5cedbc56a52e +{ config, lib, pkgs, ... }: + +with lib; +let + cfg = config.services.rt; + + configFile = pkgs.writeTextFile { + name = "RT_SiteConfig.pm"; + text = '' + use utf8; + + # System (Base configuration) + Set($rtname, '${cfg.rtName}'); # Changing this will break responses to existing tickets + Set($Organization, '${cfg.organization}'); # Changing this will break all existing tickets + Set($CorrespondAddress, '${cfg.correspondAddress}'); + Set($CommentAddress, '${cfg.commentAddress}'); + Set($WebDomain, '${cfg.domain}'); + Set($Timezone, '${cfg.timeZone}'); + + Set($DatabaseType, 'Pg'); + Set($DatabaseHost, 'localhost'); + Set($DatabaseUser, 'rt_user'); + Set($DatabaseName, 'rt5'); + # Read database password from file + open my $fh, '<', '${cfg.dbPasswordFile}' or die 'Can\'t open file $!'; + my $dbpw = do { local $/; <$fh> }; + $dbpw =~ s/^\s+|\s+$//g; + Set($DatabasePassword, $dbpw); + + # System (Logging) + Set($LogToSTDERR, undef); # Don't log twice + + # System (Incoming mail gateway) + Set($OwnerEmail, '${cfg.ownerEmail}'); + Set($MaxAttachmentSize, 15360000); + Set($CheckMoreMSMailHeaders, 1); + Set($LoopsToRTOwner, 0); + + # System (Outgoing mail) + Set($SetOutgoingMailFrom, '${cfg.ownerEmail}'); + + # System (Sendmail configuration) + Set($SendmailPath, '${cfg.sendmailPath}'); + Set($SendmailArguments, '${concatStringsSep " " cfg.sendmailArguments}'); + + # System (Application logic) + Set($ParseNewMessageForTicketCcs, 1); + + # System (Extra Security) + Set($RestrictLoginReferrer, 1); + + # System (Date and time handling) + Set($DefaultTimeUnitsToHours, 1); + Set($TimeInICal, 1); + Set($DateTimeFormat, 'RFC2822'); + + # System (Authorization and user configuration) + Set($AutoLogoff, 262800); # 6 months + Set($WebSecureCookies, 1); + + # Web Interface (Base configuration) + Set($DefaultQueue, 'General'); # Defaults to the first from the alphabet + Set($RememberDefaultQueue, 1); + Set($CanonicalizeRedirectURLs, 1); + Set($CanonicalizeURLsInFeeds, 1); + Set($WebBaseURL, '${cfg.baseUrl}'); + Set($LogoLinkURL, '${cfg.baseUrl}'); + + # Web Interface (Home page) + Set($DefaultSummaryRows, 50); + + # Web Interface (Ticket search) + Set($DefaultSearchResultOrder, 'DESC'); # Display newer tickets first + Set($SearchResultsAutoRedirect, 1); # Don't show result list when there is only one match + Set(%FullTextSearch, + Enable => 1, + Indexed => 1, + Column => 'ContentIndex', + Table => 'AttachmentsIndex', + ); + + # Web Interface (Ticket options) + Set($ShowMoreAboutPrivilegedUsers, 1); + Set($MoreAboutRequestorGroupsLimit, undef); + Set($HideUnsetFieldsOnDisplay, 1); + + # Web Interface (Articles) + Set($ArticleOnTicketCreate, 0); + + # Web Interface (Message box properties) + Set($MessageBoxRichText, 0); + Set($MessageBoxIncludeSignatureOnComment, 0); + + # Web Interface (Transaction display) + Set($MaxInlineBody, 0); + + # Web Interface (Administrative interface) + Set($ShowRTPortal, 0); + Set($ShowEditSsytemConfig, 0); + + # Features (External storage) + Set(%ExternalStorage, + Type => 'Disk', + Path => '/var/lib/rt/attachments', + ); + Set($ExternalStorageCutoffSize, 0); + + # Features (Cryptography) + Set(%Crypt, RejectOnMissingPrivateKey => 0, RejectOnBadData => 0, AllowEncryptDataInDB => 0); + Set(%SMIME, Enable => 1, Keyring => '${pkgs.cacert}/etc/ssl/certs/'); + Set(%GnuPG, Enable => 1); + Set(%GnuPGOptions, + 'keyserver' => 'hkp://keys.openpgp.org', + 'always-trust' => undef, + 'auto-key-locate' => 'keyserver', + 'keyserver-options' => 'auto-key-retrieve' + ); + + ${cfg.extraConfig} + + 1; + ''; + checkPhase = '' + ${pkgs.perl}/bin/perl -c $out + ''; + }; +in { + options.services.rt = with types; { + + enable = mkEnableOption "rt system"; + + package = mkOption { + description = "Package to use"; + default = pkgs.rt; + defaultText = "pkgs.rt"; + type = package; + }; + + baseUrl = mkOption { + description = "Base URL for web interface"; + default = "https://${cfg.domain}"; + defaultText = "https://\${cfg.domain}"; + type = str; + }; + + commentAddress = mkOption { + description = "Default address from/to which comments are sent"; + type = str; + }; + + correspondAddress = mkOption { + description = "Default address from/to which correspondences are sent"; + type = str; + }; + + dbPasswordFile = mkOption { + description = "File containing the database password"; + type = str; + default = "/etc/nixos/secret/rtpasswd"; + internal = true; + }; + + domain = mkOption { + description = "Which domain RT is running on"; + type = str; + }; + + ownerEmail = mkOption { + description = "Address of a human who manages RT. RT will send errors generated by the mail gateway to this address; it will also be displayed as the contact person on the RT's login page."; + type = str; + }; + + port = mkOption { + description = "Which port rt-server should listen on"; + type = port; + default = 4201; + }; + + sendmailPath = mkOption { + description = "Sendmail binary used to send... mail"; + default = "${pkgs.msmtp}/bin/sendmail"; + defaultText = "\${pkgs.msmtp}/bin/sendmail"; + type = str; + }; + + sendmailArguments = mkOption { + description = "Arguments to call sendmailPath with"; + default = [ ]; + type = listOf (oneOf [ str path ]); + }; + + timeZone = mkOption { + description = "Used to convert times entered by users into GMT, as they are stored in the database, and back again; users can override this"; + type = str; + default = config.time.timeZone; + defaultText = "[time.timeZone]"; + }; + + rtName = mkOption { + description = "Name of this RT instance"; + type = str; + }; + + organization = mkOption { + description = "Name of the organization of this instance"; + type = str; + }; + + extraConfig = mkOption { + description = "Verbatim config to append to generated on"; + type = lines; + default = ""; + }; + }; + + config = let + components = [ + "rt-clean-sessions" + "rt-email-dashboards" + "rt-email-digest-daily" + "rt-email-digest-weekly" + "rt-externalize-attachments" + "rt-fulltext-indexer" + "rt-validator" + ]; + + mkTimer = name: { + "${name}" = { + wantedBy = [ "timers.target" ]; + timerConfig.Unit = [ "${name}.service" ]; + }; + }; + + mkService = name: extraArgs: { + "${name}" = { + stopIfChanged = false; + + serviceConfig = { + ExecStart = if extraArgs == "" + then "${cfg.package}/bin/${name}" + else mkForce "${cfg.package}/bin/${name} ${extraArgs}"; + User = "rt"; + Group = "rt"; + + PrivateNetwork = false; + MemoryDenyWriteExecute = false; + + ReadOnlyPaths = [ cfg.dbPasswordFile ]; + }; + + environment = { + RT_SITE_CONFIG = configFile; + }; + + path = with pkgs; [ + w3m + ]; + }; + }; + in (mkIf cfg.enable { + systemd.services = mkMerge ((map (c: mkService c "") components) ++ [ + (mkService "rt-server" "--port ${toString cfg.port} --server Starman") + (mkService "rt-clean-sessions" "--skip-user") + (mkService "rt-fulltext-indexer" "--limit 500000") + (mkService "rt-validator" "--check") + { + rt-server = { + serviceConfig = { + StateDirectory = [ "rt/" "rt/attachments/" "rt/shredder/" "rt/smime/" ]; + RuntimeDirectory = [ "rt/" "rt/mason_data/" ]; + LogsDirectory = "rt/"; + }; + + wantedBy = [ "multi-user.target" ]; + }; + } + { + rt-externalize-attachments = { + serviceConfig.StateDirectory = "rt/attachments/"; + }; + } + + { rt-email-digest-daily.serviceConfig.ExecStart = mkForce "${cfg.package}/bin/rt-email-digest -m daily"; } + { rt-email-digest-weekly.serviceConfig.ExecStart = mkForce "${cfg.package}/bin/rt-email-digest -m weekly"; } + ]); + + systemd.timers = mkMerge ((map mkTimer components) ++ [ + { + rt-clean-sessions.timerConfig.OnCalendar = "daily"; + rt-email-dashboards.timerConfig.OnCalendar = "hourly"; + rt-email-digest-daily.timerConfig.OnCalendar = "daily"; + rt-email-digest-weekly.timerConfig.OnCalendar = "weekly"; + rt-externalize-attachments.timerConfig.OnCalendar = "01:00"; + rt-fulltext-indexer.timerConfig.OnCalendar = "02:00"; + rt-validator.timerConfig.OnCalendar = "*-*-01 03:00:00"; + } + ]); + + users.users.rt = { + isSystemUser = true; + }; + users.groups.rt = {}; + + systemd.tmpfiles.rules = [ + "d /var/lib/secrets/rt 0500 rt rt -" + "d /var/lib/rt/gpg 0700 rt rt -" + "z ${cfg.dbPasswordFile} 0400 rt rt -" + ]; + }); +}