From ae8404ff58896d51f839ef2d8cc9a66c74407f6e Mon Sep 17 00:00:00 2001 From: emilylange Date: Fri, 17 May 2024 23:23:17 +0200 Subject: [PATCH 1/4] forgejo: build `environment-to-ini` for use in nixos/forgejo secret refactor This is needed for the upcoming nixos/forgejo secret refactor that will leverage `environment-to-ini` instead of `pkgs.replace-secret`. https://codeberg.org/forgejo/forgejo/src/tag/v7.0.2/contrib/environment-to-ini/environment-to-ini.go To read the motivation behind this, please see the actual nixos/forgejo refactor commit following this commit. --- pkgs/by-name/fo/forgejo/package.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/by-name/fo/forgejo/package.nix b/pkgs/by-name/fo/forgejo/package.nix index ade722176d0b..01ce60731d94 100644 --- a/pkgs/by-name/fo/forgejo/package.nix +++ b/pkgs/by-name/fo/forgejo/package.nix @@ -51,7 +51,7 @@ buildGoModule rec { vendorHash = "sha256-8qMpnGL5GXJuxOpxh9a1Bcxd7tVweUKwbun8UBxCfQA="; - subPackages = [ "." ]; + subPackages = [ "." "contrib/environment-to-ini" ]; outputs = [ "out" "data" ]; From 694db856ed343d28d98f2b91a0c0b46344ea246c Mon Sep 17 00:00:00 2001 From: emilylange Date: Fri, 17 May 2024 23:23:20 +0200 Subject: [PATCH 2/4] nixos/forgejo: refactor secrets, add `cfg.secrets` This is not a breaking change. Existing setups continue to work as-is. Users of `cfg.mailerPasswordFile` will get an option rename/deprecation warning, but that's it (assuming there is no regression). This adds `cfg.secrets`, which is a wrapper over systemd's `LoadCredential=` leveraging Forgejo's `environment-to-ini`. `environment-to-ini` is intended for configuring Forgejo in OCI containers. It requires some fairly annoying escaping of the section names to fit into the allowed environment variable charset. E.g. `"log.console".COLORIZE = false` becomes `FORGEJO__LOG_0x2E_CONSOLE__COLORIZE=false`. - `.` needs to be replaced with `_0X2E_` and - `-` needs to be replaced with `_0X2D_` Those are simply the hex representation of each char from an ASCII table: . = ASCII 46 = 46 (decimal) = 2E (hex) = 0x2E = _OX2E_ To make interacting with `environment-to-ini` less annoying, we template and escape the sections/keys in nix: `cfg.secrets` takes the same free-form sections/keys as `cfg.settings`. Meaning there is now a generalized abstraction for all keys, not just those that have been manually implemented in the past. It goes as far as theoretically allowing one to have `DEFAULT.APP_NAME` read from a secret file. I don't know why one would want to do that, but it has been made possible by this :^) More reasonable examples are listed in the `cfg.secrets` option example. We also continue to bootstrap a handful of secrets like `security.SECRET_KEY`. This is done is a sort of sidecar bootstrap unit fittingly called `forgejo-secrets.service`. Overriding those is, just like before, not really intended and requires the use of `lib.mkForce` and might lead to breakage. But it is, in a way, more possible than before. --- nixos/modules/services/misc/forgejo.nix | 183 ++++++++++++++---------- 1 file changed, 111 insertions(+), 72 deletions(-) diff --git a/nixos/modules/services/misc/forgejo.nix b/nixos/modules/services/misc/forgejo.nix index babed2d5acd4..9a102918f35e 100644 --- a/nixos/modules/services/misc/forgejo.nix +++ b/nixos/modules/services/misc/forgejo.nix @@ -12,6 +12,15 @@ let usePostgresql = cfg.database.type == "postgres"; useSqlite = cfg.database.type == "sqlite3"; + secrets = let + mkSecret = section: values: lib.mapAttrsToList (key: value: { + env = envEscape "FORGEJO__${section}__${key}__FILE"; + path = value; + }) values; + # https://codeberg.org/forgejo/forgejo/src/tag/v7.0.2/contrib/environment-to-ini/environment-to-ini.go + envEscape = string: lib.replaceStrings [ "." "-" ] [ "_0X2E_" "_0X2D_" ] (lib.strings.toUpper string); + in lib.flatten (lib.mapAttrsToList mkSecret cfg.secrets); + inherit (lib) literalExpression mkChangedOptionModule @@ -34,6 +43,7 @@ in (mkRenamedOptionModule [ "services" "forgejo" "appName" ] [ "services" "forgejo" "settings" "DEFAULT" "APP_NAME" ]) (mkRemovedOptionModule [ "services" "forgejo" "extraConfig" ] "services.forgejo.extraConfig has been removed. Please use the freeform services.forgejo.settings option instead") (mkRemovedOptionModule [ "services" "forgejo" "database" "password" ] "services.forgejo.database.password has been removed. Please use services.forgejo.database.passwordFile instead") + (mkRenamedOptionModule [ "services" "forgejo" "mailerPasswordFile" ] [ "services" "forgejo" "secrets" "mailer" "PASSWD" ]) # copied from services.gitea; remove at some point (mkRenamedOptionModule [ "services" "forgejo" "cookieSecure" ] [ "services" "forgejo" "settings" "session" "COOKIE_SECURE" ]) @@ -224,13 +234,6 @@ in description = "Path to the git repositories."; }; - mailerPasswordFile = mkOption { - type = types.nullOr types.str; - default = null; - example = "/run/keys/forgejo-mailpw"; - description = "Path to a file containing the SMTP password."; - }; - settings = mkOption { default = { }; description = '' @@ -347,6 +350,44 @@ in }; }; }; + + secrets = mkOption { + default = { }; + description = '' + This is a small wrapper over systemd's `LoadCredential`. + + It takes the same sections and keys as {option}`services.forgejo.settings`, + but the value of each key is a path instead of a string or bool. + + The path is then loaded as credential, exported as environment variable + and then feed through + . + + It does the required environment variable escaping for you. + + ::: {.note} + Keys specified here take priority over the ones in {option}`services.forgejo.settings`! + ::: + ''; + example = literalExpression '' + { + metrics = { + TOKEN = "/run/keys/forgejo-metrics-token"; + }; + camo = { + HMAC_KEY = "/run/keys/forgejo-camo-hmac"; + }; + service = { + HCAPTCHA_SECRET = "/run/keys/forgejo-hcaptcha-secret"; + HCAPTCHA_SITEKEY = "/run/keys/forgejo-hcaptcha-sitekey"; + }; + } + ''; + type = types.submodule { + freeformType = with types; attrsOf (attrsOf path); + options = { }; + }; + }; }; }; @@ -381,7 +422,6 @@ in HOST = if cfg.database.socket != null then cfg.database.socket else cfg.database.host + ":" + toString cfg.database.port; NAME = cfg.database.name; USER = cfg.database.user; - PASSWD = "#dbpass#"; }) (mkIf useSqlite { PATH = cfg.database.path; @@ -397,7 +437,6 @@ in server = mkIf cfg.lfs.enable { LFS_START_SERVER = true; - LFS_JWT_SECRET = "#lfsjwtsecret#"; }; session = { @@ -405,24 +444,33 @@ in }; security = { - SECRET_KEY = "#secretkey#"; - INTERNAL_TOKEN = "#internaltoken#"; INSTALL_LOCK = true; }; - mailer = mkIf (cfg.mailerPasswordFile != null) { - PASSWD = "#mailerpass#"; - }; - - oauth2 = { - JWT_SECRET = "#oauth2jwtsecret#"; - }; - lfs = mkIf cfg.lfs.enable { PATH = cfg.lfs.contentDir; }; }; + services.forgejo.secrets = { + security = { + SECRET_KEY = "${cfg.customDir}/conf/secret_key"; + INTERNAL_TOKEN = "${cfg.customDir}/conf/internal_token"; + }; + + oauth2 = { + JWT_SECRET = "${cfg.customDir}/conf/oauth2_jwt_secret"; + }; + + database = mkIf (cfg.database.passwordFile != null) { + PASSWD = cfg.database.passwordFile; + }; + + server = mkIf cfg.lfs.enable { + LFS_JWT_SECRET = "${cfg.customDir}/conf/lfs_jwt_secret"; + }; + }; + services.postgresql = optionalAttrs (usePostgresql && cfg.database.createDatabase) { enable = mkDefault true; @@ -476,6 +524,37 @@ in "z '${cfg.lfs.contentDir}' 0750 ${cfg.user} ${cfg.group} - -" ]; + systemd.services.forgejo-secrets = mkIf (!cfg.useWizard) { + description = "Forgejo secret bootstrap helper"; + script = '' + if [ ! -s '${cfg.secrets.security.SECRET_KEY}' ]; then + ${exe} generate secret SECRET_KEY > '${cfg.secrets.security.SECRET_KEY}' + fi + + if [ ! -s '${cfg.secrets.oauth2.JWT_SECRET}' ]; then + ${exe} generate secret JWT_SECRET > '${cfg.secrets.oauth2.JWT_SECRET}' + fi + + ${optionalString cfg.lfs.enable '' + if [ ! -s '${cfg.secrets.server.LFS_JWT_SECRET}' ]; then + ${exe} generate secret LFS_JWT_SECRET > '${cfg.secrets.server.LFS_JWT_SECRET}' + fi + ''} + + if [ ! -s '${cfg.secrets.security.INTERNAL_TOKEN}' ]; then + ${exe} generate secret INTERNAL_TOKEN > '${cfg.secrets.security.INTERNAL_TOKEN}' + fi + ''; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + User = cfg.user; + Group = cfg.group; + ReadWritePaths = [ cfg.customDir ]; + UMask = "0077"; + }; + }; + systemd.services.forgejo = { description = "Forgejo (Beyond coding. We forge.)"; after = [ @@ -484,11 +563,15 @@ in "postgresql.service" ] ++ optionals useMysql [ "mysql.service" + ] ++ optionals (!cfg.useWizard) [ + "forgejo-secrets.service" ]; requires = optionals (cfg.database.createDatabase && usePostgresql) [ "postgresql.service" ] ++ optionals (cfg.database.createDatabase && useMysql) [ "mysql.service" + ] ++ optionals (!cfg.useWizard) [ + "forgejo-secrets.service" ]; wantedBy = [ "multi-user.target" ]; path = [ cfg.package pkgs.git pkgs.gnupg ]; @@ -501,61 +584,15 @@ in # lfs_jwt_secret. # We have to consider this to stay compatible with older installations. preStart = - let - runConfig = "${cfg.customDir}/conf/app.ini"; - secretKey = "${cfg.customDir}/conf/secret_key"; - oauth2JwtSecret = "${cfg.customDir}/conf/oauth2_jwt_secret"; - oldLfsJwtSecret = "${cfg.customDir}/conf/jwt_secret"; # old file for LFS_JWT_SECRET - lfsJwtSecret = "${cfg.customDir}/conf/lfs_jwt_secret"; # new file for LFS_JWT_SECRET - internalToken = "${cfg.customDir}/conf/internal_token"; - replaceSecretBin = "${pkgs.replace-secret}/bin/replace-secret"; - in '' - # copy custom configuration and generate random secrets if needed - ${lib.optionalString (!cfg.useWizard) '' + ${optionalString (!cfg.useWizard) '' function forgejo_setup { - cp -f '${format.generate "app.ini" cfg.settings}' '${runConfig}' + config='${cfg.customDir}/conf/app.ini' + cp -f '${format.generate "app.ini" cfg.settings}' "$config" - if [ ! -s '${secretKey}' ]; then - ${exe} generate secret SECRET_KEY > '${secretKey}' - fi - - # Migrate LFS_JWT_SECRET filename - if [[ -s '${oldLfsJwtSecret}' && ! -s '${lfsJwtSecret}' ]]; then - mv '${oldLfsJwtSecret}' '${lfsJwtSecret}' - fi - - if [ ! -s '${oauth2JwtSecret}' ]; then - ${exe} generate secret JWT_SECRET > '${oauth2JwtSecret}' - fi - - ${optionalString cfg.lfs.enable '' - if [ ! -s '${lfsJwtSecret}' ]; then - ${exe} generate secret LFS_JWT_SECRET > '${lfsJwtSecret}' - fi - ''} - - if [ ! -s '${internalToken}' ]; then - ${exe} generate secret INTERNAL_TOKEN > '${internalToken}' - fi - - chmod u+w '${runConfig}' - ${replaceSecretBin} '#secretkey#' '${secretKey}' '${runConfig}' - ${replaceSecretBin} '#oauth2jwtsecret#' '${oauth2JwtSecret}' '${runConfig}' - ${replaceSecretBin} '#internaltoken#' '${internalToken}' '${runConfig}' - - ${optionalString cfg.lfs.enable '' - ${replaceSecretBin} '#lfsjwtsecret#' '${lfsJwtSecret}' '${runConfig}' - ''} - - ${optionalString (cfg.database.passwordFile != null) '' - ${replaceSecretBin} '#dbpass#' '${cfg.database.passwordFile}' '${runConfig}' - ''} - - ${optionalString (cfg.mailerPasswordFile != null) '' - ${replaceSecretBin} '#mailerpass#' '${cfg.mailerPasswordFile}' '${runConfig}' - ''} - chmod u-w '${runConfig}' + chmod u+w "$config" + ${lib.getExe' cfg.package "environment-to-ini"} --config "$config" + chmod u-w "$config" } (umask 027; forgejo_setup) ''} @@ -616,6 +653,8 @@ in # System Call Filtering SystemCallArchitectures = "native"; SystemCallFilter = [ "~@cpu-emulation @debug @keyring @mount @obsolete @privileged @setuid" "setrlimit" ]; + # cfg.secrets + LoadCredential = map (e: "${e.env}:${e.path}") secrets; }; environment = { @@ -625,7 +664,7 @@ in # is resolved. GITEA_WORK_DIR = cfg.stateDir; GITEA_CUSTOM = cfg.customDir; - }; + } // lib.listToAttrs (map (e: lib.nameValuePair e.env "%d/${e.env}") secrets); }; services.openssh.settings.AcceptEnv = mkIf (!cfg.settings.START_SSH_SERVER or false) "GIT_PROTOCOL"; From fd58d2299bd10ce7e798ce77502c9f117a00f61c Mon Sep 17 00:00:00 2001 From: emilylange Date: Fri, 17 May 2024 23:24:26 +0200 Subject: [PATCH 3/4] nixos/tests/forgejo: test `cfg.secrets` using /metrics endpoint Heavily inspired by b59e5a34e782478445b6ea690fd546c8624ed705 (gitea). --- nixos/tests/forgejo.nix | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/nixos/tests/forgejo.nix b/nixos/tests/forgejo.nix index 827fae2790c6..c5bf8e32524c 100644 --- a/nixos/tests/forgejo.nix +++ b/nixos/tests/forgejo.nix @@ -41,6 +41,8 @@ let hash = "sha256-h2/UIp8IjPo3eE4Gzx52Fb7pcgG/Ww7u31w5fdKVMos="; }; + metricSecret = "fakesecret"; + supportedDbTypes = [ "mysql" "postgres" "sqlite3" ]; makeForgejoTest = type: nameValuePair type (makeTest { name = "forgejo-${type}"; @@ -59,6 +61,8 @@ let ENABLE_PUSH_CREATE_USER = true; DEFAULT_PUSH_CREATE_PRIVATE = false; }; + settings.metrics.ENABLED = true; + secrets.metrics.TOKEN = pkgs.writeText "metrics_secret" metricSecret; }; environment.systemPackages = [ config.services.forgejo.package pkgs.gnupg pkgs.jq pkgs.file pkgs.htmlq ]; services.openssh.enable = true; @@ -192,6 +196,10 @@ let timeout=10 ) + with subtest("Testing /metrics endpoint with token from cfg.secrets"): + server.fail("curl --fail http://localhost:3000/metrics") + server.succeed('curl --fail http://localhost:3000/metrics -H "Authorization: Bearer ${metricSecret}"') + with subtest("Testing runner registration and action workflow"): server.succeed( "su -l forgejo -c 'GITEA_WORK_DIR=/var/lib/forgejo gitea actions generate-runner-token' | sed 's/^/TOKEN=/' | tee /var/lib/forgejo/runner_token" From ac20219508f680c45e118b99c5c0787ec9ef3651 Mon Sep 17 00:00:00 2001 From: emilylange Date: Wed, 5 Jun 2024 01:03:10 +0200 Subject: [PATCH 4/4] nixos/rl-2411: add `services.forgejo.secrets` and the accompanying `services.forgejo.mailerPasswordFile` deprecation. --- nixos/doc/manual/release-notes/rl-2411.section.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/nixos/doc/manual/release-notes/rl-2411.section.md b/nixos/doc/manual/release-notes/rl-2411.section.md index 51904352b3b7..b0176a056e1f 100644 --- a/nixos/doc/manual/release-notes/rl-2411.section.md +++ b/nixos/doc/manual/release-notes/rl-2411.section.md @@ -18,6 +18,11 @@ nvimpager settings: user commands in `-c` and `--cmd` now override the respective default settings because they are executed later. +- `services.forgejo.mailerPasswordFile` has been deprecated by the drop-in replacement `services.forgejo.secrets.mailer.PASSWD`, + which is part of the new free-form `services.forgejo.secrets` option. + `services.forgejo.secrets` is a small wrapper over systemd's `LoadCredential=`. It has the same structure (sections/keys) as + `services.forgejo.settings` but takes file paths that will be read before service startup instead of some plaintext value. + - The Invoiceplane module now only accepts the structured `settings` option. `extraConfig` is now removed.