379 lines
11 KiB
Nix
379 lines
11 KiB
Nix
{ config, lib, pkgs, ... }:
|
|
|
|
let
|
|
cfg = config.services.burrow.forge;
|
|
forgejoCfg = config.services.forgejo;
|
|
forgejoExe = lib.getExe forgejoCfg.package;
|
|
forgejoWorkPath = forgejoCfg.stateDir;
|
|
forgejoCustomPath = "${forgejoWorkPath}/custom";
|
|
forgejoConfigFile = "${forgejoCustomPath}/conf/app.ini";
|
|
forgejoAdminArgs = "--config ${lib.escapeShellArg forgejoConfigFile} --work-path ${lib.escapeShellArg forgejoWorkPath} --custom-path ${lib.escapeShellArg forgejoCustomPath}";
|
|
homeRepoPath = "/${cfg.homeOwner}/${cfg.homeRepo}";
|
|
homeRepoUrl = "https://${cfg.gitDomain}${homeRepoPath}";
|
|
in
|
|
{
|
|
options.services.burrow.forge = {
|
|
enable = lib.mkEnableOption "the Burrow Forge host";
|
|
|
|
gitDomain = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "git.burrow.net";
|
|
description = "Public Forgejo domain.";
|
|
};
|
|
|
|
siteDomain = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "burrow.net";
|
|
description = "Root site domain.";
|
|
};
|
|
|
|
homeOwner = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "hackclub";
|
|
description = "Canonical Forgejo org/user for the Burrow home repository.";
|
|
};
|
|
|
|
homeRepo = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "burrow";
|
|
description = "Canonical Forgejo repository name for the Burrow home repository.";
|
|
};
|
|
|
|
contactEmail = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "contact@burrow.net";
|
|
description = "Operator contact email.";
|
|
};
|
|
|
|
nscAutoscalerDomain = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "nsc-autoscaler.burrow.net";
|
|
description = "Public webhook domain for the Forgejo Namespace autoscaler.";
|
|
};
|
|
|
|
adminUsername = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "contact";
|
|
description = "Initial Forgejo admin username.";
|
|
};
|
|
|
|
adminEmail = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "contact@burrow.net";
|
|
description = "Initial Forgejo admin email.";
|
|
};
|
|
|
|
adminPasswordFile = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "Host-local path to the plaintext bootstrap password file for the initial Forgejo admin.";
|
|
};
|
|
|
|
oidcDisplayName = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "burrow.net";
|
|
description = "Login button label for the Forgejo OIDC provider.";
|
|
};
|
|
|
|
oidcClientId = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "git.burrow.net";
|
|
description = "OIDC client ID that Forgejo should use against Authentik.";
|
|
};
|
|
|
|
oidcClientSecretFile = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Host-local path to the Forgejo OIDC client secret.";
|
|
};
|
|
|
|
oidcDiscoveryUrl = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "https://auth.burrow.net/application/o/git/.well-known/openid-configuration";
|
|
description = "OpenID Connect discovery URL for the Forgejo login source.";
|
|
};
|
|
|
|
authorizedKeys = lib.mkOption {
|
|
type = with lib.types; listOf str;
|
|
default = [ ];
|
|
description = "SSH keys allowed for root login and operational bootstrap.";
|
|
};
|
|
};
|
|
|
|
config = lib.mkIf cfg.enable {
|
|
networking.hostName = "burrow-forge";
|
|
networking.useDHCP = lib.mkDefault true;
|
|
|
|
services.qemuGuest.enable = true;
|
|
|
|
boot.loader.grub = {
|
|
enable = true;
|
|
efiSupport = true;
|
|
efiInstallAsRemovable = true;
|
|
device = "nodev";
|
|
};
|
|
|
|
fileSystems."/boot".neededForBoot = true;
|
|
|
|
services.postgresql = {
|
|
enable = true;
|
|
package = pkgs.postgresql_16;
|
|
};
|
|
|
|
services.openssh = {
|
|
enable = true;
|
|
settings = {
|
|
PasswordAuthentication = false;
|
|
KbdInteractiveAuthentication = false;
|
|
PermitRootLogin = "prohibit-password";
|
|
};
|
|
};
|
|
|
|
users.users.root.openssh.authorizedKeys.keys = cfg.authorizedKeys;
|
|
|
|
networking.firewall.allowedTCPPorts = [
|
|
22
|
|
80
|
|
443
|
|
2222
|
|
];
|
|
|
|
services.forgejo = {
|
|
enable = true;
|
|
database = {
|
|
type = "postgres";
|
|
createDatabase = true;
|
|
};
|
|
lfs.enable = true;
|
|
settings = {
|
|
server = {
|
|
DOMAIN = cfg.gitDomain;
|
|
ROOT_URL = "https://${cfg.gitDomain}/";
|
|
HTTP_PORT = 3000;
|
|
SSH_DOMAIN = cfg.gitDomain;
|
|
SSH_PORT = 2222;
|
|
START_SSH_SERVER = true;
|
|
};
|
|
|
|
service = {
|
|
DISABLE_REGISTRATION = true;
|
|
REQUIRE_SIGNIN_VIEW = false;
|
|
DEFAULT_ALLOW_CREATE_ORGANIZATION = false;
|
|
ENABLE_NOTIFY_MAIL = false;
|
|
NO_REPLY_ADDRESS = cfg.adminEmail;
|
|
};
|
|
|
|
session = {
|
|
COOKIE_SECURE = true;
|
|
SAME_SITE = "strict";
|
|
};
|
|
|
|
openid = {
|
|
ENABLE_OPENID_SIGNIN = false;
|
|
ENABLE_OPENID_SIGNUP = false;
|
|
};
|
|
|
|
actions = {
|
|
ENABLED = true;
|
|
};
|
|
|
|
repository = {
|
|
DEFAULT_BRANCH = "main";
|
|
ENABLE_PUSH_CREATE_USER = false;
|
|
};
|
|
|
|
ui = {
|
|
DEFAULT_THEME = "forgejo-auto";
|
|
};
|
|
};
|
|
};
|
|
|
|
services.caddy = {
|
|
enable = true;
|
|
email = cfg.contactEmail;
|
|
virtualHosts =
|
|
{
|
|
"${cfg.gitDomain}".extraConfig = ''
|
|
encode gzip zstd
|
|
@root path /
|
|
redir @root ${homeRepoPath} 308
|
|
reverse_proxy 127.0.0.1:${toString config.services.forgejo.settings.server.HTTP_PORT}
|
|
'';
|
|
"${cfg.siteDomain}".extraConfig = ''
|
|
@root path /
|
|
redir @root ${homeRepoUrl} 308
|
|
respond 404
|
|
'';
|
|
}
|
|
// lib.optionalAttrs (
|
|
config.services.burrow.forgejoNsc.enable && config.services.burrow.forgejoNsc.autoscaler.enable
|
|
) {
|
|
"${cfg.nscAutoscalerDomain}".extraConfig = ''
|
|
encode gzip zstd
|
|
reverse_proxy 127.0.0.1:8090
|
|
'';
|
|
};
|
|
};
|
|
|
|
systemd.services.burrow-forgejo-bootstrap = {
|
|
description = "Seed the initial Burrow Forgejo admin account";
|
|
after = [ "forgejo.service" ];
|
|
requires = [ "forgejo.service" ];
|
|
wantedBy = [ "multi-user.target" ];
|
|
path = [
|
|
forgejoCfg.package
|
|
pkgs.coreutils
|
|
pkgs.gnugrep
|
|
];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
User = forgejoCfg.user;
|
|
Group = forgejoCfg.group;
|
|
WorkingDirectory = forgejoCfg.stateDir;
|
|
};
|
|
script = ''
|
|
set -euo pipefail
|
|
|
|
if [ ! -s ${lib.escapeShellArg cfg.adminPasswordFile} ]; then
|
|
echo "bootstrap password file is missing; skipping admin bootstrap" >&2
|
|
exit 0
|
|
fi
|
|
|
|
password="$(tr -d '\r\n' < ${lib.escapeShellArg cfg.adminPasswordFile})"
|
|
if [ -z "$password" ]; then
|
|
echo "bootstrap password file is empty; skipping admin bootstrap" >&2
|
|
exit 0
|
|
fi
|
|
|
|
log_file="$(mktemp)"
|
|
trap 'rm -f "$log_file"' EXIT
|
|
|
|
if ! ${forgejoExe} admin user create \
|
|
${forgejoAdminArgs} \
|
|
--admin \
|
|
--username ${lib.escapeShellArg cfg.adminUsername} \
|
|
--email ${lib.escapeShellArg cfg.adminEmail} \
|
|
--password "$password" \
|
|
--must-change-password=false >"$log_file" 2>&1; then
|
|
if grep -qi "already exists" "$log_file"; then
|
|
${forgejoExe} admin user change-password \
|
|
${forgejoAdminArgs} \
|
|
--username ${lib.escapeShellArg cfg.adminUsername} \
|
|
--password "$password" \
|
|
--must-change-password=false
|
|
else
|
|
cat "$log_file" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
'';
|
|
};
|
|
|
|
systemd.services.burrow-forgejo-oidc-bootstrap = lib.mkIf (cfg.oidcClientSecretFile != null) {
|
|
description = "Seed the Burrow Forgejo OIDC login source";
|
|
after = [
|
|
"forgejo.service"
|
|
"postgresql.service"
|
|
] ++ lib.optionals config.services.burrow.authentik.enable [
|
|
"burrow-authentik-ready.service"
|
|
];
|
|
wants = lib.optionals config.services.burrow.authentik.enable [
|
|
"burrow-authentik-ready.service"
|
|
];
|
|
requires = [
|
|
"forgejo.service"
|
|
"postgresql.service"
|
|
];
|
|
wantedBy = [ "multi-user.target" ];
|
|
restartTriggers = [
|
|
cfg.oidcClientSecretFile
|
|
];
|
|
path = [
|
|
pkgs.coreutils
|
|
pkgs.gnugrep
|
|
pkgs.jq
|
|
pkgs.postgresql
|
|
];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
User = forgejoCfg.user;
|
|
Group = forgejoCfg.group;
|
|
WorkingDirectory = forgejoCfg.stateDir;
|
|
};
|
|
script = ''
|
|
set -euo pipefail
|
|
|
|
if [ ! -s ${lib.escapeShellArg cfg.oidcClientSecretFile} ]; then
|
|
echo "Forgejo OIDC client secret missing: ${cfg.oidcClientSecretFile}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
ready=0
|
|
for attempt in $(seq 1 60); do
|
|
if ${pkgs.postgresql}/bin/psql -h /run/postgresql -U forgejo forgejo -tAc \
|
|
"SELECT 1 FROM pg_tables WHERE schemaname='public' AND tablename='login_source';" \
|
|
| grep -q 1; then
|
|
ready=1
|
|
break
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
if [ "$ready" -ne 1 ]; then
|
|
echo "Forgejo login_source table did not become ready" >&2
|
|
exit 1
|
|
fi
|
|
|
|
oidc_secret="$(${pkgs.coreutils}/bin/tr -d '\r\n' < ${lib.escapeShellArg cfg.oidcClientSecretFile})"
|
|
if [ -z "$oidc_secret" ]; then
|
|
echo "Forgejo OIDC client secret is empty" >&2
|
|
exit 1
|
|
fi
|
|
|
|
cfg_json="$(${pkgs.jq}/bin/jq -nc \
|
|
--arg client_id ${lib.escapeShellArg cfg.oidcClientId} \
|
|
--arg client_secret "$oidc_secret" \
|
|
--arg discovery_url ${lib.escapeShellArg cfg.oidcDiscoveryUrl} \
|
|
'{
|
|
Provider: "openidConnect",
|
|
ClientID: $client_id,
|
|
ClientSecret: $client_secret,
|
|
OpenIDConnectAutoDiscoveryURL: $discovery_url,
|
|
CustomURLMapping: null,
|
|
IconURL: "",
|
|
Scopes: ["openid", "profile", "email"],
|
|
AttributeSSHPublicKey: "",
|
|
RequiredClaimName: "",
|
|
RequiredClaimValue: "",
|
|
GroupClaimName: "",
|
|
AdminGroup: "",
|
|
GroupTeamMap: "",
|
|
GroupTeamMapRemoval: false,
|
|
RestrictedGroup: ""
|
|
}')"
|
|
|
|
${pkgs.postgresql}/bin/psql -v ON_ERROR_STOP=1 \
|
|
-h /run/postgresql -U forgejo forgejo \
|
|
-v oidc_name=${lib.escapeShellArg cfg.oidcDisplayName} \
|
|
-v cfg_json="$cfg_json" <<'SQL'
|
|
INSERT INTO login_source (
|
|
type, name, is_active, is_sync_enabled, cfg, created_unix, updated_unix
|
|
) VALUES (
|
|
6,
|
|
:'oidc_name',
|
|
TRUE,
|
|
FALSE,
|
|
:'cfg_json',
|
|
EXTRACT(EPOCH FROM NOW())::BIGINT,
|
|
EXTRACT(EPOCH FROM NOW())::BIGINT
|
|
)
|
|
ON CONFLICT (name) DO UPDATE SET
|
|
type = EXCLUDED.type,
|
|
is_active = TRUE,
|
|
is_sync_enabled = FALSE,
|
|
cfg = EXCLUDED.cfg,
|
|
updated_unix = EXCLUDED.updated_unix;
|
|
SQL
|
|
'';
|
|
};
|
|
};
|
|
}
|