Web Dashboard per lo Shutdown Remoto: PC Windows da Raspberry Pi
In questa guida vedremo come realizzare un pannello di controllo web ospitato su un Raspberry Pi per spegnere i PC Windows con un solo tocco, con tanto di controllo di stato (Online/Offline)
1. Preparazione del PC Windows (Target)
Per ricevere il comando dal Raspberry, Windows deve avere il server SSH attivo.
Installazione OpenSSH Server
Apri PowerShell come Amministratore e lancia:
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
Start-Service sshd
Set-Service -Name sshd -StartupType 'Automatic'
Abilitazione Shutdown via SSH
Assicurati che l’utente che userai per il collegamento abbia i permessi di spegnimento. In genere un account amministratore è sufficiente.
2. Configurazione del Raspberry Pi (Server)
Sul Raspberry Pi installeremo Nginx e PHP per gestire la dashboard.
Installazione dei pacchetti necessari
sudo apt update
sudo apt install nginx php-fpm openssh-client -y
Configurazione Chiavi SSH
Per permettere al Raspberry di spegnere il PC senza inserire ogni volta la password, dobbiamo usare le chiavi SSH.
# Genera la chiave (premi invio a tutto)
ssh-keygen -t rsa
# Copia la chiave sul PC Windows
ssh-copy-id utente@indirizzo_ip_windows
3. La Web Dashboard
La dashboard interrogherà lo stato del PC e fornirà un pulsante per inviare il comando con spegnimento a 19 secondi:
shutdown /s /t 19.
4. Configurazione delle Authorized Keys su Windows
Una volta generata la chiave sul Raspberry, dobbiamo “presentarla” a Windows. Il procedimento cambia se l’utente che userai è un Amministratore o un Utente Standard.
Caso A: Utente Amministratore (Consigliato per lo Shutdown)
Se l’utente configurato nel PHP (es. administrator) ha privilegi di admin, Windows ignora la cartella dell’utente e cerca un file di sistema protetto.
Crea il file di sistema:
Apri il Blocco Note come Amministratore e crea il file:
C:\ProgramData\ssh\administrators_authorized_keys
Incolla la chiave:
Copia la chiave pubblica del Raspberry (id_ed25519.pub) all’interno del file e salva.
Configura i permessi (Fondamentale): Affinché SSH accetti la chiave, il file deve essere leggibile solo dal sistema.
- Tasto destro sul file > Proprietà > Sicurezza > Avanzate.
- Clicca su Disabilita ereditarietà e scegli “Rimuovi tutti i permessi ereditati”.
- Aggiungi l’utente SYSTEM e il gruppo Administrators con permessi di “Lettura” e “Controllo completo”.
- Rimuovi qualsiasi altro utente o gruppo dalla lista.
Caso B: Utente Standard
Se l’utente è limitato, la procedura è più “stile Linux”:
- Vai in
C:\Users\TuoUtente\. - Crea una cartella chiamata
.ssh(se non esiste). - Crea all’interno un file chiamato
authorized_keys(senza estensione.txt). - Incolla la chiave del Raspberry.
Modifica del file sshd_config
Per essere sicuri che Windows dia la precedenza alle chiavi rispetto alla password, dobbiamo istruire il server SSH.
- Apri con i privilegi di admin il file
C:\ProgramData\ssh\sshd_config. - Assicurati che queste righe siano de-commentate (senza
#davanti):
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
Riavvia il servizio per applicare le modifiche: Apri PowerShell (Admin) e digita:
Restart-Service sshd
Il test di “Fiducia” (Host Verification)
La prima volta che il Raspberry si collega, chiederà di confermare l’identità del PC. Attenzione: se non lo fai manualmente, lo script PHP fallirà silenziosamente.
Esegui questo comando una volta sola dal terminale del Raspberry:
sudo -u www-data ssh -o StrictHostKeyChecking=accept-new tuo_utente@192.168.x.x "whoami"
Se il terminale ti restituisce il nome utente senza chiedere la password, la configurazione è perfetta!
Un piccolo consiglio :
ProgramDataè una cartella nascosta.
Il codice completo della dashboard (index.php)
<?php
// dati pc
$pcs = [
"PC 1" => ["ip" => "192.168.x.x", "user" => "administrator"],
"PC 2" => ["ip" => "192.168.x.x", "user" => "user"]
];
// controllo porta RDP (3389)
function checkRDP($ip) {
$port = 3389;
$connection = @fsockopen($ip, $port, $errno, $errstr, 0.5);
if (is_resource($connection)) {
fclose($connection);
return true;
}
return false;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['pc_id'])) {
$id = $_POST['pc_id'];
if ($id === "all") {
foreach ($pcs as $nome => $data) {
$ip = $data['ip'];
$user = $data['user'];
$cmd = "ssh -o ConnectTimeout=2 -o StrictHostKeyChecking=no $user @$ip 'shutdown /s /t 19' > /dev/null 2>&1 &";
shell_exec($cmd);
}
echo "OK";
exit;
}
if (array_key_exists($id, $pcs)) {
$ip = $pcs[$id]['ip'];
$user = $pcs[$id]['user'];
$cmd = "ssh -o ConnectTimeout=3 -o StrictHostKeyChecking=no $user @$ip 'shutdown /s /t 19' 2>&1";
shell_exec($cmd);
echo "OK";
exit;
}
}
?>
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Power Control</title>
<link rel="manifest" href="manifest.json">
<meta name="theme-color" content="#ffffff">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--noob-bg: #f8fafc;
--noob-text: #1a202c;
--off-red: #ed4338;
--online-green: #48bb78;
--offline-gray: #a0aec0;
--card-border: #e2e8f0;
}
body { font-family: 'Montserrat', sans-serif; background-color: var(--noob-bg); color: var(--noob-text); margin: 0; padding: 0; }
header {
background-color: #ffffff; color: #000000; padding: 15px 25px;
display: flex; justify-content: space-between; align-items: center;
box-shadow: 0 2px 10px rgba(0,0,0,0.05); margin-bottom: 25px;
border-bottom: 1px solid var(--card-border);
}
header h1 { margin: 0; font-size: 1rem; text-transform: uppercase; font-weight: 700; }
header img { height: 35px; width: auto; filter: brightness(0); }
.container { max-width: 500px; margin: 0 auto; padding: 0 20px; }
.master-btn {
width: 100%; background-color: var(--off-red); color: white; border: none;
padding: 18px; border-radius: 12px; font-weight: 700; font-size: 0.9rem;
cursor: pointer; margin-bottom: 25px; display: flex; align-items: center;
justify-content: center; gap: 10px; box-shadow: 0 4px 12px rgba(237, 67, 56, 0.3);
transition: transform 0.1s;
}
.master-btn:active { transform: scale(0.98); }
.card {
background: #ffffff; border-radius: 12px; padding: 20px; margin-bottom: 15px;
display: flex; align-items: center; justify-content: space-between;
border: 1px solid var(--card-border);
}
.info { display: flex; align-items: center; }
.info i { font-size: 24px; margin-right: 15px; color: #4a5568; }
.status-badge { font-size: 0.7rem; display: flex; align-items: center; gap: 5px; margin-top: 4px; font-weight: 600; }
.dot { height: 8px; width: 8px; border-radius: 50%; display: inline-block; }
.online { color: var(--online-green); }
.online .dot { background-color: var(--online-green); box-shadow: 0 0 5px var(--online-green); }
.offline { color: var(--offline-gray); }
.offline .dot { background-color: var(--offline-gray); }
.btn-off {
background-color: var(--off-red); color: white; border: none;
padding: 10px 16px; border-radius: 8px; cursor: pointer;
font-weight: 700; font-size: 0.8rem; display: flex; align-items: center; gap: 8px;
}
.loading { opacity: 0.4; pointer-events: none; }
#toast {
position: fixed; bottom: 30px; left: 50%; transform: translateX(-50%);
background: #2d3748; color: white; padding: 12px 25px;
border-radius: 8px; display: none; font-size: 0.85rem; z-index: 1000;
}
</style>
</head>
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('sw.js')
.then(() => console.log("PWA: Service Worker Registrato"));
}
</script>
<body>
<header>
<h1>Power Control</h1>
<img src="logo-light.png" alt="Logo">
</header>
<div class="container">
<button class="master-btn" id="btn-all" onclick="sendShutdown('all', 'all')">
<i class="fa-solid fa-power-off"></i> SPEGNI ENTRAMBI
</button>
<?php foreach ($pcs as $nome => $data):
$isOnline = checkRDP($data['ip']);
?>
<div class="card" id="card-<?= md5($nome) ?>">
<div class="info">
<i class="fa-solid fa-desktop"></i>
<div>
<div class="name"><?= htmlspecialchars($nome) ?></div>
<div class="status-badge <?= $isOnline ? 'online' : 'offline' ?>">
<span class="dot"></span> <?= $isOnline ? 'ONLINE (RDP)' : 'OFFLINE' ?>
</div>
</div>
</div>
<button class="btn-off" <?= !$isOnline ? 'disabled style="background:#cbd5e0; cursor:not-allowed;"' : '' ?> onclick="sendShutdown('<?= $nome ?>', '<?= md5($nome) ?>')">
<i class="fa-solid fa-power-off"></i> SPEGNI
</button>
</div>
<?php endforeach; ?>
</div>
<div id="toast">Segnale inviato...</div>
<script>
function sendShutdown(pcId, cardUid) {
const isMaster = (pcId === 'all');
const target = isMaster ? document.getElementById('btn-all') : document.getElementById('card-' + cardUid);
target.classList.add('loading');
const params = new URLSearchParams();
params.append('pc_id', pcId);
fetch('', { method: 'POST', body: params })
.then(() => {
const toast = document.getElementById('toast');
toast.style.display = 'block';
setTimeout(() => {
location.reload();
}, 3000);
});
}
</script>
</body>
</html>