NAME

Fugu::Privdrop - permanent drop of root privileges

SYNOPSIS

use Fugu::Privdrop;

Fugu::Privdrop->prepare_statedir(
    path => '/var/run/myapp',
    user => '_myapp',
);
Fugu::Privdrop->drop_privileges(user => '_myapp');

DESCRIPTION

Fugu::Privdrop does the privilege drop that an OpenBSD daemon does when its privileged work is complete. The daemon binds the reserved port and prepares the state it must own. Then the daemon gives up root permanently and runs the event loop as an unprivileged user.

The module keeps no state and has two class methods.

prepare_statedir

prepare_statedir(%args) creates the state directory when it is absent, sets its mode, and gives it and the files inside to the unprivileged user. Root runs this before the drop.

The create step is not a first-install special case. OpenBSD clears /var/run at every boot, so a daemon with a state directory there finds it absent on each start.

These are the arguments:

path

The state directory. This argument is necessary.

user

The name of the user that owns the directory after the call. This argument is necessary.

group

The name of the group. If you omit it, the method uses the user's primary group.

mode

The directory mode. The default is 0700.

on_warn

A code reference that the method calls with a message for each problem. A file inside the directory that stays unchanged is a warning, not a failure: the daemon may never need that file.

drop_privileges

drop_privileges(%args) switches the process to the given user and group. It then makes sure that the process cannot get root again.

These are the arguments:

user

The name of the user to become. This argument is necessary.

group

The name of the group to become. This argument is optional. If you omit it, the method uses the user's primary group.

keep_groups

Keep the supplementary groups that the process inherits from root. The default is 0, which reduces the group list to the one group. On OpenBSD, a value of 1 is how a daemon keeps access to the mdnsd(8) socket after it drops to its own user.

The method reads both user IDs first, and acts on three cases. When the real and the effective user ID are both 0, the method does the drop. When neither ID is 0, the method changes nothing and returns 0. When exactly one ID is 0, the method dies: a process in that mixed state can call seteuid(2) and get root back, and the method cannot guess which ID the caller wants.

For the drop, the method resolves the user and the group, and it dies when the resolved user ID is 0. It calls setgid(2), then sets the supplementary groups with setgroups(2), then calls setuid(2), and sets the real and effective IDs.

The method then verifies each ID against the target, in this order:

  1. The real group ID equals the target group ID.

  2. The effective group ID equals the target group ID.

  3. With keep_groups 0, every member of the supplementary group list equals the target group ID.

  4. The real user ID equals the target user ID.

  5. The effective user ID equals the target user ID.

The method then calls setuid(2) with 0, and verifies every ID one more time. The second verification proves that the attempt to get root back changed nothing.

RETURN VALUES

prepare_statedir() returns 1 on success. It returns undef when the directory itself is not usable.

drop_privileges() returns one of two values. It never returns on failure.

  1. The method dropped privilege, and every check passed.

  2. The process was never root. The method changed nothing.

Both values are defined, so a caller can tell a drop from a no-op, and neither value reports an error. The value 0 is false. A caller that must run as root must test for 1.

EXAMPLES

This example binds a reserved port as root and then runs as _myapp:

my $socket = IO::Socket::INET->new(
    LocalPort => 80,
    ReuseAddr => 1,
    Listen    => SOMAXCONN,
) or die "Cannot bind port 80: $!";

Fugu::Privdrop->prepare_statedir(
    path    => '/var/db/myapp',
    user    => '_myapp',
    on_warn => sub ($msg) { $log->warning('%s', $msg) },
);
Fugu::Privdrop->drop_privileges(user => '_myapp');

while (my $client = $socket->accept) {
    handle_client($client);
}

ERRORS

drop_privileges() does not return a failure code. It dies in these conditions:

  • The user argument is absent.

  • Exactly one of the real and the effective user ID is 0. The message matches mixed root state.

  • The method cannot resolve the user or the group.

  • The resolved user ID is 0. A drop to root is not a drop. The message matches Refusing to drop privileges to uid 0.

  • setgid(2) or setuid(2) fails.

  • A verified ID differs from its target after the drop. The message names the check, the value it found, and the value it wanted, for example Privilege drop failed: real gid is 0, wanted 1000.

  • A later setuid(2) with 0 changes any ID.

The caller must not continue after a privilege drop that is not complete. The re-escalation check therefore runs outside an eval: a swallowed failure would report a successful drop for a process that kept root.

prepare_statedir() dies only when an argument is absent or a name does not resolve. Every other problem goes to on_warn.

SEE ALSO

setgid(2), setgroups(2), setuid(2), Fugu::Daemon, Fugu::Process

AUTHORS

Dick Olsson <hi@senzilla.io>

CAVEATS

prepare_statedir() does not walk into subdirectories. It changes the owner of the directory and of the files one level inside it.

A daemon that must keep a root-owned file, for example a PID file in /var/run, must not put that file inside the prepared directory.

With keep_groups 1 the process keeps a supplementary group list that root gave it, and that list can hold group 0. The method cannot know that list, so it does not verify it. The caller owns that risk.

1 POD Error

The following errors were encountered while parsing the POD:

Around line 148:

You have '=item 0' instead of the expected '=item 2'