Vrijwel elk project dat we opleveren heeft uiteindelijk hetzelfde weinig glamoureuze ding nodig: een buildartefact van op een server krijgen en het draaien. Niemand begroot hier tijd voor — het is "gewoon uitrollen" — dus wordt het één keer opgelost, onder tijdsdruk, met wat het snelst te typen is. Meestal is dat een private SSH-sleutel in de CI-secrets en een shellscript dat een bestand overzet met SCP en een dienst herstart. Het werkt. Het schaalt ook niet voorbij het eerste project, en we bleven voor elke klant een net iets andere versie ervan bouwen tot we eindelijk gingen zitten en de tool schreven die we werkelijk wilden: Orosu.

Uit het Japanse 降ろす (órosu) — "uitladen" of "afvoeren."

Orosu is een kleine Rust-service — orosu-server — die je één keer op een machine installeert. In plaats van dat CI een SSH-sleutel bewaart die een shell opent, bewaart CI een Ed25519-sleutel die precies één van een vaste set scripts kan starten die de eigenaar van de machine vooraf heeft vastgelegd, over een geauthenticeerde -verbinding. Dit stuk gaat over waarom dat onderscheid zwaar genoeg weegt om er een hele tool omheen te bouwen, en hoe dat er in de praktijk uitziet.

Kernpunten

  • Het probleem is niet SSH zelf — het is wat SSH-toegang impliceert. Een SSH-sleutel die kan inloggen om één uitrolscript te draaien, kan alles draaien. Elke credential die een productiemachine bereikt erft de volledige straal van een shell, of de taak dat nu nodig heeft of niet.
  • Orosu versmalt dat tot een gesloten set vooraf vastgelegde scripts. CI kiest een script op naam en geeft argumenten mee; het kan geen willekeurig commando aanleveren, wat een gecompromitteerde pijplijn ook probeert te sturen.
  • Authenticatie is een handtekening, geen geheim op de lijn. Elke taak is ondertekend met de Ed25519-sleutel van de client. Er is geen wachtwoord of gedeeld geheim dat een aanvaller onderweg kan onderscheppen.
  • Het is één Rust-binary zonder afhankelijkheden tijdens het draaien — geïnstalleerd via apt of als kant-en-klare binary van GitHub Releases, met welke reverse proxy dan ook ervoor die op die machine al TLS afsluit.
  • Een optionele laag end-to-end-versleuteling dekt het ene gat dat TLS-bij-de-proxy openlaat: scriptargumenten en uitvoer vertrouwelijk houden voor die proxy zelf.
  • Orosu valt onder de Apache-2.0-licentie en is open source — wij gebruiken het, en we zien liever dat andere teams dit ook niet meer opnieuw uitvinden.

De vorm van het probleem

Haal de branding van "uitrollen vanuit CI" en het is altijd hetzelfde verzoek: draai dit script, op die machine, met deze bestanden. De standaardmanier waarop de meeste teams dat verzoek inwilligen stapelt risico op in een specifiek, voorspelbaar patroon:

  • Een private SSH-sleutel gaat in de CI-secrets. Hij is meestal niet afgebakend tot één script — hij is afgebakend tot een shell, omdat SSH beperken tot "alleen dit ene commando" fiddly genoeg is dat vrijwel niemand het werkelijk doet.
  • Dezelfde sleutel, of een bijna-kopie ervan, wordt hergebruikt in elke repository en pijplijn die die server moet bereiken, omdat een aparte sleutel per pijplijn genereren en wisselen meer proces is dan iemand op zich wil nemen.
  • Het uitrolscript zelf wordt gekopieerd van het vorige project, kleine verschillen stapelen zich op, en niemand weet nog welke versie de leidende is.
  • Productie wordt rechtstreeks bereikbaar vanaf CI-runners — een soort machine waar een aanvaller specifiek op mikt, want een gecompromitteerde runner met SSH-toegang is een gecompromitteerde server.
  • De dag dat een sleutel wel lekt, betekent hem wisselen dat je elke pijplijn moet vinden die ernaar kan verwijzen, want er was nooit een register — alleen secrets verspreid over hoeveel repositories er ook een kopie hadden opgepikt.

Niets hiervan is een gebrek aan kunde. Het is de standaardvorm van "draai een script op een externe machine vanuit een pijplijn" wanneer het gereedschap waar je naar grijpt een algemene externe shell is. SSH is niet verkeerd om in te loggen en een machine met de hand te onderzoeken. Het is het verkeerde grondbestanddeel voor een CI-taak die alleen ooit dezelfde drie of vier uitrolscripts hoeft te starten.

Wat we werkelijk wilden

Voordat er code werd geschreven, gingen de eisen minder over cryptografie en meer over wat een uitrolcredential mag doen:

  1. Een CI-credential hoort geen shell te kunnen openen. Het hoort een script te kunnen starten, op naam, uit een lijst die de servereigenaar beheert — punt.
  2. Authenticatie hoort niet af te hangen van een geheim dat bij elk verzoek meereist. Een handtekening bewijst bezit van een sleutel zonder die bloot te stellen.
  3. Een nieuw uitroldoel toevoegen hoort niet te betekenen dat je een nieuw SSH-sleutelpaar genereert en met de hand door de serverconfiguratie rijgt — het hoort een configuratieregel en een keygen-commando te zijn.
  4. De server hoort de bron van waarheid te zijn voor wat er mag draaien, niet de pijplijn. Een gecompromitteerde of onzorgvuldige CI-taak hoort nooit zijn eigen rechten te kunnen uitbreiden.

Die lijst is een WebSocket-protocol, geen SSH-vervanger in vermomming — en dat is precies wat Orosu werd.

Hoe het werkt

orosu-server draait op de doelmachine. Hij luistert naar WebSocket-verbindingen, verifieert een Ed25519-handtekening op elke binnenkomende taak, legt de scriptnaam van de taak naast de ingestelde lijst van die client, en draait — alleen als dat allemaal klopt — het script met de argumenten en meegestuurde bestanden die de taak droeg.

listen:
  tcp: "127.0.0.1:8081"
clients:
  - name: my-ci-client
    secret_file: /etc/orosu/my-ci-client.pub
    scripts:
      - name: deploy
        command:
          - "bash"
          - "/etc/orosu/scripts/deploy.sh"

Die lijst scripts: is het hele beveiligingsmodel op één plek. my-ci-client mag deploy starten en verder niets — geen ander script op dezelfde machine, geen willekeurig shellcommando, en geen script dat bij een andere client hoort. Raakt de CI-pijplijn met deze clientsleutel gecompromitteerd, dan erft de aanvaller precies de mogelijkheden van één uitrolscript, geen login.

Het sleutelpaar van een client komt uit orosu-keygen:

orosu-keygen --name my-ci-client --private-key-output my-ci-client.key --public-key-output my-ci-client.pub

De publieke helft gaat in de serverconfiguratie hierboven; de private helft wordt een CI-secret, alleen gebruikt om verzoeken te ondertekenen — hij verleent op zichzelf nooit een sessie. Het script starten vanuit GitHub Actions gaat via de bijbehorende Action:

- name: Deploy
  uses: orosu-ci/orosu@v0
  with:
    address: ${{ secrets.OROSU_SERVER_URL }}
    script: deploy
    key: ${{ secrets.OROSU_CLIENT_KEY }}
    arguments: ${{ github.sha }}

Dat is de hele wijziging in de pijplijn: een SSH-stap wordt een Orosu-stap, en het oppervlak aan de serverkant krimpt van "alles wat de shell van deze sleutel kan doen" naar "dit ene benoemde script".

orosu-server zelf is doorgaans aan localhost gebonden met een reverse proxy ervoor die afsluit en de WebSocket-upgrade doorstuurt — in het gangbare geval nginx, op dezelfde machine die dat waarschijnlijk toch al doet voor wat het uitrolscript herstart.

Waarom niet gewoon een SSH-sleutel beperken met command=

SSH ondersteunt technisch een command=-beperking in authorized_keys, en ons wordt gevraagd waarom Orosu niet gewoon dat is. Twee redenen waarom het in de praktijk tekortschiet, allebei over wat er rond het gelukkige pad gebeurt in plaats van op het pad zelf:

  • Het is serverconfiguratie per sleutel, met de hand bewerkt, onzichtbaar voor wie de pijplijn schrijft. Niemand dwingt af dat elke uitrolsleutel in authorized_keys werkelijk een command=-beperking draagt — het bestand heeft geen schema, en een onbeperkte sleutel naast negen beperkte ziet er op het oog identiek uit.
  • Een beperkte SSH-sessie spreekt nog steeds een shell. Afhankelijk van het script en van hoe command= omgaat met de argumenten die SSH doorgeeft, is de grens tussen "draai dit ene ding" en "draai dit ene ding, met injecteerbare argumenten" subtiel misplaatst. De scriptargumenten van Orosu zijn velden op protocolniveau, geen shell-tokens samengesteld uit een SSH-opdrachtregel — er zit geen shell in het pad waar een argument uit kan ontsnappen.

Orosu beweert niet dat SSH onveilig is. Het beweert dat de veilige versie van "beperk SSH tot één commando" genoeg extra ceremonie is dat vrijwel niemand het consequent doet, en dat een tool waarvan het enige werk is een van een paar vooraf vastgelegde scripts te draaien, daar de standaard van kan maken in plaats van een keuze.

Vertrouwelijkheid voorbij de reverse proxy

WSS dekt de lijn, maar TLS afgesloten bij een reverse proxy vóór orosu-server — de standaardopstelling hierboven — betekent dat scriptargumenten, geüploade bestanden en uitvoer bij die proxy leesbare tekst zijn. Voor de meeste uitrolscripts is dat geen punt; voor sommige wel (een uitrolargument kan een credential zijn die het script doorstuurt, een terugdraaitoken, een klantidentificatie). Het antwoord van Orosu is een optionele laag — X25519 voor de sleutelovereenkomst, HKDF-SHA256 om sessiesleutels af te leiden, ChaCha20-Poly1305 om te versleutelen — die boven op het WebSocket-transport zit en aan beide kanten onafhankelijk aan te zetten is:

orosu-keygen --kind server --private-key-output server.key --public-key-output server.pub

De publieke sleutel van de server gaat in zijn configuratie en in de invoer server_key van de GitHub Action. Een server met versleuteling ingesteld bedient clients die server_key weglaten precies zoals voorheen — geen gecoördineerde omschakeling, geen vaste overgangsdatum, en geen breuk voor een client die nog niet is bijgewerkt.

Beveiliging harden, eerlijk gezegd

We behandelen dit als volwaardig onderdeel van de tool, niet als vinkje. Een paar specifieke dingen, want "we geven om beveiliging" is een bewering die we liever staven met wat er werkelijk veranderde: release 0.7.0 voegde controles op punten van lage orde toe aan de X25519-uitwisseling (waarmee een bekende klasse aanvallen op krommen wordt gedicht), hardde het uitpakken van bijlagen zodat een geprepareerde of te grote zip niet via padmanipulatie uit zijn map kan ontsnappen of de schijf kan vullen met decompressie, en liet orosu-keygen bestanden met private sleutels met rechten 0600 schrijven, ongeacht de geldende umask. Alle drie kwamen als directe upgrade — zelfde configuratie, zelfde protocol, zelfde CLI. De volledige geschiedenis staat in CHANGELOG.md.

Wat Orosu niet is

Beperkte scope is hier een eigenschap en geen gebrek, maar het is de moeite waard het hardop te zeggen. Orosu orkestreert geen uitrol, beheert geen terugdraaiacties, en begrijpt je uitroltopologie niet — het draait een script dat je al hebt geschreven, en dat script bepaalt wat uitrollen betekent. Het is geen Kubernetes-eigen tool; is je doel al een cluster met een echte uitrolcontroller, dan is die controller vrijwel zeker het betere antwoord en lost Orosu een probleem op dat je niet meer hebt. Waar het zijn plek verdient is de machine die geen clusternode is — de enkele VPS, de fysieke machine, de "we containeriseren dit later nog wel"-server waarop elk bureau en heel wat startups nog altijd een deel van hun stack draaien.

Aan de slag

orosu-server en orosu-keygen worden als Debian- en Ubuntu-pakketten geleverd:

curl -fsSL https://packages.nerdy.pro/NerdyPro.gpg | sudo gpg --dearmor -o /usr/share/keyrings/nerdy-pro.gpg
echo "deb [signed-by=/usr/share/keyrings/nerdy-pro.gpg] https://packages.nerdy.pro/ stable main" | sudo tee /etc/apt/sources.list.d/nerdy-pro.list
sudo apt update
sudo apt install orosu

GitHub Releases publiceert ook kant-en-klare binaries voor machines die geen apt-bron kunnen toevoegen. De volledige installatiedoorloop — sleutels genereren, de serverconfiguratie schrijven, de GitHub Action aansluiten — staat op de projectpagina.

Orosu is geschreven in Rust, dezelfde taal achter dxpdf, onze DOCX-naar-PDF-motor — het is waar we naar grijpen wanneer een tool snel, voorspelbaar en veilig moet zijn tegen invoer die niet volledig te vertrouwen is, zoals een bestand dat een client over de lijn meestuurt.

Veelgestelde vragen

De beperking command= in authorized_keys kan een SSH-sleutel tot één commando afbakenen, maar het is met de hand bewerkte serverconfiguratie per sleutel zonder schema — niets houdt tegen dat een onbeperkte sleutel ongemerkt naast correct beperkte staat. Een beperkte sessie spreekt bovendien nog steeds een shell, dus de afhandeling van argumenten moet met de hand goed worden gedaan. Orosu maakt de lijst van toegestane scripts het standaardgedrag op protocolniveau in plaats van een afspraak waar je voor moet kiezen, en geeft argumenten door als protocolvelden zonder shell waar ze uit kunnen ontsnappen.
De aanvaller erft precies de scripts die de sleutel van die client mag starten, met de argumenten die het protocol voor dat script toestaat — nooit een willekeurig shellcommando, en nooit de scripts van een andere client. Dat is een wezenlijk kleinere straal dan een gecompromitteerde SSH-sleutel, die doorgaans een volledige loginshell erft.
Nee. Orosu is voor machines die niet al door een clustercontroller worden beheerd — een VPS, een fysieke machine, een server met een mengeling van diensten buiten Kubernetes. Heeft je doel al een echte uitrolcontroller, dan is die controller het betere gereedschap.
WSS en TLS dekken de verbinding standaard, doorgaans afgesloten bij een reverse proxy. Een optionele end-to-end-laag (X25519 + HKDF-SHA256 + ChaCha20-Poly1305) beschermt scriptargumenten, bestanden en uitvoer daarnaast tegen die proxy zelf, en is aan de client- en serverkant onafhankelijk aan te zetten.
Ja, onder Apache-2.0. De server, het programma orosu-keygen en de GitHub Action staan allemaal op GitHub, samen met een apt-repository en kant-en-klare releasebinaries.

Rol je uit naar een server waar je niet meer op wilt inloggen via SSH?

Daar is Orosu precies voor. Lees de volledige projectdocumentatie voor de complete opzet, bekijk de rest van ons opensourcewerk, of neem contact op als je wilt dat we meekijken hoe jouw team naar productie uitlevert — uitrolpijplijnen die onder tijdsdruk zijn gebouwd zijn een specifieke, veelvoorkomende bevinding in een audit van AI-code, en Orosu is de oplossing waar we zelf net zo vaak naar grijpen als we hem aanraden.


Ilya Nixan is oprichter en lead developer bij Nerdy Production, een Flutter-eerst bureau dat ook de infrastructuurtooling bouwt en onderhoudt — zoals Orosu en dxpdf — waarop het eigen opleverwerk draait.