MEDIUM: startup: add automatic chroot feature

It is now possible to use "chroot auto" in the configuration. This lets
haproxy create an anonymous (cleaned up after the process terminates)
and read-only directory for chroot. This directory is created in /tmp;
we might want to support creating it in a different directory in the
future, either by respecting $TMPDIR or by allowing an optional
directory after the "auto" keyword.
This commit is contained in:
Maxime Henrion 2026-05-12 10:21:40 -04:00 committed by Willy Tarreau
parent 2d2980408f
commit 641fe4f119
2 changed files with 55 additions and 12 deletions

View file

@ -2126,13 +2126,21 @@ ca-base <dir>
directives. Absolute locations specified in "ca-file", "ca-verify-file" and
"crl-file" prevail and ignore "ca-base".
chroot <jail dir>
chroot { <jail dir> | auto }
Changes current directory to <jail dir> and performs a chroot() there before
dropping privileges. This increases the security level in case an unknown
vulnerability would be exploited, since it would make it very hard for the
attacker to exploit the system. This only works when the process is started
with superuser privileges. It is important to ensure that <jail_dir> is both
empty and non-writable to anyone.
attacker to exploit the system. It is important to ensure that <jail dir>
is both empty and non-writable to anyone. When the process is started with
superuser privileges, the chroot() is performed directly. On Linux, when
started unprivileged, haproxy attempts to perform it from inside a new
user namespace created with unshare(CLONE_NEWUSER); if that mechanism is
unavailable the chroot() will fail with the usual error.
As a special case, <jail dir> may be set to "auto", in which case haproxy
creates an anonymous temporary directory, unlinks it, and chroots into it.
The resulting jail has no name in the filesystem and is empty and read-only,
removing the need to prepare a dedicated jail directory.
close-spread-time <time>
Define a time window during which idle connections and active connections

View file

@ -3327,6 +3327,44 @@ static void setup_user_ns(uid_t euid, gid_t egid)
}
#endif
static int do_chroot(const char *prog, const char *path)
{
const char *chroot_dir = path;
int error = 0;
if (strcmp(path, "auto") == 0) {
/* When "chroot auto" is used, we attempt to chroot to an
* anonymous and read-only directory.
*/
char tmpdir[] = "/tmp/haproxy.XXXXXX";
chroot_dir = mkdtemp(tmpdir);
if (chroot_dir == NULL) {
ha_alert("[%s.main()] Cannot create(%s) for chroot auto.\n",
prog, tmpdir);
return -1;
}
error = chdir(tmpdir);
/* We can call rmdir() here; we hold a reference to the
* directory since it is our CWD (and if chdir() failed we still
* want to remove the directory).
*/
DISGUISE(rmdir(tmpdir));
if (!error)
error = chroot(".");
} else {
error = chroot(path);
}
if (!error)
error = chdir("/");
if (error) {
ha_alert("[%s.main()] Cannot chroot(%s).\n", prog, chroot_dir);
return -1;
}
return 0;
}
int main(int argc, char **argv)
{
struct rlimit limit;
@ -3659,14 +3697,11 @@ int main(int argc, char **argv)
/* Must chroot and setgid/setuid in the children */
/* chroot if needed */
if (global.chroot != NULL) {
if (chroot(global.chroot) == -1 || chdir("/") == -1) {
ha_alert("[%s.main()] Cannot chroot(%s).\n", argv[0], global.chroot);
if (nb_oldpids)
tell_old_pids(SIGTTIN);
protocol_unbind_all();
exit(1);
}
if (global.chroot != NULL && do_chroot(argv[0], global.chroot) != 0) {
if (nb_oldpids)
tell_old_pids(SIGTTIN);
protocol_unbind_all();
exit(1);
}
ha_free(&global.chroot);