Docker en het host filesystem owner matching probleem 

Containers blijven niet langer beperkt tot servers. Je ziet ze steeds vaker op desktops verschijnen: als CLI-apps of als ontwikkelomgevingen. Dit wordt het "container-als-OS-app" gebruik genoemd. 

docker host filesystem owner matching problem

Containers - toegang geweigerd

Binnen dit scenario lopen we tegen een veelvoorkomend probleem aan: gecontaineriseerde apps maken vaak bestanden aan die niet van jouw lokale gebruikersaccount zijn. Soms kunnen ze zelfs helemaal niet bij de bestanden op je hostmachine. Dit is het host filesystem owner matching probleem. 

  • Dit is een regelrechte beveiligingsblunder. Containers zouden sowieso niet als root moeten draaien!  
  • En wat een productiviteitskiller! Het is ontzettend irritant om steeds met verkeerde bestandsrechten te moeten worstelen! 

Er bestaan wel oplossingen, maar die hebben allemaal hun haken en ogen. Het gevolg? Je implementeert al snel een oplossing die voor jou werkt, maar niet voor iedereen. "Het werkt op mijn machine" is behoorlijk gênant wanneer je een ontwikkelomgeving deelt met een collega, die vervolgens tegen allerlei problemen aanloopt.

In deze post de oorzaak van het host filesystem owner matching probleem, en de analyse van verschillende oplossingen met hun kanttekeningen. 

Wat is het "container-als-OS-app" gebruiksscenario? 

Een "OS-app" is simpelweg een app die: 

  • Draait op jouw machine (dus niet in de browser of op een server). 
  • Bestanden leest of schrijft van/naar het hostbesturingssysteem. Bestanden die later mogelijk worden bekeken of aangepast door andere (niet-Docker) apps, zoals je favoriete tekstverwerker. 

Een OS-app hoeft niet per se grafisch te zijn. Sterker nog, de meeste OS-apps die gecontaineriseerd worden, zijn command-line tools. Denk aan: 

  • bash
  • ls
  • Git
  • De C/Go/Rust compiler
  • Je tekstverwerker 


Docker wordt steeds vaker gebruikt om zulke apps te verpakken. Enkele voorbeelden: 

  • rust-musl-builder — een handige compilatieomgeving voor Rust waarmee je statisch gelinkte binaries kunt maken.
  • Holy Build Box — een slimme compilatieomgeving voor C/C++ die draagbare Linux-binaries genereert die op elke Linux-distributie werken.

Beide voorbeelden lezen of schrijven bestanden van/naar jouw hostbesturingssysteem.

Misschien verrassend, maar veel ontwikkelomgevingen vallen ook in deze categorie. Stel je voor dat je een ontwikkelomgeving opzet voor je Ruby-, Node.js- of Go-app met Docker-Compose. Zo'n Docker-Compose-omgeving doet vaak het volgende: 

  1. Het koppelt je projectmap (op jouw systeem) aan de container.
  2. (Voor gecompileerde talen:) Binnen de container wordt de broncode gecompileerd. De resultaten en cachebestanden worden in je projectmap opgeslagen.
  3. In de container start de app en draait totdat jij besluit om te stoppen.
  4. (Voor frameworks waar dit relevant is:) Als je broncode op de host verandert, herlaadt de app in de container automatisch de nieuwe code.
  5. De app in de container schrijft logbestanden naar je projectmap. 


De belangrijkste conclusie: ontwikkelomgevingen lezen en schrijven voortdurend bestanden van/naar jouw systeem. Bestanden die jij later ook wilt kunnen bewerken met andere apps. 

Niet-matchende bestandseigenaren

Veel containers draaien apps als root. Wanneer ze bestanden naar jouw systeem schrijven, creëren ze root-bestanden die jij niet zomaar kunt aanpassen met je gewone tekstverwerker zonder allerlei trucjes uit te halen. 

Voorbeeld: op een Linux-machine (niet op macOS; daarover later meer), laten we een root-container een bestand laten aanmaken: 

docker run --rm -v "$(pwd):/host" busybox touch /host/foo.txt
 

Dit bestand is nu van root, en jij kunt er niets mee: t:

$ ls -l foo.txt
-rw-r--r-- 1 root root 0 Jan 17 10:36 foo.txt
$ echo hi > foo.txt
-bash: foo.txt: Permission denied


Sommige containers volgen betere beveiligingspraktijken en draaien als normale gebruiker. Maar dat creëert weer een nieuw probleem: ze kunnen helemaal niet meer schrijven naar jouw systeem! Waarom? Omdat jouw map alleen beschrijfbaar is voor jou als eigenaar, niet voor een willekeurige gebruiker in de container.

Hier is een voorbeeld van een container die als normale gebruiker draait in plaats van root: 

FROM debian:10

RUN addgroup --gid 1234 app && \
   adduser --uid 1234 --gid 1234 --gecos "" --disabled-password app
USER app
 

We bouwen en draaien dit, en proberen een bestand aan te maken: 

$ docker build . -t usercontainer
$ docker run --rm -v "$(pwd):/host" usercontainer touch /host/foo.txt
touch: cannot touch `/host/foo.txt': Permission denied
 

Alleen Linux-gebruikers hebben pech, macOS niet

Dit probleem speelt alleen op Linux. macOS-gebruikers hebben hier geen last van, omdat Docker voor Mac eigenlijk een Linux-VM draait, en daarbinnen koppelt het hostmappen als netwerkvolume. Het zorgt automatisch dat:

  • In de container lijken alle gekoppelde bestanden van de containergebruiker te zijn.
  • Op de host worden alle bestanden die door de container zijn geschreven, automatisch van jou.

Maar juist het feit dat macOS-gebruikers dit probleem niet ervaren, is op zich een probleem. Stel je voor: iemand maakt een container-app op macOS, alles werkt perfect, en vervolgens delen ze het met een Linux-gebruiker... die vervolgens tegen allerlei rechtenproblemen aanloopt. Oeps! 

Oplossingsstrategieën

Er zijn twee hoofdstrategieën om dit probleem aan te pakken: 

  1. De UID/GID van de container matchen met die van jouw systeem.
  2. Het pad van de host opnieuw koppelen in de container met behulp van BindFS. 


Elke strategie heeft zijn eigen uitdagingen. Laten we eens kijken hoe ze werken en wat de voor- en nadelen zijn. 

Strategie 1: UID/GID-matching 

De Linux-kernel kijkt niet naar namen, maar naar nummers: de gebruikers-ID (UID) en de groeps-ID (GID). Gebruikersnamen en groepsnamen zijn slechts een menselijke vertaling van deze getallen, opgeslagen in /etc/passwd en /etc/group.

De kernel geeft niet om namen, alleen om nummers. Zelfs bestanden op je systeem zijn niet eigendom van namen, maar van getallen.

Als we dus een app in een container draaien met dezelfde UID/GID als jouw account, dan worden de bestanden die door die app worden gemaakt, automatisch van jou. 

  • Als de container al accounts heeft met die UID/GID maar met andere namen? Geen probleem!
  • Als de container helemaal geen accounts heeft met die UID/GID? Ook geen probleem! 


De makkelijkste manier om dit te doen is via  docker run --user <HOST UID>:<HOST GID>. Dit werkt zelfs als de container geen accounts heeft met deze nummers.

Maar er zit een adder onder het gras: als er geen matchende accounts in de container zijn, kunnen veel apps zich vreemd gedragen. Dit kan variëren van kleine visuele probleempjes tot complete crashes. Veel code gaat ervan uit dat er een echte gebruikersnaam opgevraagd kan worden. Een ander probleem is dat er zonder account ook geen thuismap is - en veel apps gaan ervan uit dat ze daar kunnen lezen of schrijven.

Een betere aanpak is dus om in de container accounts aan te maken met dezelfde UID/GID als jouw account. Deze accounts kunnen elke willekeurige naam hebben - de kernel let toch alleen op nummers.

Laten we door een praktisch voorbeeld gaan om te zien hoe dit werkt. 

Voorbeeld: een container account maken met dezelfde UID/GID als de host account

Dit voorbeeld laat zien hoe UID's en GID's werken. Je moet dit op Linux uitvoeren (macOS-gebruikers hebben dit probleem immers niet). Laten we eerst checken wat jouw UID/GID is: 

hongli@host$ id
uid=1000(hongli) gid=1000(hongli) groups=1000(hongli),27(sudo),999(docker)
 

Mijn UID en GID zijn beide 1000. 

Nu starten we een interactieve Debian-container. We koppelen de huidige werkdirectory van de host aan de container, onder /host.

docker run -ti --rm -v "$(pwd):/host" debian:10


In de Debian container’s root shell, maken we:

  • Een groep genaamd matchinguser, met GID 1000.
  • Een gebruiker matchinguser met UID 1000. We hebben geen wachtwoord nodig voor dit voorbeeld. 


addgroup --gid 1000 matchinguser
adduser --uid 1000 --gid 1000 --gecos "" --disabled-password matchinguser


Nu gebruiken we dit account om een bestand aan te maken in de directory: 

apt update
apt install -y sudo
sudo -u matchinguser -H touch /host/foo2.txt


Als we de bestandsrechten van /host/foo2.txt vanuit de container bekijken, dan zien we dat deze eigendom is van matchinguser:

root@container:/# ls -l /host/foo2.txt
-rw-r--r-- 1 hostuser hostuser 0 Mar 15 09:45 /host/foo2.txt


Maar als we hetzelfde bestand vanaf de host bekijken, dan zien we dat het eigendom is van de hostgebruiker:

hongli@host:/# ls -l foo2.txt
-rw-r--r-- 1 hongli hongli 0 Mar 15 09:45 /host/foo2.txt
 

Dit komt omdat het bestand UID en GID 1000 heeft, wat in de container overeenkomt met matchinguser, maar op de host overeenkomt met hongli.

 

Voorbeeld: UID van bestaande containeraccount wijzigen

Je hoeft niet eens nieuwe containeraccounts aan te maken. Je kunt daadwerkelijk de UID/GID van bestaande accounts wijzigen.
Laten we bijvoorbeeld matchinguser verwijderen en opnieuw aanmaken met UID/GID 1500:

apt install -y perl  # needed by deluser on Debian
deluser --remove-home matchinguser

addgroup --gid 1500 matchinguser
adduser --uid 1500 --gid 1500 --gecos "" --disabled-password matchinguser
 

We kunnen dan usermod en groupmod gebruiken om de UID/GID van deze accounts te wijzigen naar 1000:

groupmod --gid 1000 matchinguser
usermod --uid 1000 matchinguser
 

Implementatie en kanttekeningen

Hier is een eenvoudige implementatiestrategie. Als je container geen vooraf aangemaakte accounts nodig heeft, kun je het als volgt doen:

  • Voeg een entrypoint-script toe dat een gebruikers-/groepsaccount aanmaakt, waarvan de UID/GID gelijk is aan de UID/GID van het host-account.
  • Het entrypoint-script vereist twee omgevingsvariabelen, HOST_UID en HOST_GID, die aangeven wat de UID en GID van het host-account zijn.
  • Het entrypoint voert vervolgens het volgende container-commando uit, onder de nieuw aangemaakte gebruikers-/groepsaccounts.
  • Gebruikers moeten de container met root-rechten uitvoeren, met de omgevingsvariabelen HOST_UID en HOST_GID. De container is verantwoordelijk voor het afstaan van rechten.
     

Als je container een vooraf aangemaakt account vereist, moet je de strategie een beetje aanpassen:

  • IIn plaats van een nieuw account aan te maken, wijzigt het entrypoint-script de UID/GID van het vooraf aangemaakte gebruikersaccount naar de UID/GID van het host-account.

Hier is een voorbeeld van een eenvoudig entrypoint-script. Het container-account dat we willen gebruiken heet app.

#!/usr/bin/env bash
set -e

if [[ -z "$HOST_UID" ]]; then
   echo "ERROR: please set HOST_UID" >&2
   exit 1
fi
if [[ -z "$HOST_GID" ]]; then
   echo "ERROR: please set HOST_GID" >&2
   exit 1
fi

# Use this code if you want to create a new user account:
addgroup --gid "$HOST_GID" matchinguser
adduser --uid "$HOST_UID" --gid "$HOST_GID" --gecos "" --disabled-password app

# -OR-
# Use this code if you want to modify an existing user account:
groupmod --gid "$HOST_GID" app
usermod --uid "$HOST_UID" app

# Drop privileges and execute next container command, or 'bash' if not specified.
if [[ $# -gt 0 ]]; then
   exec sudo -u -H app -- "$@"
else
   exec sudo -u -H app -- bash
fi

Het bovenstaande entrypoint-script is een goede poging, maar houdt geen rekening met deze belangrijke kanttekeningen:

1. Wat als er al een andere container-gebruiker/groep bestaat met dezelfde UID/GID als de host-UID/GID?
Dan is het niet mogelijk om een nieuw gebruikersaccount/groep aan te maken met de host-UID/GID.

Je kunt dit oplossen door de conflicterende container-gebruiker/groep te verwijderen. Maar afhankelijk van welk account precies wordt verwijderd (en waarvoor dat account binnen de container wordt gebruikt), kan dit het gedrag van de container op onvoorspelbare manieren verslechteren.


Als vuistregel worden accounts met UID < 1000 en groepen met GID < 1000 beschouwd als systeemaccounts en -groepen. Deze worden beheerd door de OS-onderhouders en gebruikers zouden hier niet mee moeten knoeien.


Daarentegen zijn accounts/groepen met UID/GID >= 1000 "normale accounts"/"normale groepen" die niet door OS-onderhouders worden beheerd. Gebruikers van het OS kunnen met deze accounts doen wat ze willen. Maar hier moet je je afvragen: wie zijn in deze context "gebruikers van het OS"? Als dat alleen jijzelf bent en je volledige controle hebt over welke normale accounts in je container komen, dan is er geen probleem. Maar als je een basisimage gebruikt dat door iemand anders is geleverd, en dat image komt al met vooraf aangemaakte normale accounts, dan moet je jezelf afvragen of het veilig is om deze te wijzigen.

2. Wat als de hostgebruiker root is?
De hostgebruiker als root (met UID 0) is een speciaal geval waarmee je rekening moet houden. Het is geen goed idee om het bestaande root-account in de container te verwijderen en door een ander account te vervangen. Dus als het entrypoint-script detecteert dat de host-UID 0 is, dan zou het de volgende opdracht als root moeten uitvoeren.

Maar op vreemde systemen zou de root-gebruiker van de host een niet-nul-GID kunnen hebben! Dus als het entrypoint-script detecteert dat de UID 0 is maar de GID niet-nul, dan moet het de GID van de rootgroep wijzigen. Dit kan weer leiden tot het probleem beschreven in (1): wat als er al een andere groep is met dezelfde GID?

3. In het geval van vooraf aangemaakte accounts: wat met de bestanden die het bezit?
Als je container gebruik maakt van een vooraf aangemaakt account, dan moet je je na het wijzigen van de UID en GID van dat account afvragen wat je moet doen met bestanden die eigendom waren van dat account. Moet de UID/GID van deze bestanden worden bijgewerkt naar de nieuwe UID/GID?

De Debian-versie van usermod --uid werkt automatisch de UID's bij van alle bestanden in de home directory van dat account (recursief). Groupmod werkt echter de GID's niet bij, dus je moet dat zelf doen vanuit je entrypoint-script.
usermod --uid werkt de UID's van bestanden buiten de home directory van dat account niet bij. Het is aan jouw entrypoint-script om deze bestanden bij te werken, indien aanwezig.

Verder moet je je afvragen of het een goed idee is om de UID's van deze bestanden bij te werken. Als deze bestanden voor iedereen leesbaar zijn en je container ze nooit schrijft, dan is het bijwerken van hun UID's/GID's overbodig. Als er veel bestanden zijn, kan het bijwerken van hun UID's/GID's aanzienlijk veel tijd kosten. Ik liep tegen dit probleem aan bij het gebruiken van rust-musl-builder. Rust was geïnstalleerd via rustup in de home directory, en het updaten van UIDs/GIDs van ~/.rustup kost veel tijd. 

Misschien is het alleen nodig om de UID's/GID's van specifieke bestanden bij te werken. Bijvoorbeeld, alleen de bestanden direct in de thuismap, niet recursief. Dit moet per container worden beoordeeld.
Tot slot hebben sommige Linux-kernelversies bugs in OverlayFS. Het bijwerken van de UID's/GID's van bestaande bestanden werkt niet altijd. Dit kan worden omzeild door een kopie te maken van die bestanden, de originele bestanden te verwijderen en de kopieën te hernoemen naar hun oorspronkelijke namen.

4. Vereist root-rechten
Het eenvoudige voorbeeld-entrypoint-script is verantwoordelijk voor het aanmaken en wijzigen van accounts, wat root-rechten vereist. Het is ook verantwoordelijk voor het verlagen van rechten naar een normaal account. Dit betekent echter dat we de USER-stanza in de Dockerfile niet kunnen gebruiken. Bovendien kunnen gebruikers de container niet uitvoeren met de --user vlag, wat contra-intuïtief is en sommige gebruikers bezorgd kan maken over de beveiliging van de container.
Een oplossing is om van het entrypoint-programma een setuid root executable te maken. Dit betekent dat je de "setuid filesystem bit" aanzet op het entrypoint-programma, zodat het programma root-rechten krijgt wanneer het wordt uitgevoerd, zelfs als het programma door een niet-root gebruiker werd gestart.


De setuid-bit wordt slechts door enkele specifieke programma's gebruikt die betrokken zijn bij privilege-escalatie. Sudo gebruikt bijvoorbeeld de setuid-bit. Zoals je je kunt voorstellen, is de setuid-bit zeer gevaarlijk. Bij onzorgvuldig gebruik kan iedereen root-rechten krijgen zonder authenticatie. Een setuid root-programma moet specifiek geschreven zijn om misbruik onmogelijk te maken.


Een andere complicatie is dat de setuid root-bit niet werkt op shell-scripts, alleen op "echte" uitvoerbare bestanden! Als je gebruik wilt maken van deze bit, moet je het entrypoint-programma schrijven in een taal die naar native uitvoerbare bestanden compileert, zoals C, C++, Rust of Go.


Onder welke voorwaarden is het veilig om een setuid root entrypoint-programma uit te voeren? Eén antwoord is: als het entrypoint's PID 1 is. Dit betekent dat het het allereerste programma is dat in de container wordt uitgevoerd. Dit geeft aan dat het entrypoint-programma direct door docker run wordt uitgevoerd, dus we kunnen aannemen dat het een redelijk veilige omgeving is.


Maar het controleren op PID 1 werkt niet in combinatie met docker run --init, dat een init-proces opstart (dat als taak heeft om het PID 1 zombie reaping problem op te lossen). Het init-proces kan willekeurig werk uitvoeren en willekeurige processen uitvoeren voordat het ons entrypoint-programma uitvoert. We kunnen dus ook niet aannemen dat onze PID 2 is. In plaats daarvan kunnen we controleren of we een kindproces zijn van het init-proces. Want nadat het init-proces de volgende opdracht uitvoert, zal het geen verdere opdrachten meer uitvoeren.

5. Vereist extra omgevingsvariabelen
In de ideale wereld willen we dat gebruikers onze container kunnen uitvoeren met docker run --user HOST_UID, en dat het entrypoint van de container automatisch begrijpt dat de waarden die aan --user worden doorgegeven de host-UID/GID zijn.
 

Maar ons voorbeeld-entrypoint-script vereist dat de gebruiker die informatie via omgevingsvariabelen specificeert. Gebruikers moeten dus redundante parameters doorgeven, zoals dit:

docker run \
 -e HOST_UID="$(id -u)" \
 -e HOST_GID="$(id -g)" \
 --user "$(id -u):$(id -g)" \
 ...


Dit is geen goede gebruikerservaring.


Met de bovenstaande kanttekeningen wordt het entrypoint-script niet langer triviaal. Als je kanttekening 4 wilt oplossen, kan het entrypoint niet eens meer een shell-script zijn.

Strategie 2: het opnieuw mounten van het host-pad in de container met BindFS

BindFS is een FUSE-bestandssysteem waarmee we een directory op een ander pad kunnen mounten, met andere bestandssysteemrechten. BindFS verandert de oorspronkelijke bestandssysteemrechten niet: het toont alleen een alternatieve weergave die eruitziet alsof alle rechten anders zijn.
Een container kan dus BindFS gebruiken om een alternatieve weergave van de host-directory te maken. In deze alternatieve weergave is alles eigendom van een normaal account in de container (waarvan de UID/GID niet hoeft overeen te komen met die van de host). Wanneer de container dat account gebruikt om naar de alternatieve weergave te schrijven, zijn de aangemaakte bestanden nog steeds eigendom van de eigenaar van de oorspronkelijke directory.
BindFS maakt dus een tweerichtingsmapping tussen de UID/GID van de host en de UID/GID van de container mogelijk, op een manier die transparant is voor applicaties.RetryClaude can make mistakes. Please double-check responses.

BindFS in actie

Laten we eens kijken hoe BindFS werkt. Let op: dit voorbeeld moet op Linux worden uitgevoerd, omdat het probleem met het matchen van de eigenaar van het host-bestandssysteem niet op macOS voorkomt.
Eerst bepalen we wat de UID/GID van de hostgebruiker is:

hongli@host$ id
uid=1000(hongli) gid=1000(hongli) groups=1000(hongli),27(sudo),999(docker)
 

Start vervolgens een Debian 10-container die de huidige werkdirectory in de container naar /host koppelt. Zorg ervoor dat je --privileged meegeeft zodat FUSE werkt.

docker run -ti --rm --privileged -v "$(pwd):/host" debian:10


Eenmaal in de container, installeer BindFS:

apt update
apt install -y bindfs


Maak nu een gebruikersaccount in de container om mee te werken:

addgroup --gid 1234 app
adduser --uid 1234 --gid 1234 --gecos "" --disabled-password app


Laten we BindFS gebruiken om /host te mounten naar /host.writable-by-app.

mkdir /host.writable-by-app
bindfs --force-user=app --force-group=app --create-for-user=1000 --create-for-group=1000 --chown-ignore --chgrp-ignore /host /host.writable-by-app


Dit is wat de vlaggen betekenen:

  • --force-user=app en --force-group=app betekenen: laat alles in /host eruitzien alsof het eigendom is van de gebruiker/groep genaamd app.
    --create-for-user=1000 en --create-for-group=1000 betekenen: wanneer een nieuw bestand wordt aangemaakt, maak het eigendom van UID/GID 1000 (de UID/GID van de host).
    --chown-ignore en --chgrp-ignore betekenen: negeer verzoeken om de eigenaar/groep van een bestand te wijzigen. Omdat we willen dat alle bestanden eigendom zijn van de UID/GID van de host.


Als je naar de rechten van de twee directories kijkt, zie je dat de ene eigendom is van de UID/GID van de host, en de andere van app:

root@container:/# ls -ld /host /host.writable-by-app
drwxr-xr-x 18 1000 1000 4096 Mar 15 10:10 /host
drwxr-xr-x 18 app  app  4096 Mar 15 10:10 /host.writable-by-app
 

Laten we eens kijken wat er gebeurt als we het app-account gebruiken om een bestand in beide directories aan te maken. Installeer eerst sudo:

apt install -y sudo
 

Daarna:

root@container:/# sudo -u app -H touch /host/foo3.txt
touch: cannot touch '/host/foo3.txt': Permission denied
root@container:/# sudo -u app -H touch /host.writable-by-app/foo3.txt


Het aanmaken van een bestand in /host werkt niet: app heeft geen rechten. Maar het aanmaken van een bestand in /host.writable-by-app werkt wel.


Als je naar het bestand in /host.writable-by-app kijkt, zie je dat het eigendom is van app::

root@container:/# ls -l /host.writable-by-app/foo3.txt
-rw-r--r-- 1 app app 0 Mar 16 11:06 /host.writable-by-app/foo3.txt


Maar als je naar het bestand in /host kijkt, zie je dat het eigendom is van de UID/GID van de host:

root@container:/# ls -l /host/foo3.txt
-rw-r--r-- 1 1000 1000 0 Mar 16 11:06 /host/foo3.txt


Dit wordt bevestigd door de host. Als je de container afsluit en naar foo3.txt kijkt, zie je dat het eigendom is van de hostgebruiker:

hongli@host$ ls -l foo3.txt
-rw-r--r-- 1 hongli hongli 0 Mar 16 12:06 foo3.txt
 

Implementatie

Een container die de BindFS-strategie wil gebruiken, moet de benodigde tools geïnstalleerd hebben en een vooraf aangemaakt normaal gebruikersaccount bevatten. Bijvoorbeeld:

FROM debian:10

ADD entrypoint.sh /
RUN apt update && \
   apt install bindfs sudo && \
   addgroup --gid 1234 app && \
   adduser --uid 1234 --gid 1234 --gecos "" --disabled-password app
ENTRYPOINT ["/entrypoint.sh"]


Daarna:

docker build . -t bindfstest


Het entrypoint-script zou er als volgt uit kunnen zien. In dit voorbeeld gaat het entrypoint-script ervan uit dat de container wordt gestart met /host gemount naar een host-directory.

#!/usr/bin/env bash
set -e

if [[ -z "$HOST_UID" ]]; then
   echo "ERROR: please set HOST_UID" >&2
   exit 1
fi
if [[ -z "$HOST_GID" ]]; then
   echo "ERROR: please set HOST_GID" >&2
   exit 1
fi

mkdir /host.writable-by-app
bindfs --force-user=app --force-group=app \
   --create-for-user=1000 --create-for-group=1000 \
   --chown-ignore --chgrp-ignore \
   /host /host.writable-by-app

# Drop privileges and execute next container command, or 'bash' if not specified.
if [[ $# -gt 0 ]]; then
   exec sudo -u -H app -- "$@"
else
   exec sudo -u -H app -- bash
fi


De container wordt dan als volgt uitgevoerd:

docker run -ti --rm --privileged \
 -v "/some-host-path:/host" \
 -e "HOST_UID=$(id -u)" \
 -e "HOST_GID=$(id -g)" \
 bindfstest
 

Kanttekeningen

BindFS werkt zeer goed. Maar er zijn twee kanttekeningen:

  • Het vereist geprivilegieerde modus! Omdat FUSE dit vereist. Dit kan een beveiligingsprobleem zijn.
    De container kan niet als niet-root worden gestart! Hoewel het mogelijk is om dit probleem te omzeilen door een setuid root entrypoint-programma te gebruiken, zoals beschreven in kanttekening 4 van strategie 1.


Sommige internetbronnen beweren dat --privileged kan worden vervangen door --device /dev/fuse --cap-add SYS_ADMIN. Echter:

  • SYS_ADMIN-mogelijkheden zijn vanuit beveiligingsperspectief niet veel beter dan --privileged.
    Deze truc werkt niet op Docker voor Mac. Het resulteert in een foutmelding.
     

Conclusie

Er zijn twee belangrijke strategieën om het probleem van het matchen van de eigenaar van het host-bestandssysteem op te lossen:

  • Het matchen van de UID/GID van de container met de UID/GID van de host.
    Het opnieuw mounten van het host-pad in de container met behulp van BindFS.


Beide strategieën hebben hun eigen voordelen en nadelen:

  • BindFS gebruiken is gemakkelijk zelf te implementeren, maar vereist dat de container wordt gestart met root-rechten en in geprivilegieerde modus.
    De container uitvoeren met een overeenkomende UID/GID vereist geen geprivilegieerde modus. Het maakt ook mogelijk dat de container zonder root-rechten draait. Maar het is moeilijk te implementeren als je alle kanttekeningen wilt aanpakken.

De kanttekeningen van BindFS kunnen niet worden opgelost. Maar de kanttekeningen met betrekking tot "het matchen van de container-UID/GID met die van de host" kunnen worden opgelost, ook al vereist dat behoorlijk wat engineering.
Gewapend met de kennis uit dit artikel kun je zelf een oplossing bouwen. Maar zou het niet mooi zijn als je een oplossing kunt gebruiken die al door iemand anders is gemaakt — vooral als die oplossing strategie 1 gebruikt, die moeilijk te implementeren is? Blijf kijken voor het volgende artikel, waarin we zo'n oplossing zullen introduceren!

 

 

Oorspronkelijk gepubliceerd op Joyful Bikeshedding.

Het Docker-pictogram dat in de illustraties van dit artikel wordt gebruikt, is gemaakt door  Flatart.