diff --git a/docs/default.nix b/docs/default.nix index bfc6989..e411647 100644 --- a/docs/default.nix +++ b/docs/default.nix @@ -24,7 +24,7 @@ let ghRoot = (gitHubDeclaration "ibizaman" "selfhostblocks" "").url; - buildOptionsDocs = args@{ modules, includeModuleSystemOptions ? true, ... }: + buildOptionsDocs = args@{ modules, ... }: let config = { _module.check = false; @@ -45,9 +45,7 @@ let }; }; - options = if includeModuleSystemOptions - then eval.options - else builtins.removeAttrs eval.options [ "_module" ]; + options = lib.filterAttrs (name: v: name == "shb") eval.options; in buildPackages.nixosOptionsDoc ({ inherit options; @@ -66,18 +64,14 @@ let _module.check = false; }; - optionsDocs = buildOptionsDocs { - modules = allModules ++ [ - scrubbedModule - ]; + allOptionsDocs = paths: (buildOptionsDocs { + modules = paths ++ allModules ++ [ scrubbedModule ]; variablelistId = "selfhostblocks-options"; - includeModuleSystemOptions = false; - }; + }).optionsJSON; - individualModuleOptionsDocs = path: (buildOptionsDocs { - modules = [ path scrubbedModule ]; + individualModuleOptionsDocs = paths: (buildOptionsDocs { + modules = paths ++ [ scrubbedModule ]; variablelistId = "selfhostblocks-options"; - includeModuleSystemOptions = false; }).optionsJSON; nmd = import nmdsrc { @@ -129,37 +123,47 @@ in stdenv.mkDerivation { substituteInPlace ./options.md \ --replace \ '@OPTIONS_JSON@' \ - ${optionsDocs.optionsJSON}/share/doc/nixos/options.json + ${allOptionsDocs [ + (pkgs.path + "/nixos/modules/services/misc/forgejo.nix") + ]}/share/doc/nixos/options.json substituteInPlace ./modules/blocks/ssl/docs/default.md \ --replace \ '@OPTIONS_JSON@' \ - ${individualModuleOptionsDocs ../modules/blocks/ssl.nix}/share/doc/nixos/options.json + ${individualModuleOptionsDocs [ ../modules/blocks/ssl.nix ]}/share/doc/nixos/options.json substituteInPlace ./modules/blocks/restic/docs/default.md \ --replace \ '@OPTIONS_JSON@' \ - ${individualModuleOptionsDocs ../modules/blocks/restic.nix}/share/doc/nixos/options.json + ${individualModuleOptionsDocs [ ../modules/blocks/restic.nix ]}/share/doc/nixos/options.json substituteInPlace ./modules/services/nextcloud-server/docs/default.md \ --replace \ '@OPTIONS_JSON@' \ - ${individualModuleOptionsDocs ../modules/services/nextcloud-server.nix}/share/doc/nixos/options.json + ${individualModuleOptionsDocs [ ../modules/services/nextcloud-server.nix ]}/share/doc/nixos/options.json substituteInPlace ./modules/services/vaultwarden/docs/default.md \ --replace \ '@OPTIONS_JSON@' \ - ${individualModuleOptionsDocs ../modules/services/vaultwarden.nix}/share/doc/nixos/options.json + ${individualModuleOptionsDocs [ ../modules/services/vaultwarden.nix ]}/share/doc/nixos/options.json + + substituteInPlace ./modules/services/forgejo/docs/default.md \ + --replace \ + '@OPTIONS_JSON@' \ + ${individualModuleOptionsDocs [ + ../modules/services/forgejo.nix + (pkgs.path + "/nixos/modules/services/misc/forgejo.nix") + ]}/share/doc/nixos/options.json substituteInPlace ./modules/contracts/backup/docs/default.md \ --replace \ '@OPTIONS_JSON@' \ - ${individualModuleOptionsDocs ../modules/contracts/backup/dummyModule.nix}/share/doc/nixos/options.json + ${individualModuleOptionsDocs [ ../modules/contracts/backup/dummyModule.nix ]}/share/doc/nixos/options.json substituteInPlace ./modules/contracts/ssl/docs/default.md \ --replace \ '@OPTIONS_JSON@' \ - ${individualModuleOptionsDocs ../modules/contracts/ssl/dummyModule.nix}/share/doc/nixos/options.json + ${individualModuleOptionsDocs [ ../modules/contracts/ssl/dummyModule.nix ]}/share/doc/nixos/options.json find . -name "*.md" -print0 | \ while IFS= read -r -d ''' f; do diff --git a/docs/services.md b/docs/services.md index 3ab63a2..318807c 100644 --- a/docs/services.md +++ b/docs/services.md @@ -13,7 +13,8 @@ information is provided in the respective manual sections. | Service | Backup | Reverse Proxy | SSO | LDAP | Monitoring | Profiling | |-----------------------|--------|---------------|-----|-------|------------|-----------| | [Nextcloud Server][1] | P (1) | Y | Y | Y | Y | P (2) | -| [Vaultwarden][2] | N | Y | Y | Y | N | N | +| [Vaultwarden][2] | P (1) | Y | Y | Y | N | N | +| [Forgejo][3] | Y | Y | Y | Y | N | N | Legend: **N**: no but WIP; **P**: partial; **Y**: yes @@ -22,6 +23,7 @@ Legend: **N**: no but WIP; **P**: partial; **Y**: yes [1]: services-nextcloud.html [2]: services-vaultwarden.html +[3]: services-foregjo.html ```{=include=} chapters html:into-file=//services-vaultwarden.html modules/services/vaultwarden/docs/default.md @@ -30,3 +32,7 @@ modules/services/vaultwarden/docs/default.md ```{=include=} chapters html:into-file=//services-nextcloud.html modules/services/nextcloud-server/docs/default.md ``` + +```{=include=} chapters html:into-file=//services-forgejo.html +modules/services/forgejo/docs/default.md +``` diff --git a/flake.nix b/flake.nix index 41b831b..0869c01 100644 --- a/flake.nix +++ b/flake.nix @@ -47,6 +47,7 @@ modules/services/arr.nix modules/services/audiobookshelf.nix modules/services/deluge.nix + modules/services/forgejo.nix modules/services/grocy.nix modules/services/hledger.nix modules/services/home-assistant.nix @@ -55,7 +56,7 @@ modules/services/vaultwarden.nix ]; - # Only used for documentation. + # The contract dummies are used to show options for contracts. contractDummyModules = [ modules/contracts/backup/dummyModule.nix modules/contracts/ssl/dummyModule.nix @@ -116,6 +117,7 @@ // (vm_test "arr" ./test/services/arr.nix) // (vm_test "audiobookshelf" ./test/services/audiobookshelf.nix) // (vm_test "deluge" ./test/services/deluge.nix) + // (vm_test "forgejo" ./test/services/forgejo.nix) // (vm_test "grocy" ./test/services/grocy.nix) // (vm_test "home-assistant" ./test/services/home-assistant.nix) // (vm_test "jellyfin" ./test/services/jellyfin.nix) diff --git a/modules/blocks/nginx.nix b/modules/blocks/nginx.nix index dea0e09..5c6f4d1 100644 --- a/modules/blocks/nginx.nix +++ b/modules/blocks/nginx.nix @@ -42,6 +42,7 @@ let autheliaRules = lib.mkOption { type = lib.types.listOf (lib.types.attrsOf lib.types.anything); + default = []; description = "Authelia rule configuration"; example = lib.literalExpression ''[{ policy = "two_factor"; diff --git a/modules/services/forgejo.nix b/modules/services/forgejo.nix new file mode 100644 index 0000000..52985f2 --- /dev/null +++ b/modules/services/forgejo.nix @@ -0,0 +1,519 @@ +{ config, options, pkgs, lib, ... }: + +let + cfg = config.shb.forgejo; + + contracts = pkgs.callPackage ../contracts {}; +in +{ + options.shb.forgejo = { + enable = lib.mkEnableOption "selfhostblocks.forgejo"; + + subdomain = lib.mkOption { + type = lib.types.str; + description = '' + Subdomain under which Forgejo will be served. + + ``` + .[:] + ``` + ''; + example = "forgejo"; + }; + + domain = lib.mkOption { + description = '' + Domain under which Forgejo is served. + + ``` + .[:] + ``` + ''; + type = lib.types.str; + example = "domain.com"; + }; + + ssl = lib.mkOption { + description = "Path to SSL files"; + type = lib.types.nullOr contracts.ssl.certs; + default = null; + }; + + ldap = lib.mkOption { + description = '' + LDAP Integration. + ''; + default = {}; + type = lib.types.nullOr (lib.types.submodule { + options = { + enable = lib.mkEnableOption "LDAP integration."; + + provider = lib.mkOption { + type = lib.types.enum [ "LLDAP" ]; + description = "LDAP provider name, used for display."; + default = "LLDAP"; + }; + + host = lib.mkOption { + type = lib.types.str; + description = '' + Host serving the LDAP server. + ''; + default = "127.0.0.1"; + }; + + port = lib.mkOption { + type = lib.types.port; + description = '' + Port of the service serving the LDAP server. + ''; + default = 389; + }; + + dcdomain = lib.mkOption { + type = lib.types.str; + description = "dc domain for ldap."; + example = "dc=mydomain,dc=com"; + }; + + adminName = lib.mkOption { + type = lib.types.str; + description = "Admin user of the LDAP server."; + default = "admin"; + }; + + adminPasswordFile = lib.mkOption { + type = lib.types.path; + description = '' + File containing the admin password of the LDAP server. + + Must be readable by the forgejo system user. + ''; + default = ""; + }; + + userGroup = lib.mkOption { + type = lib.types.str; + description = "Group users must belong to be able to login."; + default = "forgejo_user"; + }; + + adminGroup = lib.mkOption { + type = lib.types.str; + description = "Group users must belong to be admins."; + default = "forgejo_admin"; + }; + }; + }); + }; + + sso = lib.mkOption { + description = '' + Setup SSO integration. + ''; + default = {}; + type = lib.types.submodule { + options = { + enable = lib.mkEnableOption "SSO integration."; + + provider = lib.mkOption { + type = lib.types.enum [ "Authelia" ]; + description = "OIDC provider name, used for display."; + default = "Authelia"; + }; + + endpoint = lib.mkOption { + type = lib.types.str; + description = "OIDC endpoint for SSO."; + example = "https://authelia.example.com"; + }; + + clientID = lib.mkOption { + type = lib.types.str; + description = "Client ID for the OIDC endpoint."; + default = "forgejo"; + }; + + authorization_policy = lib.mkOption { + type = lib.types.enum [ "one_factor" "two_factor" ]; + description = "Require one factor (password) or two factor (device) authentication."; + default = "one_factor"; + }; + + secretFile = lib.mkOption { + type = lib.types.path; + description = '' + File containing the secret for the OIDC endpoint. + + Must be readable by the forgejo system user. + ''; + }; + + secretFileForAuthelia = lib.mkOption { + type = lib.types.path; + description = '' + File containing the secret for the OIDC endpoint, must be readable by the Authelia user. + + Must be readable by the authelia system user. + ''; + }; + }; + }; + }; + + adminPasswordFile = lib.mkOption { + type = lib.types.path; + description = "File containing the Forgejo admin user password."; + example = "/run/secrets/forgejo/adminPassword"; + }; + + databasePasswordFile = lib.mkOption { + type = lib.types.path; + description = "File containing the Forgejo database password."; + example = "/run/secrets/forgejo/databasePassword"; + }; + + repositoryRoot = lib.mkOption { + type = lib.types.nullOr lib.types.str; + description = "Path where to store the repositories. If null, uses the default under the Forgejo StateDir."; + default = null; + example = "/srv/forgejo"; + }; + + localActionRunner = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Enable local action runner that runs for all labels. + ''; + }; + + backup = lib.mkOption { + type = contracts.backup; + description = '' + Backup configuration. This is an output option. + + Use it to initialize a block implementing the "backup" contract. + For example, with the restic block: + + ``` + shb.restic.instances."forgejo" = { + enable = true; + + # Options specific to Restic. + } // config.shb.forgejo.backup; + ``` + ''; + readOnly = true; + default = { + user = options.services.forgejo.user.value; + sourceDirectories = [ + options.services.forgejo.dump.backupDir.value + ] ++ lib.optionals (cfg.repositoryRoot != null) [ + cfg.repositoryRoot + ]; + }; + }; + + mount = lib.mkOption { + type = contracts.mount; + description = '' + Mount configuration. This is an output option. + + Use it to initialize a block implementing the "mount" contract. + For example, with a zfs dataset: + + ``` + shb.zfs.datasets."forgejo" = { + poolName = "root"; + } // config.shb.forgejo.mount; + ``` + ''; + readOnly = true; + default = { path = config.services.forgejo.stateDir; }; + }; + + smtp = lib.mkOption { + description = '' + Send notifications by smtp. + ''; + default = null; + type = lib.types.nullOr (lib.types.submodule { + options = { + from_address = lib.mkOption { + type = lib.types.str; + description = "SMTP address from which the emails originate."; + example = "authelia@mydomain.com"; + }; + host = lib.mkOption { + type = lib.types.str; + description = "SMTP host to send the emails to."; + }; + port = lib.mkOption { + type = lib.types.port; + description = "SMTP port to send the emails to."; + default = 25; + }; + username = lib.mkOption { + type = lib.types.str; + description = "Username to connect to the SMTP host."; + }; + passwordFile = lib.mkOption { + type = lib.types.str; + description = "File containing the password to connect to the SMTP host."; + }; + }; + }); + }; + + debug = lib.mkOption { + description = "Enable debug logging."; + type = lib.types.bool; + default = false; + }; + }; + + config = lib.mkMerge [ + (lib.mkIf cfg.enable { + services.forgejo = { + enable = true; + repositoryRoot = lib.mkIf (cfg.repositoryRoot != null) cfg.repositoryRoot; + settings = { + server = { + DOMAIN = cfg.domain; + PROTOCOL = "http+unix"; + ROOT_URL = "https://${cfg.subdomain}.${cfg.domain}/"; + }; + + service.DISABLE_REGISTRATION = true; + + log.LEVEL = if cfg.debug then "Debug" else "Info"; + + cron = { + ENABLE = true; + RUN_AT_START = true; + SCHEDULE = "@every 1h"; + }; + }; + }; + + # 1 lower than default, to solve conflict between shb.postgresql and nixpkgs' forgejo module. + services.postgresql.enable = lib.mkOverride 999 true; + + # https://github.com/NixOS/nixpkgs/issues/258371#issuecomment-2271967113 + systemd.services.forgejo.serviceConfig.Type = lib.mkForce "exec"; + + shb.nginx.vhosts = [{ + inherit (cfg) domain subdomain ssl; + upstream = "http://unix:${config.services.forgejo.settings.server.HTTP_ADDR}"; + }]; + }) + + (lib.mkIf cfg.enable { + services.forgejo.database = { + type = "postgres"; + + passwordFile = cfg.databasePasswordFile; + }; + }) + + (lib.mkIf cfg.enable { + services.forgejo.dump = { + enable = true; + type = "tar.gz"; + interval = "hourly"; + }; + }) + + # For Forgejo setup: https://github.com/lldap/lldap/blob/main/example_configs/gitea.md + # For cli info: https://docs.gitea.com/usage/command-line + # Security protocols in: https://codeberg.org/forgejo/forgejo/src/branch/forgejo/services/auth/source/ldap/security_protocol.go#L27-L31 + (lib.mkIf (cfg.enable && cfg.ldap.enable != false) { + # The delimiter in the `cut` command is a TAB! + systemd.services.forgejo.preStart = let + provider = "SHB-${cfg.ldap.provider}"; + in '' + auth="${lib.getExe config.services.forgejo.package} admin auth" + + echo "Trying to find existing ldap configuration for ${provider}"... + set +e -o pipefail + id="$($auth list | grep "${provider}.*LDAP" | cut -d' ' -f1)" + found=$? + set -e +o pipefail + + if [[ $found = 0 ]]; then + echo Found ldap configuration at id=$id, updating it if needed. + $auth update-ldap \ + --id $id \ + --name ${provider} \ + --host ${cfg.ldap.host} \ + --port ${toString cfg.ldap.port} \ + --bind-dn uid=${cfg.ldap.adminName},ou=people,${cfg.ldap.dcdomain} \ + --bind-password $(tr -d '\n' < ${cfg.ldap.adminPasswordFile}) \ + --security-protocol Unencrypted \ + --user-search-base ou=people,${cfg.ldap.dcdomain} \ + --user-filter '(&(memberof=cn=${cfg.ldap.userGroup},ou=groups,${cfg.ldap.dcdomain})(|(uid=%[1]s)(mail=%[1]s)))' \ + --admin-filter '(memberof=cn=${cfg.ldap.adminGroup},ou=groups,${cfg.ldap.dcdomain})' \ + --username-attribute uid \ + --firstname-attribute givenName \ + --surname-attribute sn \ + --email-attribute mail \ + --avatar-attribute jpegPhoto \ + --synchronize-users + echo "Done updating LDAP configuration." + else + echo Did not find any ldap configuration, creating one with name ${provider}. + $auth add-ldap \ + --name ${provider} \ + --host ${cfg.ldap.host} \ + --port ${toString cfg.ldap.port} \ + --bind-dn uid=${cfg.ldap.adminName},ou=people,${cfg.ldap.dcdomain} \ + --bind-password $(tr -d '\n' < ${cfg.ldap.adminPasswordFile}) \ + --security-protocol Unencrypted \ + --user-search-base ou=people,${cfg.ldap.dcdomain} \ + --user-filter '(&(memberof=cn=${cfg.ldap.userGroup},ou=groups,${cfg.ldap.dcdomain})(|(uid=%[1]s)(mail=%[1]s)))' \ + --admin-filter '(memberof=cn=${cfg.ldap.adminGroup},ou=groups,${cfg.ldap.dcdomain})' \ + --username-attribute uid \ + --firstname-attribute givenName \ + --surname-attribute sn \ + --email-attribute mail \ + --avatar-attribute jpegPhoto \ + --synchronize-users + echo "Done adding LDAP configuration." + fi + ''; + }) + + # For Authelia to Forgejo integration: https://www.authelia.com/integration/openid-connect/gitea/ + # For Forgejo config: https://forgejo.org/docs/latest/admin/config-cheat-sheet + # For cli info: https://docs.gitea.com/usage/command-line + (lib.mkIf (cfg.enable && cfg.sso.enable != false) { + services.forgejo.settings = { + oauth2 = { + ENABLED = true; + }; + + openid = { + ENABLE_OPENID_SIGNIN = false; + ENABLE_OPENID_SIGNUP = true; + WHITELISTED_URIS = cfg.sso.endpoint; + }; + + service = { + # DISABLE_REGISTRATION = lib.mkForce false; + # ALLOW_ONLY_EXTERNAL_REGISTRATION = false; + SHOW_REGISTRATION_BUTTON = false; + }; + }; + + # The delimiter in the `cut` command is a TAB! + systemd.services.forgejo.preStart = let + provider = "SHB-${cfg.sso.provider}"; + in '' + auth="${lib.getExe config.services.forgejo.package} admin auth" + + echo "Trying to find existing sso configuration for ${provider}"... + set +e -o pipefail + id="$($auth list | grep "${provider}.*OAuth2" | cut -d' ' -f1)" + found=$? + set -e +o pipefail + + if [[ $found = 0 ]]; then + echo Found sso configuration at id=$id, updating it if needed. + $auth update-oauth \ + --id $id \ + --name ${provider} \ + --provider openidConnect \ + --key forgejo \ + --secret $(tr -d '\n' < ${cfg.sso.secretFile}) \ + --auto-discover-url ${cfg.sso.endpoint}/.well-known/openid-configuration + else + echo Did not find any sso configuration, creating one with name ${provider}. + $auth add-oauth \ + --name ${provider} \ + --provider openidConnect \ + --key forgejo \ + --secret $(tr -d '\n' < ${cfg.sso.secretFile}) \ + --auto-discover-url ${cfg.sso.endpoint}/.well-known/openid-configuration + fi + ''; + + shb.authelia.oidcClients = lib.lists.optionals (!(isNull cfg.sso)) [ + (let + provider = "SHB-${cfg.sso.provider}"; + in { + client_id = cfg.sso.clientID; + client_name = "Forgejo"; + client_secret.source = cfg.sso.secretFileForAuthelia; + public = false; + authorization_policy = cfg.sso.authorization_policy; + redirect_uris = [ "https://${cfg.subdomain}.${cfg.domain}/user/oauth2/${provider}/callback" ]; + }) + ]; + }) + + (lib.mkIf cfg.enable { + systemd.services.forgejo.preStart = '' + admin="${lib.getExe config.services.forgejo.package} admin user" + $admin create --admin --email "root@localhost" --username meadmin --password "$(tr -d '\n' < ${cfg.adminPasswordFile})" || true + $admin change-password --username meadmin --password "$(tr -d '\n' < ${cfg.adminPasswordFile})" || true +''; + }) + + (lib.mkIf (cfg.enable && cfg.smtp != null) { + services.forgejo.settings.mailer = { + ENABLED = true; + SMTP_ADDR = "${cfg.smtp.host}:${toString cfg.smtp.port}"; + FROM = cfg.smtp.from_address; + USER = cfg.smtp.username; + }; + + services.forgejo.mailerPasswordFile = cfg.smtp.passwordFile; + }) + + # https://wiki.nixos.org/wiki/Forgejo#Runner + (lib.mkIf cfg.enable { + services.forgejo.settings.actions = { + ENABLED = true; + DEFAULT_ACTIONS_URL = "github"; + }; + + services.gitea-actions-runner = lib.mkIf cfg.localActionRunner { + package = pkgs.forgejo-actions-runner; + instances.local = { + enable = true; + name = "local"; + url = let + protocol = if cfg.ssl != null then "https" else "http"; + in "${protocol}://${cfg.subdomain}.${cfg.domain}"; + tokenFile = ""; # Empty variable to satisfy an assertion. + labels = [ + # "ubuntu-latest:docker://node:16-bullseye" + # "ubuntu-22.04:docker://node:16-bullseye" + # "ubuntu-20.04:docker://node:16-bullseye" + # "ubuntu-18.04:docker://node:16-buster" + "native:host" + ]; + }; + }; + + # This combined with the next statement takes care of + # automatically registering a forgejo runner. + systemd.services.forgejo.postStart = lib.mkIf cfg.localActionRunner (lib.mkBefore '' + ${pkgs.bash}/bin/bash -c '(while ! ${pkgs.netcat-openbsd}/bin/nc -z -U ${config.services.forgejo.settings.server.HTTP_ADDR}; do echo "Waiting for unix ${config.services.forgejo.settings.server.HTTP_ADDR} to open..."; sleep 2; done); sleep 2' + actions="${lib.getExe config.services.forgejo.package} actions" + echo -n TOKEN= > /run/forgejo/forgejo-runner-token + $actions generate-runner-token >> /run/forgejo/forgejo-runner-token + ''); + + systemd.services.gitea-runner-local.serviceConfig = { + # LoadCredential = "TOKEN_FILE:/run/forgejo/forgejo-runner-token"; + # EnvironmentFile = [ "$CREDENTIALS_DIRECTORY/TOKEN_FILE" ]; + EnvironmentFile = [ "/run/forgejo/forgejo-runner-token" ]; + }; + + systemd.services.gitea-runner-local.wants = [ "forgejo.service" ]; + systemd.services.gitea-runner-local.after = [ "forgejo.service" ]; + }) + ]; +} diff --git a/modules/services/forgejo/docs/default.md b/modules/services/forgejo/docs/default.md new file mode 100644 index 0000000..aa070c3 --- /dev/null +++ b/modules/services/forgejo/docs/default.md @@ -0,0 +1,245 @@ +# Forgejo Service {#services-forgejo} + +Defined in [`/modules/services/forgejo.nix`](@REPO@/modules/services/forgejo.nix). + +This NixOS module is a service that sets up a [Forgejo](https://forgejo.org/) instance. + +Compared to the stock module from nixpkgs, +this one sets up, in a fully declarative manner, +LDAP and SSO integration as well as one local runner. + +## Features {#services-forgejo-features} + +- Declarative [LDAP](#services-forgejo-options-shb.forgejo.ldap) Configuration. [Manual](#services-forgejo-usage-ldap). +- Declarative [SSO](#services-forgejo-options-shb.forgejo.sso) Configuration. [Manual](#services-forgejo-usage-sso). +- Declarative [local runner](#services-forgejo-options-shb.forgejo.localActionRunner) Configuration. +- Access through [subdomain](#services-forgejo-options-shb.forgejo.subdomain) using reverse proxy. [Manual](#services-forgejo-usage-basic). +- Access through [HTTPS](#services-forgejo-options-shb.forgejo.ssl) using reverse proxy. [Manual](#services-forgejo-usage-basic). +- [Backup](#services-forgejo-options-shb.forgejo.sso) through the [backup block](./blocks-backup.html) with the . [Manual](#services-forgejo-usage-backup). + +## Usage {#services-forgejo-usage} + +### Secrets {#services-forgejo-secrets} + +All the secrets should be readable by the forgejo user. + +Secrets should not be stored in the nix store. +If you're using [sops-nix](https://github.com/Mic92/sops-nix) +and assuming your secrets file is located at `./secrets.yaml`, +you can define a secret with: + +```nix +sops.secrets."forgejo/adminPasswordFile" = { + sopsFile = ./secrets.yaml; + mode = "0400"; + owner = "forgejo"; + group = "forgejo"; + restartUnits = [ "forgejo.service" ]; +}; +``` + +Then you can use that secret: + +```nix +shb.forgejo.adminPasswordFile = config.sops.secrets."forgejo/adminPasswordFile".path; +``` + +### Forgejo through HTTP(S) {#services-forgejo-usage-basic} + +This will set up a Forgejo service that runs on the NixOS target machine, +reachable at `http://forgejo.example.com`. + +```nix +shb.forgejo = { + enable = true; + domain = "example.com"; + subdomain = "forgejo"; +}; +``` + +If the `shb.ssl` block is used (see [manual](blocks-ssl.html#usage) on how to set it up), +the instance will be reachable at `https://fogejo.example.com`. + +Here is an example with self-signed certificates: + +```nix +shb.certs = { + cas.selfsigned.myca = { + name = "My CA"; + }; + certs.selfsigned = { + foregejo = { + ca = config.shb.certs.cas.selfsigned.myca; + domain = "forgejo.example.com"; + }; + }; +}; +``` + +Then you can tell Forgejo to use those certificates. + +```nix +shb.forgejo = { + ssl = config.shb.certs.certs.selfsigned.forgejo; +}; +``` + +### With LDAP Support {#services-forgejo-usage-ldap} + +:::: {.note} +We will build upon the [Forgejo through HTTP(S)](#services-forgejo-usage-basic) section, +so please follow that first. +:::: + +We will use the LDAP block provided by Self Host Blocks +to setup a [LLDAP](https://github.com/lldap/lldap) service. + +```nix +shb.ldap = { + enable = true; + domain = "example.com"; + subdomain = "ldap"; + ldapPort = 3890; + webUIListenPort = 17170; + dcdomain = "dc=example,dc=com"; + ldapUserPasswordFile = ; + jwtSecretFile = ; +}; +``` + +We also need to configure the `forgejo` service +to talk to the LDAP server we just defined: + +```nix +shb.forgejo.ldap + enable = true; + host = "127.0.0.1"; + port = config.shb.ldap.ldapPort; + dcdomain = config.shb.ldap.dcdomain; + adminPasswordFile = ; +}; +``` + +The `shb.forgejo.ldap.adminPasswordFile` must be the same +as the `shb.ldap.ldapUserPasswordFile`. +The other secrets can be randomly generated with +`nix run nixpkgs#openssl -- rand -hex 64`. + +And that's it. +Now, go to the LDAP server at `http://ldap.example.com`, +create the `forgejo_user` and `forgejo_admin` groups, +create a user and add it to one or both groups. +When that's done, go back to the Forgejo server at +`http://forgejo.example.com` and login with that user. + +### With SSO Support {#services-forgejo-usage-sso} + +:::: {.note} +We will build upon the [With LDAP Support](#services-forgejo-usage-ldap) section, +so please follow that first. +:::: + +Here though, we must setup SSL certificates +because the SSO provider only works with the https protocol. +Let's add self-signed certificates for Authelia and LLDAP: + +```nix +shb.certs = { + certs.selfsigned = { + auth = { + ca = config.shb.certs.cas.selfsigned.myca; + domain = "auth.example.com"; + }; + ldap = { + ca = config.shb.certs.cas.selfsigned.myca; + domain = "ldap.example.com"; + }; + }; +}; +``` + +We then need to setup the SSO provider, +here Authelia thanks to the corresponding SHB block: + +```nix +shb.authelia = { + enable = true; + domain = "example.com"; + subdomain = "auth"; + ssl = config.shb.certs.certs.selfsigned.auth; + + ldapHostname = "127.0.0.1"; + ldapPort = config.shb.ldap.ldapPort; + dcdomain = config.shb.ldap.dcdomain; + + secrets = { + jwtSecretFile = ; + ldapAdminPasswordFile = ; + sessionSecretFile = ; + storageEncryptionKeyFile = ; + identityProvidersOIDCHMACSecretFile = ; + identityProvidersOIDCIssuerPrivateKeyFile = ; + }; +}; +``` + +The `shb.authelia.secrets.ldapAdminPasswordFile` must be the same +as the `shb.ldap.ldapUserPasswordFile` defined in the previous section. +The other secrets can be randomly generated +with `nix run nixpkgs#openssl -- rand -hex 64`. + +Now, on the forgejo side, you need to add the following options: + +```nix +shb.forgejo.sso = { + enable = true; + endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; + + secretFile = ; + secretFileForAuthelia = ; +}; +``` + +Passing the `ssl` option will auto-configure nginx to force SSL connections with the given +certificate. + +The `shb.foregejo.sso.secretFile` and `shb.forgejo.sso.secretFileForAuthelia` options +must have the same content. The former is a file that must be owned by the `forgejo` user while +the latter must be owned by the `authelia` user. I want to avoid needing to define the same secret +twice with a future secrets SHB block. + +### Backup {#services-forgejo-usage-backup} + +Backing up Forgejo using the [Restic block](blocks-restic.html) is done like so: + +```nix +shb.restic.instances."forgejo" = config.shb.forgejo.backup // { + enable = true; +}; +``` + +The name `"foregjo"` in the `instances` can be anything. +The `config.shb.forgejo.backup` option provides what directories to backup. +You can define any number of Restic instances to backup Foregejo multiple times. + +### Extra Settings {#services-forgejo-usage-extra-settings} + +Other Forgejo settings can be accessed through the nixpkgs [stock service][]. + +[stock service]: https://search.nixos.org/options?channel=24.05&from=0&size=50&sort=alpha_asc&type=packages&query=services.forgejo + +## Debug {#services-forgejo-debug} + +In case of an issue, check the logs for systemd service `forgejo.service`. + +Enable verbose logging by setting the `shb.forgejo.debug` boolean to `true`. + +Access the database with `sudo -u forgejo psql`. + +## Options Reference {#services-forgejo-options} + +```{=include=} options +id-prefix: services-forgejo-options- +list-id: selfhostblocks-service-forgejo-options +source: @OPTIONS_JSON@ +``` diff --git a/modules/services/jellyfin.nix b/modules/services/jellyfin.nix index c5cda7a..74c048e 100644 --- a/modules/services/jellyfin.nix +++ b/modules/services/jellyfin.nix @@ -112,6 +112,12 @@ in default = "jellyfin_user"; }; + authorization_policy = lib.mkOption { + type = lib.types.enum [ "one_factor" "two_factor" ]; + description = "Require one factor (password) or two factor (device) authentication."; + default = "one_factor"; + }; + secretFile = lib.mkOption { type = lib.types.path; description = "File containing the OIDC shared secret."; @@ -419,7 +425,7 @@ in client_name = "Jellyfin"; client_secret.source = cfg.sso.secretFile; public = false; - authorization_policy = "one_factor"; + authorization_policy = cfg.sso.authorization_policy; redirect_uris = [ "https://${cfg.subdomain}.${cfg.domain}/sso/OID/r/${cfg.sso.provider}" ]; } ]; diff --git a/test/services/forgejo.nix b/test/services/forgejo.nix new file mode 100644 index 0000000..8cd2060 --- /dev/null +++ b/test/services/forgejo.nix @@ -0,0 +1,145 @@ +{ pkgs, ... }: +let + pkgs' = pkgs; + + testLib = pkgs.callPackage ../common.nix {}; + + subdomain = "f"; + domain = "example.com"; + + adminPassword = "AdminPassword"; + + commonTestScript = testLib.accessScript { + inherit subdomain domain; + hasSSL = { node, ... }: !(isNull node.config.shb.forgejo.ssl); + waitForServices = { ... }: [ + "forgejo.service" + "nginx.service" + ]; + waitForUnixSocket = { node, ... }: [ + node.config.services.forgejo.settings.server.HTTP_ADDR + ]; + extraScript = { node, ... }: '' + server.wait_for_unit("gitea-runner-local.service", timeout=10) + server.succeed("journalctl -o cat -u gitea-runner-local.service | grep -q 'Runner registered successfully'") + ''; + }; + + base = testLib.base pkgs' [ + ../../modules/services/forgejo.nix + ]; + + basic = { + shb.forgejo = { + enable = true; + inherit domain subdomain; + + adminPasswordFile = pkgs.writeText "adminPasswordFile" adminPassword; + databasePasswordFile = pkgs.writeText "databasePassword" "databasePassword"; + }; + + # Needed for gitea-runner-local to be able to ping forgejo. + networking.hosts = { + "127.0.0.1" = [ "${subdomain}.${domain}" ]; + }; + }; + + https = { config, ... }: { + shb.forgejo = { + ssl = config.shb.certs.certs.selfsigned.n; + }; + }; + + ldap = { config, ... }: { + shb.forgejo = { + ldap = { + enable = true; + host = "127.0.0.1"; + port = config.shb.ldap.ldapPort; + dcdomain = config.shb.ldap.dcdomain; + adminPasswordFile = config.shb.ldap.ldapUserPasswordFile; + }; + }; + }; + + sso = { config, ... }: { + shb.forgejo = { + sso = { + enable = true; + endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; + secretFile = pkgs.writeText "ssoSecretFile" "ssoSecretFile"; + secretFileForAuthelia = pkgs.writeText "ssoSecretFile" "ssoSecretFile"; + }; + }; + }; +in +{ + basic = pkgs.testers.runNixOSTest { + name = "forgejo_basic"; + + nodes.server = { + imports = [ + base + basic + ]; + }; + + nodes.client = {}; + + testScript = commonTestScript; + }; + + https = pkgs.testers.runNixOSTest { + name = "forgejo_https"; + + nodes.server = { + imports = [ + base + (testLib.certs domain) + basic + https + ]; + }; + + nodes.client = {}; + + testScript = commonTestScript; + }; + + ldap = pkgs.testers.runNixOSTest { + name = "forgejo_ldap"; + + nodes.server = { + imports = [ + base + basic + (testLib.ldap domain pkgs') + ldap + ]; + }; + + nodes.client = {}; + + testScript = commonTestScript; + }; + + sso = pkgs.testers.runNixOSTest { + name = "forgejo_sso"; + + nodes.server = { config, pkgs, ... }: { + imports = [ + base + (testLib.certs domain) + basic + https + (testLib.ldap domain pkgs') + (testLib.sso domain pkgs' config.shb.certs.certs.selfsigned.n) + sso + ]; + }; + + nodes.client = {}; + + testScript = commonTestScript; + }; +}