bda8ac0546
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
288 lines
9.1 KiB
Markdown
288 lines
9.1 KiB
Markdown
<div align="center">
|
|
|
|
<img src="frontend/public/favicon.svg" width="96" height="96" alt="" />
|
|
|
|
# Stupid Simple Network Inventory
|
|
|
|
**Aplicación web autoalojada para inventario de red y visualización de topología lógica**
|
|
|
|
[English](README.md) · [Français](README.fr.md)
|
|
|
|

|
|

|
|

|
|

|
|

|
|

|
|
|
|
</div>
|
|
|
|
---
|
|
|
|

|
|
|
|
---
|
|
|
|
## ✨ Características
|
|
|
|
- 🗂️ **Inventario manual** — añade y gestiona dispositivos (18 tipos) con IPs, VLANs, descripciones y enlaces web opcionales
|
|
- 🗺️ **Vista de topología** — disposición en tarjetas por red (LAN / VLAN 802.1Q), con secciones WAN y puerta de enlace
|
|
- 📡 **Ping ICMP** — comprueba la accesibilidad de todos los hosts conocidos con un clic
|
|
- 🔍 **Descubrimiento automático** — ping sweep + consulta PTR DNS en una subred para importar nuevos hosts
|
|
- 🏷️ **Logos de fabricantes** — detección y visualización automáticas de logos de editores/fabricantes (Proxmox, Cisco, Synology, Docker, y más de 30 más)
|
|
- 🔐 **Autenticación** — inicio de sesión JWT con cambio de contraseña obligatorio en el primer uso
|
|
- 🌙 **Modo oscuro** — alternancia entre tema claro y oscuro
|
|
- 🌍 **i18n** — francés, inglés, español
|
|
|
|
## 🛠️ Stack
|
|
|
|
| Capa | Tecnología |
|
|
|------|-----------|
|
|
| Backend | FastAPI + SQLAlchemy + SQLite (Python 3.11) |
|
|
| Frontend | Vue 3 + Vite, servido por Nginx |
|
|
| Auth | JWT HS256, expiración 24h |
|
|
| Runtime | Docker Compose |
|
|
|
|
---
|
|
|
|
## 🚀 Inicio rápido
|
|
|
|
```bash
|
|
# 1. Clonar y entrar en el proyecto
|
|
git clone https://git.raspot.in/olivier/stupid-simple-network-inventory.git
|
|
cd stupid-simple-network-inventory
|
|
|
|
# 2. Crear el directorio de datos propiedad del usuario actual
|
|
mkdir -p db_data
|
|
|
|
# 3. Configurar el entorno (necesario para la propiedad correcta del bind-mount)
|
|
cp .env.example .env
|
|
# Editar .env:
|
|
# DOCKER_UID / DOCKER_GID → resultado de: id -u && id -g
|
|
# INITIAL_ADMIN_PASSWORD → definir para evitar el bootstrap admin/admin por defecto
|
|
|
|
# 4. Construir e iniciar
|
|
docker compose --env-file .env up --build -d
|
|
|
|
# 5. Abrir http://localhost:8080 en el navegador
|
|
```
|
|
|
|
### Primer inicio de sesión
|
|
|
|
| Caso | Credenciales | Comportamiento |
|
|
|------|-------------|----------------|
|
|
| `INITIAL_ADMIN_PASSWORD` definido | `admin` / `<tu contraseña>` | Inicio de sesión normal |
|
|
| `INITIAL_ADMIN_PASSWORD` no definido | `admin` / `admin` | Cambio de contraseña obligatorio antes de acceder a la aplicación |
|
|
|
|
---
|
|
|
|
## ⚙️ Configuración
|
|
|
|
Toda la configuración se realiza mediante variables de entorno. Ver `.env.example` para la lista completa con descripciones.
|
|
|
|
| Variable | Por defecto | Descripción |
|
|
|----------|-------------|-------------|
|
|
| `SECRET_KEY` | auto-generada | Clave de firma JWT. Definir explícitamente en producción. |
|
|
| `INITIAL_ADMIN_PASSWORD` | _(vacío)_ | Contraseña de bootstrap del admin. Si no se define, se usa `admin/admin` con cambio forzado. |
|
|
| `ALLOWED_ORIGINS` | `*` | Orígenes CORS permitidos (separados por comas). Definir en tu dominio en producción. |
|
|
| `BIND_ADDRESS` | `0.0.0.0` | Dirección IP de escucha. Definir en la interfaz frente al reverse proxy. |
|
|
| `DOCKER_UID` / `DOCKER_GID` | `1000` | UID/GID para el proceso backend. Debe coincidir con el usuario propietario de `./db_data/`. |
|
|
|
|
```bash
|
|
cp .env.example .env
|
|
# Editar .env — definir como mínimo DOCKER_UID, DOCKER_GID, INITIAL_ADMIN_PASSWORD
|
|
docker compose --env-file .env up --build -d
|
|
```
|
|
|
|
---
|
|
|
|
## 🔒 Seguridad
|
|
|
|
### Gestión de secretos
|
|
|
|
Dos opciones según sus requisitos de seguridad.
|
|
|
|
#### Opción A — Secreto auto-generado (recomendado para nodo único)
|
|
|
|
Dejar `SECRET_KEY` sin definir (o vacío) en `.env`. En el primer arranque, el backend genera una clave hexadecimal aleatoria de 64 caracteres, la escribe en `db_data/secret_key.txt` con permisos **0600** y la reutiliza en cada reinicio posterior. El secreto nunca aparece en una variable de entorno, un archivo compose o un log.
|
|
|
|
```bash
|
|
# .env — dejar la línea vacía o eliminarla
|
|
SECRET_KEY=
|
|
```
|
|
|
|
El único requisito es que `db_data/` esté respaldado (ya contiene la base de datos).
|
|
|
|
#### Opción B — Secreto mediante archivo Docker Compose
|
|
|
|
Almacena el secreto en un archivo en el host, fuera del control de versiones, y lo monta en el contenedor. El valor nunca aparece en una variable de entorno.
|
|
|
|
```bash
|
|
# Generar y almacenar el secreto fuera del directorio del proyecto
|
|
mkdir -p ~/.secrets
|
|
python3 -c "import secrets; print(secrets.token_hex(32))" > ~/.secrets/topologie_secret_key
|
|
chmod 600 ~/.secrets/topologie_secret_key
|
|
```
|
|
|
|
Luego descomentar los bloques `secrets:` en `docker-compose.yml` (ver los comentarios en ese archivo) y eliminar `SECRET_KEY` de `.env`. Docker Compose fusiona la anulación automáticamente:
|
|
|
|
```bash
|
|
docker compose up -d
|
|
```
|
|
|
|
### Rotación de clave
|
|
|
|
Para rotar el secreto JWT (invalida todas las sesiones activas):
|
|
|
|
```bash
|
|
# Opción A — variable de entorno (recomendado)
|
|
# Definir un nuevo SECRET_KEY en la configuración de despliegue y reiniciar
|
|
|
|
# Opción B — rotación por archivo
|
|
docker compose stop backend
|
|
rm db_data/secret_key.txt
|
|
docker compose start backend
|
|
# Todos los usuarios deberán iniciar sesión de nuevo
|
|
```
|
|
|
|
### HTTPS
|
|
|
|
Esta aplicación no termina TLS. Para uso en producción, colócala detrás de un reverse proxy que gestione HTTPS.
|
|
|
|
#### Nginx
|
|
|
|
```nginx
|
|
server {
|
|
listen 443 ssl;
|
|
server_name inventory.example.com;
|
|
|
|
ssl_certificate /etc/letsencrypt/live/inventory.example.com/fullchain.pem;
|
|
ssl_certificate_key /etc/letsencrypt/live/inventory.example.com/privkey.pem;
|
|
|
|
location / {
|
|
proxy_pass http://127.0.0.1:8080;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Traefik — Labels Docker
|
|
|
|
Añadir estas etiquetas al servicio `frontend` y conectarlo a la red compartida con Traefik:
|
|
|
|
```yaml
|
|
# docker-compose.override.yml
|
|
services:
|
|
frontend:
|
|
labels:
|
|
- "traefik.enable=true"
|
|
- "traefik.http.routers.inventory.rule=Host(`inventory.example.com`)"
|
|
- "traefik.http.routers.inventory.entrypoints=websecure"
|
|
- "traefik.http.routers.inventory.tls.certresolver=letsencrypt"
|
|
- "traefik.http.services.inventory.loadbalancer.server.port=8080"
|
|
networks:
|
|
- internal
|
|
- traefik_public # red compartida con tu instancia de Traefik
|
|
|
|
networks:
|
|
traefik_public:
|
|
external: true
|
|
```
|
|
|
|
#### Traefik — Configuración dinámica (file provider)
|
|
|
|
Si Traefik no corre en Docker (o prefieres el file provider), deposita un archivo en tu directorio de configuración dinámica:
|
|
|
|
```yaml
|
|
# /etc/traefik/dynamic/inventory.yml
|
|
http:
|
|
routers:
|
|
inventory:
|
|
rule: "Host(`inventory.example.com`)"
|
|
entryPoints:
|
|
- websecure
|
|
tls:
|
|
certResolver: letsencrypt
|
|
service: inventory-svc
|
|
|
|
services:
|
|
inventory-svc:
|
|
loadBalancer:
|
|
servers:
|
|
- url: "http://127.0.0.1:8080"
|
|
```
|
|
|
|
Traefik detecta el archivo automáticamente — no se requiere reinicio.
|
|
|
|
Para uso solo local, enlazar a loopback para evitar exposición accidental en la LAN:
|
|
|
|
```yaml
|
|
# docker-compose.override.yml
|
|
services:
|
|
frontend:
|
|
ports:
|
|
- "127.0.0.1:8080:8080"
|
|
```
|
|
|
|
### Endurecimiento de contenedores
|
|
|
|
| Medida | Backend | Frontend |
|
|
|--------|---------|----------|
|
|
| Usuario no-root | `DOCKER_UID:DOCKER_GID` (usuario host) | `nginx` (UID 101) |
|
|
| `cap_drop: ALL` | ✓ | ✓ |
|
|
| `cap_add: NET_RAW` | ✓ (ping) | — |
|
|
| `no-new-privileges` | — ¹ | ✓ |
|
|
| Healthcheck | ✓ | ✓ |
|
|
|
|
¹ Omitido en el backend: el ping usa file capabilities (`cap_net_raw=ep`); `no-new-privileges` suprimiría el bit efectivo del archivo e impediría al subproceso adquirir `CAP_NET_RAW` en su conjunto efectivo aunque el padre lo tenga en su conjunto permitido.
|
|
|
|
---
|
|
|
|
## 💾 Persistencia de datos
|
|
|
|
Todos los datos se almacenan en `./db_data/`:
|
|
|
|
| Archivo | Descripción |
|
|
|---------|-------------|
|
|
| `topology.db` | Base de datos SQLite |
|
|
| `secret_key.txt` | Secreto JWT auto-generado (permisos 0600) |
|
|
|
|
**Copia de seguridad**: `cp -r db_data/ db_data.bak/`
|
|
|
|
**Restauración**: detener la stack, reemplazar `db_data/`, reiniciar.
|
|
|
|
---
|
|
|
|
## 🧑💻 Desarrollo
|
|
|
|
### Tests del backend
|
|
|
|
```bash
|
|
cd backend
|
|
pip install -r requirements.txt -r requirements-test.txt
|
|
pytest tests/ -v
|
|
```
|
|
|
|
### Dev local (sin Docker)
|
|
|
|
```bash
|
|
# Backend
|
|
cd backend
|
|
pip install -r requirements.txt
|
|
uvicorn main:app --reload
|
|
|
|
# Frontend (terminal separada)
|
|
cd frontend
|
|
npm install
|
|
npm run dev # Servidor dev Vite en :5173, proxifica /api/ hacia :8000
|
|
```
|
|
|
|
---
|
|
|
|
## 🏗️ Arquitectura
|
|
|
|
Ver [`docs/architecture.md`](docs/architecture.md) para el flujo de solicitudes detallado, la configuración Docker y el modelo de autenticación.
|