2009-11-20 7 views
32

У меня есть давно работающий, демонизированный процесс Python, который использует подпроцесс для создания новых дочерних процессов при возникновении определенных событий. Длительный процесс запускается пользователем с привилегиями суперпользователя. Мне нужны дочерние процессы, которые он запускает для запуска как другой пользователь (например, «никто»), сохраняя при этом права суперпользователя для родительского процесса.Запуск дочерних процессов как разных пользователей из процесса с длительным ходом

настоящее время я использую

su -m nobody -c <program to execute as a child> 

, но это, кажется, тяжеловесных и не умирает очень чисто.

Есть ли способ сделать это программно, а не использовать su? Я смотрю на методы os.set * uid, но документ в Python std lib довольно разрежен в этой области.

ответ

63

Поскольку вы упомянули демона, я могу заключить, что вы работаете в Unix-подобной операционной системе. Это важно, потому что, как это сделать, это зависит от операционной системы. Этот ответ относится только к Unix, включая Linux и Mac OS X.

  1. Определить функцию, которая будет устанавливать GID и УИД запущенного процесса.
  2. Передайте эту функцию в качестве параметра preexec_fn к subprocess.Popen

subprocess.Popen будет использовать вилку/EXEC модели использовать preexec_fn. Это эквивалентно вызову os.fork(), preexec_fn() (в дочернем процессе) и os.exec() (в дочернем процессе) в этом порядке. Поскольку os.setuid, os.setgid и preexec_fn поддерживаются только в Unix, это решение не переносимо для других видов операционных систем.

Следующий код представляет собой скрипт (Python 2.4+), что свидетельствует о том, как это сделать:

import os 
import pwd 
import subprocess 
import sys 


def main(my_args=None): 
    if my_args is None: my_args = sys.argv[1:] 
    user_name, cwd = my_args[:2] 
    args = my_args[2:] 
    pw_record = pwd.getpwnam(user_name) 
    user_name  = pw_record.pw_name 
    user_home_dir = pw_record.pw_dir 
    user_uid  = pw_record.pw_uid 
    user_gid  = pw_record.pw_gid 
    env = os.environ.copy() 
    env[ 'HOME'  ] = user_home_dir 
    env[ 'LOGNAME' ] = user_name 
    env[ 'PWD'  ] = cwd 
    env[ 'USER'  ] = user_name 
    report_ids('starting ' + str(args)) 
    process = subprocess.Popen(
     args, preexec_fn=demote(user_uid, user_gid), cwd=cwd, env=env 
    ) 
    result = process.wait() 
    report_ids('finished ' + str(args)) 
    print 'result', result 


def demote(user_uid, user_gid): 
    def result(): 
     report_ids('starting demotion') 
     os.setgid(user_gid) 
     os.setuid(user_uid) 
     report_ids('finished demotion') 
    return result 


def report_ids(msg): 
    print 'uid, gid = %d, %d; %s' % (os.getuid(), os.getgid(), msg) 


if __name__ == '__main__': 
    main() 

Вы можете вызывать этот скрипт так:

Start как корень ...

(hale)/tmp/demo$ sudo bash --norc 
(root)/tmp/demo$ ls -l 
total 8 
drwxr-xr-x 2 hale wheel 68 May 17 16:26 inner 
-rw-r--r-- 1 hale staff 1836 May 17 15:25 test-child.py 

Стать некорневая в дочернем процессе ...

(root)/tmp/demo$ python test-child.py hale inner /bin/bash --norc 
uid, gid = 0, 0; starting ['/bin/bash', '--norc'] 
uid, gid = 0, 0; starting demotion 
uid, gid = 501, 20; finished demotion 
(hale)/tmp/demo/inner$ pwd 
/tmp/demo/inner 
(hale)/tmp/demo/inner$ whoami 
hale 

Когда дочерний процесс завершается, мы возвращаемся к корню в родительском ...

(hale)/tmp/demo/inner$ exit 
exit 
uid, gid = 0, 0; finished ['/bin/bash', '--norc'] 
result 0 
(root)/tmp/demo$ pwd 
/tmp/demo 
(root)/tmp/demo$ whoami 
root 

Примечание, что наличие родительского процесса ждать для дочернего процесса выхода для демонстрационных целей только. Я сделал это так, чтобы родитель и ребенок могли делиться терминалом. У демона нет терминала и редко будет ждать, пока дочерний процесс выйдет.

+1

* «редко ждем, чтобы процесс выхода из ребенка» * может привести ко многим процессам зомби (долгоживущие родительские, недолговечные дочерние процессы). – jfs

+3

Возможно, это очевидно (не для меня), но ... вы * должны * сначала изменить 'gid', как показано в примере! – Jamie

+0

Обратите внимание, что в приведенном выше примере используется только ** основной gid пользователя **, если вы хотите использовать ** ВСЕ ** из групп пользователя, тогда вы можете использовать ** os.initgroups (user_name, user_gid) ** вместо os.setgid , Для этого необходимо передать имя пользователя в demote() –

10

Метод os.setuid(). Вы можете использовать его для изменения текущего пользователя для этого скрипта.

Одно из решений, где-то, где ребенок начинает, чтобы позвонить os.setuid() и os.setgid() изменить идентификатор пользователя и группы, и после этого разговора один из os.exec* методов, чтобы породить новый ребенок. Новорожденный ребенок будет работать с менее мощным пользователем, не имея возможности снова стать более мощным.

Другой - это делать, когда запускается демон (главный процесс), а затем все вновь возникшие процессы будут запускаться под одним и тем же пользователем.

Для получения дополнительной информации см. manpage for setuid.

+3

Вам также может понадобиться 'os.setgroups()', если вы переходите к пользователю с дополнительными значениями. Кроме этого, да, это довольно просто. – bobince

+1

Последующие действия. Процессы, предназначенные для запуска, поскольку никто не является ненадежным, сторонние приложения. Я не могу полагаться на них, переключаясь на другой uid/gid. Я также не могу полностью переключить процесс демона на другой uid/gid, когда он запускается, потому что ему все еще нужны привилегии суперпользователя для некоторых операций, кроме запуска этих дочерних процессов. Возможно ли следующее? 1. Запустите демон как суперпользователя. 2. Когда демон собирается запустить ребенка, оставьте никому пользователя. Убедитесь, что ребенок снова не может стать суперпользователем. 3. После запуска ребенка переключите демона обратно на привилегии суперпользователя. –

+1

№ После того, как вы стали менее мощным пользователем, пути назад нет. Я отредактировал сообщение выше, которое должно работать для вас - посмотрите на первый вариант. –

-4

Позволяет пользователю на Судо, как не требует никакого пароля

username ALL=(ALL) NOPASSWD: ALL 

Затем вызовите функцию корня с sudo, например .:

import pexpect 
child = pexpect.spawn('sudo apachectl restart') 
for i in child: print i #if you want to see the output from the process 
+3

** Это опасно. ** Нет смысла выполнять программу с ограниченными правами пользователя, а затем предоставлять этому пользователю права на sudo; это не обеспечивает никакой защиты от злоупотребления процессом и открывает дыру для злоупотреблений на консоли. Если 'sudo' необходимо, вы должны по крайней мере ограничить его локальным хостом и необходимыми командами/бинарными данными, e. г.' = (корень) NOPASSWD:/sbin/mkfs.ext4' – Murphy

+0

То, что вы говорите, в принципе, и ограничение привилегий возможно, как вы показываете. Иногда вам нужно делать такие вещи, как перезапуск Apache из надежной учетной записи без полномочий root; это общий подход, и ваше совершенствование улучшает его. Благодарю. – user2099484

0

На самом деле, пример с preexec_fn не работает для меня.
Мое решение, которое работает нормально, чтобы запустить некоторые команды оболочки от другого пользователя и получить его выход:

apipe=subprocess.Popen('sudo -u someuser /execution',shell=True,stdout=subprocess.PIPE) 

Затем, если вам нужно читать из процесса стандартный вывод:

cond=True 
while (cond): 
    line=apipe.stdout.getline() 
    if (....): 
    cond=False 

Надежда, это полезно не только в моем случае.