lol

nixos/lemmy: limit impurity by secrets

Split `services.lemmy.secretFile` into
multiple options to allow only secrets.

authored by

Eric Wolf and committed by
Yt
318d8cc4 e6f2df77

+58 -34
+56 -26
nixos/modules/services/web-apps/lemmy.nix
··· 1 - { lib, pkgs, config, ... }: 1 + { lib, pkgs, config, utils, ... }: 2 2 with lib; 3 3 let 4 4 cfg = config.services.lemmy; ··· 41 41 default = null; 42 42 description = lib.mdDoc "The connection URI to use. Takes priority over the configuration file if set."; 43 43 }; 44 + 45 + uriFile = mkOption { 46 + type = with types; nullOr path; 47 + default = null; 48 + description = lib.mdDoc "File which contains the database uri."; 49 + }; 50 + }; 51 + 52 + pictrsApiKeyFile = mkOption { 53 + type = with types; nullOr path; 54 + default = null; 55 + description = lib.mdDoc "File which contains the value of `pictrs.api_key`."; 56 + }; 57 + 58 + smtpPasswordFile = mkOption { 59 + type = with types; nullOr path; 60 + default = null; 61 + description = lib.mdDoc "File which contains the value of `email.smtp_password`."; 62 + }; 63 + 64 + adminPasswordFile = mkOption { 65 + type = with types; nullOr path; 66 + default = null; 67 + description = lib.mdDoc "File which contains the value of `setup.admin_password`."; 44 68 }; 45 69 46 70 settings = mkOption { ··· 76 100 }; 77 101 }; 78 102 }; 79 - 80 - secretFile = mkOption { 81 - type = with types; nullOr path; 82 - default = null; 83 - description = lib.mdDoc "Path to a secret JSON configuration file which is merged at runtime with the one generated from {option}`services.lemmy.settings`."; 84 - }; 85 103 }; 86 104 87 105 config = 106 + let 107 + secretOptions = { 108 + pictrsApiKeyFile = { setting = [ "pictrs" "api_key" ]; path = cfg.pictrsApiKeyFile; }; 109 + smtpPasswordFile = { setting = [ "email" "smtp_password" ]; path = cfg.smtpPasswordFile; }; 110 + adminPasswordFile = { setting = [ "setup" "admin_password" ]; path = cfg.adminPasswordFile; }; 111 + uriFile = { setting = [ "database" "uri" ]; path = cfg.database.uriFile; }; 112 + }; 113 + secrets = lib.filterAttrs (option: data: data.path != null) secretOptions; 114 + in 88 115 lib.mkIf cfg.enable { 89 - services.lemmy.settings = (mapAttrs (name: mkDefault) 116 + services.lemmy.settings = lib.attrsets.recursiveUpdate (mapAttrs (name: mkDefault) 90 117 { 91 118 bind = "127.0.0.1"; 92 119 tls_enabled = true; ··· 104 131 rate_limit.image = 6; 105 132 rate_limit.image_per_second = 3600; 106 133 } // { 107 - database = mapAttrs (name: mkDefault) { 108 - user = "lemmy"; 109 - host = "/run/postgresql"; 110 - port = 5432; 111 - database = "lemmy"; 112 - pool_size = 5; 113 - }; 114 - }); 134 + database = mapAttrs (name: mkDefault) { 135 + user = "lemmy"; 136 + host = "/run/postgresql"; 137 + port = 5432; 138 + database = "lemmy"; 139 + pool_size = 5; 140 + }; 141 + }) (lib.foldlAttrs (acc: option: data: acc // lib.setAttrByPath data.setting { _secret = option; }) {} secrets); 142 + # the option name is the id of the credential loaded by LoadCredential 115 143 116 144 services.postgresql = mkIf cfg.database.createLocally { 117 145 enable = true; ··· 202 230 assertion = (!(hasAttrByPath ["federation"] cfg.settings)) && (!(hasAttrByPath ["federation" "enabled"] cfg.settings)); 203 231 message = "`services.lemmy.settings.federation` was removed in 0.17.0 and no longer has any effect"; 204 232 } 233 + { 234 + assertion = cfg.database.uriFile != null -> cfg.database.uri == null && !cfg.database.createLocally; 235 + message = "specifying a database uri while also specifying a database uri file is not allowed"; 236 + } 205 237 ]; 206 238 207 239 systemd.services.lemmy = let 208 - configFile = settingsFormat.generate "config.hjson" cfg.settings; 209 - mergedConfig = "/run/lemmy/config.hjson"; 240 + substitutedConfig = "/run/lemmy/config.hjson"; 210 241 in { 211 242 description = "Lemmy server"; 212 243 213 244 environment = { 214 - LEMMY_CONFIG_LOCATION = if cfg.secretFile == null then configFile else mergedConfig; 245 + LEMMY_CONFIG_LOCATION = if secrets == {} then settingsFormat.generate "config.hjson" cfg.settings else substitutedConfig; 215 246 LEMMY_DATABASE_URL = if cfg.database.uri != null then cfg.database.uri else (mkIf (cfg.database.createLocally) "postgres:///lemmy?host=/run/postgresql&user=lemmy"); 216 247 }; 217 248 ··· 226 257 227 258 requires = lib.optionals cfg.database.createLocally [ "postgresql.service" ]; 228 259 229 - path = mkIf (cfg.secretFile != null) [ pkgs.jq ]; 230 - 231 - # merge the two configs and prevent others from reading the result 260 + # substitute secrets and prevent others from reading the result 232 261 # if somehow $CREDENTIALS_DIRECTORY is not set we fail 233 - preStart = mkIf (cfg.secretFile != null) '' 262 + preStart = mkIf (secrets != {}) '' 234 263 set -u 235 - umask 177 236 - jq --slurp '.[0] * .[1]' ${lib.escapeShellArg configFile} "$CREDENTIALS_DIRECTORY/secretFile" > ${lib.escapeShellArg mergedConfig} 264 + umask u=rw,g=,o= 265 + cd "$CREDENTIALS_DIRECTORY" 266 + ${utils.genJqSecretsReplacementSnippet cfg.settings substitutedConfig} 237 267 ''; 238 268 239 269 serviceConfig = { 240 270 DynamicUser = true; 241 271 RuntimeDirectory = "lemmy"; 242 272 ExecStart = "${cfg.server.package}/bin/lemmy_server"; 243 - LoadCredential = mkIf (cfg.secretFile != null) "secretFile:${toString cfg.secretFile}"; 273 + LoadCredential = lib.foldlAttrs (acc: option: data: acc ++ [ "${option}:${toString data.path}" ]) [] secrets; 244 274 PrivateTmp = true; 245 275 MemoryDenyWriteExecute = true; 246 276 NoNewPrivileges = true;
+2 -8
nixos/tests/lemmy.nix
··· 26 26 admin_email = "mightyiam@example.com"; 27 27 }; 28 28 }; 29 - secretFile = /etc/lemmy-config.hjson; 29 + adminPasswordFile = /etc/lemmy-admin-password.txt; 30 30 caddy.enable = true; 31 31 }; 32 32 33 - environment.etc."lemmy-config.hjson".text = '' 34 - { 35 - "setup": { 36 - "admin_password": "ThisIsWhatIUseEverywhereTryIt" 37 - } 38 - } 39 - ''; 33 + environment.etc."lemmy-admin-password.txt".text = "ThisIsWhatIUseEverywhereTryIt"; 40 34 41 35 networking.firewall.allowedTCPPorts = [ 80 ]; 42 36