1
0
Fork 0

add forgejo service

This commit is contained in:
ibizaman 2024-09-08 08:33:22 +02:00 committed by Pierre Penninckx
parent eb11a94d44
commit 253ec980d9
8 changed files with 951 additions and 23 deletions

View file

@ -24,7 +24,7 @@ let
ghRoot = (gitHubDeclaration "ibizaman" "selfhostblocks" "").url; ghRoot = (gitHubDeclaration "ibizaman" "selfhostblocks" "").url;
buildOptionsDocs = args@{ modules, includeModuleSystemOptions ? true, ... }: buildOptionsDocs = args@{ modules, ... }:
let let
config = { config = {
_module.check = false; _module.check = false;
@ -45,9 +45,7 @@ let
}; };
}; };
options = if includeModuleSystemOptions options = lib.filterAttrs (name: v: name == "shb") eval.options;
then eval.options
else builtins.removeAttrs eval.options [ "_module" ];
in buildPackages.nixosOptionsDoc ({ in buildPackages.nixosOptionsDoc ({
inherit options; inherit options;
@ -66,18 +64,14 @@ let
_module.check = false; _module.check = false;
}; };
optionsDocs = buildOptionsDocs { allOptionsDocs = paths: (buildOptionsDocs {
modules = allModules ++ [ modules = paths ++ allModules ++ [ scrubbedModule ];
scrubbedModule
];
variablelistId = "selfhostblocks-options"; variablelistId = "selfhostblocks-options";
includeModuleSystemOptions = false; }).optionsJSON;
};
individualModuleOptionsDocs = path: (buildOptionsDocs { individualModuleOptionsDocs = paths: (buildOptionsDocs {
modules = [ path scrubbedModule ]; modules = paths ++ [ scrubbedModule ];
variablelistId = "selfhostblocks-options"; variablelistId = "selfhostblocks-options";
includeModuleSystemOptions = false;
}).optionsJSON; }).optionsJSON;
nmd = import nmdsrc { nmd = import nmdsrc {
@ -129,37 +123,47 @@ in stdenv.mkDerivation {
substituteInPlace ./options.md \ substituteInPlace ./options.md \
--replace \ --replace \
'@OPTIONS_JSON@' \ '@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 \ substituteInPlace ./modules/blocks/ssl/docs/default.md \
--replace \ --replace \
'@OPTIONS_JSON@' \ '@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 \ substituteInPlace ./modules/blocks/restic/docs/default.md \
--replace \ --replace \
'@OPTIONS_JSON@' \ '@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 \ substituteInPlace ./modules/services/nextcloud-server/docs/default.md \
--replace \ --replace \
'@OPTIONS_JSON@' \ '@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 \ substituteInPlace ./modules/services/vaultwarden/docs/default.md \
--replace \ --replace \
'@OPTIONS_JSON@' \ '@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 \ substituteInPlace ./modules/contracts/backup/docs/default.md \
--replace \ --replace \
'@OPTIONS_JSON@' \ '@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 \ substituteInPlace ./modules/contracts/ssl/docs/default.md \
--replace \ --replace \
'@OPTIONS_JSON@' \ '@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 | \ find . -name "*.md" -print0 | \
while IFS= read -r -d ''' f; do while IFS= read -r -d ''' f; do

View file

@ -13,7 +13,8 @@ information is provided in the respective manual sections.
| Service | Backup | Reverse Proxy | SSO | LDAP | Monitoring | Profiling | | Service | Backup | Reverse Proxy | SSO | LDAP | Monitoring | Profiling |
|-----------------------|--------|---------------|-----|-------|------------|-----------| |-----------------------|--------|---------------|-----|-------|------------|-----------|
| [Nextcloud Server][1] | P (1) | Y | Y | Y | Y | P (2) | | [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 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 [1]: services-nextcloud.html
[2]: services-vaultwarden.html [2]: services-vaultwarden.html
[3]: services-foregjo.html
```{=include=} chapters html:into-file=//services-vaultwarden.html ```{=include=} chapters html:into-file=//services-vaultwarden.html
modules/services/vaultwarden/docs/default.md modules/services/vaultwarden/docs/default.md
@ -30,3 +32,7 @@ modules/services/vaultwarden/docs/default.md
```{=include=} chapters html:into-file=//services-nextcloud.html ```{=include=} chapters html:into-file=//services-nextcloud.html
modules/services/nextcloud-server/docs/default.md modules/services/nextcloud-server/docs/default.md
``` ```
```{=include=} chapters html:into-file=//services-forgejo.html
modules/services/forgejo/docs/default.md
```

View file

@ -47,6 +47,7 @@
modules/services/arr.nix modules/services/arr.nix
modules/services/audiobookshelf.nix modules/services/audiobookshelf.nix
modules/services/deluge.nix modules/services/deluge.nix
modules/services/forgejo.nix
modules/services/grocy.nix modules/services/grocy.nix
modules/services/hledger.nix modules/services/hledger.nix
modules/services/home-assistant.nix modules/services/home-assistant.nix
@ -55,7 +56,7 @@
modules/services/vaultwarden.nix modules/services/vaultwarden.nix
]; ];
# Only used for documentation. # The contract dummies are used to show options for contracts.
contractDummyModules = [ contractDummyModules = [
modules/contracts/backup/dummyModule.nix modules/contracts/backup/dummyModule.nix
modules/contracts/ssl/dummyModule.nix modules/contracts/ssl/dummyModule.nix
@ -116,6 +117,7 @@
// (vm_test "arr" ./test/services/arr.nix) // (vm_test "arr" ./test/services/arr.nix)
// (vm_test "audiobookshelf" ./test/services/audiobookshelf.nix) // (vm_test "audiobookshelf" ./test/services/audiobookshelf.nix)
// (vm_test "deluge" ./test/services/deluge.nix) // (vm_test "deluge" ./test/services/deluge.nix)
// (vm_test "forgejo" ./test/services/forgejo.nix)
// (vm_test "grocy" ./test/services/grocy.nix) // (vm_test "grocy" ./test/services/grocy.nix)
// (vm_test "home-assistant" ./test/services/home-assistant.nix) // (vm_test "home-assistant" ./test/services/home-assistant.nix)
// (vm_test "jellyfin" ./test/services/jellyfin.nix) // (vm_test "jellyfin" ./test/services/jellyfin.nix)

View file

@ -42,6 +42,7 @@ let
autheliaRules = lib.mkOption { autheliaRules = lib.mkOption {
type = lib.types.listOf (lib.types.attrsOf lib.types.anything); type = lib.types.listOf (lib.types.attrsOf lib.types.anything);
default = [];
description = "Authelia rule configuration"; description = "Authelia rule configuration";
example = lib.literalExpression ''[{ example = lib.literalExpression ''[{
policy = "two_factor"; policy = "two_factor";

View file

@ -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.
```
<subdomain>.<domain>[:<port>]
```
'';
example = "forgejo";
};
domain = lib.mkOption {
description = ''
Domain under which Forgejo is served.
```
<subdomain>.<domain>[:<port>]
```
'';
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" ];
})
];
}

View file

@ -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 = <path/to/ldapUserPasswordSecret>;
jwtSecretFile = <path/to/ldapJwtSecret>;
};
```
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 = <path/to/ldapUserPasswordSecret>;
};
```
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 = <path/to/autheliaJwtSecret>;
ldapAdminPasswordFile = <path/to/ldapUserPasswordSecret>;
sessionSecretFile = <path/to/autheliaSessionSecret>;
storageEncryptionKeyFile = <path/to/autheliaStorageEncryptionKeySecret>;
identityProvidersOIDCHMACSecretFile = <path/to/providersOIDCHMACSecret>;
identityProvidersOIDCIssuerPrivateKeyFile = <path/to/providersOIDCIssuerSecret>;
};
};
```
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 = <path/to/oidcForgejoSharedSecret>;
secretFileForAuthelia = <path/to/oidcForgejoSharedSecret>;
};
```
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@
```

View file

@ -112,6 +112,12 @@ in
default = "jellyfin_user"; 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 { secretFile = lib.mkOption {
type = lib.types.path; type = lib.types.path;
description = "File containing the OIDC shared secret."; description = "File containing the OIDC shared secret.";
@ -419,7 +425,7 @@ in
client_name = "Jellyfin"; client_name = "Jellyfin";
client_secret.source = cfg.sso.secretFile; client_secret.source = cfg.sso.secretFile;
public = false; 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}" ]; redirect_uris = [ "https://${cfg.subdomain}.${cfg.domain}/sso/OID/r/${cfg.sso.provider}" ];
} }
]; ];

145
test/services/forgejo.nix Normal file
View file

@ -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;
};
}