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:
The real group ID equals the target group ID.
The effective group ID equals the target group ID.
With
keep_groups0, every member of the supplementary group list equals the target group ID.The real user ID equals the target user ID.
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.
The method dropped privilege, and every check passed.
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
userargument 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'