Compare commits
2 Commits
e29df62f7a
...
bc50948455
| Author | SHA1 | Date | |
|---|---|---|---|
| bc50948455 | |||
| d7fe855ec0 |
+30
-13
@@ -12,7 +12,7 @@ Navegador
|
||||
v
|
||||
Contenedor monitor-rpi
|
||||
|
|
||||
| SSH con usuario/password o clave privada
|
||||
| SSH con usuario/password o clave asimetrica
|
||||
v
|
||||
Raspberry Pi activas
|
||||
```
|
||||
@@ -118,20 +118,36 @@ Usuario SSH.
|
||||
|
||||
### `password`
|
||||
|
||||
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`.
|
||||
Password SSH. Se guarda en `config.json`, por lo que el archivo debe permanecer protegido. Si el dispositivo usa `authMethod: "key"`, no se usa `password`.
|
||||
|
||||
### `authMethod`
|
||||
|
||||
Metodo de autenticacion SSH por dispositivo. Valores recomendados:
|
||||
|
||||
- `password`: usa `username` y `password`.
|
||||
- `key`: usa `username` y la clave privada global configurada en `sshPrivateKeyPath`. Operativamente, instala la clave publica del monitor en la RPi.
|
||||
|
||||
### `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.
|
||||
Campo heredado para configuraciones antiguas con clave privada por dispositivo. La interfaz nueva usa la clave global del monitor y deja este campo vacio.
|
||||
|
||||
## Autenticacion SSH por Clave Privada
|
||||
## Autenticacion SSH por Clave Asimetrica
|
||||
|
||||
El monitor soporta dos metodos de autenticacion por dispositivo:
|
||||
|
||||
- Password SSH: usar `password` y dejar `privateKeyPath` vacio.
|
||||
- Clave privada: dejar `password` vacio y configurar `privateKeyPath`.
|
||||
- Clave asimetrica: dejar `password` vacio, seleccionar `authMethod: "key"` y copiar la clave publica del monitor en la RPi.
|
||||
|
||||
Cuando `privateKeyPath` tiene valor, el backend usa clave privada y no usa el password.
|
||||
Cuando un dispositivo tiene `authMethod: "key"`, el backend usa la clave privada global configurada en `sshPrivateKeyPath`. El campo `privateKeyPath` por dispositivo queda solo como compatibilidad con configuraciones antiguas.
|
||||
|
||||
### Configuracion global de claves
|
||||
|
||||
```json
|
||||
"sshPrivateKeyPath": "/ssh/monitor_rpi_ed25519",
|
||||
"sshPublicKeyPath": "/ssh/monitor_rpi_ed25519.pub"
|
||||
```
|
||||
|
||||
La clave privada se queda solo en el monitor. La clave publica se muestra en el configurador y puede copiarse o descargarse para instalarla en las RPi remotas.
|
||||
|
||||
### 1. Guardar la clave privada en el volumen del monitor
|
||||
|
||||
@@ -144,15 +160,15 @@ En la RPi donde corre el contenedor, guarda las claves en:
|
||||
Ejemplo:
|
||||
|
||||
```text
|
||||
/home/yamaray/docker/monitorRPi/ssh/carabanes_monitor_ed25519
|
||||
/home/yamaray/docker/monitorRPi/ssh/carabanes_monitor_ed25519.pub
|
||||
/home/yamaray/docker/monitorRPi/ssh/monitor_rpi_ed25519
|
||||
/home/yamaray/docker/monitorRPi/ssh/monitor_rpi_ed25519.pub
|
||||
```
|
||||
|
||||
La clave privada debe quedar protegida:
|
||||
|
||||
```bash
|
||||
chmod 700 /home/yamaray/docker/monitorRPi/ssh
|
||||
chmod 600 /home/yamaray/docker/monitorRPi/ssh/carabanes_monitor_ed25519
|
||||
chmod 600 /home/yamaray/docker/monitorRPi/ssh/monitor_rpi_ed25519
|
||||
```
|
||||
|
||||
El `docker-compose.yml` debe montar esa carpeta dentro del contenedor:
|
||||
@@ -167,8 +183,9 @@ volumes:
|
||||
Dentro de la configuracion del dispositivo, la ruta debe ser la ruta vista por el contenedor:
|
||||
|
||||
```json
|
||||
"authMethod": "key",
|
||||
"password": "",
|
||||
"privateKeyPath": "/ssh/carabanes_monitor_ed25519"
|
||||
"privateKeyPath": ""
|
||||
```
|
||||
|
||||
### 2. Instalar la clave publica en la Raspberry destino
|
||||
@@ -187,7 +204,7 @@ chmod 600 ~/.ssh/authorized_keys
|
||||
En `authorized_keys` pega el contenido de la clave publica:
|
||||
|
||||
```text
|
||||
carabanes_monitor_ed25519.pub
|
||||
monitor_rpi_ed25519.pub
|
||||
```
|
||||
|
||||
El archivo correcto en Linux/OpenSSH es:
|
||||
@@ -203,13 +220,13 @@ No es `authorised_keys`.
|
||||
Una vez montada la clave, puedes probar desde la RPi host:
|
||||
|
||||
```bash
|
||||
docker exec -it monitor-rpi ssh -i /ssh/carabanes_monitor_ed25519 -o IdentitiesOnly=yes pi@HOST_REMOTO
|
||||
docker exec -it monitor-rpi ssh -i /ssh/monitor_rpi_ed25519 -o IdentitiesOnly=yes pi@HOST_REMOTO
|
||||
```
|
||||
|
||||
Si el dispositivo usa un puerto distinto:
|
||||
|
||||
```bash
|
||||
docker exec -it monitor-rpi ssh -i /ssh/carabanes_monitor_ed25519 -o IdentitiesOnly=yes -p 34000 pi@192.168.0.1
|
||||
docker exec -it monitor-rpi ssh -i /ssh/monitor_rpi_ed25519 -o IdentitiesOnly=yes -p 34000 pi@192.168.0.1
|
||||
```
|
||||
|
||||
Si la conexion funciona sin pedir password, el monitor tambien podra consultar ese dispositivo.
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
"refreshIntervalSeconds": 30,
|
||||
"idleScanIntervalSeconds": 300,
|
||||
"sshTimeoutSeconds": 8,
|
||||
"sshPrivateKeyPath": "/ssh/monitor_rpi_ed25519",
|
||||
"sshPublicKeyPath": "/ssh/monitor_rpi_ed25519.pub",
|
||||
"temperatureThresholdsC": {
|
||||
"warning": 60,
|
||||
"hot": 70,
|
||||
@@ -37,6 +39,7 @@
|
||||
"host": "192.168.0.46",
|
||||
"username": "pi",
|
||||
"password": "",
|
||||
"authMethod": "password",
|
||||
"privateKeyPath": "",
|
||||
"role": "Home Assistant Local",
|
||||
"port": 22,
|
||||
@@ -50,6 +53,7 @@
|
||||
"host": "192.168.0.57",
|
||||
"username": "pi",
|
||||
"password": "",
|
||||
"authMethod": "password",
|
||||
"privateKeyPath": "",
|
||||
"role": "Home Assistant Cabaña",
|
||||
"port": 22,
|
||||
@@ -63,6 +67,7 @@
|
||||
"host": "192.168.0.53",
|
||||
"username": "yamaray",
|
||||
"password": "",
|
||||
"authMethod": "password",
|
||||
"privateKeyPath": "",
|
||||
"role": "Docker",
|
||||
"port": 22,
|
||||
@@ -76,6 +81,7 @@
|
||||
"host": "192.168.0.130",
|
||||
"username": "pi",
|
||||
"password": "",
|
||||
"authMethod": "password",
|
||||
"privateKeyPath": "",
|
||||
"role": "Docker - Daniel",
|
||||
"port": 22,
|
||||
@@ -89,6 +95,7 @@
|
||||
"host": "192.168.0.254",
|
||||
"username": "yamaray",
|
||||
"password": "",
|
||||
"authMethod": "password",
|
||||
"privateKeyPath": "",
|
||||
"role": "nginx - wireguard ",
|
||||
"port": 22,
|
||||
@@ -102,6 +109,7 @@
|
||||
"host": "192.168.0.37",
|
||||
"username": "pi",
|
||||
"password": "",
|
||||
"authMethod": "password",
|
||||
"privateKeyPath": "",
|
||||
"role": "MQTT - varios",
|
||||
"port": 22,
|
||||
@@ -115,6 +123,7 @@
|
||||
"host": "192.168.0.60",
|
||||
"username": "yamaray",
|
||||
"password": "",
|
||||
"authMethod": "password",
|
||||
"privateKeyPath": "",
|
||||
"role": "Meshcore-Interface",
|
||||
"port": 22,
|
||||
@@ -128,6 +137,7 @@
|
||||
"host": "192.168.1.47",
|
||||
"username": "pi",
|
||||
"password": "",
|
||||
"authMethod": "password",
|
||||
"privateKeyPath": "",
|
||||
"role": "Reserva",
|
||||
"port": 22,
|
||||
|
||||
+117
-14
@@ -154,6 +154,45 @@
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.ssh-key-panel {
|
||||
margin-top: 16px;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 16px;
|
||||
background: var(--panel-2);
|
||||
}
|
||||
|
||||
.ssh-key-fields {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(220px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.public-key-box {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.public-key-box textarea {
|
||||
width: 100%;
|
||||
min-height: 78px;
|
||||
resize: vertical;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--input);
|
||||
color: var(--text);
|
||||
padding: 10px;
|
||||
font: 13px ui-monospace, "Cascadia Code", Consolas, monospace;
|
||||
}
|
||||
|
||||
.public-key-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.threshold-row {
|
||||
display: grid;
|
||||
grid-template-columns: 110px repeat(3, minmax(120px, 1fr));
|
||||
@@ -198,9 +237,8 @@
|
||||
.field-port { grid-column: span 1; }
|
||||
.field-username { grid-column: span 2; }
|
||||
.field-auth { grid-column: span 2; }
|
||||
.password-credential, .key-credential { grid-column: span 4; }
|
||||
.password-credential { grid-column: span 4; }
|
||||
.field-delete { grid-column: span 2; }
|
||||
.device[data-auth-method="password"] .key-credential { display: none; }
|
||||
.device[data-auth-method="key"] .password-credential { display: none; }
|
||||
|
||||
.credential-note {
|
||||
@@ -262,6 +300,17 @@
|
||||
}
|
||||
|
||||
button:hover, a:hover { border-color: var(--info); }
|
||||
.secondary-action {
|
||||
background: var(--panel);
|
||||
border-color: var(--info);
|
||||
color: var(--info);
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.secondary-action:hover {
|
||||
background: color-mix(in srgb, var(--info) 18%, var(--panel));
|
||||
}
|
||||
|
||||
.danger {
|
||||
background: var(--offline-bg);
|
||||
border-color: var(--offline-line);
|
||||
@@ -396,11 +445,12 @@
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.device { grid-template-columns: repeat(6, minmax(0, 1fr)); align-items: start; }
|
||||
.field-name, .field-role, .field-host, .password-credential, .key-credential { grid-column: span 3; }
|
||||
.field-name, .field-role, .field-host, .password-credential { grid-column: span 3; }
|
||||
.field-id, .field-model, .field-location, .field-username, .field-auth, .field-active, .field-delete { grid-column: span 2; }
|
||||
.field-port { grid-column: span 1; }
|
||||
.credential-note { grid-column: span 4; }
|
||||
.top-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.ssh-key-fields { grid-template-columns: 1fr; }
|
||||
.threshold-row { grid-template-columns: 90px repeat(3, minmax(100px, 1fr)); }
|
||||
}
|
||||
|
||||
@@ -410,7 +460,7 @@
|
||||
.top-grid, .device { grid-template-columns: 1fr; }
|
||||
.field-active, .field-name, .field-id, .field-role, .field-model, .field-location,
|
||||
.field-host, .field-port, .field-username, .field-auth, .password-credential,
|
||||
.key-credential, .credential-note, .field-delete { grid-column: span 1; }
|
||||
.credential-note, .field-delete { grid-column: span 1; }
|
||||
.threshold-row { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
@@ -452,6 +502,26 @@
|
||||
<input id="sshTimeoutSeconds" type="number" min="2" step="1">
|
||||
</label>
|
||||
</div>
|
||||
<div class="ssh-key-panel">
|
||||
<h2>Clave SSH del monitor</h2>
|
||||
<div class="ssh-key-fields">
|
||||
<label title="Ruta de la clave privada dentro del contenedor. Esta clave se queda solo en el monitor.">Clave privada global
|
||||
<input id="sshPrivateKeyPath" value="/ssh/monitor_rpi_ed25519">
|
||||
</label>
|
||||
<label title="Ruta de la clave publica dentro del contenedor. Esta es la clave que se copia a las RPi remotas.">Clave publica global
|
||||
<input id="sshPublicKeyPath" value="/ssh/monitor_rpi_ed25519.pub">
|
||||
</label>
|
||||
</div>
|
||||
<div class="public-key-box">
|
||||
<label title="Copia esta clave publica en ~/.ssh/authorized_keys del usuario SSH de la RPi remota.">Clave publica para instalar en las RPi
|
||||
<textarea id="sshPublicKey" readonly placeholder="Cargando clave publica..."></textarea>
|
||||
</label>
|
||||
<div class="public-key-actions">
|
||||
<button class="secondary-action" id="copyPublicKey" type="button">Copiar clave publica</button>
|
||||
<a class="secondary-action" id="downloadPublicKey" href="/api/ssh-public-key/download">Descargar .pub</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="threshold-groups">
|
||||
<div class="threshold-row">
|
||||
<div class="threshold-title">Temperatura</div>
|
||||
@@ -588,9 +658,10 @@
|
||||
const devicesEl = document.querySelector("#devices");
|
||||
const statusEl = document.querySelector("#status");
|
||||
const dashboardLink = document.querySelector("#dashboardLink");
|
||||
const publicKeyEl = document.querySelector("#sshPublicKey");
|
||||
|
||||
function deviceTemplate(device, index) {
|
||||
const authMethod = device.privateKeyPath ? "key" : "password";
|
||||
const authMethod = device.authMethod || (device.privateKeyPath ? "key" : "password");
|
||||
return `
|
||||
<div class="device" data-index="${index}" data-auth-method="${authMethod}">
|
||||
<label class="field-name" title="Nombre visible de la tarjeta en el dashboard.">Nombre
|
||||
@@ -622,17 +693,14 @@
|
||||
<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>
|
||||
<option value="key" ${authMethod === "key" ? "selected" : ""}>Clave asimetrica</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
|
||||
<label class="password-credential" title="Password SSH. Se guarda en config.json dentro del volumen persistente. Si eliges clave asimetrica, se guardara vacio.">Password
|
||||
<input data-field="password" type="password" value="${device.password || ""}">
|
||||
</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>
|
||||
Metodo activo: <strong>${authMethod === "key" ? "Clave asimetrica: copiar clave publica del monitor en la RPi" : "Password"}</strong>
|
||||
</div>
|
||||
<label class="field-active" 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" : ""}>
|
||||
@@ -648,6 +716,8 @@
|
||||
document.querySelector("#refreshIntervalSeconds").value = state.config.refreshIntervalSeconds || 30;
|
||||
document.querySelector("#idleScanIntervalSeconds").value = state.config.idleScanIntervalSeconds || 300;
|
||||
document.querySelector("#sshTimeoutSeconds").value = state.config.sshTimeoutSeconds || 8;
|
||||
document.querySelector("#sshPrivateKeyPath").value = state.config.sshPrivateKeyPath || "/ssh/monitor_rpi_ed25519";
|
||||
document.querySelector("#sshPublicKeyPath").value = state.config.sshPublicKeyPath || `${document.querySelector("#sshPrivateKeyPath").value}.pub`;
|
||||
document.querySelector("#warningTemp").value = state.config.temperatureThresholdsC?.warning || 60;
|
||||
document.querySelector("#hotTemp").value = state.config.temperatureThresholdsC?.hot || 70;
|
||||
document.querySelector("#criticalTemp").value = state.config.temperatureThresholdsC?.critical || 80;
|
||||
@@ -671,6 +741,8 @@
|
||||
state.config.refreshIntervalSeconds = Number(document.querySelector("#refreshIntervalSeconds").value || 30);
|
||||
state.config.idleScanIntervalSeconds = Number(document.querySelector("#idleScanIntervalSeconds").value || 300);
|
||||
state.config.sshTimeoutSeconds = Number(document.querySelector("#sshTimeoutSeconds").value || 8);
|
||||
state.config.sshPrivateKeyPath = document.querySelector("#sshPrivateKeyPath").value || "/ssh/monitor_rpi_ed25519";
|
||||
state.config.sshPublicKeyPath = document.querySelector("#sshPublicKeyPath").value || `${state.config.sshPrivateKeyPath}.pub`;
|
||||
state.config.temperatureThresholdsC = {
|
||||
...(state.config.temperatureThresholdsC || {}),
|
||||
warning: Number(document.querySelector("#warningTemp").value || 60),
|
||||
@@ -709,8 +781,9 @@
|
||||
else device[field] = input.value;
|
||||
});
|
||||
const authMethod = row.querySelector("[data-auth-method]")?.value || "password";
|
||||
if (authMethod === "key") device.password = "";
|
||||
else device.privateKeyPath = "";
|
||||
device.authMethod = authMethod === "key" ? "key" : "password";
|
||||
device.privateKeyPath = "";
|
||||
if (device.authMethod === "key") device.password = "";
|
||||
return device;
|
||||
});
|
||||
}
|
||||
@@ -719,6 +792,18 @@
|
||||
const res = await fetch("/api/config", { cache: "no-store" });
|
||||
state.config = await res.json();
|
||||
render();
|
||||
loadPublicKey().catch((error) => {
|
||||
publicKeyEl.value = error.message;
|
||||
});
|
||||
}
|
||||
|
||||
async function loadPublicKey() {
|
||||
publicKeyEl.value = "Cargando clave publica...";
|
||||
const res = await fetch("/api/ssh-public-key", { cache: "no-store" });
|
||||
const payload = await res.json();
|
||||
if (!res.ok) throw new Error(payload.error || `HTTP ${res.status}`);
|
||||
publicKeyEl.value = payload.publicKey || "";
|
||||
document.querySelector("#sshPublicKeyPath").value = payload.publicKeyPath || document.querySelector("#sshPublicKeyPath").value;
|
||||
}
|
||||
|
||||
async function save() {
|
||||
@@ -762,6 +847,7 @@
|
||||
username: "pi",
|
||||
password: "",
|
||||
privateKeyPath: "",
|
||||
authMethod: "password",
|
||||
model: "RPi 4",
|
||||
role: "",
|
||||
location: "Rack",
|
||||
@@ -797,7 +883,24 @@
|
||||
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";
|
||||
if (note) note.textContent = selector.value === "key" ? "Clave asimetrica: copiar clave publica del monitor en la RPi" : "Password";
|
||||
});
|
||||
|
||||
document.querySelector("#copyPublicKey").addEventListener("click", async () => {
|
||||
const button = document.querySelector("#copyPublicKey");
|
||||
try {
|
||||
await navigator.clipboard.writeText(publicKeyEl.value);
|
||||
button.classList.add("action-ok");
|
||||
button.textContent = "Copiada";
|
||||
setTimeout(() => {
|
||||
button.classList.remove("action-ok");
|
||||
button.textContent = "Copiar clave publica";
|
||||
}, 1200);
|
||||
} catch {
|
||||
publicKeyEl.select();
|
||||
statusEl.className = "status save-error";
|
||||
statusEl.textContent = "No se pudo copiar automaticamente. Selecciona la clave y copiala manualmente.";
|
||||
}
|
||||
});
|
||||
|
||||
load().catch((error) => {
|
||||
|
||||
@@ -86,26 +86,28 @@
|
||||
<li><code>name</code>: nombre visible.</li>
|
||||
<li><code>host</code>: IP o DNS.</li>
|
||||
<li><code>port</code>: puerto 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>username/password</code> o <code>authMethod: key</code>: credenciales SSH. Con clave asimetrica, copia la publica del monitor en la RPi.</li>
|
||||
<li><code>model</code>: modelo visible.</li>
|
||||
<li><code>role</code>: funcion del equipo.</li>
|
||||
<li><code>location</code>: ubicacion.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Autenticacion SSH por clave privada</h2>
|
||||
<p>Por dispositivo se puede usar password o clave privada. Si <code>privateKeyPath</code> tiene valor, el backend usa la clave privada y no usa el password.</p>
|
||||
<h2>Autenticacion SSH por clave asimetrica</h2>
|
||||
<p>Por dispositivo se puede usar password o clave asimetrica. Si el dispositivo usa <code>authMethod: key</code>, el backend usa la clave privada global del monitor y no usa el password.</p>
|
||||
|
||||
<h3>Clave en el contenedor</h3>
|
||||
<p>Guarda la clave privada en la RPi donde corre el monitor:</p>
|
||||
<pre><code>/home/yamaray/docker/monitorRPi/ssh/carabanes_monitor_ed25519</code></pre>
|
||||
<pre><code>/home/yamaray/docker/monitorRPi/ssh/monitor_rpi_ed25519
|
||||
/home/yamaray/docker/monitorRPi/ssh/monitor_rpi_ed25519.pub</code></pre>
|
||||
<p>Protege permisos:</p>
|
||||
<pre><code>chmod 700 /home/yamaray/docker/monitorRPi/ssh
|
||||
chmod 600 /home/yamaray/docker/monitorRPi/ssh/carabanes_monitor_ed25519</code></pre>
|
||||
chmod 600 /home/yamaray/docker/monitorRPi/ssh/monitor_rpi_ed25519</code></pre>
|
||||
<p>El compose debe montar la carpeta como solo lectura:</p>
|
||||
<pre><code>- /home/yamaray/docker/monitorRPi/ssh:/ssh:ro</code></pre>
|
||||
<p>En el configurador usa la ruta vista desde el contenedor:</p>
|
||||
<pre><code>password: ""
|
||||
privateKeyPath: /ssh/carabanes_monitor_ed25519</code></pre>
|
||||
<p>En el configurador general usa las rutas vistas desde el contenedor:</p>
|
||||
<pre><code>sshPrivateKeyPath: /ssh/monitor_rpi_ed25519
|
||||
sshPublicKeyPath: /ssh/monitor_rpi_ed25519.pub</code></pre>
|
||||
<p>En cada dispositivo selecciona <code>Auth SSH = Clave asimetrica</code>. El dispositivo solo necesita IP/host, puerto y usuario SSH.</p>
|
||||
|
||||
<h3>Clave publica en la Raspberry destino</h3>
|
||||
<p>En el usuario SSH remoto, pega la clave publica en:</p>
|
||||
@@ -118,7 +120,7 @@ chmod 600 ~/.ssh/authorized_keys</code></pre>
|
||||
<p>El nombre correcto del archivo es <code>authorized_keys</code>, no <code>authorised_keys</code>.</p>
|
||||
|
||||
<h3>Prueba manual</h3>
|
||||
<pre><code>docker exec -it monitor-rpi ssh -i /ssh/carabanes_monitor_ed25519 -o IdentitiesOnly=yes -p 34000 pi@192.168.0.1</code></pre>
|
||||
<pre><code>docker exec -it monitor-rpi ssh -i /ssh/monitor_rpi_ed25519 -o IdentitiesOnly=yes -p 34000 pi@192.168.0.1</code></pre>
|
||||
<p>Si entra sin pedir password, el monitor podra usar esa clave para el scan.</p>
|
||||
|
||||
<h2>API</h2>
|
||||
|
||||
@@ -10,6 +10,8 @@ const DEFAULT_CONFIG_PATH = path.join(ROOT, "config.json");
|
||||
const PORT = Number(process.env.PORT || 8787);
|
||||
const AUTH_USERNAME = process.env.MONITOR_USERNAME || "";
|
||||
const AUTH_PASSWORD = process.env.MONITOR_PASSWORD || "";
|
||||
const DEFAULT_SSH_PRIVATE_KEY_PATH = "/ssh/monitor_rpi_ed25519";
|
||||
const DEFAULT_SSH_PUBLIC_KEY_PATH = "/ssh/monitor_rpi_ed25519.pub";
|
||||
|
||||
let config = null;
|
||||
let latestStatus = null;
|
||||
@@ -42,21 +44,26 @@ async function readConfig() {
|
||||
parsed.refreshIntervalSeconds = Number(parsed.refreshIntervalSeconds || 30);
|
||||
parsed.idleScanIntervalSeconds = Number(parsed.idleScanIntervalSeconds || 300);
|
||||
parsed.sshTimeoutSeconds = Number(parsed.sshTimeoutSeconds || 8);
|
||||
parsed.sshPrivateKeyPath = String(parsed.sshPrivateKeyPath || DEFAULT_SSH_PRIVATE_KEY_PATH);
|
||||
parsed.sshPublicKeyPath = String(parsed.sshPublicKeyPath || `${parsed.sshPrivateKeyPath}.pub` || DEFAULT_SSH_PUBLIC_KEY_PATH);
|
||||
parsed.temperatureThresholdsC = normalizeTemperatureThresholds(parsed.temperatureThresholdsC);
|
||||
parsed.metricThresholdsPercent = normalizeMetricThresholds(parsed.metricThresholdsPercent);
|
||||
parsed.devices = Array.isArray(parsed.devices) ? parsed.devices : [];
|
||||
parsed.devices = Array.isArray(parsed.devices) ? parsed.devices.map(normalizeDeviceConfig) : [];
|
||||
return parsed;
|
||||
}
|
||||
|
||||
async function writeConfig(nextConfig) {
|
||||
const sshPrivateKeyPath = String(nextConfig.sshPrivateKeyPath || DEFAULT_SSH_PRIVATE_KEY_PATH);
|
||||
const normalized = {
|
||||
...nextConfig,
|
||||
refreshIntervalSeconds: Number(nextConfig.refreshIntervalSeconds || 30),
|
||||
idleScanIntervalSeconds: Number(nextConfig.idleScanIntervalSeconds || 300),
|
||||
sshTimeoutSeconds: Number(nextConfig.sshTimeoutSeconds || 8),
|
||||
sshPrivateKeyPath,
|
||||
sshPublicKeyPath: String(nextConfig.sshPublicKeyPath || `${sshPrivateKeyPath}.pub` || DEFAULT_SSH_PUBLIC_KEY_PATH),
|
||||
temperatureThresholdsC: normalizeTemperatureThresholds(nextConfig.temperatureThresholdsC),
|
||||
metricThresholdsPercent: normalizeMetricThresholds(nextConfig.metricThresholdsPercent),
|
||||
devices: Array.isArray(nextConfig.devices) ? nextConfig.devices : []
|
||||
devices: Array.isArray(nextConfig.devices) ? nextConfig.devices.map(normalizeDeviceConfig) : []
|
||||
};
|
||||
await fs.mkdir(path.dirname(CONFIG_PATH), { recursive: true });
|
||||
await fs.writeFile(CONFIG_PATH, JSON.stringify(normalized, null, 2) + "\n", "utf8");
|
||||
@@ -64,6 +71,15 @@ async function writeConfig(nextConfig) {
|
||||
restartScanner();
|
||||
}
|
||||
|
||||
function normalizeDeviceConfig(device = {}) {
|
||||
const authMethod = device.authMethod || (device.privateKeyPath ? "key" : "password");
|
||||
return {
|
||||
...device,
|
||||
authMethod: authMethod === "key" ? "key" : "password",
|
||||
privateKeyPath: device.privateKeyPath || ""
|
||||
};
|
||||
}
|
||||
|
||||
function classifyTemp(tempC) {
|
||||
const thresholds = config.temperatureThresholdsC || {};
|
||||
if (tempC >= (thresholds.critical || 80)) return "critical";
|
||||
@@ -364,8 +380,12 @@ function runCommandWithInput(command, args, input, timeoutMs) {
|
||||
}
|
||||
|
||||
async function sshMetrics(device) {
|
||||
const hasPrivateKey = Boolean(device.privateKeyPath);
|
||||
if (!hasPrivateKey && !device.password) {
|
||||
const usesPrivateKey = device.authMethod === "key" || Boolean(device.privateKeyPath);
|
||||
const privateKeyPath = device.privateKeyPath || config.sshPrivateKeyPath;
|
||||
if (usesPrivateKey && !privateKeyPath) {
|
||||
throw new Error("Clave privada SSH global no configurada");
|
||||
}
|
||||
if (!usesPrivateKey && !device.password) {
|
||||
throw new Error("Credenciales SSH no configuradas");
|
||||
}
|
||||
|
||||
@@ -388,10 +408,10 @@ async function sshMetrics(device) {
|
||||
|
||||
let command = "ssh";
|
||||
let args = sshArgs;
|
||||
if (hasPrivateKey) {
|
||||
if (usesPrivateKey) {
|
||||
args = [
|
||||
"-i",
|
||||
device.privateKeyPath,
|
||||
privateKeyPath,
|
||||
"-o",
|
||||
"BatchMode=yes",
|
||||
"-o",
|
||||
@@ -627,6 +647,21 @@ function sendJson(res, statusCode, payload) {
|
||||
res.end(body);
|
||||
}
|
||||
|
||||
function sendText(res, statusCode, body, headers = {}) {
|
||||
res.writeHead(statusCode, {
|
||||
"Content-Type": "text/plain; charset=utf-8",
|
||||
"Cache-Control": "no-store",
|
||||
...headers
|
||||
});
|
||||
res.end(body);
|
||||
}
|
||||
|
||||
async function readSshPublicKey() {
|
||||
const publicKeyPath = config.sshPublicKeyPath || `${config.sshPrivateKeyPath || DEFAULT_SSH_PRIVATE_KEY_PATH}.pub`;
|
||||
const publicKey = (await fs.readFile(publicKeyPath, "utf8")).trim();
|
||||
return { publicKeyPath, publicKey };
|
||||
}
|
||||
|
||||
function isAuthEnabled() {
|
||||
return Boolean(AUTH_USERNAME && AUTH_PASSWORD);
|
||||
}
|
||||
@@ -737,6 +772,30 @@ async function handleRequest(req, res) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === "GET" && pathname === "/api/ssh-public-key") {
|
||||
try {
|
||||
sendJson(res, 200, await readSshPublicKey());
|
||||
} catch (error) {
|
||||
sendJson(res, 404, {
|
||||
error: `No se pudo leer la clave publica SSH: ${error.message}`,
|
||||
publicKeyPath: config.sshPublicKeyPath || `${config.sshPrivateKeyPath || DEFAULT_SSH_PRIVATE_KEY_PATH}.pub`
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === "GET" && pathname === "/api/ssh-public-key/download") {
|
||||
try {
|
||||
const { publicKeyPath, publicKey } = await readSshPublicKey();
|
||||
sendText(res, 200, `${publicKey}\n`, {
|
||||
"Content-Disposition": `attachment; filename="${path.basename(publicKeyPath)}"`
|
||||
});
|
||||
} catch (error) {
|
||||
sendText(res, 404, `No se pudo leer la clave publica SSH: ${error.message}\n`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === "POST" && pathname === "/api/config") {
|
||||
const body = await readBody(req);
|
||||
await writeConfig(JSON.parse(body));
|
||||
|
||||
Reference in New Issue
Block a user