Írtam egy SSH szervert ami konténereket indít, üssétek!

Fórumok

Sziasztok kollégák,

múlt héten kicsit elszaladt velem a paci és újraírtam egy projektet amit régesrégen már egyszer megcsináltam: egy olyan SSH szerver ami Docker containereket vagy Kubernetes podokat indít.

A cél az hogy a user SSH-zva közvetlenül a konténerben kössön ki mindenféle shellout és egyéb huncutságok nélkül. Ezt aztán lehet használni pl. webhostinghoz ahol a userhez tartozó site-ok a containerbe vannak mountolva a permission mátrixnak megfelelően, honeypotnak vagy egyéb érdekes és izgalmas dolgokra.

Hogy dinamikusan confolható legyen az egész cucc az autentikációt valamint a container környezet konfigurálását egy külső szolgáltatáson keresztül végzi.

Most hozzátok a kérésem: üssétek, nyomkodjátok, találjatok benne bugokat.

Köszönöm a segítségeteket!

Update: megjelent a 0.1.0-s verzió

Hozzászólások

Ket kerdes:

  • Tamogajta-e ez a megoldas, hogy a usernek legyen egy fix containere, amibe periodikusan be tud lepni? (Ilyen pl az emlitett webhosting eseten a hosting environment, ahonnan kiszolgalunk, de pl egy SSH kapcsolat szakadas utan se lenne rossz bizonyos timeouton belul visszaterni tudni az elozo feladathoz (mint egy okos screen/tmux))
  • Kilepes/kapcsolatbontas utan gondoskodik-e a garbage collectionrol, vagyis a mar nem hasznalt kontenerek elpusztitasarol?

Blog | @hron84

valahol egy üzemeltetőmaci most mérgesen toppant a lábával 

via @snq-

Tamogajta-e ez a megoldas, hogy a usernek legyen egy fix containere, amibe periodikusan be tud lepni? (Ilyen pl az emlitett webhosting eseten a hosting environment, ahonnan kiszolgalunk, de pl egy SSH kapcsolat szakadas utan se lenne rossz bizonyos timeouton belul visszaterni tudni az elozo feladathoz (mint egy okos screen/tmux))

Jelenleg nem, bár gondolkoztam egy "dockerexec" illetve "kubeexec" backenden ami pont ezt valósítja meg. A webhosting környezetnél ez annyira nem szempont. (A ContainerSSH egy elődje pont webhostingot szolgált ki és soha nem volt erre panasz.) A nehézség itt az hogy Kubernetes környezetben a pod adott esetben átkerül máshová amitől a state nem marad meg.

Kilepes/kapcsolatbontas utan gondoskodik-e a garbage collectionrol, vagyis a mar nem hasznalt kontenerek elpusztitasarol?

Félig. Jelenleg ha megszakad a kapcsolat akkor eltakarítja a containert azonnal. Ha a ContainerSSH maga pusztul meg akkor erre sajnos még nem képes, és még nem is egészen tudom hogy hogyan oldom meg mivel a config szerver dinamikusan adhat át configot, tehát a ContainerSSH eleinte adott esetben nem is tudja hogy hova fog csatlakozni.

Ez egy nagyszerű dolog! Synology NAS-ra megpróbálom felrakni.

Kuberneteses use case-t hogy képzeljem el, hol futna ez a szolgáltatás? Külön valami frontend szerveren, vagy k8s PODként? Utóbbi esetben a service exposure kicsit problémás lehet (NodePort)

Az embert 2 éven át arra tanítják hogyan álljon meg a 2 lábán, és hogyan beszéljen... Aztán azt mondják neki: -"Ülj le és kuss legyen!"..

Mindkettő működik. Vagy NodePort-tal expose-olod, vagy pedig load balancer erőforrás mögé rakod. Ha a host key-t secretben tárolod akkor semmi nem szól az ellen hogy akár több példányt futtass belőle.

Megpróbálok belőle összerakni valami Kubernetes YAML filet ami példaszerűen deployolja, hátha úgy egyszerűbb lesz.

Szia!

Nagyon király! Ment a csillag.

Nekem csak a Docker nem tetszik benne. Sokkal nyugodtabb vagyok, amikor nem kell egy root joggal futó szupercsillagharcos daemon-hoz kötnöm magam.

Valamelyik Red Hat alternatíván, pl. Podman-en gondolkoztál már? Nekem nagyon bevált.

(A docker debug kurzusodból pár éve nagyon sokat tanultam, hatalmas köszi érte!!!)

"If I had six hours to chop down a tree, I'd spend the first four hours sharpening the axe."

Nem kötelező Dockert használni, használhatsz Kubernetest is. A k3s pl. könnyebben települ mint a Docker. Kubernetessel tudsz pod- és network security policyt tenni rá.

A Podmannel az a baj hogy nincs HTTP API-ja (mivel ugye nincs daemon), így ha beépítem a Podman támogatást az csak és kizárólag Linuxon fog működni. Lehetne build flagekkel trükközni de ez jelenleg elég fájdalmas megoldásnak tűnik amíg nincs értelmes plugin rendszer.

Rájöttem hogy marhaságot beszélek, az API doksi úgy néz ki mintha lehetne legalább socketen keresztül beszélgetni vele. Valamiért úgy volt meg, hogy a Podman daemonless. Légyszi nyiss egy feature requestet Githubon a leírással és ha kap 1-2 upvote-ot átküzdöm magam rajta.

Na közben picit részletesebben elolvastam a Podman doksit.

Egyrészt ez akasztott meg:

 It is to be considered only as experimental as this point.

Másrészt pedig úgy tűnik, hogy ahhoz hogy az API elérhető legyen sajnos kötelezően futnia kell a Podman service-nek, azaz semmit nem nyersz vele a Dockerrel szemben. A konténer kezelést magát pedig nem szívesen gyógyítanám bele az SSH szerverbe hacsak nincs valami nyomós indok rá.

Ha jól értem, az a lényege, hogy egy ssh szerveren keresztül lövöd be a konténereket másik szerveren?

Ha igen, akkor nem lenne elég simán a context-et használni?

Én sem vagyok Systemd szakértő, de ha jól értettem az úgy működne, hogy jön egy kapcsolat a 22-es portra, mire ő indít egy konténert amiben benne fut az SSH szerver ami fogadja a kapcsolatot. Azt nem oldja meg, hogy usertől függően pl. más könyvtárakat mountoljon. Azt persze megteheted, hogy rendszer szinten confolod a usereket és bekorlátozod arra hogy csak docker run-t vagy kubectl run-t futtathat, de őszintén, mennyi az esélyed úgy beconfolni hogy ne legyen benne kibúvó? Akkor már inkább megbízom a Go SSH libjében.

Most gyorsan összeraktam egy authorized_keys command-os megoldást, és valami működést azért sikerült belőle kicsiholnom

szóval ssh-keygen után egy új user .ssh/authorized_keys-ébe:

command="docker run -it --rm ubuntu:18.04",no-port-forwarding,no-X11-forwarding,no-agent-forwarding ssh-rsa <ssh kulcs>

Még annó gitolite-nál láttam ezt így használva, sok ssh kulccsal is működik (ezért kell git clone-nál a git user-rel "belépni"), aztán lehet scriptelni, hogy ki milyen command-ot futat.

Nem tudom ez mennyire biztonságos, és mik a hátülűtői, aki jobban ért hozzá megmondaná, hogy ez miben tud kevesebbet?

(Közben megtaláltam a commentet lentebb, azért itt hagyom, hátha valaki nem ismerte)

Igen, ez lenne az alternatíva. Sajnos én sem tudom, hogy ez mennyire biztonságos, de az biztos, hogy meg kell oldanod a user managementet, illetve a Docker paraméterezését. ContainerSSH-val ez egy pár soros HTTP szerver.

Edit: még be kellene illesztened azt, hogy $SSH_ORIGINAL_COMMAND. Ott viszont izgalmas kérdés lesz, hogy nem lehet-e kijátszani és a Dockernek plusz paramétereket átadni.

és mik a hátülűtői

Pl. ki és hogyan fogja feltakarítani a leszakadó kapcsolatok után bentragadt konténereket? (ez amúgy Jani kódjánál is megoldandó feladat, csak az ő kódjával ellentétben itt nem nagyon látom a triviális megoldás lehetőségét, amihez nem kell pl. az sshd-be mélyen beletúrni).

A másik, hogy így a gw gépre a usereknek identitást kell adni, míg ez elkerülhető a Jani verziójában - ez nagy könnyebbség, ha pl. MSSP-t akarsz játszani, és tenantonként mindenkinek saját user adatbázis lehetőséget kell adnod (= otthon, a saját kis LDAP-jában/AD-jában szeretné a saját usereit menedzselni).

Nem akarsz ilyen stateful szarságot.

Inkább rakni a konténerekre/podokra cimkéket, és vagy a létrehozó processzt újraindítani, és újraindítás után a cimkék alapján ismerje fel a korábbi futásának "ereményeit", vagy valami tőle független nagyon egyszerű takarítót futtatni (esetleg per Docker host / K8s cluster), ami valami heartbeatszerű jelzés hiánya esetén takarít. A K8s esetén a heartbeat lehet egy rendszeresen frissített státuszmező, az kb. semmibe nem kerül. A Docker nem tudom, hogy tud-e bármi hasonlót...

Nem. Ez csak arra jó, hogy a Docker újraindítson saját magán egy megállt komponenst.

Mi azt szeretnénk, hogy ha bármi okból a konténereket indító és vezérlő komponens abba a helyzetbe kerül, hogy nem tudja vezérelni a konténereket, akkor helyette valaki más takarítson. Mivel ők vagy ugyanazon a gépen futnak, vagy nem, így az is hibalehetőség, hogy mindenki működik, csak éppen a hálózat nem. Emiatt az nem segít senkin, ha ilyenkor újraindítom akármelyik komponenst, az segít, ha a konténer futtató környezeten belül van ez a takarító processz, mert nála csak azzal a hibával kell számolni, hogy rosszul írtam meg, és leáll/végtelen ciklusba kerül.

Valóban, de a ContainerSSHnál az a problematika, hogy a backendeket teljesen dinamikusan tudod konfigurálni. Vagyis a ContainerSSH nem tudja hogy milyen Kubernetes vagy Docker backendjeid vannak, csak a kapcsolat létrejöttekor eszmél rá. Erre valamilyen fájdalommentes megoldást kell találni.

De ez önmagában miért baj? Gondolom a végén csak belebeszélsz a docker socketbe (legalábbis az example composeod beteszi volumeként). Ha van egy containerssh instance specifikus prefixed, akkor azzal tudsz labelezni, és egyrészt induláskor ki tudod takarítani a fenébe az előző futás után maradt szemetet, másrészt egyébként is tudod használni pl az event apit, ami egyébként az rm problémára is megoldást adhat, mert az event streamen visszajön a konténer exit codeja, nem feltétlen kell neked direkt superviseolni, ahogy gondolom teszed. pl:

2020-06-22T08:27:35.643104953+02:00 container die 0d1fd546b19d91a11ee81f4c611c5eb885352816d05e754a1971ae7d7fb00cc9 (exitCode=0, image=randomimage:latest, name=dazzling_dijkstra)

A probléma abban rejlik hogy induláskor a ContainerSSH-nak nem feltétlenül vannak meg a hozzáférési adatok a backendhez mivel azokat a config szerver is visszaadhatja usertől függően. Tehát pl. tudsz olyat csinálni hogy minden usert a saját Docker hostjára vagy Kubernetes clusterére küldesz be és ezeket csak a config szerver ismeri (amit Te írsz), a ContainerSSH nem.

Az rm egy érdekes ötlet de sajnos csak a Docker backenddel működne. A Kubernetes API nem támogatja a podok törlését futás után.

Na, megnézegettem. Mikor csak átfutottam, akkor még féltem tőle, hogy kódot is kell túrni, mert a hogyan indulunk környékén lesz valami varázslat, de a readmet olvasva sajnos az van, hogy annyira frankó dinamikus architektúrát rajzoltál, hogy azzal kissé lábon is lőtted magad.

Mivel még a backendek listája sem ismert, ezért esélyed sincs saját hatáskörben stateless megoldani a dolgot. Kb a következő lehetőségeid vannak, ahogy én látom:

  • beismered, hogy nincs esélyed, és egyszerűen a backendre hagyod a takarítást, maximum valami konfigurálható labelezést vagy ilyesmit csinálsz, hogy segítsd a túloldalt megtalálni mi nem kell már. Ez szép tiszta, csak hááát...
  • Megpróbálhatsz magad minden backend példányra letenni valami autonóm cleanup mechanizmust magad, akár per backend akár per container. Ez utóbbi talán kicsit egyszerűbb mondjuk sidecar vagy valami, de az egész bűzlik a 22es csapdájától (ki takarítja a takarítókat), meg egyébként is antipattern szaga van egy ilyen környezetben
  • Poor man's cron jelleggel mikor egyébként is mész a backendre, akkor takarítasz. Ez olyan poor man's, meg a sohatöbbet nem használt backendeken még szemetet is hagy.
  • beismered, hogy nem fog menni memória nélkül, és választasz az sqlitenál valami testhezállóbbat. Mondjuk egy etcd-t vagy ilyesmi, amit a user úgy skáláz elosztottan, ahogy akar. Aztán a containerssh abba szépen teszi el, hogy mit hol indított, aztán ha egyszerűre akarod venni, akkor néha takarít az, ha meg szépre, akkor csinálsz egy takarító microservicet
  • esetleg ezt még tovább lehet szépezni, és etcd helyett mondjuk egy consult használni, és serviceként regisztrálni a containereket, még valami healthchecket is kitalálni rájuk. Így minimum egy tiszta APIt kapsz a takarító service alá, ha szerencséd van akkor a consul watchokkal lehet, hogy fel is lehet takarítani, ha a healthcheck azt mondja, hogy már nem fut.

Illetve ha te takarítasz, akkor az tulajdonképp lehet opcionális, ha valakinek nem kell, akkor goto 1.

Én inkább arra gondoltam, hogy bevezetek plusz egy endpointot a config szerveren ami visszaad egy backend-listát takarítás céljából induláskor. Illetve természetesen ugyanezt a konfigurációban is megadhatod. Ha nem adsz meg semmit akkor nem takarít.

Igazából csak dockerből indultam ki, ott van egy úgynevezett context amivel be tudsz állítani egy szerver elérést egy azonosítóval (ssh, kulcsot pedig .ssh/config).

Utána ki tudsz adn bármilyen parancsot amit egy adott szerveren fog futtatni a docker.

pl.:

#A valami-szerver alatt futó docker konténereket listázza
docker --context valami-szerver ps

#Ez pedig a valami-masik szerveren
docker --context valami-masik ps

 

Nagy hiányosság volt, hogy docker-compose alatt ez nem működött, csak dev verziókban volt benne, az is bugos volt tapasztalataim szerint.
Legutóbb már úgy láttam, hogy stable verzióban is bekerült.

Ja értem. (Itt hangsúlyosan nem arról van szó hogy nekem kell containerekbe SSH-znom, hanem arról, hogy nagy számú usernek akarsz SSH szolgáltatást nyújtani.)

Persze, meg tudod csinálni, hogy minden usernek van egy context-je és az OpenSSH-t rákényszeríted hogy minden usernek fixen a "docker" parancsokat hajtsa végre (force-command ha jól emlékszem). Nekem ezzel az a bajom, hogy 1. minden usernek léteznie kell a gazdagépen 2. a konténeren kívül kerül végrehajtásra egy külső parancs, tehát ha abban van bármivéle escape baki, el van csettintve az OpenSSH config ezer tekerentyűje közül egy, stb. akkor a user a konténeren kívül köt ki és mindent lát.

Arról lehet vitatkozni, hogy az OpenSSH elconfolása-e a nagyobb veszély vagy a Go SSH libje, de nekem személy szerint az OpenSSH-s megoldás eléggé bántja a szememet.

Szuper! Ügyes vagy, köszönjük! Én már nyomkodom.

De ez nem is javaban van irva, mi tortent?!?! :P

OpenSSH kód alapján kezdtem nulláról, merrt nem találtam olyat, ami csak az encryptiont kezeli, és a többit vagyis a plain text csomagokat már szabadon kezelhetem. A titkosítást megírni meg elég bonyolult. Host key, sokféle algo. Én kitettem a kódomat githubra, de még semmire sem jó.

Köszi, bármi segítséget szívesen fogadok. Az nagyon hasznos, ha látom, hogyan kell használni ezt a libet.