beveiligde commando uitvoering met Python: subprocess.Popen

Veiligheid is belangrijk voor mij tijdens het ontwikkelen van de picockpit-client.

Het volgende geldt voor Linux-systemen (maar is waarschijnlijk van toepassing op alle Unix-achtige systemen, inclusief macOS)

Python maakt het mogelijk om externe commando's uit te voeren met behulp van de subprocess module.

importeren subproces

In de komende versie van PiCockpit zullen gebruikers hun eigen knoppen kunnen maken (door eenvoudig een JSON-bestand op de Pi te bewerken) om opdrachten uit te voeren.

PiCockpit wordt standaard geleverd met drie knoppen:

afbeelding

Deze commando's vereisen allemaal root privileges. picockpit-client, draait standaard met root privileges op uw systeem.

Maar andere commando's die u wilt uitvoeren, zouden moeten draaien als minder bevoorrechte gebruikers (wij zijn standaard ingesteld op de gebruiker "pi"). In feite zullen sommige commando's, zoals de Chromium Browser afval om als root te draaien.

Kortom, we hebben een oplossing nodig om deze processen uit te voeren met een andere gebruiker.

Dit is mogelijk, dankzij de preexec_fn parameter voor popen.

Hier is mijn code:

def thread_run(self):
     def sanitize(input_string):
         #https://maxharp3r.wordpress.com/2008/05/15/pythons-minidom-xml-and-illegal-unicode-characters/
         RE_XML_ILLEGAL = u'([\u0000-\u0008-\u000b-\u000c\u000e-\u001f-\ufffe-\uffff])' + \
                          u'|' + \
                          u'([%s-%s][^%s-%s])|([^%s-%s][%s-%s])|([%s-%s]$)|(^[%s-%s])’ % \
                          (chr(0xd800), chr(0xdbff), chr(0xdc00), chr(0xdfff),
                           chr(0xd800), chr(0xdbff), chr(0xdc00), chr(0xdfff),
                           chr(0xd800), chr(0xdbff), chr(0xdc00), chr(0xdfff))
         return re.sub(RE_XML_ILLEGAL, "", input_string)

    def demote(gebruiker, uid, gid):
         def demote_function():
             print("start")
             print ('uid, gid = %d, %d' % (os.getuid(), os.getgid())
             print (os.getgroups())
             # initgroups moeten worden uitgevoerd voordat we het privilege verliezen om het in te stellen!
             os.initgroups(user, gid)
             os.setgid(gid)
             # dit moet als laatste worden uitgevoerd
             os.setuid(uid)
             print("klaar demotie")
             print('uid, gid = %d, %d' % (os.getuid(), os.getgid())
             print (os.getgroups())
         terug demote_functie

    def run_cmd(cmd, cmd_def):
         return_code = 0
         return_error = ""
         return_result = ""
         timeout = Geen
         user = "pi"
         als "timeout" in cmd_def:
             indien isinstance(cmd_def["timeout"], int):
                 als cmd_def["timeout"] > 0:
                     timeout = cmd_def["timeout"]
         als "user" in cmd_def:
             indien isinstance(cmd_def["user"], str):
                 user = cmd_def["user"]
         Probeer:
             print("picontrol :: instellen van gebruikersrechten")
             uid = pwd.getpwnam(user).pw_uid
             gid = pwd.getpwnam(user).pw_gid
             print("picontrol :: aangevraagde gebruiker " + gebruiker + " uid " + str(uid) + " gid " + str(gid))
             print("picontrol :: start commando ... ")
             afdrukken(cmd)
             proc = subprocess.Popen(
                 cmd,
                 stdout=subprocess.PIPE,
                 stderr=subprocess.PIPE,
                 preexec_fn=demote(gebruiker, uid, gid)
             )
             # TODO: time-out anders behandelen
                 # timeout=timeout)               
             print("picontrol :: AFTER subproces.Popen")
         behalve FileNotFoundError:
             return_code = errno.ENOENT
             return_error = "Opdracht niet gevonden op uw systeem " + " ".join(cmd)
         # TODO: zal dit ooit opgeroepen worden?
         behalve subprocess.TimeoutExpired:
             return_code = errno.ETIME
             return_error = "Time-out commando verstreken (" + str(cmd_def["timeout"]) + " sec)"
         behalve Uitzondering als e:
             indien hasattr(e, 'errno'):
                 return_code = e.errno
             anders:
                 return_code = 1
             indien hasattr(e, 'message'):
                 return_error = str(e.message)
             anders:
                 return_error = str(e)
         anders:
             # proces kan normaal worden uitgevoerd, geen uitzonderingen bij het opstarten
             process_running = True
             terwijl process_running:
                 als proc.poll() niet Geen is:
                     process_running = False
                 als self.stop_flag.is_set():
                     print("stopvlag ontvangen in process_running, waardoor process_running op false wordt gezet")
                     process_running = False
                     # en doe daarna ook stappen voor actieve beëindiging.
                 # een halve seconde van resolutie
                 time.sleep(0.5)
             return_code = proc.returncode
             return_error = sanitize(proc.stderr.read().decode('UTF-8').rstrip("\n"))
             return_result = sanitize(proc.stdout.read().decode('UTF-8').rstrip("\n"))

        terugkeer (return_code, return_error, return_result)

Deze code is een werk in uitvoering, dus het kan zijn dat delen ervan niet werken. De delen die ik in deze blogpost wil bespreken, werken echter wel.

Gebruiker onder Linux

Een gebruikersnaam, onder Linux, krijgt een gebruikers-id toegewezen, een gebruikersgroep, en de gebruiker wordt een lid van andere groepen.

/etc/passwd bevat de gebruikers die op het systeem zijn gedefinieerd:

afbeelding

De vermeldingen kunnen als volgt worden gedecodeerd:

gebruiker : wachtwoord : gebruikers-id (uid) : groeps-id (gid) : volledige naam van de gebruiker (GECOS) : home directory van de gebruiker : login-shell

Interessante kanttekening: het opslaan van het wachtwoord in /etc/passwd (in een versleutelde vorm) is optioneel (in het geval het wachtwoord niet wordt opgeslagen, kan een x wordt hier opgeslagen); Omdat dit bestand wereld-leesbaar is, kiezen veel systemen ervoor om in plaats daarvan het root-leesbare /etc/shadow te gebruiken:

afbeelding

dat inderdaad het wachtwoord bevat.

Voor picockpit halen we de gebruikersnaam uit het JSON instellingenbestand, en willen al de rest overeenkomstig instellen. Dus we beginnen met:

uid = pwd.getpwnam(user).pw_uid
gid = pwd.getpwnam(user).pw_gid

(Zorg ervoor dat importeren pwd).

De pwd module laat ons toe om het /etc/passwd bestand te benaderen met Python. getpwnam zoekt de overeenkomstige lijn op met de naam van de gebruiker, in ons geval "pi", of "root" (of wat je maar wil, wat er ook in het JSON bestand staat).

Dit zal ons helpen om de gebruikers-id (uid) en de groeps-id (gid) van de gebruiker te verkrijgen.

Vervolgens roepen we subproces.Popen op:

proc = subprocess.Popen(
     cmd,
     stdout=subprocess.PIPE,
     stderr=subprocess.PIPE,
     preexec_fn=demote(gebruiker, uid, gid)
)

De stdout en stderr zijn PIPEd, zodat we ze later kunnen opvangen.

de preexec_fn is ingesteld op demote(user, uid, gid).

Belangrijk!

Dit is een belangrijk stukje: preexec_fn verwacht een functie handle, die hij zal uitvoeren in de context van het nieuwe proces dat hij start (dus het proces zal zichzelf degraderen!). Deze functie zal voor de eigenlijke code lopen, zo kunnen we er zeker van zijn dat de code wordt uitgevoerd in de context die wij willen dat hij uitvoert.

De manier waarop we hier demote doorgeven, lijkt het alsof de functie wordt aangeroepen.

Maar kijk naar de definitie van degraderen:

def demote(gebruiker, uid, gid):
     def demote_function():
        print("start")
         print ('uid, gid = %d, %d' % (os.getuid(), os.getgid())
         print (os.getgroups())
         # initgroups moeten worden uitgevoerd voordat we het privilege verliezen om het in te stellen!
         os.initgroups(user, gid)
         os.setgid(gid)
         # dit moet als laatste worden uitgevoerd
         os.setuid(uid)
         print("klaar demotie")
         print('uid, gid = %d, %d' % (os.getuid(), os.getgid())
         print (os.getgroups())
     terug demote_functie

Deze functie omwikkelt een andere functie in zichzelf, die hij teruggeeft. Met behulp van de syntaxis die hierboven is gespecificeerd kunnen we eigenlijk een functie teruggeven maar kunnen er ook parameters aan doorgeven.

Zoiets als je cake hebben, en er ook nog van eten. Nice Glimlach

Hier is een screenshot, voor het geval WordPress knoeit met de syntaxis hierboven:

afbeelding

OK, nu zal ik de code uitleggen in deze demote functie:

os.initgroups:

we stellen de groepen in waarvan het proces lid moet zijn (ja, we moeten dit doen - anders zal het proces alleen lid zijn van de standaard pi groep) - dit commando verwacht de gebruiker naam

os.setgid:

hier stellen we de groep id in voor het nieuwe proces.

Er is ook een functie os.setegid() die wordt beschreven als "Stel het effectieve groeps-id van het huidige proces in." 

Wat is het verschil?

setegid zet de gid op een tijdelijke manier, zodat het proces later terug kan keren naar de originele gid. Dit is niet wat we willen, omdat het proces dat we aanroepen zijn rechten weer zou kunnen opwaarderen.

os.setuid(uid):

op dezelfde manier is er een os.seteuid(euid) functie, die het uitgevoerde proces zou toestaan om "terug te upgraden". Dus dat gebruiken we niet.

De volgorde van deze commando's is belangrijk! Als we eerst de gebruikers-id zouden instellen, zouden we niet voldoende rechten hebben om bijvoorbeeld de groepen in te stellen. Dus stel de gebruiker id als laatste in.

Sidenote: preexec_fn zou ook kunnen gebruikt worden om omgevingsvariabelen en andere dingen in te stellen, om meer flexibiliteit toe te laten. Mogelijk in een toekomstige picockpit-client upgrade, laten we eens kijken.

Sidenote 2: de preexec_fn schrijft naar de uitvoer stream van het nieuwe proces (denk er aan, het is voor-uitgevoerd in zijn context):

Dat is waarom de debug output naar de picockpit frontend wordt gestuurd:

afbeelding

Ref

Extra links van belang:

Bonus: Chromium

chromium-browser -display=:0 https://www.picockpit.com

afbeelding

kunt u de Chromium browser vanaf de commandoregel (in de commandoregel) starten. -display=:0 stelt het X-scherm in dat Chromium moet gebruiken voor uitvoer, u hoeft dus niet de juiste omgevingsvariabele in te stellen.

Peter.sh geeft tonnen opdrachtregel-switches, die je kunt gebruiken om Chromium naar wens te configureren (bijv. kioskmodus, etc).

Merk op dat u een X-server MOET draaien, als u Chromium wilt weergeven. Op de Raspberry Pi is de eenvoudigste manier om dit te doen de Raspberry Pi Desktop te draaien.

Op zoek naar professionele consulting & ontwikkeling voor het Raspberry Pi platform?

Wij bieden advies- en ontwikkelingsdiensten voor het geval u iemand zoekt die een veilige Chromium gebaseerde Kiosk applicatie voor u kan opzetten. Neem vandaag nog contact op.