From 6d6e1a695c28d828deabd21d1a45975cfdce6de8 Mon Sep 17 00:00:00 2001 From: "Gustavo \"Guz\" L de Mello" Date: Thu, 9 Oct 2025 21:55:12 -0300 Subject: [PATCH] feat(modules,gitea): secret handling for Gitea module --- flake.nix | 1 + modules/gitea.nix | 251 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 252 insertions(+) create mode 100644 modules/gitea.nix diff --git a/flake.nix b/flake.nix index 1e3e7dd..0f14c48 100644 --- a/flake.nix +++ b/flake.nix @@ -53,6 +53,7 @@ }; nixosModules = { medama = ./modules/medama.nix; + gitea = ./modules/gitea.nix; }; }; } diff --git a/modules/gitea.nix b/modules/gitea.nix new file mode 100644 index 0000000..9d18f79 --- /dev/null +++ b/modules/gitea.nix @@ -0,0 +1,251 @@ +/* +This file has code copied from NixOS nixpkgs's repostiory, and can be located at + + +The original code is licensed under the MIT License (SPDX-License-Identifier: MIT), +which a copy of the copyright notice and license can be found at + and below: + +Copyright (c) 2003-2025 Eelco Dolstra and the Nixpkgs/NixOS contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +{ + config, + lib, + pkgs, + ... +}: +with lib; let + cfg = config.services.gitea; + + exe = getExe cfg.package; + format = pkgs.formats.ini {}; + + secrets = let + mkSecret = section: values: + mapAttrsToList (k: v: { + env = envEscape "GITEA__${section}__${k}__FILE"; + path = v; + }) + values; + envEscape = str: replaceStrings ["." "-"] ["_0X2E_" "_0X2D_"] (strings.toUpper str); + in + flatten (mapAttrsToList mkSecret cfg.secrets); +in { + options.services.gitea = { + 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/gitea-metrics-token"; + }; + camo = { + HMAC_KEY = "/run/keys/gitea-camo-hmac"; + }; + service = { + HCAPTCHA_SECRET = "/run/keys/gitea-hcaptcha-secret"; + HCAPTCHA_SITEKEY = "/run/keys/gitea-hcaptcha-sitekey"; + }; + } + ''; + type = with types; + submodule { + freeformType = attrsOf (attrsOf path); + options = {}; + }; + }; + }; + config = mkIf cfg.enable { + services.gitea.settings = { + DEFAULT = { + RUN_MODE = mkDefault "prod"; + RUN_USER = mkDefault cfg.user; + WORK_PATH = mkDefault cfg.stateDir; + }; + + database = mkMerge [ + { + DB_TYPE = cfg.database.type; + } + (mkIf (cfg.database.type == "postgres" || cfg.database.type == "mysql") { + 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; + }) + (mkIf (cfg.database.type == "sqlite3") { + PATH = cfg.database.path; + }) + (mkIf (cfg.database.type == "postgres") { + SSL_MODE = "disable"; + }) + ]; + + repository = { + ROOT = cfg.repositoryRoot; + }; + + server = mkIf cfg.lfs.enable { + LFS_START_SERVER = true; + }; + + session = { + COOKIE_NAME = mkDefault "session"; + }; + + security = { + INSTALL_LOCK = true; + }; + + lfs = mkIf cfg.lfs.enable { + PATH = cfg.lfs.contentDir; + }; + }; + + services.gitea.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"; + }; + }; + + systemd.services.gitea-secrets = mkIf (!cfg.useWizard) { + description = "Gitea 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.gitea = { + after = + [ + "network.target" + ] + ++ optionals (cfg.database.type == "postgres") [ + "postgresql.service" + ] + ++ optionals (cfg.database.type == "mysql") [ + "mysql.service" + ] + ++ optionals (!cfg.useWizard) [ + "gitea-secrets.service" + ]; + requires = + optionals (cfg.database.createDatabase && cfg.database.type == "postgres") [ + "postgresql.service" + ] + ++ optionals (cfg.database.createDatabase && cfg.database.type == "mysql") [ + "mysql.service" + ] + ++ optionals (!cfg.useWizard) [ + "gitea-secrets.service" + ]; + + # Copied from, to add secrets handling + # https://github.com/NixOS/nixpkgs/blob/20c4598c84a671783f741e02bf05cbfaf4907cff/nixos/modules/services/misc/forgejo.nix#L696 + preStart = '' + # copy custom configuration and generate random secrets if needed + ${optionalString (!cfg.useWizard) '' + function gitea_setup { + config='${cfg.customDir}/conf/app.ini' + cp -f '${format.generate "app.ini" cfg.settings}' "$config" + + chmod u+w "$config" + ${getExe' cfg.package "environment-to-ini"} --config "$config" + chmod u-w "$config" + } + (umask 027; gitea_setup) + ''} + + # run migrations/init the database + ${exe} migrate + + # update all hooks' binary paths + ${exe} admin regenerate hooks + + # update command option in authorized_keys + if [ -r ${cfg.stateDir}/.ssh/authorized_keys ] + then + ${exe} admin regenerate keys + fi + ''; + + serviceConfig.LoadCredential = map (e: "${e.env}:${e.path}") secrets; + + environment = listToAttrs (map (e: nameValuePair e.env "%d/${e.env}") secrets); + }; + }; +}