Revizuirea periodica a conturilor inactive din Active Directory si dezactivarea controlata cu PowerShell

In orice domeniu Active Directory se aduna in timp conturi care nu mai sunt folosite: angajati plecati carora nu li s-a inchis contul, conturi de test, conturi create pentru colaboratori externi sau stagiari. Aceste conturi raman active, au parole care nu se mai schimba si reprezinta o poarta de intrare usor de exploatat, pentru ca nimeni nu observa daca sunt folosite de altcineva.

Standardul ISO/IEC 27001:2022 cere revizuirea periodica a drepturilor de acces (controlul A.5.18) si eliminarea accesului care nu mai este necesar. Procedura de mai jos produce un raport lunar cu conturile inactive de peste 90 de zile, il trece printr-o aprobare si apoi dezactiveaza conturile aprobate intr-un mod controlat si reversibil. Raportul si fisierul aprobat raman ca dovada de audit.

Backup

  1. Verificati daca este activat Active Directory Recycle Bin. Fara el, un cont sters ulterior nu mai poate fi recuperat cu toate atributele:
Get-ADOptionalFeature -Filter 'Name -like "Recycle Bin Feature"' | Select-Object Name, EnabledScopes

Daca EnabledScopes este gol, functia nu este activa. Activarea ei este ireversibila si se face separat, cu acordul administratorului de domeniu.

  1. Exportati starea actuala a tuturor conturilor de utilizator, inainte de orice modificare:
New-Item -ItemType Directory -Force -Path "C:\Audit\AD" | Out-Null
Get-ADUser -Filter * -Properties Enabled, Description, DistinguishedName, LastLogonTimestamp |
    Select-Object SamAccountName, Name, Enabled, Description, DistinguishedName,
        @{N='UltimaLogare';E={ if ($_.LastLogonTimestamp) { [DateTime]::FromFileTime($_.LastLogonTimestamp) } }} |
    Export-Csv "C:\Audit\AD\stare-conturi-$(Get-Date -Format yyyy-MM-dd).csv" -NoTypeInformation -Encoding UTF8
  1. Procedura se ruleaza dintr-o consola PowerShell pornita ca Administrator, pe un server sau o statie care are modulul ActiveDirectory (RSAT), cu un cont care are drept de modificare pe conturile vizate.

Pasul 1 – Pregatim grupul de exceptii si OU-ul de carantina

Unele conturi nu se logheaza interactiv niciodata, dar sunt necesare: conturi de serviciu, conturi pentru scanere sau aplicatii, conturi de urgenta (break-glass). Le punem intr-un grup dedicat, ca scriptul sa le ignore:

New-ADGroup -Name "GRP-Exceptii-Inactivitate" -GroupScope Global -GroupCategory Security `
    -Description "Conturi excluse din revizuirea conturilor inactive. Fiecare membru trebuie justificat."
Add-ADGroupMember -Identity "GRP-Exceptii-Inactivitate" -Members "svc-backup","svc-scanner"

Cream si OU-ul in care vor fi mutate conturile dezactivate (inlocuiti DC=firma,DC=local cu domeniul vostru):

New-ADOrganizationalUnit -Name "Conturi Dezactivate" -Path "DC=firma,DC=local" -ProtectedFromAccidentalDeletion $true

Important: daca domeniul este sincronizat cu Microsoft Entra ID (Entra Connect / Cloud Sync) si sincronizarea este filtrata pe OU-uri, OU-ul “Conturi Dezactivate” trebuie inclus in sincronizare. Altfel, la mutare, utilizatorul este sters din Entra ID, iar casuta lui de mail ajunge in starea soft-deleted.

Pasul 2 – Generam raportul cu conturile inactive

Salvam scriptul de mai jos ca C:\Scripts\Raport-ConturiInactive.ps1:

Import-Module ActiveDirectory

$Zile     = 90
$Prag     = (Get-Date).AddDays(-$Zile)
$Folder   = "C:\Audit\AD"
$Raport   = Join-Path $Folder "conturi-inactive-$(Get-Date -Format yyyy-MM-dd).csv"
$Sistem   = @('krbtgt','Administrator','Guest','DefaultAccount')

New-Item -ItemType Directory -Force -Path $Folder | Out-Null

$Exceptii = Get-ADGroupMember -Identity "GRP-Exceptii-Inactivitate" -Recursive |
    Select-Object -ExpandProperty SamAccountName

$Inactive = Get-ADUser -Filter 'Enabled -eq $true' `
        -Properties LastLogonTimestamp, whenCreated, PasswordLastSet, Description, ServicePrincipalName, Manager |
    Select-Object SamAccountName, Name, Description, whenCreated, PasswordLastSet, DistinguishedName,
        @{N='UltimaLogare';E={ if ($_.LastLogonTimestamp) { [DateTime]::FromFileTime($_.LastLogonTimestamp) } }},
        @{N='AreSPN';E={ [bool]$_.ServicePrincipalName }},
        @{N='Manager';E={ if ($_.Manager) { (Get-ADUser $_.Manager).Name } }} |
    Where-Object {
        ((-not $_.UltimaLogare) -or ($_.UltimaLogare -lt $Prag)) -and
        ($_.whenCreated -lt $Prag) -and
        (-not $_.AreSPN) -and
        ($_.SamAccountName -notin $Exceptii) -and
        ($_.SamAccountName -notin $Sistem)
    } |
    Sort-Object UltimaLogare

$Inactive | Export-Csv $Raport -NoTypeInformation -Encoding UTF8
Write-Host "Conturi inactive de peste $Zile zile: $($Inactive.Count). Raport: $Raport"

Ce face filtrul:

  • pastreaza doar conturile active care nu s-au logat in ultimele 90 de zile sau nu s-au logat niciodata;
  • ignora conturile create in ultimele 90 de zile (angajati noi care inca nu au inceput);
  • ignora conturile cu SPN (de regula conturi de serviciu), conturile din grupul de exceptii si conturile de sistem.

Rulam scriptul:

C:\Scripts\Raport-ConturiInactive.ps1

Pasul 3 – Revizuim si aprobam lista

Raportul nu se aplica automat. Il deschidem in Excel si il trimitem managerului fiecarui utilizator sau departamentului HR spre confirmare. Pentru fiecare cont se decide:

  • se dezactiveaza;
  • se pastreaza, cu justificare (de exemplu concediu prelungit), caz in care contul se adauga temporar in grupul de exceptii.

Liniile aprobate pentru dezactivare se salveaza intr-un fisier separat, de exemplu C:\Audit\AD\conturi-inactive-2026-10-01-aprobat.csv. Acest fisier, impreuna cu emailul de aprobare, este dovada pentru auditul ISO 27001.

Pasul 4 – Simulam dezactivarea cu -WhatIf

Inainte de modificarea reala, rulam procedura in mod simulare. PowerShell afiseaza ce ar face, fara sa schimbe nimic:

$Aprobate   = Import-Csv "C:\Audit\AD\conturi-inactive-2026-10-01-aprobat.csv"
$Carantina  = "OU=Conturi Dezactivate,DC=firma,DC=local"

foreach ($c in $Aprobate) {
    Disable-ADAccount -Identity $c.SamAccountName -WhatIf
    Move-ADObject -Identity $c.DistinguishedName -TargetPath $Carantina -WhatIf
}

Verificam ca lista afisata contine exact conturile aprobate.

Pasul 5 – Dezactivam si mutam conturile aprobate

$Aprobate   = Import-Csv "C:\Audit\AD\conturi-inactive-2026-10-01-aprobat.csv"
$Carantina  = "OU=Conturi Dezactivate,DC=firma,DC=local"
$Data       = Get-Date -Format yyyy-MM-dd
$Jurnal     = "C:\Audit\AD\jurnal-dezactivare-$Data.csv"

$Rezultat = foreach ($c in $Aprobate) {
    try {
        Disable-ADAccount -Identity $c.SamAccountName -ErrorAction Stop
        Set-ADUser -Identity $c.SamAccountName -Description "Dezactivat $Data - inactiv peste 90 zile - revizuire periodica acces" -ErrorAction Stop
        Move-ADObject -Identity $c.DistinguishedName -TargetPath $Carantina -ErrorAction Stop
        [pscustomobject]@{ Cont = $c.SamAccountName; OU_Original = $c.DistinguishedName; DescriereOriginala = $c.Description; Status = 'Dezactivat'; Data = $Data }
    }
    catch {
        [pscustomobject]@{ Cont = $c.SamAccountName; OU_Original = $c.DistinguishedName; DescriereOriginala = $c.Description; Status = "Eroare: $($_.Exception.Message)"; Data = $Data }
    }
}

$Rezultat | Export-Csv $Jurnal -NoTypeInformation -Encoding UTF8
$Rezultat | Format-Table Cont, Status -AutoSize

Jurnalul pastreaza OU-ul si descrierea originala ale fiecarui cont, necesare pentru restaurare.

Pasul 6 – Programam raportul lunar

Programam doar generarea raportului, nu si dezactivarea. Dezactivarea ramane un pas manual, facut dupa aprobare:

schtasks /Create /TN "Raport conturi AD inactive" /TR "powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\Scripts\Raport-ConturiInactive.ps1" /SC MONTHLY /D 1 /ST 08:00 /RU SYSTEM

Task-ul ruleaza in prima zi a fiecarei luni la ora 08:00 si salveaza raportul in C:\Audit\AD.

Pasul 7 – Stergerea definitiva (dupa perioada de retentie)

Conturile din “Conturi Dezactivate” se pastreaza o perioada stabilita in politica interna, de regula 30-90 de zile. Abia apoi se sterg, tot pe baza unei liste aprobate. Daca Recycle Bin este activ, un cont sters mai poate fi recuperat pe durata configurata in domeniu (implicit 180 de zile).

Impact colateral

Atributul LastLogonTimestamp se replica intre controllerele de domeniu cu o intarziere de pana la aproximativ 14 zile. De aceea pragul se tine la 90 de zile sau mai mult, nu la 15-30 de zile.

Utilizatorii care lucreaza doar in Microsoft 365 (Outlook Web, Teams, OneDrive din browser) pot aparea inactivi in AD-ul local, desi lucreaza zilnic. Inainte de aprobare, verificati in Entra ID ultima autentificare in cloud pentru conturile din raport.

Un cont dezactivat in AD si sincronizat cu Entra ID este blocat si in Microsoft 365: utilizatorul nu mai are acces la mail, Teams si OneDrive. Datele raman pastrate. Conturile folosite de aplicatii, scanere sau task-uri programate, care nu au SPN si nu sunt in grupul de exceptii, pot fi dezactivate gresit si opresc serviciul respectiv. Revizuirea din Pasul 3 exista tocmai pentru a prinde aceste cazuri.

Mutarea intr-un OU nou schimba politicile GPO aplicate contului si poate scoate contul din scopul sincronizarii cu Entra ID, daca OU-ul nu este inclus.

Plan de restaurare in caz de nefunctionare

  1. Pentru un singur cont dezactivat gresit, il reactivam si il mutam inapoi in OU-ul original, folosind jurnalul din Pasul 5:
$Jurnal = Import-Csv "C:\Audit\AD\jurnal-dezactivare-2026-10-01.csv"
$c = $Jurnal | Where-Object Cont -eq "ion.popescu"

$OU_Parinte = ($c.OU_Original -split ',', 2)[1]
Enable-ADAccount -Identity $c.Cont
if ($c.DescriereOriginala) { Set-ADUser -Identity $c.Cont -Description $c.DescriereOriginala } else { Set-ADUser -Identity $c.Cont -Clear Description }
Move-ADObject -Identity (Get-ADUser $c.Cont).DistinguishedName -TargetPath $OU_Parinte
  1. Pentru toate conturile din jurnal, rulam acelasi bloc intr-o bucla foreach ($c in $Jurnal).
  2. Daca un cont a fost deja sters si Recycle Bin este activ, il recuperam cu Get-ADObject -Filter 'SamAccountName -eq "ion.popescu"' -IncludeDeletedObjects | Restore-ADObject.
  3. Dupa restaurare, contul trebuie adaugat in grupul GRP-Exceptii-Inactivitate, daca motivul inactivitatii este justificat, ca sa nu apara din nou in raportul urmator.
[mai mult...]

Script dezarhivare automata arhive multiple dintr-un folder si mutare arhive dupa dezarhivare si verificare

Urmează aceste instrucțiuni pentru a procesa toate arhivele dintr-un folder, una câte una, eliminând riscul de a pierde fișiere.

Pasul 1: Pregătirea terenului

Scriptul folosește 7-Zip pentru a citi fișierele .rar și .zip și pentru a-ți afișa procentele.Asigură-te că ai programul instalat. Dacă nu îl ai, descarcă-l gratuit de pe 7-zip.org.Lasă-l să se instaleze în calea lui standard: C:\Program Files\7-Zip.

Pasul 2: Crearea fișierului de comandă (Scriptul)

Deschide aplicația Notepad (M text) pe calculatorul tău. Copiază textul codului de aici

@echo off
setlocal enabledelayedexpansion

cd /d “%~dp0”
echo Se porneste scriptul de dezarhivare ULTRA-SIGUR…
echo =========================================================
echo.

set “7zip_path=C:\Program Files\7-Zip\7z.exe”
set “folder_siguranta=_Arhive_Procesate”

rem Verificam daca 7-Zip este instalat
if not exist “!7zip_path!” (
echo EROARE: Nu s-a gasit 7-Zip la C:\Program Files\7-Zip\7z.exe
goto final
)

rem Cream folderul de siguranta daca nu exista deja
if not exist “!folder_siguranta!” mkdir “!folder_siguranta!”

for %%F in (*.zip *.rar) do (
set “nume_arhiva=%%~nF”
set “fisier_complet=%%F”

rem Ignoram arhivele care au fost deja mutate in folderul de siguranta
if not “%%F”==”!folder_siguranta!” (
echo.
echo ———————————————————
echo [PROCESARE] Se extrage: !fisier_complet!
echo ———————————————————

if not exist “!nume_arhiva!” mkdir “!nume_arhiva!”

rem Extragere cu afisare procente (%)
“!7zip_path!” x “!fisier_complet!” -o”!nume_arhiva!” -y

if !errorlevel! equ 0 (
if exist “!nume_arhiva!” (
rem Verificam daca folderul destinatie NU este gol
dir /b /a “!nume_arhiva!” | findstr “^” >nul
if !errorlevel! equ 0 (
echo.
echo [SUCCES] Extras complet.
echo [SIGURANTA] Se muta arhiva in: !folder_siguranta!\!fisier_complet!

rem MUTARE in loc de STERGERE (Fisierele tale sunt 100% in siguranta)
move /y “!fisier_complet!” “!folder_siguranta!\” >nul
) else (
echo.
echo [ATENTIE] Folderul !nume_arhiva! este gol. Arhiva NU a fost mutata.
)
)
) else (
echo.
echo [EROARE] Extractia a esuat. Arhiva ramane pe loc.
)
)
)

:final
echo.
echo =========================================================
echo GATA! Toate arhivele au fost mutate in folderul: !folder_siguranta!
pause

și lipește-l în Notepad.

Mergi în meniul de sus la

File ➡️ Save As… (Salvare ca). La secțiunea Encoding (jos, lângă butonul de salvare), schimbă din UTF-8 în ANSI (foarte important pentru a preveni erorile).

La Save as type, selectează All Files (.). Pune-i numele dezarhivare_sigura.bat și apasă Save.

Pasul 3:Rularea și ProcesareaMută fișierul proaspăt creat (dezarhivare_sigura.bat) în folderul exact unde se află toate arhivele tale .zip și .rar.

Fă dublu-clic pe fișierul dezarhivare_sigura.bat pentru a-l lansa

Ce se va întâmpla pe ecranul tău 
Faza 1: Scanarea. O fereastră neagră se va deschide. Scriptul detectează prima arhivă din listă.
Faza 2: Extragerea. Va apărea textul [PROCESARE] și vei vedea programul cum numără în timp real progresul: 0%… 25%… 70%… 100%.
Faza 3: Verificarea. În background, scriptul se asigură că noul folder cu numele arhivei a fost creat și că conține fișierele tale (nu este gol).
Faza 4: Curățenia. Dacă totul este în regulă, pe ecran apare mesajul [SUCCES] Extras complet. Se sterge DOAR arhiva: …. În acest moment, fișierul .zip sau .rar este șters definitiv pentru a-ți elibera spațiu, iar scriptul trece automat la următoarea arhivă.

Ce se întâmplă dacă o arhivă are probleme? Dacă o arhivă este coruptă, descărcată incomplet sau dă o eroare, scriptul va afișa mesajul [EROARE]. Scutul de siguranță se activează instantaneu, dezarhivarea se oprește pentru acel fișier, iar arhiva originală NU va fi ștearsă, fiind lăsată neatinsă ca să nu pierzi nicio informație.

[mai mult...]

Cum folosești o cheie de securitate FIDO2 pentru autentificare SSH pe Linux

OpenSSH poate folosi chei FIDO2 direct pentru autentificare SSH. Partea privată efectivă rămâne în dispozitivul hardware, iar OpenSSH folosește cheia pentru a semna provocarea de autentificare când cheia este conectată. Tipurile principale sunt ed25519-sk și ecdsa-sk.

Exemplul de mai jos folosește o cheie FIDO2 compatibilă cu OpenSSH și un server Linux cu SSH activ.

1. Verifică versiunea OpenSSH

Pe client:

ssh -V

Pe server:

sshd -V

Verifică dacă OpenSSH recunoaște algoritmii FIDO:

ssh -Q key | grep sk-

Ar trebui să apară algoritmi precum:

sk-ssh-ed25519@openssh.com
sk-ecdsa-sha2-nistp256@openssh.com

OpenSSH include suport pentru aceste tipuri de chei FIDO.

2. Conectează cheia FIDO2

Introdu cheia în portul USB.

Verifică dacă sistemul o vede:

lsusb

În funcție de distribuție și dispozitiv, identificarea exactă poate diferi.

3. Generează cheia SSH pe dispozitivul FIDO2

Recomandarea uzuală este Ed25519:

ssh-keygen -t ed25519-sk -f ~/.ssh/id_ed25519_sk

OpenSSH va cere de regulă confirmarea prin atingerea cheii. Poți seta și un PIN pentru cheia FIDO2 dacă dispozitivul îl suportă și poate fi necesară verificarea utilizatorului.

Veți obține:

~/.ssh/id_ed25519_sk
~/.ssh/id_ed25519_sk.pub

Fișierul public poate fi copiat pe server. Partea privată a cheii este asociată cu authenticatorul hardware și nu poate fi exportată din acesta.

4. Verifică cheia publică

cat ~/.ssh/id_ed25519_sk.pub

Veți vedea ceva asemănător cu:

sk-ssh-ed25519@openssh.com AAAA... user@machine

Cheia publică nu este secretă și poate fi instalată pe server.

5. Adaugă cheia pe server

Pe server:

mkdir -p ~/.ssh
chmod 700 ~/.ssh
nano ~/.ssh/authorized_keys

Adaugă cheia publică pe o linie nouă.

Apoi:

chmod 600 ~/.ssh/authorized_keys

6. Testează autentificarea

De pe client:

ssh -i ~/.ssh/id_ed25519_sk user@server

OpenSSH va folosi cheia FIDO2 și, în mod normal, va cere acțiunea fizică de pe dispozitiv. OpenSSH este proiectat astfel încât autentificarea FIDO să poată necesita prezența fizică a utilizatorului.

7. Activează verificarea cu PIN

Pentru protecție suplimentară poți genera cheia cu verificare obligatorie a utilizatorului:

ssh-keygen -t ed25519-sk \
  -O verify-required \
  -f ~/.ssh/id_ed25519_sk

Cu această opțiune, autentificarea necesită verificarea utilizatorului pe authenticator, în prezent de regulă prin PIN. Nu toate dispozitivele FIDO2 suportă această funcție.

Fluxul devine:

conectezi cheia
      ↓
ssh
      ↓
introduci PIN
      ↓
atingi cheia
      ↓
autentificare

8. Configurează serverul să accepte FIDO2

Verifică algoritmii acceptați:

ssh -Q PubkeyAcceptedAlgorithms | grep sk-

În OpenSSH modern, algoritmii FIDO SSH sunt incluși în lista implicită de algoritmi acceptați.

Poți verifica configurația serverului:

sudo sshd -t

Această comandă verifică sintaxa configurației înainte de restart.

9. Impune touch pentru cheile FIDO

Pe server, în fișierul de configurare SSH:

/etc/ssh/sshd_config

poți folosi:

PubkeyAuthOptions touch-required

Această opțiune obligă autentificarea cu chei FIDO să dovedească prezența fizică a utilizatorului.

După modificare:

sudo sshd -t
sudo systemctl restart sshd

Pe unele distribuții serviciul se numește ssh:

sudo systemctl restart ssh

10. Impune și verificarea utilizatorului

Pentru un nivel suplimentar:

PubkeyAuthOptions verify-required

Acest mod cere ca semnătura FIDO să confirme verificarea utilizatorului, de exemplu prin PIN.

Poți combina cerințele:

PubkeyAuthOptions touch-required,verify-required

Astfel ai:

cheie conectată
      +
PIN
      +
atingerea cheii
      ↓
SSH login

11. Restricționează cheia în authorized_keys

Poți limita ce poate face cheia.

Exemplu:

restrict,command="/usr/local/bin/my-script" sk-ssh-ed25519@openssh.com AAAA...

Opțiunea restrict poate dezactiva facilități precum forwarding-ul și PTY, iar command poate forța o comandă specifică.

Pentru un cont administrativ interactiv, nu folosi command deoarece va împiedica sesiunea SSH normală.

12. Configurează clientul SSH

Poți evita specificarea manuală a cheii la fiecare conectare.

Editează:

nano ~/.ssh/config

Exemplu:

Host server-prod
    HostName 192.168.1.100
    User admin
    IdentityFile ~/.ssh/id_ed25519_sk
    IdentitiesOnly yes

Apoi:

ssh server-prod

13. Verifică autentificarea în detaliu

Dacă login-ul eșuează:

ssh -vvv server-prod

Uită-te după mesaje referitoare la:

sk-ssh-ed25519@openssh.com

Pe server verifică:

journalctl -u sshd -b

sau:

journalctl -u ssh -b

14. Folosește cheia FIDO cu ssh-agent

OpenSSH poate încărca și chei resident stocate pe authenticator. Pentru acestea:

ssh-add -K

Opțiunea descarcă cheile resident de pe authenticator.

Poți verifica apoi:

ssh-add -L

15. Ce sunt resident keys?

În mod normal, SSH păstrează local informațiile necesare pentru a folosi cheia hardware. Cu resident key, cheia este stocată pe authenticator și poate fi descoperită ulterior de pe alte calculatoare compatibile:

ssh-keygen -K

Resident keys sunt utile când vrei să folosești aceeași cheie hardware pe mai multe sisteme. Pentru un mediu sensibil, trebuie însă protejată foarte bine cheia fizică și PIN-ul.

16. Nu dezactiva verificarea host key

FIDO2 protejează autentificarea utilizatorului. Nu înlocuiește verificarea identității serverului.

Verifică host key-urile prin:

ssh-keygen -l -f ~/.ssh/known_hosts

Nu dezactiva:

StrictHostKeyChecking

doar pentru a elimina avertismentele. OpenSSH folosește host keys pentru verificarea serverului și pentru protecția împotriva conectării la un server impostor.

17. Păstrează o metodă de recuperare

Nu elimina imediat toate metodele alternative de autentificare.

Un scenariu practic este:

FIDO2 key principală
        +
FIDO2 key de rezervă
        +
metodă de recovery

Dacă pierzi singura cheie hardware și nu există o metodă alternativă, poți pierde accesul la server.

18. Fluxul complet

                CLIENT
                   │
                   │ ssh
                   ▼
             SSH SERVER
                   │
           challenge SSH
                   │
                   ▼
             CHEIE FIDO2
                   │
          PIN + touch, opțional
                   │
                   ▼
             semnătură
                   │
                   ▼
           autentificare OK

Checklist

[ ] OpenSSH suportă ed25519-sk
[ ] cheia FIDO2 este detectată
[ ] cheia SSH este generată cu ed25519-sk
[ ] cheia publică este în authorized_keys
[ ] login-ul SSH funcționează
[ ] touch-required este activat
[ ] verify-required este folosit unde este necesar
[ ] cheia este restricționată în authorized_keys
[ ] StrictHostKeyChecking rămâne activ
[ ] există o cheie FIDO2 de rezervă
[ ] există o metodă de recovery
[mai mult...]

Cum protejezi cheile API ale unui serviciu cu systemd-creds și LoadCredential=

O metodă mai sigură decât păstrarea unei chei API în variabile de mediu sau direct în fișierul unității este să folosești credentialele systemd.

Modelul este:

cheie API
   ↓
systemd-creds encrypt
   ↓
fișier criptat
   ↓
LoadCredentialEncrypted=
   ↓
systemd decriptează la pornire
   ↓
/run/credentials/...
   ↓
serviciul citește cheia

Credentialele systemd sunt încărcate la pornirea serviciului, sunt read-only în timpul execuției și sunt accesibile procesului serviciului prin directorul indicat de variabila CREDENTIALS_DIRECTORY.

1. Verifică dacă ai systemd-creds

systemd-creds --version

Comenzile pentru criptare și decriptare sunt disponibile în versiunile moderne de systemd; mecanismul de credentiale a fost introdus începând cu systemd 247, iar criptarea cu systemd-creds din documentația actuală este disponibilă începând cu versiunea 250.

2. Creează cheia API

Exemplu:

sudo install -d -m 700 /etc/myapp
sudo nano /etc/myapp/api-key.txt

Pune cheia în fișier:

sk-example-very-secret-key

Protejează fișierul:

sudo chmod 600 /etc/myapp/api-key.txt
sudo chown root:root /etc/myapp/api-key.txt

Acest fișier este doar etapa inițială. Nu vrem ca aplicația să citească direct această copie plaintext.

3. Criptează cheia cu systemd-creds

sudo systemd-creds encrypt \
  --name=api_key \
  /etc/myapp/api-key.txt \
  /etc/myapp/api-key.cred

Rezultatul este un credential criptat și autentificat. systemd folosește AES-256-GCM, iar protecția poate folosi o cheie derivată din TPM2, cheia locală din /var/lib/systemd/credential.secret sau ambele, în funcție de configurația sistemului.

După ce ai verificat că fișierul criptat există:

sudo ls -l /etc/myapp/api-key.cred

șterge copia plaintext:

sudo shred -u /etc/myapp/api-key.txt

4. Configurează serviciul

Editează serviciul:

sudo systemctl edit myapp.service

Adaugă:

[Service]
LoadCredentialEncrypted=api_key:/etc/myapp/api-key.cred

Aici:

  • api_key este numele credentialului;
  • /etc/myapp/api-key.cred este fișierul criptat;
  • systemd îl decriptează la pornirea serviciului;
  • aplicația primește credentialul în directorul său de credentiale.

5. Citește cheia din aplicație

Directorul credentialelor este disponibil prin:

echo "$CREDENTIALS_DIRECTORY"

Credentialul nostru va fi disponibil astfel:

$CREDENTIALS_DIRECTORY/api_key

De exemplu, un script poate face:

#!/bin/sh

API_KEY=$(cat "$CREDENTIALS_DIRECTORY/api_key")

exec /usr/local/bin/myapp --api-key "$API_KEY"

Mai bine este ca aplicația să știe să citească direct fișierul credentialului, fără să îl transforme într-o variabilă de mediu. Documentația systemd recomandă acest model deoarece credentialele nu sunt propagate prin proces tree precum variabilele de mediu.

6. Reîncarcă systemd

sudo systemctl daemon-reload

Verifică unitatea:

systemd-analyze verify myapp.service

7. Pornește serviciul

sudo systemctl restart myapp.service

Verifică:

systemctl status myapp.service

Și logurile:

journalctl -u myapp.service -b

8. Verifică existența credentialului fără să afișezi cheia

Poți verifica faptul că serviciul are directorul de credentiale:

systemctl show myapp.service -p Environment

Pentru o verificare mai practică, aplicația sau wrapper-ul poate verifica existența fișierului:

test -r "$CREDENTIALS_DIRECTORY/api_key"

Evită comenzi precum:

cat /run/credentials/myapp.service/api_key

deoarece afișează secretul în terminal.

9. Adaugă și sandboxing

Credentialele sunt și mai utile împreună cu izolarea serviciului:

[Service]
LoadCredentialEncrypted=api_key:/etc/myapp/api-key.cred

User=myapp
Group=myapp

NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes

Documentația systemd recomandă și namespacing-ul mount-urilor pentru serviciile care folosesc credentiale; mai multe opțiuni de sandboxing, precum ProtectSystem, implică deja PrivateMounts. Astfel, directorul de credentiale al serviciului poate fi ascuns față de alte servicii.

10. Unde ajunge cheia?

Pentru un serviciu system-level, credentialele sunt în mod obișnuit disponibile sub:

/run/credentials/<numele-serviciului>/

Nu este recomandat să hardcodezi această cale în aplicație. Folosește CREDENTIALS_DIRECTORY.

11. De ce este mai bine decât Environment=

O abordare de evitat este:

[Service]
Environment=API_KEY=secret123

Variabilele de mediu sunt moștenite de procesele copil și pot traversa anumite limite de securitate ale proceselor. Documentația systemd recomandă LoadCredential, LoadCredentialEncrypted sau SetCredentialEncrypted pentru date sensibile.

De asemenea, nu pune o cheie secretă direct în:

SetCredential=api_key:secret123

Unit files sunt accesibile și prin D-Bus, deci SetCredential nu este potrivit pentru secrete sensibile. Pentru astfel de date se recomandă LoadCredential sau LoadCredentialEncrypted.

12. Fluxul complet

/etc/myapp/api-key.txt
        │
        │ systemd-creds encrypt
        ▼
/etc/myapp/api-key.cred
        │
        │ LoadCredentialEncrypted=
        ▼
systemd
        │
        │ decrypt + authenticate
        ▼
CREDENTIALS_DIRECTORY/api_key
        │
        ▼
myapp.service

Checklist

[ ] cheia nu este în unit file
[ ] cheia nu este în Environment=
[ ] cheia plaintext este ștearsă după criptare
[ ] fișierul .cred este protejat
[ ] LoadCredentialEncrypted= este folosit
[ ] aplicația citește CREDENTIALS_DIRECTORY
[ ] serviciul rulează cu User= dedicat
[ ] NoNewPrivileges=yes
[ ] ProtectSystem=strict
[ ] logurile nu afișează cheia
[ ] systemd-analyze verify trece

Nu tratezi cheia API ca pe o configurație obișnuită. O criptezi la storage, o atașezi serviciului prin LoadCredentialEncrypted=, iar systemd o decriptează doar când serviciul pornește. Aplicația primește cheia ca fișier read-only într-un spațiu dedicat serviciului, fără să fie nevoie să o pui în unit file sau în variabile de mediu.

[mai mult...]

Cum auditezi și securizezi un serviciu Linux folosind systemd-analyze

systemd-analyze are două comenzi foarte utile pentru securizarea unui serviciu:

  • verify verifică sintaxa și problemele de configurare ale unității.
  • security analizează mecanismele de sandboxing și calculează un nivel de expunere. O valoare mai mică înseamnă, în general, mai puține privilegii și o izolare mai strictă.

Vom folosi serviciul fictiv myapp.service. Înlocuiește numele cu serviciul pe care vrei să îl verifici.

1. Verifică serviciul

Începe cu:

systemctl status myapp.service

Vezi configurația efectivă:

systemctl cat myapp.service

Și verifică proprietățile importante:

systemctl show myapp.service \
  -p User \
  -p Group \
  -p ExecStart \
  -p AmbientCapabilities \
  -p CapabilityBoundingSet \
  -p NoNewPrivileges

Scopul este să afli cu ce utilizator rulează serviciul, ce executabil pornește și ce privilegii primește.

2. Rulează auditul de securitate

systemd-analyze security --no-pager myapp.service

Vei primi o listă cu verificări de tipul:

NAME                         DESCRIPTION                         EXPOSURE
✗ User=/DynamicUser=         Service runs as root user            ...
✗ PrivateTmp=                Service has access to /tmp           ...
✓ ProtectSystem=             Service has limited filesystem       ...
...

La final apare nivelul general de expunere. Acest scor trebuie folosit ca instrument de audit, nu ca verdict absolut. systemd verifică mecanismele sale de izolare, nu securitatea internă a aplicației.

3. Verifică fișierul unității înainte de modificări

systemd-analyze verify /etc/systemd/system/myapp.service

Comanda poate identifica directive necunoscute, probleme de sintaxă și alte erori de configurare. Pentru unitățile existente, poți verifica și configurația instalată de distribuție:

systemd-analyze verify myapp.service

4. Rulează serviciul cu un utilizator dedicat

Un serviciu care nu are nevoie de privilegii administrative nu ar trebui să ruleze ca root.

Exemplu:

[Service]
User=myapp
Group=myapp

Verifică mai întâi că utilizatorul există:

id myapp

Separarea utilizatorului este una dintre primele măsuri de reducere a impactului unei eventuale compromiteri.

5. Activează NoNewPrivileges

Adaugă:

[Service]
NoNewPrivileges=yes

Această opțiune împiedică procesul și procesele sale copil să obțină privilegii suplimentare prin mecanisme precum setuid, setgid sau file capabilities. La nivel de kernel, flag-ul devine permanent pentru procesele care îl moștenesc.

6. Izolează filesystem-ul

O configurație uzuală pentru un serviciu care nu trebuie să modifice sistemul este:

[Service]
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes

ProtectSystem=strict face ierarhia filesystem-ului în principal read-only pentru serviciu. ProtectHome ascunde sau restricționează accesul la directoarele utilizatorilor. PrivateTmp oferă serviciului un spațiu separat pentru /tmp și /var/tmp.

Dacă aplicația trebuie să scrie într-un director precis, nu transforma întregul filesystem în writable. Permite doar acea zonă:

[Service]
ProtectSystem=strict
ReadWritePaths=/var/lib/myapp

7. Restricționează accesul la dispozitive

Pentru un serviciu obișnuit care nu are nevoie de dispozitive hardware:

[Service]
PrivateDevices=yes

Acest lucru reduce accesul procesului la dispozitivele sistemului.

8. Protejează interfața kernelului

Pentru servicii normale:

[Service]
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes

Ideea este ca procesul să nu poată modifica facilități ale kernelului sau configurația control groups de care nu are nevoie. Documentația systemd recomandă folosirea acestor mecanisme de sandboxing atunci când compatibilitatea aplicației permite acest lucru.

9. Limitează rețeaua

Dacă aplicația folosește numai conexiuni TCP/IP obișnuite și socketuri Unix:

[Service]
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6

Astfel limitezi familiile de adrese pe care procesul le poate folosi prin socket. Nu aplica această regulă unui serviciu înainte să verifici ce tipuri de socket folosește.

10. Limitează capabilities

Verifică mai întâi ce capabilities sunt necesare:

systemctl show myapp.service -p CapabilityBoundingSet

Dacă aplicația nu are nevoie de capabilities, acestea pot fi eliminate:

[Service]
CapabilityBoundingSet=
AmbientCapabilities=

Capability bounding set limitează capabilities care pot fi moștenite de procesele executate ulterior. Nu elimina capabilities necesare pentru operația normală a serviciului.

11. Aplică modificările printr-un drop-in

Pentru un serviciu instalat de distribuție, evită modificarea directă a fișierului original.

Folosește:

sudo systemctl edit myapp.service

Poți introduce, de exemplu:

[Service]
User=myapp
Group=myapp

NoNewPrivileges=yes

ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes

PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes

RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6

Acesta este un punct de pornire. Nu toate serviciile acceptă toate restricțiile.

12. Reîncarcă systemd

sudo systemctl daemon-reload

Apoi verifică din nou unitatea:

systemd-analyze verify myapp.service

13. Repornește serviciul

sudo systemctl restart myapp.service

Verifică imediat:

systemctl status myapp.service

Dacă serviciul nu mai pornește, consultă logurile:

journalctl -u myapp.service -b

Pentru urmărire live:

journalctl -u myapp.service -f

14. Rulează din nou auditul

systemd-analyze security --no-pager myapp.service

Compară rezultatul cu cel inițial.

Important: nu urmări doar reducerea scorului. O restricție care face serviciul inutilizabil nu este o îmbunătățire practică. Auditul trebuie să fie urmat de testarea funcționalității. systemd însuși recomandă activarea cât mai multor mecanisme de sandboxing fără a afecta funcționarea serviciului.

Workflow recomandat

systemctl cat
      ↓
systemd-analyze verify
      ↓
systemd-analyze security
      ↓
systemctl edit
      ↓
daemon-reload
      ↓
restart
      ↓
journalctl
      ↓
systemd-analyze security

Checklist rapid

[ ] Nu rulează ca root fără motiv
[ ] NoNewPrivileges=yes
[ ] ProtectSystem=strict
[ ] ProtectHome=yes
[ ] PrivateTmp=yes
[ ] PrivateDevices=yes
[ ] ProtectKernelTunables=yes
[ ] ProtectKernelModules=yes
[ ] ProtectControlGroups=yes
[ ] RestrictAddressFamilies configurat
[ ] Capabilities limitate
[ ] systemd-analyze verify trece
[ ] Serviciul funcționează după restart
[ ] Logurile nu arată erori
[ ] systemd-analyze security a fost rulat din nou

Auditarea cu systemd-analyze nu înseamnă doar să obții un scor. Procesul corect este:

identifici privilegiile → reduci suprafața de atac → verifici compatibilitatea → testezi serviciul → rulezi auditul din nou.

[mai mult...]

Cum rulezi containere Podman automat cu Quadlet și systemd

Quadlet îți permite să definești containere Podman în fișiere simple, iar systemd se ocupă de pornire, oprire și restart automat. Un fișier de tip .container este transformat de Podman într-un serviciu systemd.

1. Verifică Podman

Pe sistemul unde rulezi containerul:

podman --version
podman info --format '{{.Host.CgroupsVersion}}'

Quadlet necesită cgroups v2.

2. Creează directorul Quadlet

Pentru un container rootless, folosește:

mkdir -p ~/.config/containers/systemd

Acesta este unul dintre directoarele standard în care Quadlet caută configurațiile utilizatorului.

3. Creează definiția containerului

Exemplu: un server Nginx care ascultă pe portul 8080.

Creează fișierul:

nano ~/.config/containers/systemd/nginx.container

Conținut:

[Unit]
Description=NGINX container

[Container]
Image=docker.io/library/nginx:alpine
PublishPort=8080:80

[Service]
Restart=always

Cheia Image definește imaginea, iar PublishPort mapează portul 8080 al sistemului la portul 80 din container. Pentru imagine este recomandată folosirea unui nume complet calificat.

4. Reîncarcă systemd

systemctl --user daemon-reload

Quadlet generează serviciul systemd pornind de la fișierul .container.

5. Pornește containerul

systemctl --user start nginx.service

Verifică starea:

systemctl --user status nginx.service

Și verifică direct containerul:

podman ps

6. Testează aplicația

Pe aceeași mașină:

curl http://localhost:8080

Ar trebui să primești pagina implicită Nginx.

7. Pornește automat după restart

Pentru Quadlet nu este necesar să activezi manual serviciul generat cu systemctl enable. Pornirea automată se configurează în fișierul Quadlet prin secțiunea Install.

Modifică fișierul astfel:

[Unit]
Description=NGINX container

[Container]
Image=docker.io/library/nginx:alpine
PublishPort=8080:80

[Service]
Restart=always

[Install]
WantedBy=default.target

Apoi:

systemctl --user daemon-reload

Pentru un serviciu rootless care trebuie să ruleze și atunci când utilizatorul nu este autentificat, activează lingering:

sudo loginctl enable-linger $USER

Lingering permite instanței systemd a utilizatorului să rămână activă după logout și după reboot.

8. Vezi logurile

journalctl --user -u nginx.service

Pentru urmărire în timp real:

journalctl --user -u nginx.service -f

Podman recomandă journalctl pentru verificarea logurilor serviciilor Quadlet.

9. Dacă serviciul nu este găsit

Dacă primești o eroare de tip „Unit not found”, verifică generarea unității:

systemd-analyze --user --generators=true verify nginx.service

Pentru o analiză mai detaliată poți rula generatorul Quadlet în mod dry-run:

/usr/lib/systemd/system-generators/podman-system-generator --user --dryrun

Aceste comenzi ajută la identificarea erorilor de sintaxă sau a opțiunilor Quadlet incompatibile.

Fluxul de bază

În practică, ciclul este:

nginx.container
      ↓
systemd --user daemon-reload
      ↓
nginx.service
      ↓
podman run
      ↓
containerul rulează

Avantajul principal este că configurația containerului rămâne declarativă și este gestionată de systemd, inclusiv restartul și pornirea automată.

Rootless vs. rootful

Pentru rootless:

~/.config/containers/systemd/
systemctl --user
podman ps

Pentru rootful:

/etc/containers/systemd/
sudo systemctl
sudo podman ps

Directorul rootless și cel rootful sunt diferite, iar comenzile systemd trebuie executate în contextul corespunzător.

Pe scurt: scrii un fișier .container, îl pui în directorul Quadlet, rulezi daemon-reload, apoi gestionezi containerul cu systemctl. Asta elimină nevoia de scripturi separate pentru pornire și restart.

[mai mult...]