My python program needs elevated privileges and is therefore started by root (with a setuid-binary-wrapper).
To keep the attack surface (and the impact of coding errors) as minimal as possible, I decided to split my code into two parts: One part is to be executed as root
and the other with regular user
permissions. The problem is, that the code is interdependent and therefore needs secure two-way communication
.
I do not know if this is the right approach (other ideas welcome), but I decided to go with two processes
- one parent process with elevated privileges and one child process with the privileges of a regular user.
The idea:
The questions:
Is subprocess
(.Popen) sufficient for this? Would multiprocessing
be better suited?
How can the child process and the parent communicate in an interactive and secure way (Is subprocess.PIPE
secure)?
Do you know of any simple code examples somewhere for this scenario?
Based on Gil Hamilton's suggestions, I came up with below code
Some questions remain:
os.setuid(<unprivileged UID>)
sufficient?privileged.py
:
#!/bin/python
from multiprocessing import Process, Pipe
from unprivileged import Unprivileged
if __name__ == '__main__':
privilegedProcessPipeEnd, unprivilegedProcessPipeEnd = Pipe()
unprivilegedProcess = Process(target=Unprivileged(unprivilegedProcessPipeEnd).operate)
unprivilegedProcess.start()
print(privilegedProcessPipeEnd.recv())
privilegedProcessPipeEnd.send("ok")
print(privilegedProcessPipeEnd.recv())
privilegedProcessPipeEnd.send("nok")
privilegedProcessPipeEnd.close()
unprivilegedProcessPipeEnd.close()
unprivilegedProcess.join()
unprivileged.py
:
import os
class Unprivileged:
def __init__(self, unprivilegedProcessPipeEnd):
self._unprivilegedProcessPipeEnd = unprivilegedProcessPipeEnd
def operate(self):
invokerUid = os.getuid()
if invokerUid == 0:
# started by root; TODO: drop to predefined standard user
# os.setuid(standardUid)
pass
else:
# started by a regular user through a setuid-binary
os.setuid(invokerUid) # TODO: drop to predefined standard user (save invokerUid for future stuff)
# os.setuid(0) # not permitted anymore, cannot become root again
print("os.getuid(): " + str(os.getuid()))
self._unprivilegedProcessPipeEnd.send("invoke privilegedFunction1")
print(self._unprivilegedProcessPipeEnd.recv())
self._unprivilegedProcessPipeEnd.send("invoke privilegedFunction2")
print(self._unprivilegedProcessPipeEnd.recv())
return
main.c
(setuid-wrapper program):
#include <unistd.h>
#define SCRIPT_PATH "/home/u1/project/src/privileged.py"
int
main(int argc,
char **argv) {
return execv(SCRIPT_PATH, argv);
}
/* compile and run like this:
$ gcc -std=c99 main.c -o main
# chown root:root main
# chmod 6771 main
$ chmod +x /home/u1/project/src/privileged.py
$ ./main
*/
This can be done with Popen
but it's a bit clumsy IMO because you have no control over the process transition with Popen
. If you're relying on the UID to attenuate privileges, you need to fork
and then in the child, adjust your UID, before invoking other child code.
(No real reason you couldn't put your child code in a separate program that you invoke with Popen
and have it adjust its UID as the first step, it just seems to me a strange way to structure it.)
I would recommend that you look at using the multiprocessing
module. That module makes it easy to create a new process (it will handle the fork for you). Then you can easily drop in code that adjusts the UID (see below) and then you can just run the child code within the same "code base". That is, you don't necessarily need to invoke a separate program.
The multiprocessing
module also provides its own Pipe
object as well as a Queue
object, both of which are inter-process communication mechanisms. Both are secure -- in the sense that no external user can horn in on them (without already having root privilege). But of course if your unprivileged child process is compromised, it can send whatever it wants to the parent so your privileged parent will still need to validate / audit its input.
The documentation for the multiprocessing
module provides several simple examples that should get you started. Once created, using a pipe is as easy as reading and writing a file.
As for adjusting the UID, that's simply a single call to os.setuid
in the child before invoking the code you want to run as an unprivileged user. Read the setuid(2)
and credentials(7)
manual pages for more information.