Vuoi limitare il numero di sessioni simultanee RDP per ogni gruppo AD in Windows Server 2022? Non esiste un interruttore nativo “per‑gruppo”, ma si può ottenere il risultato combinando criteri, architetture d’accesso e—se serve—automazione PowerShell. Ecco come progettare una soluzione solida e sostenibile.
Scenario e obiettivo
Un’organizzazione con centinaia di gruppi Active Directory deve imporre un tetto agli accessi contemporanei ai server (es. 35 sessioni per Gruppo A, 50 per Gruppo B, ecc.). La domanda chiave è: esiste un parametro nativo di AD o delle Group Policy che imponga direttamente una “quota di sessioni per gruppo”?
La risposta breve è no. Windows Server e Active Directory non offrono un costrutto che mantenga il conteggio di sessioni attive per gruppo e lo usi in modo transazionale per ammettere o bloccare ulteriori logon. Tuttavia, ci sono più strade—più o meno strutturate—per raggiungere lo stesso obiettivo.
Cosa c’è di serie e perché non basta
Cosa c’è “di serie” | Perché non basta |
---|---|
Group Policy Limit number of connections (solo Desktop Remoto) | Imposta un limite per server, non per gruppo: tutti gli utenti contano allo stesso modo. |
Impostazioni RDS “Per‑User licensing” | Riguardano la conformità licenze, non il numero di sessioni aperte in tempo reale. |
Restrizioni Logon hours o Deny log on locally | Regolano quando o dove un utente può collegarsi, non quante sessioni simultanee avrà il suo gruppo. |
Perché non esiste una “quota per gruppo” nativa
- Il conteggio delle sessioni è per host: RDS/Winlogon ragiona sul singolo server (o, con Connection Broker, sulla collection), non su oggetti AD come i gruppi.
- AD non tiene contatori di runtime: i gruppi AD sono insiemi statici; non hanno attributi “live” che variano a ogni logon.
- Le GPO sono declarative: applicano policy uniformi per computer o utenti; non “ramificano” dinamicamente in base a quante sessioni hanno i membri di un gruppo in quel momento.
Opzioni pratiche per ottenere il risultato
Automazione/Script PowerShell (controllo reattivo)
Come funziona: un processo schedulato interroga periodicamente tutte le sessioni attive, conta quante appartengono a ciascun gruppo “quotato” e, se il limite è superato, scollega o chiude le sessioni in eccesso secondo regole definite (idle più vecchie, disconnesse, ecc.).
- Pro: nessun costo licenza; controllo granulare; rollout rapido.
- Contro: è reattivo (l’overshoot può durare fino al prossimo ciclo); richiede parsing accurato delle sessioni; rischio di disconnessioni improvvise se non ben progettato.
- Quando sceglierla: ambienti senza RDS Gateway e senza IAM; requisiti di costo bassi; tolleranza a una lieve latenza nel blocco.
Remote Desktop Gateway + Connection Authorization Policies (controllo in ingresso)
Come funziona: segmenti l’accesso facendo passare gli utenti da RD Gateway; ogni gruppo AD è associato a un Gateway o a un endpoint logico; imposti sul Gateway il max connections consentito. In pratica, realizzi la quota per‑gruppo indirettamente, dedicando una capacità a ciascun gruppo.
- Pro: soluzione Microsoft supportata; blocco preventivo prima che la sessione arrivi al server.
- Contro: richiede infrastruttura RDS (Gateway/Connection Broker) e RDS CAL; per quote molto frammentate potresti dover creare più gateway/farm logiche.
- Quando sceglierla: accessi Internet/VPN; necessità di enforcement all’ingresso; audit centralizzato.
Piattaforme IAM di terze parti (Okta, Duo, CyberArk, Thales, …)
Come funziona: l’IAM si integra con AD e gestisce limiti di sessioni concurrent per gruppo/ruolo al momento dell’autenticazione o del rilascio del token.
- Pro: governance centralizzata, reportistica, MFA, session intelligence; enforcement pre‑autenticazione.
- Contro: costi e progetto di integrazione; dipendenza dal vendor.
- Quando sceglierla: ambienti regolamentati, SLA stringenti, necessità di audit fine-grained e sanzioni automatiche.
Broker di accesso / VDI (Citrix, VMware Horizon, Azure Virtual Desktop)
Come funziona: definisci pool di macchine o collezioni dedicati a un gruppo; limiti la capienza massima del pool. Gli utenti del gruppo possono aprire sessioni solo entro la capacità di quel pool.
- Pro: interfaccia ricca, bilanciamento, metriche; integrazione con MFA e policy avanzate.
- Contro: complessità, costi e gestione di piattaforme VDI complete.
- Quando sceglierla: uso intensivo di applicazioni multi‑utente; bisogno di controllo fine del capacity planning.
Blueprint pratico: approccio PowerShell
Di seguito un disegno operativo completo per realizzare una quota “per gruppo” con uno script che gira ogni 1–2 minuti e applica la policy con il minor impatto possibile.
Principi di progettazione
- Conta prima di agire: calcola le sessioni correnti per ogni gruppo; agisci solo se superi la soglia.
- Preferisci la disconnessione alla chiusura: prima disconnect dei client idle o già disc; logoff solo come ultima ratio.
- Definisci il criterio di selezione: es. “prima le sessioni Disc, poi le Idle più vecchie, infine le Active con idle > X”.
- Grace period: invia un avviso all’utente (es. 120 secondi) prima di forzare.
- Audit: log dettagliato di chi è stato disconnesso, perché, e su quale host.
Prerequisiti
- RSAT Active Directory e modulo
ActiveDirectory
sul server di gestione. - Account di servizio con diritti di lettura AD e privilegi di amministrazione remota sui server RDS/host.
- Firewall/WinRM/CIM aperti o esecuzione locale agent‑based.
- Elenco dei server gestiti (statico o ricavato via OU/gruppo).
Modello di configurazione
Conserva i limiti in un file JSON così:
{
"Groups": [
{ "GroupDN": "CN=Gruppo A,OU=Sicurezza,DC=contoso,DC=local", "Limit": 35, "Priority": 100 },
{ "GroupDN": "CN=Gruppo B,OU=Sicurezza,DC=contoso,DC=local", "Limit": 50, "Priority": 90 }
],
"Servers": [ "RDSH01.contoso.local", "RDSH02.contoso.local", "APP01.contoso.local" ],
"Broker": "",
"DisconnectInsteadOfLogoff": true,
"GraceMinutes": 2,
"DryRun": true,
"LogPath": "C:\\Logs\\GroupQuota"
}
Script PowerShell di esempio
Lo script sotto implementa:
- Raccolta sessioni da Get‑RDUserSession (se esiste un Connection Broker) o, in alternativa, parsing di quser.
- Cache della membership per evitare chiamate AD ripetute.
- Scelta delle sessioni da scollegare per riportare il gruppo sotto soglia, con grace period e dry‑run.
#requires -Version 5.1
Import-Module ActiveDirectory -ErrorAction Stop
param(
[string]$ConfigPath = "C:\ProgramData\GroupQuota.json"
)
---------- Utilità ----------
function Write-Log {
param([string]$Message,[string]$Level = "INFO")
$ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$line = "[$ts][$Level] $Message"
Write-Output $line
if ($script:LogFile) { Add-Content -LiteralPath $script:LogFile -Value $line }
}
function Ensure-Folder($Path) { if (-not (Test-Path -LiteralPath $Path)) { New-Item -ItemType Directory -Path $Path | Out-Null } }
function Load-Config {
param([string]$Path)
if (-not (Test-Path -LiteralPath $Path)) { throw "Config non trovata: $Path" }
$json = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
if (-not $json.Groups -or -not $json.Servers) { throw "Config non valida: mancano Groups o Servers" }
return $json
}
Recupera sessioni: prova Broker, altrimenti quser
function Get-AllSessions {
param([string[]]$Servers,[string]$Broker)
$sessions = @()
$hasRD = $false
if ($Broker) {
if (Get-Command Get-RDUserSession -ErrorAction SilentlyContinue) {
try {
$rd = Get-RDUserSession -ConnectionBroker $Broker -ErrorAction Stop
$hasRD = $true
foreach ($s in $rd) {
$sessions += [pscustomobject]@{
Server = $s.HostServer
User = "$($s.DomainName)$($s.UserName)"
SessionId = $s.SessionId
State = $s.SessionState
Idle = $null
LogonTime = $s.SessionCreateTime
}
}
} catch { Write-Log "Errore Get-RDUserSession: $_" "WARN" }
}
}
if (-not $hasRD) {
foreach ($srv in $Servers) {
$out = & quser /server:$srv 2>&1
if ($LASTEXITCODE -ne 0) { Write-Log "quser fallito su $srv: $out" "WARN"; continue }
Parsing robusto delle righe di quser (gestisce > davanti alle sessioni)
$lines = $out -split "`n" | Select-Object -Skip 1
foreach ($line in $lines) {
if (-not $line.Trim()) { continue }
$l = $line -replace "^>","" -replace "\s+"," "
Username [2], ID [3], Stato [4], Idle [5], Logon [6..]
$parts = $l.Trim() -split " "
if ($parts.Count -lt 6) { continue }
$username = $parts[1]
$id = [int]$parts[2]
$state = $parts[3]
$idle = $parts[4]
$logon = ($parts[5..($parts.Count-1)] -join " ")
$sessions += [pscustomobject]@{
Server = $srv
User = $username
SessionId = $id
State = $state
Idle = $idle
LogonTime = $logon
}
}
}
}
return $sessions
}
Converte "DOM\utente" in SID e memoizza
$SidCache = @{}
function Get-SID {
param([string]$Account)
if ($SidCache.ContainsKey($Account)) { return $SidCache[$Account] }
try {
if ($Account -notmatch "\") {
Tenta \utente
$obj = New-Object System.Security.Principal.NTAccount($env:USERDOMAIN,$Account)
} else {
$dom,$usr = $Account.Split("",2)
$obj = New-Object System.Security.Principal.NTAccount($dom,$usr)
}
$sid = $obj.Translate([System.Security.Principal.SecurityIdentifier]).Value
$SidCache[$Account] = $sid
return $sid
} catch { return $null }
}
Precalcola i membri (SID) dei gruppi quotati
function Build-GroupMembership {
param($Groups)
$map = @{}
foreach ($g in $Groups) {
$dn = $g.GroupDN
$sids = New-Object System.Collections.Generic.HashSet[string]
try {
$members = Get-ADGroupMember -Identity $dn -Recursive -ErrorAction Stop | Where-Object {$*.ObjectClass -eq "user"}
foreach ($m in $members) {
try {
$u = Get-ADUser -Identity $m.DistinguishedName -Properties SID -ErrorAction Stop
[void]$sids.Add($u.SID.Value)
} catch {}
}
$map[$dn] = @{
Limit = [int]$g.Limit
Priority = [int]($g.Priority | ForEach-Object { $* }) # default 0
Members = $sids
}
} catch { Write-Log "Errore membership per $dn: $_" "WARN" }
}
return $map
}
Calcola idle in minuti da stringa quser (es. "none", "1", "3:22")
function Parse-IdleMinutes($idle) {
if (-not $idle -or $idle -eq "none") { return 0 }
if ($idle -match "^\d+$") { return [int]$idle }
if ($idle -match "^\d+:\d+$") {
$h,$m = $idle.Split(":")
return ([int]$h*60 + [int]$m)
}
return 0
}
Notifica e disconnette / chiude
function Enforce-Action {
param(
[pscustomobject]$Session,
[switch]$Disconnect,
[int]$GraceSeconds = 0,
[switch]$DryRun
)
$srv = $Session.Server
$id = $Session.SessionId
if ($DryRun) {
Write-Log "[DRY‑RUN] $($Disconnect?"DISCONNECT":"LOGOFF") session $id su $srv per utente $($Session.User)"
return
}
if ($GraceSeconds -gt 0) {
try { & msg.exe /server:$srv $id "Quota di gruppo raggiunta. La sessione verrà $($Disconnect?'disconnessa':'chiusa') tra $GraceSeconds secondi." } catch {}
Start-Sleep -Seconds $GraceSeconds
}
if ($Disconnect) {
tsdiscon non accetta /server; usa cmd remoto
$script = "tsdiscon $id"
try { Invoke-Command -ComputerName $srv -ScriptBlock { param($cmd) cmd.exe /c $cmd } -ArgumentList $script -ErrorAction Stop | Out-Null; Write-Log "DISCONNECT eseguito ID $id su $srv" }
catch { Write-Log "DISCONNECT fallito ID $id su $srv: $*" "WARN" }
} else {
try { & logoff $id /server:$srv; Write-Log "LOGOFF eseguito ID $id su $srv" }
catch { Write-Log "LOGOFF fallito ID $id su $srv: $*" "WARN" }
}
}
---------- MAIN ----------
try {
$cfg = Load-Config -Path $ConfigPath
Ensure-Folder -Path $cfg.LogPath
$script:LogFile = Join-Path $cfg.LogPath ("enforce_" + (Get-Date -Format "yyyyMMdd") + ".log")
Write-Log "Avvio controllo quote per gruppi…"
$groupMap = Build-GroupMembership -Groups $cfg.Groups
$sessions = Get-AllSessions -Servers $cfg.Servers -Broker $cfg.Broker
Arricchisci le sessioni con SID e IdleMinutes
foreach ($s in $sessions) {
$s | Add-Member -NotePropertyName SID -NotePropertyValue (Get-SID -Account $s.User)
$s | Add-Member -NotePropertyName IdleMinutes -NotePropertyValue (Parse-IdleMinutes $s.Idle)
}
Conta le sessioni per gruppo
$usage = @{}
foreach ($g in $cfg.Groups) {
$dn = $g.GroupDN
$usage[$dn] = @()
foreach ($s in $sessions) {
if (-not $s.SID) { continue }
if ($groupMap[$dn].Members.Contains($s.SID)) {
$usage[$dn] += $s
}
}
}
Enforce per ogni gruppo
foreach ($g in ($cfg.Groups | Sort-Object -Property @{Expression="Priority";Descending=$true})) {
$dn = $g.GroupDN
$limit = [int]$g.Limit
$curr = @($usage[$dn]).Count
Write-Log "Gruppo: $dn — Limite: $limit — Correnti: $curr"
if ($curr -le $limit) { continue }
```
$excess = $curr - $limit
Write-Log "Eccesso: $excess sessioni per $dn. Selezione candidati…"
# Ordina: 1) Disc, 2) Idle desc, 3) LogonTime più vecchia
$candidates = $usage[$dn] | Sort-Object -Property `
@{Expression={ $_.State -eq "Disc" ? 0 : 1 }; Ascending=$true}, `
@{Expression="IdleMinutes";Descending=$true}, `
@{Expression="LogonTime";Ascending=$true}
$toAct = $candidates | Select-Object -First $excess
foreach ($sess in $toAct) {
$disconnect = [bool]$cfg.DisconnectInsteadOfLogoff
Enforce-Action -Session $sess -Disconnect:$disconnect -GraceSeconds ($cfg.GraceMinutes*60) -DryRun:([bool]$cfg.DryRun)
}
```
}
Write-Log "Controllo completato."
} catch {
Write-Log "Errore non gestito: $_" "ERROR"
exit 1
}
Note operative:
- Imposta il Task Scheduler con trigger ogni 1–2 minuti, esecuzione singola (impedisci sovrapposizioni) e Stop the task if it runs longer than… per evitare code.
- DryRun iniziale: lascia
DryRun=true
per una settimana e analizza i log; poi passa afalse
. - Per ambienti RDS completi,
Get‑RDUserSession
è preferibile aquser
perché fornisce dominio/utente e stato nativi.
Varianti e hardening
- Selezione “fair‑share”: conserva per utente un numero max di sessioni prima di scollegare.
- Prevenzione overshoot: aggiungi un watcher sugli eventi TerminalServices‑LocalSessionManager (logon) per reagire in <10 s.
- File‑lock: usa un mutex di sistema per garantire che non partano due istanze concorrenti.
- Telemetria: esporta un CSV aggregato (gruppo, corrente, limite, eccedenza) verso il tuo sistema di monitoring.
Approccio RD Gateway: enforcement all’ingresso
Quando gli accessi passano da Internet o VPN, RD Gateway è il posto giusto per applicare una quota preventiva.
Passaggi consigliati
- Distribuisci il ruolo Remote Desktop Gateway (idealmente in alta affidabilità) con NPS locale o centralizzato.
- Definisci una CAP (Connection Authorization Policy) per ogni gruppo o “cluster” di gruppi con requisiti simili. La CAP filtra per appartenenza a gruppi AD e controlli MFA.
- Definisci una RAP (Resource Authorization Policy) per limitare le destinazioni (host o gruppi di host) che ciascuna CAP può raggiungere.
- Sul Gateway (o su ciascun listener se separi i nomi DNS), imposta il numero massimo di connessioni simultanee consono alla quota del/i gruppo/i serviti da quel Gateway.
- Pubblica nomi DNS separati (es.
rdg‑finance.contoso.com
,rdg‑ops.contoso.com
) e instrada i gruppi al relativo endpoint via GPO/VPN/portal.
Perché funziona: non limiti “per gruppo” dentro al Gateway, ma per endpoint; mappando 1:1 (o 1:N) endpoint→gruppi, il limite diventa effettivo per quel set di utenti. Se ti servono quote diverse per molti gruppi, raggruppali in tier (bronze/argento/oro) con capacità proporzionate.
Piattaforme IAM: quando e perché
Le soluzioni IAM moderne possono implementare Concurrent Session Limit per gruppo o ruolo al momento dell’autenticazione. In pratica, negano la creazione di ulteriori sessioni quando la quota per quel ruolo è satura, spesso con messaggi d’errore personalizzati e code/approvazioni.
- Punti di forza: MFA, flussi di approvazione, report compliance, integrazione SIEM, session revocation.
- Valutazioni: latenza d’autenticazione, affidabilità del connettore AD, modalità fail‑open/fail‑closed, costi.
Broker di accesso/VDI: controllo per pool
Citrix, Horizon e AVD permettono di definire pool/collezioni per “servire” uno o più gruppi, impostando contemporaneamente la capienza massima del pool. È una forma efficace di capacity shaping: se la collezione è piena, gli utenti del relativo gruppo non possono avviare nuove sessioni.
Raccomandazioni operative
- Valuta se un limite per utente è già sufficiente: la GPO Restrict Remote Desktop Services users to a single Remote Desktop Services session riduce le moltiplicazioni di sessioni.
- Configura idle timeout e disconnection timeout sugli host per recuperare capacità inutilizzata.
- Se scegli lo script, implementa logging, grace period e dry‑run. Prevedi esclusioni (account di servizio, break‑glass).
- Per ambienti con requisiti di audit e SLA rigidi, preferisci RD Gateway/IAM/VDI: enforcement preventivo e supporto enterprise.
- Documenta una policy aziendale sulle quote: cosa succede al superamento, chi è prioritario, canali di escalation.
Checklist di implementazione
- Inventario: server interessati, gruppi AD, soglie richieste.
- Decisione architetturale: Script reattivo, RD Gateway, IAM o VDI.
- Prototipo: in ambiente pilota con due gruppi e due soglie.
- Policy utente: messaggi di avviso, orari di picco, regole di priorità.
- Roll‑out controllato: progressivo per dipartimenti; monitoraggio metriche.
- Runbook: procedure incident per sblocco, whitelist temporanea, emergenze.
FAQ
Posso usare solo GPO senza script o gateway?
No: puoi avvicinarti al risultato con limiti per server e per utente, ma non otterrai un vero limite per gruppo senza un livello di controllo superiore.
La disconnessione fa perdere dati?
Le app session‑aware reggono la disconnessione, ma se l’utente ha lavoro non salvato, il rischio esiste. Per questo serve un grace period con avviso.
Meglio logoff o disconnect?
Disconnect è più sicuro e reversibile; logoff libera più risorse ma è intrusivo. Usa il logoff solo oltre una certa soglia o su sessioni in stato Disc da molto tempo.
Come gestisco gli utenti che appartengono a più gruppi quotati?
Definisci una Priority per ogni gruppo nel file di configurazione e risolvi i conflitti in favore del gruppo con priorità più alta.
Serve un Connection Broker per contare le sessioni?
No: puoi interrogare ogni host (es. con quser
). Il Broker però centralizza la visione e semplifica.
Esempio di policy aziendale
- Tier di quota: Bronze (25), Silver (50), Gold (100).
- Priorità: i gruppi Gold non vengono mai disconnessi per far posto ai Bronze; i Silver possono essere disconnessi solo se idle > 30 min.
- Messaggistica: avviso 2 min prima; link alla pagina interna con spiegazioni e finestre orarie alternative.
- Eccezioni: “break‑glass” con validità 2 ore, tracciate a ticket.
Conclusioni
Windows Server 2022 e Active Directory non espongono un’impostazione nativa per limitare per gruppo le sessioni contemporanee. La soluzione passa da un livello architetturale o di automazione che “misuri” e agisca al momento giusto. Se vuoi contenere costi e tempi, lo script PowerShell qui proposto è un ottimo punto di partenza; per enforcement preventivo e audit enterprise, RD Gateway o una piattaforma IAM offrono il controllo più solido. In tutte le varianti, definisci regole chiare, avvisi agli utenti e una telemetria affidabile: sono questi gli ingredienti che trasformano una quota tecnica in una politica di accesso sostenibile.
Appendice: impostazioni utili di contorno
- Per utente una sola sessione: GPO Restrict Remote Desktop Services users to a single Remote Desktop Services session.
- Timeout:
- Set time limit for disconnected sessions: recupera capacità da sessioni orfane.
- Set time limit for active but idle Remote Desktop Services sessions: limita l’inerzia.
- Limit number of connections (per server): utile come “cintura e bretelle” per impedire il sovraccarico del singolo host.
- Auditing: abilita i log RDS e sicurezza per tracciare 1149/21/24 (logon, connessione/disconnessione).
Se desideri un pacchetto pronto (script, config JSON, GPO, istruzioni Task Scheduler) da adattare al tuo dominio, puoi partire dal blueprint qui sopra e personalizzare soglie, priorità e messaggistica.
Riepilogo rapido delle scelte
Opzione | Tipo di enforcement | Granularità | Intrusività | Costi | Quando preferirla |
---|---|---|---|---|---|
Script PowerShell | Reattivo | Per gruppo (config) | Media (disconnect/logoff) | Bassi | Budget ridotto, tempi rapidi |
RD Gateway + CAP/RAP | Preventivo | Per endpoint/cluster | Bassa | Medi | Accessi Internet/VPN, audit |
Piattaforme IAM | Preventivo | Per gruppo/ruolo | Bassa | Variabili | Regolamentato, MFA/zero‑trust |
Broker/VDI | Preventivo | Per pool | Bassa | Alti | App multi‑utente, performance |
Raccomandazioni finali
- Pianifica la capacità: la quota non è un obiettivo in sé, ma un mezzo per garantire equità e stabilità del servizio.
- Comunica: avvisa i team dell’introduzione della quota e fornisci finestre preferenziali o sandbox alternative.
- Itera: misura l’utilizzo reale e ritocca le soglie per evitare “saturazioni croniche”.
Con un progetto mirato e un po’ di automazione, la “quota per gruppo” in Windows Server 2022 è assolutamente realizzabile e, soprattutto, governabile nel tempo.