svnserve-sgidtunnel - secure wrapper for svnserve
A probléma: Szeretném biztonságosabbá tenni a subversion tárolók svn+ssh:// elérését. Ha az subversion nem mond semmit, olvass utána egy kicsit. Röviden: egy verziókezelőről beszélünk, ami több elérési módot is támogat:
- file:// - helyi diszken közvetlen repo hozzáférés
- http:// és https:// - WebDAV hozzáférés Apache webszerveren keresztül
- svn:// - állatpotteljes TCP/IP protokollal kapcsolódik a kiszolgálón futó svnserve démonhoz
- svn+ssh:// - SSH tunnelen keresztül indít egy ideiglenes svnserve folyamatot
A szerverhez többen rendelkeznek ssh hozzáféréssel. Mindenki nevesített felhasználói fiókkal rendelkezik, nincsenek "közös" fiókok. Jellemzően nincsen szükség teljes shell hozzáférésre, a felhasználóknak elég, ha a weblapjaikat tudják frissíteni, fájlokat másolgatni. Erre az scp/sftp hozzáférés bőven elég, így ezek a felhasználók rssh shell-t kaptak. A fejlesztők a subversion tárolókhoz célszerűen ssh-n keresztül férnek hozzá, így nincsen szükség újabb jelszavakra, konfigurálásra.
Milyen problémák merülhetnek fel ilyen környezetben?
Az svn+ssh hozzáférés úgy működik, hogy bejelentkezés után az ssh csatorna szerver oldalán elindul egy svnserve folyamat a felhasználó nevében, ami kimenet és bemenet segítségével kommunikál a kliens oldalon futó svn klienssel. A kapcsolat leépítésével ez az svnserve folyamat is megszűnik létezni. Ez egyrészt előnyös, mert nincsen extra nyitott port, és nem kell, hogy olyankor is fusson az svnserve, amikor épp senki sem használja. Másrészt viszont azt is jelenti, hogy az felhasználók nevében indított svnserve folyamatoknak írás/olvasás jogokkal hozzá kell férnie a repository bizonyos részeihez.
Olvasási/írási jogok hiánya
Amennyiben a felhasználó file:// vagy svn+ssh:// protokolon keresztük használja a
repository-t, úgy olvasni kell tudnia a repository összes fájlját, valamit a commit során létrejövő fájlokat létre kell
tudnia hozni. Ezek részben ideiglenes állományok, lock-ok, másrészt maguk a revízió állományai és azok metaadatai. Célszerű a
jogosultságokat úgy beállítani, hogy a repo konfigurációs állományai, hook-jait csak olvasni tudják a felhasználók. Elfogadott
gyakorlat, hogy a fejlesztőket egy közös csoportba teszik, és majd a repository állományait ennek a csoportnak "adják" (
chown -R :svn *
). Ilyenkor a csoport jogosultságokat kell csupán beállítani. Vagy ez mégsem elég?
Umask problémák, group sticky bit
Ha a felhasználó nevében futó program létrehoz egy fájlt, akkor alapértelmezetten annak a fájlnak a tulajdonosa a
felhasználó, csoportja a felhasználó elsődleges csoportja lesz és jogosultságai a felhasználó umask-ja alapján kerülnek
beállításra. Ez tipikusan olyan állományokat eredményezhet a repository-ban, amit már felhasználók nem tudnak olvasni, sőt,
maga az svn technikai felhasználó sem. (például
640 alice:alice
).
Régebben ezt úgy oldották meg, hogy a valódi svn binárisokat olyan wrapper-eken keresztül érték el, amik a valódi program meghívása előtt először átállították az umask-ot. Az umask probléma a subversion nem túl régi verzióiban már gyakorlatilag nem létezik. Az FSFS 'fájlrendszer' az új revízió állományokat a korábbi revízió-fájlokéval megegyező jogosultsággal hozza létre, így már nem kell aggódni amiatt, hogy a komittáló felhasználó umask-ja miatt más felhasználók nem tudják majd eléreni a repository-t.
A jogosultság-biteken így már nem jelentenek problémát, viszont azt továbbra is meg kell oldani, hogy a db mappában létrehozott új állományok csoportja ne az őket létrehozó felhasználó elsődleges csoportja legyen, hanem az a bizonyos "közös" svn csoport, aminek minden fejlesztő tagja. Ennek az elterjedt módja a db mappa (amit ugye svn:svn birtokol) SGID bitjének beállítása (kivéve *BSD Unixokat, ahol erre nincsen szükség). Ezután az összes itt létrehozott fájl a szülő mappa csoportát örökli.
Rosszindulat
A subversion tároló jogosultságai a nagykönyv ajánlásai alapján egy technikai user és csoport, svn:svn birtokában van, SGID bit beállítva a szükséges könyvtárakra. A fejlesztők, amint megkapják az svn-csoport tagságot, gond nélkül tudnak dolgozni, hiszen írási joguk van a tárolókra, és nem fogják elrontani a jogosultságaikat, mert egyrészt a SGID bit be van állíta, így az újonnan létrehozott revision fájlok a csoport tulajdonában leszenek, és a subversion a nagyon régi verzióktól eltekintve jól állítja be az umask-okat, pontosabban a revision környező fájlok mintájára.
A subversion belső jogosultság kezelése meghatározza, melyik fejlesztőnk a projekt melyik részét írhatja-olvashatja. Viszont, mivel az összes fejlesztőnk tagra az svn csoportnak, közvetlen fájlműveletekkel a revision fájlok bármelyikét tudják írni, olvasni, törölni. Gyakorlatilag olyan, mint amikor a buta adatbázis admin SQL szinten jól levédi az tábláit, viszont a nyers adatbázis fájlokat (táblatereket) bárki haza tudja vinni... De itt még törölni is lehet.
Megoldás
Vegyük ki a felhasználókat az svn csoportból. Nem az a helyes kérdés, hogy milyen jogosultságokkal ruházzuk fel őket ahoz, hogy elérjék a repository-t. Clark-Wilson szemlélettel a helyes kérdés az, hogy miként tudjuk kikényszeríteni, hogy csak bizonyos subversion progikon keresztül érhessék el a repository-t, másként ne. Tovább szűkítve a problémát, svn+ssh:// hozzáféréshez elég, ha csupán a svnserve folyamaton keresztül tudják elérni a repo-t. Az svnserve futtatható binárison beállított SUID/SGID bit megoldja, hogy a progit meghívó felhasználó jogosultságainál erősebb hozzáféréssel rendelkezzen a folyamat. Az svnserve meg kikényszeríti az svn belső hozzáférés verzérlését.
Mivel az svnserve megengedi, hogy bizonyos parancssori kapcsolókkal alternatív konfigurációs fájlt adjunk
meg, a belső jogosultság kezelést a szorgalmas rosszakaró könnyen kikerülheti. Egy másik parancssori kapcsoló segítségével
más felhasználó nevében ténykedhetünk. Az ilyen lehetőségek miatt nem tartom célszerűnek az svnserve
SUID/SGID beállítását. Helyette inkább egy olyan kis wrappert csinálok, ami ellenőrzi, hogy tunnel módban, trükkös
paraméterek nélkül hívták-e meg a parancsot, és ha nem, úgy eldobja a többlet-jogosultságokat és úgy adja át a vezérlést az svnservenek.
Majd ezt a wrappert ruházom fel SGID bittel, és állítom át a csoportját az svn technikai csoportra. A parancssori kapcsolók
és az svn+ssh:// működésének tanulmányozása azt a felismerést hozta, hogy tunnel módban buherálás nélkül
csak és kizárólag a
-t
(tunnel mode) paramétert adja át az svn kliens. A többi paraméter vagy a démon módra vonatkozik és ilyenkor nincsen szükség
többlet jogosultságra, vagy pedig biztonsági okból nem szeretném engedni a többlet jogosultságok használatát a paraméter
jelenlétében.
A wrapper működése tehát egyszerű: megvizsgálja, hogy pontosan egy paraméterrel hívták-e meg, és az pontosan
-t
. Ha igen, úgy átadja a vezérlést az svnservenek, ilyenkor ez a process is az svn csoport jogait örökli. Ha
más paraméterezéssel hívják meg, akkor először eldobja a jogosultságokat, és csak ez után hívja meg az eredeti binárist. Két
apró bónusz is belekerült a wrapper-be: egyrészt a buzgálkodás (pl.: a PATH
változó manipulálás) elkerülése
érdekében saját, minimális környezeti változókkal hívja meg az svnserve folyamatot, másrészt lehetőség adatik
beállítani egy virtuális repository root-t, ami segítségével a repository pontos helyét is elrejthetjük a kíváncsi de viszonylag
tudatlan szemek elől.
Mivel a SGID bit szkriptek esetén nem működik, C-ben írtam meg, ez biztonsági szempontból is jobb megoldás. Míg szkripteknek
szükségszerűen
r-x
jogosultságot kell adni (hiszen az interpreternek előbb fel kell olvasni őket), addig a gépi kódnak elég a
--x
. Mivel most a biztonság fontosabb szempont most, a további trükközések elkerülése végett a konfiguráció
#define
okkal történik és a binárisba szépen bele lesz sütve.
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#define REAL_PATH "/usr/bin/svnserve.real"
#define ROOT "/var/svn"
char *secargv[] = { "", "--tunnel", "--root=" ROOT, NULL }, *newenv[] = { "PATH=/bin:/usr/bin", "SHELL=/bin/sh", NULL };
char **newargv = secargv;
int main(int argc, char **argv) {
if (argc != 2 || strcmp(argv[1], "-t")) {
if (setgid(getgid()) == -1) { //permanently drop privs
perror("setgid()");
return 1;
}
newargv = argv;
}
execve(REAL_PATH, newargv, newenv);
perror("could not execute " REAL_PATH);
return -1;
}
Forgatás és telepítés
A szerveren nincsen gcc (ugye?!) ezért a build/deploy is 2 részre bomlik.
#!/bin/sh
F=svnserve-sgidtunnel
gcc -Wall -O2 -o $F ${F}.c
strip $F
# # run on target machine:
# chmod 2111 $F
# sudo chown .svn $F
# sudo mv $F /usr/local/bin
# sudo dpkg-divert --add --rename --divert /usr/bin/svnserve.real /usr/bin/svnserve
# sudo ln -s /usr/local/bin/$F /usr/bin/svnserve