Files
Sam Pointon 50281dab55 nixos/haproxy: drop redundant and faulty config check
See https://github.com/haproxy/haproxy/issues/3341.

There are multiple issues with the current code:

0. It doesn't work at all if you're doing socket activation and passing 
file descriptors to HAProxy to bind to: systemd doesn't pass the FDs in 
ExecStartPre, but haproxy -c requires them.

1. More generally, HAProxy upstream recommends making the invocation of 
haproxy -c identical to the actual invocation of haproxy. This is 
awkward to achieve without putting the check immediately before the 
service start... but what's the point in that when it fails anyway?

2. Hence, upstream recommends just removing the check and letting the 
(re)start fail if it's going to.

So, let's just do that and remove it: the check isn't load bearing and 
is actively causing problems.
2026-06-09 14:39:28 +01:00

107 lines
3.0 KiB
Nix

{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.haproxy;
haproxyCfg = pkgs.writeText "haproxy.conf" ''
global
# needed for hot-reload to work without dropping packets in multi-worker mode
stats socket /run/haproxy/haproxy.sock mode 600 expose-fd listeners level user
${cfg.config}
'';
in
{
options = {
services.haproxy = {
enable = lib.mkEnableOption "HAProxy, the reliable, high performance TCP/HTTP load balancer";
package = lib.mkPackageOption pkgs "haproxy" { };
user = lib.mkOption {
type = lib.types.str;
default = "haproxy";
description = "User account under which haproxy runs.";
};
group = lib.mkOption {
type = lib.types.str;
default = "haproxy";
description = "Group account under which haproxy runs.";
};
config = lib.mkOption {
type = lib.types.nullOr lib.types.lines;
default = null;
description = ''
Contents of the HAProxy configuration file,
{file}`haproxy.conf`.
'';
};
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.config != null;
message = "You must provide services.haproxy.config.";
}
];
# configuration file indirection is needed to support reloading
environment.etc."haproxy.cfg".source = haproxyCfg;
systemd.services.haproxy = {
description = "HAProxy";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
User = cfg.user;
Group = cfg.group;
Type = "notify";
ExecStartPre = [
# when the master process receives USR2, it reloads itself using exec(argv[0]),
# so we create a symlink there and update it before reloading
"${pkgs.coreutils}/bin/ln -sf ${lib.getExe cfg.package} /run/haproxy/haproxy"
];
ExecStart = "/run/haproxy/haproxy -Ws -f /etc/haproxy.cfg -p /run/haproxy/haproxy.pid";
# support reloading
ExecReload = [
"${pkgs.coreutils}/bin/ln -sf ${lib.getExe cfg.package} /run/haproxy/haproxy"
"${pkgs.coreutils}/bin/kill -USR2 $MAINPID"
];
KillMode = "mixed";
SuccessExitStatus = "143";
Restart = "always";
RuntimeDirectory = "haproxy";
# upstream hardening options
NoNewPrivileges = true;
ProtectHome = true;
ProtectSystem = "strict";
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
SystemCallFilter = "~@cpu-emulation @keyring @module @obsolete @raw-io @reboot @swap @sync";
# needed in case we bind to port < 1024
AmbientCapabilities = "CAP_NET_BIND_SERVICE";
};
};
users.users = lib.optionalAttrs (cfg.user == "haproxy") {
haproxy = {
group = cfg.group;
isSystemUser = true;
};
};
users.groups = lib.optionalAttrs (cfg.group == "haproxy") {
haproxy = { };
};
};
}