Soportar autenticacion SSH por clave privada

This commit is contained in:
2026-06-03 00:49:10 +02:00
parent 8a0e1e83ed
commit ffdf787aa3
6 changed files with 86 additions and 14 deletions
+7 -4
View File
@@ -12,7 +12,7 @@ Navegador
v v
Contenedor monitor-rpi Contenedor monitor-rpi
| |
| SSH con usuario/password | SSH con usuario/password o clave privada
v v
Raspberry Pi activas Raspberry Pi activas
``` ```
@@ -118,7 +118,11 @@ Usuario SSH.
### `password` ### `password`
Password SSH. Se guarda en `config.json`, por lo que el archivo debe permanecer protegido. Password SSH. Se guarda en `config.json`, por lo que el archivo debe permanecer protegido. Si `privateKeyPath` esta configurado, la clave privada tiene prioridad y no se usa `password`.
### `privateKeyPath`
Ruta de la clave privada SSH dentro del contenedor o del host donde corre el monitor. Debe apuntar a una clave privada legible por el proceso del monitor. Para uso en Docker, monta la clave o una carpeta `.ssh` dentro del contenedor. Las claves con passphrase requieren agente SSH disponible; para monitorizacion unattended suele usarse una clave sin passphrase protegida por permisos de archivo.
### `model` ### `model`
@@ -197,7 +201,7 @@ Invoke-RestMethod http://192.168.0.53:8787/api/status -Headers @{Authorization="
Devuelve la configuracion actual. Devuelve la configuracion actual.
Nota: actualmente devuelve tambien passwords porque `config.html` permite editarlas. Nota: actualmente devuelve tambien credenciales porque `config.html` permite editarlas. Protege el acceso HTTP y el archivo `config.json`.
### `POST /api/config` ### `POST /api/config`
@@ -254,4 +258,3 @@ Reconstruir:
cd /home/yamaray/docker/monitorRPi cd /home/yamaray/docker/monitorRPi
docker compose up -d --build docker compose up -d --build
``` ```
+8
View File
@@ -37,6 +37,7 @@
"host": "192.168.0.46", "host": "192.168.0.46",
"username": "pi", "username": "pi",
"password": "", "password": "",
"privateKeyPath": "",
"role": "Home Assistant Local", "role": "Home Assistant Local",
"port": 22, "port": 22,
"id": "rpi5-ha-main", "id": "rpi5-ha-main",
@@ -49,6 +50,7 @@
"host": "192.168.0.57", "host": "192.168.0.57",
"username": "pi", "username": "pi",
"password": "", "password": "",
"privateKeyPath": "",
"role": "Home Assistant Cabaña", "role": "Home Assistant Cabaña",
"port": 22, "port": 22,
"id": "rpi4-ha-second", "id": "rpi4-ha-second",
@@ -61,6 +63,7 @@
"host": "192.168.0.53", "host": "192.168.0.53",
"username": "yamaray", "username": "yamaray",
"password": "", "password": "",
"privateKeyPath": "",
"role": "Docker", "role": "Docker",
"port": 22, "port": 22,
"id": "rpi5-jose-docker", "id": "rpi5-jose-docker",
@@ -73,6 +76,7 @@
"host": "192.168.0.130", "host": "192.168.0.130",
"username": "pi", "username": "pi",
"password": "", "password": "",
"privateKeyPath": "",
"role": "Docker - Daniel", "role": "Docker - Daniel",
"port": 22, "port": 22,
"id": "rpi5-dani-docker", "id": "rpi5-dani-docker",
@@ -85,6 +89,7 @@
"host": "192.168.0.254", "host": "192.168.0.254",
"username": "yamaray", "username": "yamaray",
"password": "", "password": "",
"privateKeyPath": "",
"role": "nginx - wireguard ", "role": "nginx - wireguard ",
"port": 22, "port": 22,
"id": "rpi3-nginx", "id": "rpi3-nginx",
@@ -97,6 +102,7 @@
"host": "192.168.0.37", "host": "192.168.0.37",
"username": "pi", "username": "pi",
"password": "", "password": "",
"privateKeyPath": "",
"role": "MQTT - varios", "role": "MQTT - varios",
"port": 22, "port": 22,
"id": "rpi3-pi3home", "id": "rpi3-pi3home",
@@ -109,6 +115,7 @@
"host": "192.168.0.60", "host": "192.168.0.60",
"username": "yamaray", "username": "yamaray",
"password": "", "password": "",
"privateKeyPath": "",
"role": "Meshcore-Interface", "role": "Meshcore-Interface",
"port": 22, "port": 22,
"id": "rpi3-meshcore", "id": "rpi3-meshcore",
@@ -121,6 +128,7 @@
"host": "192.168.1.47", "host": "192.168.1.47",
"username": "pi", "username": "pi",
"password": "", "password": "",
"privateKeyPath": "",
"role": "Reserva", "role": "Reserva",
"port": 22, "port": 22,
"id": "rpi3-node-3", "id": "rpi3-node-3",
+42 -2
View File
@@ -192,6 +192,20 @@
.field-id { grid-column: span 2; } .field-id { grid-column: span 2; }
.field-role { grid-column: span 2; } .field-role { grid-column: span 2; }
.field-location { grid-column: span 2; } .field-location { grid-column: span 2; }
.field-auth { grid-column: span 1; }
.device[data-auth-method="password"] .key-credential { display: none; }
.device[data-auth-method="key"] .password-credential { display: none; }
.credential-note {
grid-column: span 2;
color: var(--muted);
font-size: 12px;
line-height: 1.35;
align-self: center;
}
.device[data-auth-method="key"] .credential-note strong { color: var(--info); }
.device[data-auth-method="password"] .credential-note strong { color: var(--ok); }
label { label {
display: grid; display: grid;
@@ -547,8 +561,9 @@
const dashboardLink = document.querySelector("#dashboardLink"); const dashboardLink = document.querySelector("#dashboardLink");
function deviceTemplate(device, index) { function deviceTemplate(device, index) {
const authMethod = device.privateKeyPath ? "key" : "password";
return ` return `
<div class="device" data-index="${index}"> <div class="device" data-index="${index}" data-auth-method="${authMethod}">
<label title="Si esta activo, este dispositivo se consulta por SSH. Si esta inactivo, se muestra deshabilitado y no se escanea.">Activo <label title="Si esta activo, este dispositivo se consulta por SSH. Si esta inactivo, se muestra deshabilitado y no se escanea.">Activo
<input data-field="active" type="checkbox" ${device.active ? "checked" : ""}> <input data-field="active" type="checkbox" ${device.active ? "checked" : ""}>
</label> </label>
@@ -561,9 +576,21 @@
<label title="Usuario SSH para este dispositivo.">Usuario <label title="Usuario SSH para este dispositivo.">Usuario
<input data-field="username" value="${device.username || ""}"> <input data-field="username" value="${device.username || ""}">
</label> </label>
<label title="Password SSH. Se guarda en config.json dentro del volumen persistente.">Password <label class="field-auth" title="Metodo de autenticacion SSH que usara el monitor para este dispositivo.">Auth SSH
<select data-auth-method>
<option value="password" ${authMethod === "password" ? "selected" : ""}>Password</option>
<option value="key" ${authMethod === "key" ? "selected" : ""}>Clave privada</option>
</select>
</label>
<label class="password-credential" title="Password SSH. Se guarda en config.json dentro del volumen persistente. Si eliges clave privada, se guardara vacio.">Password
<input data-field="password" type="password" value="${device.password || ""}"> <input data-field="password" type="password" value="${device.password || ""}">
</label> </label>
<label class="key-credential" title="Ruta de la clave privada SSH dentro del contenedor. Ejemplo: /ssh/carabanes_monitor_ed25519. Si eliges password, se guardara vacia.">Clave privada
<input data-field="privateKeyPath" value="${device.privateKeyPath || ""}">
</label>
<div class="credential-note">
Metodo activo: <strong>${authMethod === "key" ? "Clave privada" : "Password"}</strong>
</div>
<label class="field-id" title="Identificador interno unico. Se usa para tracking de metricas como red.">ID <label class="field-id" title="Identificador interno unico. Se usa para tracking de metricas como red.">ID
<input data-field="id" value="${device.id || ""}"> <input data-field="id" value="${device.id || ""}">
</label> </label>
@@ -652,6 +679,9 @@
else if (input.type === "number") device[field] = Number(input.value); else if (input.type === "number") device[field] = Number(input.value);
else device[field] = input.value; else device[field] = input.value;
}); });
const authMethod = row.querySelector("[data-auth-method]")?.value || "password";
if (authMethod === "key") device.password = "";
else device.privateKeyPath = "";
return device; return device;
}); });
} }
@@ -702,6 +732,7 @@
port: 22, port: 22,
username: "pi", username: "pi",
password: "", password: "",
privateKeyPath: "",
model: "RPi 4", model: "RPi 4",
role: "", role: "",
location: "Rack", location: "Rack",
@@ -731,6 +762,15 @@
render(); render();
}); });
devicesEl.addEventListener("change", (event) => {
const selector = event.target.closest("[data-auth-method]");
if (!selector) return;
const row = selector.closest(".device");
row.dataset.authMethod = selector.value;
const note = row.querySelector(".credential-note strong");
if (note) note.textContent = selector.value === "key" ? "Clave privada" : "Password";
});
load().catch((error) => { load().catch((error) => {
statusEl.textContent = `Error cargando configuracion: ${error.message}`; statusEl.textContent = `Error cargando configuracion: ${error.message}`;
}); });
+1
View File
@@ -16,3 +16,4 @@ services:
volumes: volumes:
- /home/yamaray/docker/monitorRPi/app:/app:ro - /home/yamaray/docker/monitorRPi/app:/app:ro
- /home/yamaray/docker/monitorRPi/data:/data - /home/yamaray/docker/monitorRPi/data:/data
- /home/yamaray/docker/monitorRPi/ssh:/ssh:ro
+1 -1
View File
@@ -86,7 +86,7 @@
<li><code>name</code>: nombre visible.</li> <li><code>name</code>: nombre visible.</li>
<li><code>host</code>: IP o DNS.</li> <li><code>host</code>: IP o DNS.</li>
<li><code>port</code>: puerto SSH.</li> <li><code>port</code>: puerto SSH.</li>
<li><code>username/password</code>: credenciales SSH.</li> <li><code>username/password</code> o <code>privateKeyPath</code>: credenciales SSH. Si hay clave privada configurada, tiene prioridad sobre el password.</li>
<li><code>model</code>: modelo visible.</li> <li><code>model</code>: modelo visible.</li>
<li><code>role</code>: funcion del equipo.</li> <li><code>role</code>: funcion del equipo.</li>
<li><code>location</code>: ubicacion.</li> <li><code>location</code>: ubicacion.</li>
+27 -7
View File
@@ -364,17 +364,15 @@ function runCommandWithInput(command, args, input, timeoutMs) {
} }
async function sshMetrics(device) { async function sshMetrics(device) {
if (!device.password) { const hasPrivateKey = Boolean(device.privateKeyPath);
throw new Error("Password SSH no configurada"); if (!hasPrivateKey && !device.password) {
throw new Error("Credenciales SSH no configuradas");
} }
const timeoutMs = Math.max(2, config.sshTimeoutSeconds || 8) * 1000; const timeoutMs = Math.max(2, config.sshTimeoutSeconds || 8) * 1000;
const port = Number(device.port || 22); const port = Number(device.port || 22);
const destination = `${device.username || "pi"}@${device.host}`; const destination = `${device.username || "pi"}@${device.host}`;
const args = [ const sshArgs = [
"-p",
device.password,
"ssh",
"-o", "-o",
"StrictHostKeyChecking=no", "StrictHostKeyChecking=no",
"-o", "-o",
@@ -388,7 +386,29 @@ async function sshMetrics(device) {
"-s" "-s"
]; ];
const { stdout } = await runCommandWithInput("sshpass", args, REMOTE_METRICS_SCRIPT, timeoutMs); let command = "ssh";
let args = sshArgs;
if (hasPrivateKey) {
args = [
"-i",
device.privateKeyPath,
"-o",
"BatchMode=yes",
"-o",
"IdentitiesOnly=yes",
...sshArgs
];
} else {
command = "sshpass";
args = [
"-p",
device.password,
"ssh",
...sshArgs
];
}
const { stdout } = await runCommandWithInput(command, args, REMOTE_METRICS_SCRIPT, timeoutMs);
const values = parseKeyValueOutput(stdout); const values = parseKeyValueOutput(stdout);
const temp = toNumber(values.cpuTempC, 0); const temp = toNumber(values.cpuTempC, 0);
const loadAverage = String(values.loadAverage || "") const loadAverage = String(values.loadAverage || "")