nixos/sourcehut: full rewrite, with fixes and hardening
This commit is contained in:
parent
ddaef72e49
commit
8ed7fd0f3a
3 changed files with 1662 additions and 201 deletions
File diff suppressed because it is too large
Load diff
|
@ -1,66 +1,375 @@
|
|||
{ config, pkgs, lib }:
|
||||
serviceCfg: serviceDrv: iniKey: attrs:
|
||||
srv:
|
||||
{ configIniOfService
|
||||
, srvsrht ? "${srv}srht" # Because "buildsrht" does not follow that pattern (missing an "s").
|
||||
, iniKey ? "${srv}.sr.ht"
|
||||
, webhooks ? false
|
||||
, extraTimers ? {}
|
||||
, mainService ? {}
|
||||
, extraServices ? {}
|
||||
, extraConfig ? {}
|
||||
, port
|
||||
}:
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
let
|
||||
inherit (config.services) postgresql;
|
||||
redis = config.services.redis.servers."sourcehut-${srvsrht}";
|
||||
inherit (config.users) users;
|
||||
cfg = config.services.sourcehut;
|
||||
cfgIni = cfg.settings."${iniKey}";
|
||||
pgSuperUser = config.services.postgresql.superUser;
|
||||
|
||||
setupDB = pkgs.writeScript "${serviceDrv.pname}-gen-db" ''
|
||||
#! ${cfg.python}/bin/python
|
||||
from ${serviceDrv.pname}.app import db
|
||||
db.create()
|
||||
'';
|
||||
configIni = configIniOfService srv;
|
||||
srvCfg = cfg.${srv};
|
||||
baseService = serviceName: { allowStripe ? false }: extraService: let
|
||||
runDir = "/run/sourcehut/${serviceName}";
|
||||
rootDir = "/run/sourcehut/chroots/${serviceName}";
|
||||
in
|
||||
mkMerge [ extraService {
|
||||
after = [ "network.target" ] ++
|
||||
optional cfg.postgresql.enable "postgresql.service" ++
|
||||
optional cfg.redis.enable "redis-sourcehut-${srvsrht}.service";
|
||||
requires =
|
||||
optional cfg.postgresql.enable "postgresql.service" ++
|
||||
optional cfg.redis.enable "redis-sourcehut-${srvsrht}.service";
|
||||
path = [ pkgs.gawk ];
|
||||
environment.HOME = runDir;
|
||||
serviceConfig = {
|
||||
User = mkDefault srvCfg.user;
|
||||
Group = mkDefault srvCfg.group;
|
||||
RuntimeDirectory = [
|
||||
"sourcehut/${serviceName}"
|
||||
# Used by *srht-keys which reads ../config.ini
|
||||
"sourcehut/${serviceName}/subdir"
|
||||
"sourcehut/chroots/${serviceName}"
|
||||
];
|
||||
RuntimeDirectoryMode = "2750";
|
||||
# No need for the chroot path once inside the chroot
|
||||
InaccessiblePaths = [ "-+${rootDir}" ];
|
||||
# g+rx is for group members (eg. fcgiwrap or nginx)
|
||||
# to read Git/Mercurial repositories, buildlogs, etc.
|
||||
# o+x is for intermediate directories created by BindPaths= and like,
|
||||
# as they're owned by root:root.
|
||||
UMask = "0026";
|
||||
RootDirectory = rootDir;
|
||||
RootDirectoryStartOnly = true;
|
||||
PrivateTmp = true;
|
||||
MountAPIVFS = true;
|
||||
# config.ini is looked up in there, before /etc/srht/config.ini
|
||||
# Note that it fails to be set in ExecStartPre=
|
||||
WorkingDirectory = mkDefault ("-"+runDir);
|
||||
BindReadOnlyPaths = [
|
||||
builtins.storeDir
|
||||
"/etc"
|
||||
"/run/booted-system"
|
||||
"/run/current-system"
|
||||
"/run/systemd"
|
||||
] ++
|
||||
optional cfg.postgresql.enable "/run/postgresql" ++
|
||||
optional cfg.redis.enable "/run/redis-sourcehut-${srvsrht}";
|
||||
# LoadCredential= are unfortunately not available in ExecStartPre=
|
||||
# Hence this one is run as root (the +) with RootDirectoryStartOnly=
|
||||
# to reach credentials wherever they are.
|
||||
# Note that each systemd service gets its own ${runDir}/config.ini file.
|
||||
ExecStartPre = mkBefore [("+"+pkgs.writeShellScript "${serviceName}-credentials" ''
|
||||
set -x
|
||||
# Replace values begining with a '<' by the content of the file whose name is after.
|
||||
gawk '{ if (match($0,/^([^=]+=)<(.+)/,m)) { getline f < m[2]; print m[1] f } else print $0 }' ${configIni} |
|
||||
${optionalString (!allowStripe) "gawk '!/^stripe-secret-key=/' |"}
|
||||
install -o ${srvCfg.user} -g root -m 400 /dev/stdin ${runDir}/config.ini
|
||||
'')];
|
||||
# The following options are only for optimizing:
|
||||
# systemd-analyze security
|
||||
AmbientCapabilities = "";
|
||||
CapabilityBoundingSet = "";
|
||||
# ProtectClock= adds DeviceAllow=char-rtc r
|
||||
DeviceAllow = "";
|
||||
LockPersonality = true;
|
||||
MemoryDenyWriteExecute = true;
|
||||
NoNewPrivileges = true;
|
||||
PrivateDevices = true;
|
||||
PrivateMounts = true;
|
||||
PrivateNetwork = mkDefault false;
|
||||
PrivateUsers = true;
|
||||
ProcSubset = "pid";
|
||||
ProtectClock = true;
|
||||
ProtectControlGroups = true;
|
||||
ProtectHome = true;
|
||||
ProtectHostname = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectProc = "invisible";
|
||||
ProtectSystem = "strict";
|
||||
RemoveIPC = true;
|
||||
RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
|
||||
RestrictNamespaces = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictSUIDSGID = true;
|
||||
#SocketBindAllow = [ "tcp:${toString srvCfg.port}" "tcp:${toString srvCfg.prometheusPort}" ];
|
||||
#SocketBindDeny = "any";
|
||||
SystemCallFilter = [
|
||||
"@system-service"
|
||||
"~@aio" "~@keyring" "~@memlock" "~@privileged" "~@resources" "~@timer"
|
||||
"@chown" "@setuid"
|
||||
];
|
||||
SystemCallArchitectures = "native";
|
||||
};
|
||||
} ];
|
||||
in
|
||||
with serviceCfg; with lib; recursiveUpdate
|
||||
{
|
||||
environment.HOME = statePath;
|
||||
path = [ config.services.postgresql.package ] ++ (attrs.path or [ ]);
|
||||
restartTriggers = [ config.environment.etc."sr.ht/config.ini".source ];
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
User = user;
|
||||
Group = user;
|
||||
Restart = "always";
|
||||
WorkingDirectory = statePath;
|
||||
} // (if (cfg.statePath == "/var/lib/sourcehut/${serviceDrv.pname}") then {
|
||||
StateDirectory = [ "sourcehut/${serviceDrv.pname}" ];
|
||||
} else {})
|
||||
;
|
||||
options.services.sourcehut.${srv} = {
|
||||
enable = mkEnableOption "${srv} service";
|
||||
|
||||
preStart = ''
|
||||
if ! test -e ${statePath}/db; then
|
||||
# Setup the initial database
|
||||
${setupDB}
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = srvsrht;
|
||||
description = ''
|
||||
User for ${srv}.sr.ht.
|
||||
'';
|
||||
};
|
||||
|
||||
# Set the initial state of the database for future database upgrades
|
||||
if test -e ${cfg.python}/bin/${serviceDrv.pname}-migrate; then
|
||||
# Run alembic stamp head once to tell alembic the schema is up-to-date
|
||||
${cfg.python}/bin/${serviceDrv.pname}-migrate stamp head
|
||||
fi
|
||||
group = mkOption {
|
||||
type = types.str;
|
||||
default = srvsrht;
|
||||
description = ''
|
||||
Group for ${srv}.sr.ht.
|
||||
Membership grants access to the Git/Mercurial repositories by default,
|
||||
but not to the config.ini file (where secrets are).
|
||||
'';
|
||||
};
|
||||
|
||||
printf "%s" "${serviceDrv.version}" > ${statePath}/db
|
||||
fi
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = port;
|
||||
description = ''
|
||||
Port on which the "${srv}" backend should listen.
|
||||
'';
|
||||
};
|
||||
|
||||
# Update copy of each users' profile to the latest
|
||||
# See https://lists.sr.ht/~sircmpwn/sr.ht-admins/<20190302181207.GA13778%40cirno.my.domain>
|
||||
if ! test -e ${statePath}/webhook; then
|
||||
# Update ${iniKey}'s users' profile copy to the latest
|
||||
${cfg.python}/bin/srht-update-profiles ${iniKey}
|
||||
redis = {
|
||||
host = mkOption {
|
||||
type = types.str;
|
||||
default = "unix:/run/redis-sourcehut-${srvsrht}/redis.sock?db=0";
|
||||
example = "redis://shared.wireguard:6379/0";
|
||||
description = ''
|
||||
The redis host URL. This is used for caching and temporary storage, and must
|
||||
be shared between nodes (e.g. git1.sr.ht and git2.sr.ht), but need not be
|
||||
shared between services. It may be shared between services, however, with no
|
||||
ill effect, if this better suits your infrastructure.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
touch ${statePath}/webhook
|
||||
fi
|
||||
postgresql = {
|
||||
database = mkOption {
|
||||
type = types.str;
|
||||
default = "${srv}.sr.ht";
|
||||
description = ''
|
||||
PostgreSQL database name for the ${srv}.sr.ht service,
|
||||
used if <xref linkend="opt-services.sourcehut.postgresql.enable"/> is <literal>true</literal>.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
${optionalString (builtins.hasAttr "migrate-on-upgrade" cfgIni && cfgIni.migrate-on-upgrade == "yes") ''
|
||||
if [ "$(cat ${statePath}/db)" != "${serviceDrv.version}" ]; then
|
||||
# Manage schema migrations using alembic
|
||||
${cfg.python}/bin/${serviceDrv.pname}-migrate -a upgrade head
|
||||
gunicorn = {
|
||||
extraArgs = mkOption {
|
||||
type = with types; listOf str;
|
||||
default = ["--timeout 120" "--workers 1" "--log-level=info"];
|
||||
description = "Extra arguments passed to Gunicorn.";
|
||||
};
|
||||
};
|
||||
} // optionalAttrs webhooks {
|
||||
webhooks = {
|
||||
extraArgs = mkOption {
|
||||
type = with types; listOf str;
|
||||
default = ["--loglevel DEBUG" "--pool eventlet" "--without-heartbeat"];
|
||||
description = "Extra arguments passed to the Celery responsible for webhooks.";
|
||||
};
|
||||
celeryConfig = mkOption {
|
||||
type = types.lines;
|
||||
default = "";
|
||||
description = "Content of the <literal>celeryconfig.py</literal> used by the Celery responsible for webhooks.";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# Mark down current package version
|
||||
printf "%s" "${serviceDrv.version}" > ${statePath}/db
|
||||
fi
|
||||
''}
|
||||
config = lib.mkIf (cfg.enable && srvCfg.enable) (mkMerge [ extraConfig {
|
||||
users = {
|
||||
users = {
|
||||
"${srvCfg.user}" = {
|
||||
isSystemUser = true;
|
||||
group = mkDefault srvCfg.group;
|
||||
description = mkDefault "sourcehut user for ${srv}.sr.ht";
|
||||
};
|
||||
};
|
||||
groups = {
|
||||
"${srvCfg.group}" = { };
|
||||
} // optionalAttrs (cfg.postgresql.enable
|
||||
&& hasSuffix "0" (postgresql.settings.unix_socket_permissions or "")) {
|
||||
"postgres".members = [ srvCfg.user ];
|
||||
} // optionalAttrs (cfg.redis.enable
|
||||
&& hasSuffix "0" (redis.settings.unixsocketperm or "")) {
|
||||
"redis-sourcehut-${srvsrht}".members = [ srvCfg.user ];
|
||||
};
|
||||
};
|
||||
|
||||
${attrs.preStart or ""}
|
||||
'';
|
||||
services.nginx = mkIf cfg.nginx.enable {
|
||||
virtualHosts."${srv}.${cfg.settings."sr.ht".global-domain}" = mkMerge [ {
|
||||
forceSSL = true;
|
||||
locations."/".proxyPass = "http://${cfg.listenAddress}:${toString srvCfg.port}";
|
||||
locations."/static" = {
|
||||
root = "${pkgs.sourcehut.${srvsrht}}/${pkgs.sourcehut.python.sitePackages}/${srvsrht}";
|
||||
extraConfig = mkDefault ''
|
||||
expires 30d;
|
||||
'';
|
||||
};
|
||||
} cfg.nginx.virtualHost ];
|
||||
};
|
||||
|
||||
services.postgresql = mkIf cfg.postgresql.enable {
|
||||
authentication = ''
|
||||
local ${srvCfg.postgresql.database} ${srvCfg.user} trust
|
||||
'';
|
||||
ensureDatabases = [ srvCfg.postgresql.database ];
|
||||
ensureUsers = map (name: {
|
||||
inherit name;
|
||||
ensurePermissions = { "DATABASE \"${srvCfg.postgresql.database}\"" = "ALL PRIVILEGES"; };
|
||||
}) [srvCfg.user];
|
||||
};
|
||||
|
||||
services.sourcehut.services = mkDefault (filter (s: cfg.${s}.enable)
|
||||
[ "builds" "dispatch" "git" "hg" "hub" "lists" "man" "meta" "pages" "paste" "todo" ]);
|
||||
|
||||
services.sourcehut.settings = mkMerge [
|
||||
{
|
||||
"${srv}.sr.ht".origin = mkDefault "https://${srv}.${cfg.settings."sr.ht".global-domain}";
|
||||
}
|
||||
|
||||
(mkIf cfg.postgresql.enable {
|
||||
"${srv}.sr.ht".connection-string = mkDefault "postgresql:///${srvCfg.postgresql.database}?user=${srvCfg.user}&host=/run/postgresql";
|
||||
})
|
||||
];
|
||||
|
||||
services.redis.servers."sourcehut-${srvsrht}" = mkIf cfg.redis.enable {
|
||||
enable = true;
|
||||
databases = 3;
|
||||
syslog = true;
|
||||
# TODO: set a more informed value
|
||||
save = mkDefault [ [1800 10] [300 100] ];
|
||||
settings = {
|
||||
# TODO: set a more informed value
|
||||
maxmemory = "128MB";
|
||||
maxmemory-policy = "volatile-ttl";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services = mkMerge [
|
||||
{
|
||||
"${srvsrht}" = baseService srvsrht { allowStripe = srv == "meta"; } (mkMerge [
|
||||
{
|
||||
description = "sourcehut ${srv}.sr.ht website service";
|
||||
before = optional cfg.nginx.enable "nginx.service";
|
||||
wants = optional cfg.nginx.enable "nginx.service";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
path = optional cfg.postgresql.enable postgresql.package;
|
||||
# Beware: change in credentials' content will not trigger restart.
|
||||
restartTriggers = [ configIni ];
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
Restart = mkDefault "always";
|
||||
#RestartSec = mkDefault "2min";
|
||||
StateDirectory = [ "sourcehut/${srvsrht}" ];
|
||||
StateDirectoryMode = "2750";
|
||||
ExecStart = "${cfg.python}/bin/gunicorn ${srvsrht}.app:app --name ${srvsrht} --bind ${cfg.listenAddress}:${toString srvCfg.port} " + concatStringsSep " " srvCfg.gunicorn.extraArgs;
|
||||
};
|
||||
preStart = let
|
||||
version = pkgs.sourcehut.${srvsrht}.version;
|
||||
stateDir = "/var/lib/sourcehut/${srvsrht}";
|
||||
in mkBefore ''
|
||||
set -x
|
||||
# Use the /run/sourcehut/${srvsrht}/config.ini
|
||||
# installed by a previous ExecStartPre= in baseService
|
||||
cd /run/sourcehut/${srvsrht}
|
||||
|
||||
if test ! -e ${stateDir}/db; then
|
||||
# Setup the initial database.
|
||||
# Note that it stamps the alembic head afterward
|
||||
${cfg.python}/bin/${srvsrht}-initdb
|
||||
echo ${version} >${stateDir}/db
|
||||
fi
|
||||
|
||||
${optionalString cfg.settings.${iniKey}.migrate-on-upgrade ''
|
||||
if [ "$(cat ${stateDir}/db)" != "${version}" ]; then
|
||||
# Manage schema migrations using alembic
|
||||
${cfg.python}/bin/${srvsrht}-migrate -a upgrade head
|
||||
echo ${version} >${stateDir}/db
|
||||
fi
|
||||
''}
|
||||
|
||||
# Update copy of each users' profile to the latest
|
||||
# See https://lists.sr.ht/~sircmpwn/sr.ht-admins/<20190302181207.GA13778%40cirno.my.domain>
|
||||
if test ! -e ${stateDir}/webhook; then
|
||||
# Update ${iniKey}'s users' profile copy to the latest
|
||||
${cfg.python}/bin/srht-update-profiles ${iniKey}
|
||||
touch ${stateDir}/webhook
|
||||
fi
|
||||
'';
|
||||
} mainService ]);
|
||||
}
|
||||
|
||||
(mkIf webhooks {
|
||||
"${srvsrht}-webhooks" = baseService "${srvsrht}-webhooks" {}
|
||||
{
|
||||
description = "sourcehut ${srv}.sr.ht webhooks service";
|
||||
after = [ "${srvsrht}.service" ];
|
||||
wantedBy = [ "${srvsrht}.service" ];
|
||||
partOf = [ "${srvsrht}.service" ];
|
||||
preStart = ''
|
||||
cp ${pkgs.writeText "${srvsrht}-webhooks-celeryconfig.py" srvCfg.webhooks.celeryConfig} \
|
||||
/run/sourcehut/${srvsrht}-webhooks/celeryconfig.py
|
||||
'';
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
Restart = "always";
|
||||
ExecStart = "${cfg.python}/bin/celery --app ${srvsrht}.webhooks worker --hostname ${srvsrht}-webhooks@%%h " + concatStringsSep " " srvCfg.webhooks.extraArgs;
|
||||
# Avoid crashing: os.getloadavg()
|
||||
ProcSubset = mkForce "all";
|
||||
};
|
||||
};
|
||||
})
|
||||
|
||||
(mapAttrs (timerName: timer: (baseService timerName {} (mkMerge [
|
||||
{
|
||||
description = "sourcehut ${timerName} service";
|
||||
after = [ "network.target" "${srvsrht}.service" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${cfg.python}/bin/${timerName}";
|
||||
};
|
||||
}
|
||||
(timer.service or {})
|
||||
]))) extraTimers)
|
||||
|
||||
(mapAttrs (serviceName: extraService: baseService serviceName {} (mkMerge [
|
||||
{
|
||||
description = "sourcehut ${serviceName} service";
|
||||
# So that extraServices have the PostgreSQL database initialized.
|
||||
after = [ "${srvsrht}.service" ];
|
||||
wantedBy = [ "${srvsrht}.service" ];
|
||||
partOf = [ "${srvsrht}.service" ];
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
Restart = mkDefault "always";
|
||||
};
|
||||
}
|
||||
extraService
|
||||
])) extraServices)
|
||||
];
|
||||
|
||||
systemd.timers = mapAttrs (timerName: timer:
|
||||
{
|
||||
description = "sourcehut timer for ${timerName}";
|
||||
wantedBy = [ "timers.target" ];
|
||||
inherit (timer) timerConfig;
|
||||
}) extraTimers;
|
||||
} ]);
|
||||
}
|
||||
(builtins.removeAttrs attrs [ "path" "preStart" ])
|
||||
|
|
|
@ -14,13 +14,12 @@
|
|||
<title>Basic usage</title>
|
||||
<para>
|
||||
Sourcehut is a Python and Go based set of applications.
|
||||
<literal><link linkend="opt-services.sourcehut.enable">services.sourcehut</link></literal>
|
||||
by default will use
|
||||
This NixOS module also provides basic configuration integrating Sourcehut into locally running
|
||||
<literal><link linkend="opt-services.nginx.enable">services.nginx</link></literal>,
|
||||
<literal><link linkend="opt-services.nginx.enable">services.redis</link></literal>,
|
||||
<literal><link linkend="opt-services.nginx.enable">services.cron</link></literal>,
|
||||
<literal><link linkend="opt-services.redis.servers">services.redis.servers.sourcehut</link></literal>,
|
||||
<literal><link linkend="opt-services.postfix.enable">services.postfix</link></literal>
|
||||
and
|
||||
<literal><link linkend="opt-services.postgresql.enable">services.postgresql</link></literal>.
|
||||
<literal><link linkend="opt-services.postgresql.enable">services.postgresql</link></literal> services.
|
||||
</para>
|
||||
|
||||
<para>
|
||||
|
@ -42,18 +41,23 @@ in {
|
|||
|
||||
services.sourcehut = {
|
||||
<link linkend="opt-services.sourcehut.enable">enable</link> = true;
|
||||
<link linkend="opt-services.sourcehut.originBase">originBase</link> = fqdn;
|
||||
<link linkend="opt-services.sourcehut.services">services</link> = [ "meta" "man" "git" ];
|
||||
<link linkend="opt-services.sourcehut.git.enable">git.enable</link> = true;
|
||||
<link linkend="opt-services.sourcehut.man.enable">man.enable</link> = true;
|
||||
<link linkend="opt-services.sourcehut.meta.enable">meta.enable</link> = true;
|
||||
<link linkend="opt-services.sourcehut.nginx.enable">nginx.enable</link> = true;
|
||||
<link linkend="opt-services.sourcehut.postfix.enable">postfix.enable</link> = true;
|
||||
<link linkend="opt-services.sourcehut.postgresql.enable">postgresql.enable</link> = true;
|
||||
<link linkend="opt-services.sourcehut.redis.enable">redis.enable</link> = true;
|
||||
<link linkend="opt-services.sourcehut.settings">settings</link> = {
|
||||
"sr.ht" = {
|
||||
environment = "production";
|
||||
global-domain = fqdn;
|
||||
origin = "https://${fqdn}";
|
||||
# Produce keys with srht-keygen from <package>sourcehut.coresrht</package>.
|
||||
network-key = "SECRET";
|
||||
service-key = "SECRET";
|
||||
network-key = "/run/keys/path/to/network-key";
|
||||
service-key = "/run/keys/path/to/service-key";
|
||||
};
|
||||
webhooks.private-key= "SECRET";
|
||||
webhooks.private-key= "/run/keys/path/to/webhook-key";
|
||||
};
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue