Anslutningshooks på en VPN-daemon körs som daemonen, och det betyder oftast att de körs som root. I samma ögonblick som en av dessa hooks skriver i någons hemkatalog eller startar om en skrivbordshjälpare har du gett en nätverkshändelse förmågan att agera som vilken lokal användare som helst. Den vanliga genvägen, att läsa ett användarnamn från en miljövariabel och skicka vidare det, löser inte det. Den märker bara om åtgärden utan att ändra vilket konto som utför den. Det som fungerar är en överlämning i tre delar, där det privilegierade steget säger vad som hände och en separat enhet utför åtgärden under rätt identitet.

Ett användarnamn i miljön är inte en identitetsbrytare

Unix avgör vad en process får röra utifrån sitt effektiva användar-ID, inte utifrån strängarna i sin miljö. Att sätta USER=alice eller exportera LOGNAME ändrar vad oförsiktig programvara skriver ut i sin utdata; det ändrar ingenting i filernas ägarkontroller. En hook som körs som root med den variabeln satt öppnar fortfarande Alices filer som root.

Namnen är inte heller samma namn. Fjärråtkomstinloggningen, det lokala Unix-kontot och certifikatets vanliga namn är tre identifierare som ofta sammanfaller och ibland inte. En användare som autentiserar sig som a.smith har kanske inget lokalt konto alls, eller mappas till asmith via en katalogtjänst. Varje design som antar att en enda sträng tjänar alla tre rollerna kommer att gå sönder hos den första entreprenören.

Så den första frågan är inte ”vilken användare är ansluten” utan ”vilket lokalt konto får utföra den här åtgärden”. Svara på det utifrån din egen kontodatabas, inte utifrån vad klienten skickade:

id -u alice
getent passwd alice | cut -d: -f1,3,6
stat -c '%U:%G %a %n' /home/alice/.local/state/vpn-status.json

Det tredje kommandot är det som folk hoppar över. Om målfilen ägs av root eftersom den förra hooken skapade den, kommer en korrekt konfigurerad användaråtgärd att misslyckas med ett behörighetsfel som ser ut som ett systemd-problem och inte är det.

Daemonen stannar där den är

Att ändra VPN-tjänstens egen User= till Alice är fel spak. Det ändrar hur daemonen binder gränssnitt, läser nycklar, skriver sin statusfil och hanterar rutter, och det flyttar all den risken in i en enhet som fungerade. Daemonen behöver sina privilegier; uppföljningsåtgärden gör det inte.

Titta på vad enheten faktiskt gör innan du rör den. Två kommandon berättar körningsidentiteten och de exakta kommandorader som gäller, inklusive allt som en åsidosättning har lagt till:

systemctl show -p User -p Group -p ExecStart openvpn-server@server
systemctl cat openvpn-server@server

Vid en vanlig paketinstallation är daemonen root och --up-skriptet ärver det. Bekräfta det i stället för att anta det, för en härdad distribution kan ha flyttat det till ett dedikerat konto, och en handskriven drop-in kan köra hooken genom ett omslag med andra regler.

Tre namngivna delar, en smal utlösare

Upplägget som överlever underhåll har en händelse, en utlösare och en åtgärd. Daemonen rapporterar att en namngiven session har kommit upp. En utlösare accepterar det lilla faktumet och begär en fast åtgärd. Åtgärden körs under det lokala konto som äger filerna den rör. Inget i kedjan accepterar en godtycklig kommandosökväg.

Åtgärden själv är en vanlig tjänsteenhet, och identiteten är ett enda direktiv i den:

[Service]
Type=oneshot
User=alice
Group=alice
ExecStart=/usr/local/bin/refresh-vpn-status --slot %i

Notera att User=alice gäller för hela enheten, och systemd dokumenterar ett undantag som är värt att känna till: ett kommando med prefixet + körs med fulla privilegier oavsett detta. Det prefixet är ett legitimt verktyg för ett enda privilegierat steg och ett bekvämt sätt att upphäva den separation du just byggt, så behandla varje + i den enheten som något som måste motiveras i granskningen.

Håll utlösarens indata minimal och icke-hemlig. Sessionsidentifieraren, det mappade kontot och en tidsstämpel är vanligtvis allt som behövs. Skicka aldrig ett skalfragment, en sökväg till en körbar fil eller ett klientlevererat miljöblock över gränsen, eftersom mottagarsidan inte har något sätt att skilja ett legitimt värde från ett injicerat.

Verifiera identiteten som faktiskt körde

Konfigurationsgranskning är inte bevis. Efter en godkänd testanslutning, bekräfta UID:t som rörde filen och UID:t som daemonen fortfarande använder:

stat -c '%U %G %y %n' /home/alice/.local/state/vpn-status.json
ps -o user=,pid=,cmd= -p "$(pgrep -o -f 'openvpn --daemon')"
systemd-run --uid=alice --pipe /usr/local/bin/refresh-vpn-status --dry-run

Den första raden bör visa Alice och en tidsstämpel inom ditt testfönster. Den andra bör fortfarande visa root eller paketets tjänstekonto, vilket bevisar att daemonen inte flyttade sig. Den tredje är det billigaste sättet att testa åtgärdens behörigheter utan att vänta på en riktig återanslutning.

Om filens ägare är Alice men dess grupp eller läge ändrades mellan körningar, leta efter en UMask= eller en skillnad i katalogbehörigheter i stället för ett logikfel. Om tidsstämpeln inte rörde sig alls utlöstes aldrig utlösaren, och nästa användbara steg är journalctl -u för åtgärdsenheten i stället för ännu en klientåteranslutning.

Vad en anslutningshändelse fortfarande inte bevisar

En etablerad tunnel är ett enda faktum. Det betyder inte att skrivbordssessionen är upplåst, att sessionsbussen är nåbar eller att den privata resurs som åtgärden vill ha redan är tillgänglig. En oneshot-enhet under Alices UID har hennes identitet och ingenting av hennes session, så allt som kräver en grafisk buss måste gå genom den stödda användarsessionsmekanismen och definiera vad som händer när hon är utloggad.

Återanslutningar upprepas också. En bärbar dator som sover på ett tåg kan producera ett dussin anslutningshändelser på en timme, och en hook som lägger till en rad varje gång förvandlar en statusfil till en logg som ingen kan läsa. Gör åtgärden idempotent, eller låt den ersätta en enda post med nyckel på session.

Begränsa slutligen skaderadien i själva enheten: en skrivskyddad filsystemsvy utanför statuskatalogen, inga nya privilegier och en uttrycklig tidsgräns. Separationen mellan daemon och användaråtgärd är värd något i sig, men den är värd mycket mer när åtgärden inte kan göra något annat än det enda du godkände.

Relaterade artiklar