
Monitorización centralizada para múltiples instancias de Nextcloud
Monitoreo centralizado para múltiples instancias de Nextcloud
NcStatusCheck es una herramienta de monitoreo que te permite rastrear el estado de múltiples servidores Nextcloud desde una única interfaz web. Analiza las versiones de Nextcloud y PHP y proporciona recomendaciones de actualización.

< 15d< 7docc app:list contra la tienda de aplicaciones de Nextcloud para señalar aplicaciones a revisar — bloqueantes (que bloquean la actualización, incompatibles, aplicaciones de prueba en producción) más señales informativas de madurez/turbulencia (publicadas recientemente, pre-1.0, compilación alpha/beta/rc, ráfaga de lanzamientos, versión recién publicada)📦) y su gemelo "Contenedores a vigilar" (🐳) agregan cada aplicación / imagen Docker señalada de todas las instancias en una sola entrada cada uno, con filtros por tipo por grupo y una ventana emergente que lista las instancias afectadas y sus versionesALERT_WEBHOOK_URL), y/o un correo de resumen por lote (ALERT_EMAIL_TO) enviado mediante envío SMTP directo a tu servidor de correo (PHPMailer incluido; recurre al MTA local cuando no hay un relay SMTP configurado). También cubre señales de evolución lenta (ALERT_CHECKS): caducidad del certificado SSL (por niveles), versión de Nextcloud vulnerable/obsoleta, sonda Push obsoleta, informe de auditoría crítico, hallazgo de aplicación bloqueante — una alerta por cada nueva condición, sin spam de recordatoriosnc_update_grace_days), para evitar perseguir un lanzamiento con errores del mismo día⚠️ advertencias, 📦 aplicaciones, 🔄 Docker, 🔒 SSL, 🔴 sin conexión, 🚧 mantenimiento) cuando hay algo que informarnc-audit.shlocalStoragechmod 700) para la instancia remota de Nextcloud, actualizado automáticamente a medida que cambian las opcionesncstatuscheck/ ├── Frontend │ ├── index.php # Main entry point │ ├── template.html # HTML template (dashboard) │ ├── admin.html # Administration interface │ ├── detail.php # Server detail page │ ├── audit.php # Server-audit script distribution page (nc-audit.sh) │ ├── troubleshooting.php # Probe troubleshooting guide │ ├── app.js / admin.js / detail.js / audit.js / troubleshooting.js │ └── style*.css # One stylesheet per page family ├── APIs (HTTP) │ ├── api.php # Main monitoring API │ ├── detail-api.php # Detail page API (serverinfo + warnings + acks + availability) │ ├── push-api.php # Push reception + trigger + audit-report reception │ ├── ack-api.php # Warning acknowledge / unmute │ ├── admin-api.php # Version configuration routing │ ├── servers-admin-api.php # Server list management │ ├── nextcloud-versions-api.php # Official version scraping │ ├── nextcloud-apps-api.php # App store catalog (slim cache) for the apps audit │ ├── desktop-releases-api.php # Desktop client releases (GitHub, slim cache) │ ├── php-versions-api.php # PHP branch support data │ └── apps-warnings-api.php # Manual app warnings (known-bug list) CRUD ├── Shared modules (lib/) │ ├── auth.php # Auth + CSRF + URL redaction (defense in depth) │ ├── csrf-client.js # Auto-inject X-CSRF-Token in fetch() │ ├── nextcloud-client.php # Centralized HTTP client → remote Nextclouds │ ├── servers-store.php # Single source of truth for servers.json │ ├── uptime-state.php # Up/down state machine + transition journal + availability │ ├── alerts.php # Proactive alert dispatch: webhook + email digest │ ├── alerts-checks.php # Slow-signal alerts (SSL/version/push/audit/apps) + dedup state │ ├── smtp-mailer.php # SMTP transport adapter over vendored PHPMailer │ ├── phpmailer/ # Vendored PHPMailer (3 files + LICENSE, pinned in VERSION) │ ├── apps-warnings-manager.php # Manual app warnings storage │ ├── ui-common.js # NcUI: notify / confirm / prompt + shared app-audit messages │ ├── url-guard.php # Anti-SSRF (loopback, RFC1918, link-local…) │ ├── json-cache.php # Locked JSON read/write helpers │ └── version-config-manager.php # Version rules CRUD ├── Business logic │ ├── version-rules.php # NC / PHP status analysis engine │ ├── warnings-rules.php # Configuration warning engine │ ├── apps-rules.php # Installed-apps audit engine (store catalog cross-check) │ ├── cron-update.php # Full collection script, CLI only (twice a day) │ └── cron-ping.php # Lightweight up/down probe, CLI only (every 5 min) ├── Tools (never web-served — blocked by nginx/.htaccess) │ ├── tools/nc-audit.sh # Standalone server audit script (root, read-only) │ └── tools/ncstatuscheck-push-core.sh # Generic Push probe core (fleet-shared) ├── Tests │ └── tests/run.php # Plain-PHP test suite (no framework): php tests/run.php ├── Configuration │ ├── config.php # Central configuration (git-ignored) │ └── servers.json # Server list with tokens (git-ignored) └── Cache ├── servers_data.json # All server data ├── serverinfo_.json # Raw per-server cache (Extended) ├── push_.json # Last push payload per server ├── ack_.json # Acknowledged warnings per server ├── audit_.json # Last nc-audit.sh report per server ├── version-config.json # Version configuration ├── uptime_state.json # Up/down state per server (mini uptime) ├── uptime_history.json # Bounded up/down transition journal (availability % + incidents) ├── alerts_state.json # "Already alerted" memory of the check alerts ├── nextcloud_versions.json # Official NC versions ├── nextcloud_apps.json # App store slim catalog (apps audit) ├── apps-warnings.json # Manual app warnings (admin-curated) ├── .csrf_secret # CSRF HMAC secret (binary, 0600) └── *.log # Activity logs
deploy/ansible/ # Fleet deployment of the Push core (Ansible / scp) deploy/docker/ # Container packaging of the monitor itself
## 🔌 Modos de recopilación
Los modos no son mutuamente excluyentes: un servidor puede ser Extended y Push simultáneamente.
| Modo | Insignia | Fuente | Datos recopilados |
|------|----------|--------|-------------------|
| **Basic** | *(ninguna)* | `/status.php` + cabeceras HTTP | Versión de Nextcloud (PHP/servidor web si está expuesto) |
| **Extended** | `⚡ Extended` (púrpura → naranja en error/desactualizado) | `/ocs/v2.php/apps/serverinfo/api/v1/info` con `NC-Token` | Versión de NC, PHP, servidor web, OPcache, Redis, BD, usuarios activos… |
| **Push** | `📡 Push` (azul → naranja en error/desactualizado) | POST a `push-api.php` | Datos enviados por la instancia NC remota mediante script cron |
El **NC-Token de serverinfo** está disponible en **Nextcloud Configuración → Administración → Sistema**.
El **token push** se genera desde la interfaz de administración; el administrador proporciona un script cron bash listo para usar (`chmod 700`) para desplegar en la instancia monitorizada.
Los datos del modo Extended los proporciona la aplicación [nextcloud/serverinfo](https://github.com/nextcloud/serverinfo), que debe estar instalada y habilitada en la instancia monitorizada.
**Comportamiento de respaldo**: si la API Extended no es accesible (error de conexión, token no válido, aplicación no instalada), NcStatusCheck recurre automáticamente a `/status.php` para obtener al menos la versión de Nextcloud.
**Umbral de desactualización Push**: un servidor push se considera desactualizado si no se han recibido datos dentro de `auto_push_interval + 30 minutos`. El intervalo push predeterminado es de 12 horas.
### Columnas de la tabla del panel
El panel principal muestra 5 columnas: **Servidor** | **Versión de NC** | **PHP** | **Sondas** | **Estado**
La columna **Sondas** muestra los modos de recopilación activos para cada servidor:
- Insignia `⚡ Extended` (púrpura, se vuelve naranja en error de conexión o datos desactualizados)
- Insignia `📡 Push` (azul, se vuelve naranja cuando no se reciben datos dentro del umbral)
- Ambas insignias pueden aparecer simultáneamente si ambos modos están activos
- Sin insignia = solo modo Basic
### Columna de estado
La columna de estado solo muestra algo cuando hay algo sobre lo que actuar:
| Indicador | Insignia | Significado |
|-----------|----------|-------------|
| Sin conexión | `🔴 Offline` | Instancia inaccesible (sonda HTTP fallida), con "sin conexión durante X" |
| Advertencias activas | `⚠️ N` | N problemas de configuración |
| Auditoría de aplicaciones | `📦 N` | N aplicaciones instaladas para revisar (bloquean actualización/incompatibles) |
| Caducidad SSL | `🔒 N d` | El certificado caduca pronto — naranja `< 15d`, rojo `< 7d` o caducado |
| Actualizaciones Docker | `🔄 M` | M actualizaciones de contenedores disponibles |
| Todo correcto | *(vacío)* | Nada que informar |
| Sin datos | `?` | Modo Basic sin datos push |
#### Activo/inactivo y caducidad SSL
NcStatusCheck mantiene un estado activo/inactivo **mínimo** por servidor (solo estado actual + fecha del último cambio — sin series temporales, sin página de historial). "Activo" significa que la sonda HTTPS saliente alcanzó la instancia; una insignia roja **Offline** aparece solo cuando está inactiva. Durante esa misma sonda HTTPS, la **caducidad del certificado SSL** se lee de forma gratuita (`CURLOPT_CERTINFO`) y se muestra cuando se acerca. Ambos son visibles en su totalidad en la página de detalle. *Nota: estas comprobaciones salientes no se aplican a instancias solo Push con las que el monitor nunca contacta.*
#### Auditoría de aplicaciones (`📦`)
Cuando un servidor Push informa de sus aplicaciones instaladas (`occ app:list`, script push v3+), NcStatusCheck las contrasta con el catálogo de la tienda de aplicaciones de Nextcloud y marca las aplicaciones que merecen revisión. **Solo señalización** — la herramienta nunca desactiva nada; muestra candidatos (no puede saber si una aplicación se usa realmente). Solo se utilizan señales **fácticas y binarias**. La insignia `📦 N` cuenta los hallazgos bloqueantes (sin versión compatible para la versión actual de NC, sin versión para NC N+1 → bloquea la actualización, o una aplicación de prueba/desarrollo dejada habilitada en producción). Los hallazgos informativos (aplicación desactualizada en la instancia, abandonada upstream, PHP incompatible) se muestran solo en la página de detalle. Los hallazgos pueden silenciarse mediante el mismo mecanismo de reconocimiento que las advertencias.
### Formato de `servers.json````json
[
{"url": "https://cloud.example.com"},
{"url": "https://cloud2.example.com", "serverinfo_token": "abc123def456"},
{"url": "https://cloud3.example.com", "serverinfo_token": "...", "push_token": "xyz789"}
]
server { server_name monitoring.your-domain.com; root /var/www/ncstatuscheck; index index.php;
# HTTP Basic Authentication
auth_basic "Monitoring Access";
auth_basic_user_file /etc/nginx/.htpasswd;
# Protect sensitive files/dirs (tests/run.php has no CLI-only guard — it must
# never be reachable over HTTP; same blocklist as deploy/docker/nginx.conf)
location ~ ^/(cache/|\.git|deploy/|tools/|tests/) {
deny all;
return 404;
}
# .txt covers servers.txt (legacy server list — real monitored URLs)
location ~* \.(log|json|txt)$ {
deny all;
return 404;
}
# Security headers for static HTML pages (admin.html, template.html).
# PHP pages (index.php, detail.php) send the same headers themselves
# via send_security_headers() in lib/auth.php.
location ~* \.html$ {
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
add_header Referrer-Policy no-referrer always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'" always;
try_files $uri =404;
}
# Standard PHP configuration (adjust the socket to your PHP version —
# use a security-supported one: 8.2 has been EOL since December 2025)
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
location / {
try_files $uri $uri/ =404;
}
}
> **Apache**: el repositorio incluye archivos `.htaccess` que reflejan las reglas `deny`
> anteriores (raíz: bloquea `*.log`/`*.json`/`*.txt` y `.git`; `cache/`, `tools/`,
> `deploy/`, `tests/`: `Require all denied`). Solo funcionan si el vhost establece
> `AllowOverride FileInfo AuthConfig` (o `All`) — el valor predeterminado de Debian para
> `/var/www` es `AllowOverride None`, en cuyo caso replica las reglas directamente en el
> vhost. La autenticación básica HTTP aún debe configurarse en el vhost de cualquier manera.
### Implementación
1. **Clonar el repositorio**```bash
git clone https://gitlab.com/jp.louvel/ncstatuscheck.git
cd ncstatuscheck
Edita `config.php` y ajusta las rutas y la URL a tu entorno:```php
define('MONITOR_PATH', '/var/www/ncstatuscheck');
define('MONITOR_URL', 'https://monitoring.your-domain.com'); // your public URL
define('CACHE_DIR', MONITOR_PATH . '/cache');
Los servidores se gestionan directamente desde la interfaz de administración (botón ⚙️ Admin).
También puedes crear servers.json manualmente:```json
[
{"url": "https://cloud.example.com"},
{"url": "https://nextcloud.mycompany.org", "serverinfo_token": "your_token_here"}
]
> **Migración desde `servers.txt`**: si existe un archivo `servers.txt`, se convierte automáticamente a `servers.json` en el primer acceso. Luego puedes eliminar `servers.txt`.
4. **Establecer permisos**
nginx/PHP-FPM se ejecutan con su propio usuario (`www-data` en Debian/Ubuntu, `nginx`/`apache`
en la familia RHEL — ajusta según corresponda) — si clonaste como tu propio usuario de inicio de sesión, ese usuario
casi con seguridad no será `www-data` ni estará en su grupo, por lo que `chmod` por sí solo deja al
servidor web **sin acceso alguno**, ni siquiera de lectura (cada solicitud devuelve 403/404):```bash
chown -R www-data:www-data /var/www/ncstatuscheck # adjust the user:group to your distro
chmod 750 /var/www/ncstatuscheck
chmod 750 cache img
chmod 640 *.php *.html *.js *.css *.md
chmod 600 config.php servers.json servers.txt # secrets / serverinfo & push tokens
chmod 660 cache/*.json cache/*.log
Si
servers.jsonaún no existe (estás dejando que la interfaz de administración lo cree en lugar del paso manual anterior), elchmod 600anterior simplemente no tiene nada sobre lo que actuar — eso está bien:ServersStore::save()aplica chmod al archivo a0600por sí mismo en cada escritura, por lo que unservers.json(re)creado a través de la interfaz de administración nunca permanece legible por grupo/mundo con tokens de serverinfo/push dentro.
6. **Tareas programadas (opcional)**
Dos crons complementarios: instala ambos **en la misma llamada `crontab -`**:
`crontab -` instala un crontab completo nuevo desde stdin, no añade, por lo
que ejecutarlo dos veces (una por línea) deja solo el *segundo* trabajo — el
primero desaparece silenciosamente, sin error. Esto también conserva cualquier
cosa que ya esté en tu crontab (`crontab -l` canalizado primero) en lugar de
borrarlo:```bash
(crontab -l 2>/dev/null; cat <<'EOF'
# Full collection (NC/PHP versions, serverinfo, app-store catalog) — twice a day
0 6,18 * * * cd /var/www/ncstatuscheck && php cron-update.php
# Lightweight reachability probe (status.php only -> up/down state) — every 5 min
*/5 * * * * cd /var/www/ncstatuscheck && php cron-ping.php
EOF
) | crontab -
Re-ejecutar esto añade duplicados si estas líneas ya están presentes — comprueba
con crontab -l primero si no estás seguro.
cron-ping.php es intencionadamente mínimo: solo comprueba el status.php de cada
instancia y actualiza el estado arriba/abajo (cache/uptime_state.json), por lo que puede
ejecutarse con frecuencia sin carga. Una instancia solo se marca como caída después de
UPTIME_FAIL_THRESHOLD sondeos fallidos consecutivos (por defecto 2 → ~10 min con una
cadencia de 5 min); la recuperación a activa es inmediata. El cron-update.php
completo permanece sin cambios para todo lo demás.
En lugar de los pasos 1–6 anteriores, NcStatusCheck también puede ejecutarse como un pequeño
stack de docker compose (PHP-FPM + nginx + un contenedor cron) — el repositorio se monta
por bind tal cual, sin paso de compilación ni Composer, por lo que refleja exactamente la
estructura de bare-metal, solo que contenerizado. Sirve únicamente HTTP plano (puerto 8080
por defecto) — coloca tu propio proxy inverso con terminación TLS delante.
La configuración completa — configuración, los problemas de permisos (uid 82, pre-creación
de servers.json), autenticación HTTP Basic, cron, actualizaciones y copias de seguridad —
vive íntegramente en deploy/docker/README.md. Empieza ahí;
esta sección es intencionadamente solo una referencia, para evitar mantener dos copias de
los mismos pasos sincronizadas.
https://monitoring.your-domain.comAccesible haciendo clic en cualquier nombre de servidor o en su indicador de Health.
Para servidores Basic (sin sonda Extended ni Push), una página simplificada muestra los datos disponibles (versión de NC, servidor web, protocolo HTTP) con un aviso y una sugerencia para habilitar una sonda.
Para servidores Extended / Push, la página de detalle completa muestra secciones separadas:
| Sección | Campos |
|---|---|
| Sistema Nextcloud | Versión, modo debug, memcache local/distribuida, bloqueo de archivos, espacio en disco |
| PHP | Versión, memory_limit, upload_max_filesize, max_execution_time, FPM, OPcache |
| Servidor web | Nombre + versión, protocolo HTTP |
| Base de datos | Tipo, versión, tamaño |
| Caché | Redis, tasa de aciertos de APCu |
| Usuarios activos | Últimos 5 min, 1 h, 24 h, 7 días |
NcStatusCheck expone varios endpoints REST:
API principal (api.php)
GET ?action=get_data — Obtener datos (caché o actualización)POST ?action=refresh_data — Forzar la actualización de todos los servidoresAPI Push (push-api.php)
POST con cabecera push_token — Recibir datos push de una instancia NC remotaPOST ?action=request_push_all — Solicitar un push inmediato de todos los servidores push configurados (establece un indicador de activación consumido por el script cron remoto)El script cron generado por la interfaz de administración se divide en dos: un núcleo genérico
/usr/local/bin/ncstatuscheck-push.sh— idéntico en cada servidor (toda la lógica) — controlado por una pequeña configuración por instancia/etc/ncstatuscheck/<slug>.conf(SERVER_URL,SLUG,OCC_CMD,DOCKER_ENABLED,SKOPEO_ENABLED). Se invoca comoncstatuscheck-push.sh /etc/ncstatuscheck/<slug>.conf [--test]. El núcleo se niega a cargar una configuración escribible por grupo/todos (anti inyección de código).Es multi-destino (fan-out): los datos se recopilan una vez y se envían a cada monitor listado en
/etc/ncstatuscheck/targets-<slug>.conf(una líneaurl|push_token[|http_user|http_pass]por monitor). El administrador de cada monitor emite un comando idempotente para registrarse.Múltiples instancias de Nextcloud en un mismo host: las rutas por instancia se sufijan con un
<slug>derivado de la URL monitorizada (p. ej.latest.ezeo.coop→latest_ezeo_coop):<slug>.conf,/etc/cron.d/ncstatuscheck-<slug>,targets-<slug>.conf,ncstatuscheck-push-<slug>.log, estado…-<slug>.<md5>.last. Solo el núcleo se comparte, por lo que las instancias co-ubicadas nunca colisionan.Nextcloud ejecutándose en Docker (imagen oficial, compose, AIO): totalmente compatible — el script se instala en el host (cron root + acceso al daemon de Docker), nunca dentro del contenedor, y
occpasa pordocker exec:OCC_CMD=docker exec -u www-data <container> php occ(contenedor AIO:nextcloud-aio-nextcloud). El generador de scripts de administración tiene un preajuste de tipo de instalación que rellena esto. Nunca añadas-t(sin TTY bajo cron); mantén-u www-data(la imagen oficial rechaza occ como root).Despliegue/actualizaciones de flota: como el núcleo es un único archivo idéntico, actualizar la lógica en muchos servidores = reemplazar ese único archivo (el marcador
↑señala servidores que ejecutan una versión anterior). Consultadeploy/ansible/para un playbook listo para usar (o un simple buclescp). El monitor permanece pasivo — nunca envía código a la flota; el ancla de confianza es tu propio acceso SSH, no el monitor.Migración desde una instalación anterior a v4 (script monolítico por instancia): elimina el antiguo
/usr/local/bin/ncstatuscheck-push-<slug>.shy/etc/cron.d/ncstatuscheck-<slug>antes de instalar el núcleo + la configuración (eltargets-<slug>.confse reutiliza tal cual), de lo contrario harás doble push.
API de detalle (detail-api.php)
GET ?server=<url> — Datos completos de serverinfo + advertencias calculadas para un servidor Extended/PushAPIs de administración
admin-api.php — Configuración de versionesservers-admin-api.php — Gestión de servidores (get_servers, add_server, remove_server, update_server_token, generate_push_token, remove_push_token)nextcloud-versions-api.php — Versiones oficialesdesktop-releases-api.php — Lanzamientos del cliente de escritorio de Nextcloud (get_desktop_releases, refresh_desktop_releases)nc-audit.sh)Un subsistema separado de la monitorización: un script bash independiente y de solo lectura
(tools/nc-audit.sh) ejecutado como root en un servidor Nextcloud para una auditoría única / mensual
del ajuste de web + PHP + base de datos, contrastada con la capacidad física de la máquina
(RAM, CPU, tipo de disco). Orientado a una oferta de supervisión gestionada:
el cliente lo instala, el monitor solo recibe informes — sin acceso a la máquina/red
requerido. El script solo lee la configuración (sin cambios), imprime
un informe en color y escribe una copia en /tmp.
Qué comprueba: capacidad del servidor (RAM/CPU/SSD-HDD, swappiness, detección de
servidor compartido) · Nextcloud (versiones, cron, caché, Redis en tiempo de ejecución, tipo de BD, registros) ·
PHP/PHP-FPM (SAPI de servicio real, OPcache en tiempo de ejecución, memoria multi-pool) · Apache
(memoria de trabajadores consciente de MPM) · Nginx · PostgreSQL · MariaDB · higiene de seguridad
(fail2ban o CrowdSec + bouncer + blocklist comunitaria; actualizaciones pendientes /
reinicio / servicios en librerías obsoletas) · conciliación del presupuesto de RAM (InnoDB +
FPM + Apache frente a RAM real) · análisis profundo con herramientas opcionales si ya están presentes
(mysqltuner, pt-variable-advisor, apache2buddy, sar/iostat).```bash
curl -fsSL https://gitlab.com/jp.louvel/ncstatuscheck/-/raw/master/tools/nc-audit.sh -o /usr/local/bin/nc-audit.sh chmod 700 /usr/local/bin/nc-audit.sh
sudo nc-audit.sh # auto-detect, dedicated server sudo nc-audit.sh /var/www/nextcloud # explicit path (or NC_PATH=…) sudo NC_RAM_BUDGET_PCT=50 nc-audit.sh # shared host: size to 50% of RAM
**Hosts multi-instancia** (varios Nextclouds + una base de datos compartida). `NC_RAM_BUDGET_PCT`
es entonces el presupuesto **total** de la pila; `NC_PHP_SHARE_PCT`% de este (por defecto 60, el resto
cubre DB + web + SO — redúcelo en servidores con mucha carga de DB) es la parte de PHP, dividida entre
los pools de FPM por **peso** (una importancia relativa — no un porcentaje, ni MB) para dar un
objetivo `pm.max_children` por pool:```
target = PHP_share × (weight / Σ weights) / ~50 MB per process
El objetivo es un techo que el presupuesto permite, no un valor que debas fijar (solo aumenta un pool que realmente se satura). Los pesos son decisión tuya: la herramienta nunca los adivina.```bash
sudo NC_RAM_BUDGET_PCT=70 NC_INSTANCES="poolA:4,poolB:2,poolC:1" nc-audit.sh
sudo NC_RAM_BUDGET_PCT=70 nc-audit.sh --tune-fpm
**Informe push-back** (opcional, reutiliza la infraestructura Push): `nc-audit.sh --push
/etc/ncstatuscheck/<slug>.conf` ejecuta la auditoría y envía el informe mediante POST al
monitor(es), que lo almacenan y lo muestran en la página de detalles del servidor (sección "🩺
Auditoría del servidor"). Normalmente un cron mensual. La página web (admin, beta) en
`audit.php` distribuye el script (descarga + inline + one-liner de GitLab) y muestra
su versión.
> **Las herramientas de análisis profundo nunca se instalan** mediante el script — solo se ejecutan si
> ya están presentes (sin `curl | bash`, sin autoinstalación), cada una limitada por `timeout`.
## 🔧 Configuración avanzada
### Personalización de las reglas de versión
Las reglas de evaluación son configurables a través de la interfaz de administración:
**Estados de Nextcloud:**
- `dev` — Versión de desarrollo
- `stable` — Versión estable actual
- `oldstable` — Versión estable anterior compatible
- `deprecated` — Versión obsoleta
**Estados de PHP:**
- `recommended` — Versión recomendada
- `supported` — Versión compatible
- `deprecated` — Versión obsoleta
### Variables de configuración
Edita `config.php` para adaptar la configuración:```php
// Environment: 'dev' or 'prod'
define('ENV', 'prod');
// Paths and URLs
define('MONITOR_PATH', '/var/www/ncstatuscheck');
define('MONITOR_URL', 'https://monitoring.your-domain.com');
// Main server cache duration
define('CACHE_MAX_AGE', 86400); // 24 hours
// Official Nextcloud versions cache duration
define('VERSIONS_CACHE_AGE', 86400);
// Consecutive failed probes before a server is marked "down" (min 1)
define('UPTIME_FAIL_THRESHOLD', 2);
// Proactive alerts — webhook on a confirmed up/down state change.
// Empty URL = disabled. Format: 'slack' (default, also Mattermost/Google Chat),
// 'discord', or 'raw' (structured JSON). The URL usually carries a secret, so it
// is never logged in full — see config-example.php for details.
define('ALERT_WEBHOOK_URL', '');
define('ALERT_WEBHOOK_FORMAT', 'slack');
// Check alerts on top of up/down (cron-update cadence, 2×/day): SSL expiry
// tiers, vulnerable (below min_secure) or deprecated Nextcloud version, stale
// Push data, critical audit report, blocking apps-audit finding. Edge-triggered with a persisted state
// (cache/alerts_state.json): one alert per NEW condition, no reminders, re-arms
// when resolved (renewed cert, fixed/acked app…). First run arms silently.
define('ALERT_CHECKS', 'ssl,version,push_stale,audit,apps'); // '' = up/down only
define('ALERT_SSL_DAYS', '30,14,7'); // days-left tiers
// Email channel, independent of the webhook (either one arms the alerting).
// One digest mail per batch. Recommended transport: direct SMTP submission to
// your mail server (vendored PHPMailer, lib/phpmailer/ — nothing to set up on
// the host). Without ALERT_SMTP_HOST it falls back to PHP mail() (local MTA).
define('ALERT_EMAIL_TO', ''); // comma list of recipients, '' = off
define('ALERT_EMAIL_FROM', ''); // default: ncstatuscheck@<hostname>
define('ALERT_SMTP_HOST', ''); // e.g. 'mail.example.org', '' = mail() fallback
define('ALERT_SMTP_PORT', 587);
define('ALERT_SMTP_SECURITY', 'starttls'); // 'starttls' | 'tls' | 'none'
define('ALERT_SMTP_USER', '');
define('ALERT_SMTP_PASS', '');
Consulta
config-example.phppara ver la lista completa y comentada de opciones (incluyendoDEMO_MODEyPUSH_SCRIPT_VERSION).
lib/csrf-client.js + csrf_require()).htaccess incluidos para Apache; servers.json con chmod 0600 automático (contiene tokens)X-Frame-Options, nosniff, Referrer-Policy) en cada página servida por PHPhash_equals() pasa con el token de esa entrada, por lo que un servidor monitorizado comprometido no puede leer ni sobrescribir los datos de otro. request_push / request_push_all están detrás de autenticación de administrador + CSRF. Verificado de extremo a extremo contra un escenario de cliente comprometidotargets.conf copiaría silenciosamente cada push a un tercero<>"'& en la ingesta, además del escape en el momento del renderizadotry/catch puede capturar, lo que mataría la ejecución de recopilación a mitad del bucle y, con ella, todas las alertas de toda la flotaconfig.php está en gitignore y se edita a mano por servidor, por lo que se desvía — silenciosamente,
ya que casi todas las constantes tienen un valor de respaldo en el código. Un banner en la parte superior de
la página de administración informa de lo que realmente está mal, y solo cuando algo lo está:
un PUSH_SCRIPT_VERSION dejado atrás por una actualización, ningún transporte de alertas configurado en
absoluto, una etiqueta de cierre final que emite un byte antes de cualquier header(), un directorio
de caché no escribible, constantes ausentes que caen silenciosamente en valores predeterminados.
Solo lectura por diseño y sin acción de guardado, por la misma razón que la pestaña de
Notificaciones: config.php pertenece a root y contiene secretos. El contenido del archivo
nunca viaja — solo hechos sobre él — y ningún secreto se lee.
Todo lo anterior es a nivel de aplicación: cualquiera en internet aún puede alcanzar el monitor y sondearlo, y solo la contraseña lo detiene. La pestaña de filtrado de IP genera las reglas que colocan una lista blanca delante de la aplicación, de modo que los hosts desconocidos no puedan hablar con ella en absoluto. Es defensa en profundidad, no un reemplazo de la autenticación Basic ni de los tokens de push — y solo produce texto para revisar y pegar, nunca escribe una configuración de servidor web o firewall.
Dos clases de origen, deliberadamente desiguales, para que un servidor monitorizado comprometido no pueda alcanzar la administración:
| Clase | Quién | Puede alcanzar |
|---|---|---|
push | instancias monitorizadas solo en modo Push | /push-api.php, nada más |
admin | bastión / VPN / IP de oficina fija | todo |
Las instancias sondeadas en modo Basic/Extended no abren ninguna conexión entrante y no reciben ninguna entrada de lista blanca.
Las direcciones provienen de dos fuentes, y la diferencia importa: el registro DNS de un dominio
monitorizado es su dirección de ingreso, mientras que su push sale desde su egreso.
Donde difieren, solo funciona la segunda. Por lo tanto, push-api.php registra
la dirección de origen real de cada push (source_ip en la caché de push),
y la pestaña la incluye en la lista blanca, informando de la discrepancia. Hasta que un servidor haya hecho push
una vez, recurre a DNS A+AAAA y lo indica.
Tres salidas:
conf.d autocontenido (geo + map) más una
única línea if ($ncsc_forbidden) { return 403; } en el vhost. No es necesario
duplicar el bloque fastcgi, los archivos estáticos también están cubiertos (admin.html es
uno), y /.well-known/acme-challenge/ permanece abierto para que la renovación de certificados
no se rompa silenciosamente.<LocationMatch> con una búsqueda negativa hacia adelante más un <Location>
para el endpoint de push, de modo que las dos secciones no puedan solaparse y nada dependa
del orden de fusión de Apache. Todas las direcciones de una regla van en una línea
Require ip: varias líneas dentro de <RequireAll> se combinan con AND, lo que nadie puede satisfacer.El generador se niega a emitir cualquier cosa cuando no se proporciona ninguna dirección de administración,
advierte cuando la propia dirección del operador no está cubierta, y advierte cuando la petición
llegó a través de un proxy (tanto geo como Require ip leen el peer de transporte, por lo que
detrás de un proxy todos los clientes parecen iguales). El fragmento ufw generado coloca la regla SSH
primero, mantiene el puerto 80 abierto para el desafío HTTP-01, y explica la
trampa de IPv6: a diferencia de nginx, que rechaza una dirección v6 no listada, ufw no
filtra v6 en absoluto a menos que IPV6=yes esté configurado — un host de doble pila estaría de otro modo
completamente abierto a través de IPv6.
Límite conocido, señalado en la propia página: una vez aplicadas las reglas, esta
pestaña no descubre nada nuevo. Un push rechazado es rechazado por el servidor web antes
de llegar a PHP, por lo que la dirección registrada sigue siendo la última que logró pasar —
y aún parece verificada. Dos consecuencias: añadir un servidor Push significa
regenerar y reaplicar las reglas, o su primer push será rechazado; y si la
dirección de una instancia cambia, la nueva solo se puede leer en el registro de acceso
del servidor web (grep 'push-api.php' access.log | grep ' 403 '). Por lo tanto, la pestaña
muestra la fecha de última aparición de cada dirección observada y la marca una vez que es más antigua
que un ciclo completo de push perdido — el mismo umbral que la alerta push_stale,
que cubre el mismo punto ciego desde el otro lado.
Distinguir un problema de filtrado de cualquier otro: un GET simple en el endpoint
de push separa las capas limpiamente, sin efectos secundarios y sin necesidad de token —
ejecútalo desde la máquina en cuestión, ya que lo que se juzga es la dirección saliente de esa máquina:```bash
curl -sS -o /dev/null -w '%{http_code}\n' https://your-monitor/push-api.php
| Respuesta | Significado |
|---|---|
| `403` | bloqueado por el filtrado de IP |
| `401` | el filtrado pasó, Basic auth está respondiendo — el problema está en otro lugar |
| `405` | la solicitud llegó a la aplicación (GET no es un método aceptado allí) |
| nada / timeout | no es el filtrado: un filtro responde, no se queda en silencio |
Vuelve a ejecutar con `-u user:password` para resolver una duda sobre un `403`: si el código no
cambia, realmente es el filtrado. Verificado tanto en nginx como en Apache (incluyendo
con `Require valid-user` habilitado), el filtro responde *antes* de la autenticación —
y un `403` proveniente de la propia aplicación siempre lleva JSON en el cuerpo.
La lógica de los snippets vive en `lib/hardening-rules.php`, que es pura y está cubierta
por `tests/run.php`: los snippets son el producto aquí, y uno incorrecto o bien
bloquea al operador o deja un agujero. Tanto la salida de nginx como la de Apache
han sido verificadas conductualmente (servidores reales, direcciones de origen reales, incluyendo
intentos de path-traversal de la clase `push`).
### Análisis automatizado (etapa `security` del CI)
El escaneo de dependencias (`npm/pnpm audit`, Snyk Open Source, Dependabot) es un no-op
aquí: no hay `package.json` ni `composer.json` — nada que escanear. El
riesgo vive en el código personalizado (~15k líneas de PHP, ~6k de JS) y en los scripts
de shell que se ejecutan **como root** en las instancias monitoreadas (`tools/*.sh`). El pipeline
está dirigido allí:
| Trabajo | Herramienta | Bloqueante | Alcance |
|---|---|---|---|
| `secrets_scan` | gitleaks | sí | secretos commiteados (árbol de trabajo) |
| `sast_semgrep` | semgrep (`p/php`, `p/javascript`, `p/owasp-top-ten`) | sí | SSRF, falta de authz/CSRF, XSS |
| `shellcheck` | shellcheck (`--severity=warning`) | sí | `tools/*.sh` — root en hosts de clientes |
| `dockerfile_misconfig` | trivy misconfig | sí | `deploy/docker/` |
| `container_cve` | trivy image | no (`allow_failure`) | la imagen que `deploy/docker` construye, más `nginx:alpine` |
| `ui_tests` | node (sin dependencias) | sí | invariantes de escape de `lib/ui-common.js` (ambas regresiones XSS pasadas) |
| `phpmailer_freshness` | GitHub API | no (`allow_failure`) | pin vendored vs release upstream |
| `deploy_selfcheck` | nc-selfcheck.sh | sí | el ruleset nginx enviado (reglas de denegación + cabeceras de seguridad) levantado en un contenedor desechable |
Todos los trabajos bloqueantes tienen una **línea base de cero hallazgos**, por lo que cualquier nueva alerta es una señal
real. Dos decisiones deliberadas, documentadas en línea en `.gitlab-ci.yml`:
- **`php.lang.security.injection.echoed-request` está excluida** de semgrep: marca
cada `echo json_encode()` como XSS, que es lo que cada endpoint de API aquí
hace legítimamente (respuestas JSON, no HTML). Representó 10 de 10
hallazgos en la primera ejecución, todos falsos. Mantenerla entrenaría a todos a
ignorar el trabajo.
- **Los dos trabajos `allow_failure` reportan hechos upstream** (un CVE en `nginx:alpine`
o uno cuyo fix aún no ha llegado a la rama Alpine, una nueva release de PHPMailer)
que un merge request no puede arreglar. Rojo-pero-tolerado es la señal
precisa — "momento de reconstruir / refrescar el vendoring" — no una razón para bloquear
trabajo no relacionado. `phpmailer_freshness` reporta una GitHub API inalcanzable o con
límite de tasa como un *skip*, nunca como "desactualizado".
- **`container_cve` escanea la imagen que construye, no la etiqueta `FROM`.** El
Dockerfile endurece la base con `apk --no-cache upgrade` (la imagen oficial de PHP
va por detrás de los repos de Alpine — envió c-ares 1.34.6-r0 mientras
1.34.8-r0, que corrige CVE-2026-33630, ya estaba publicada). Escanear la etiqueta
base reportaría por tanto CVEs que la imagen enviada ya no tiene: un
trabajo permanentemente naranja que nadie lee.
Las listas de permitidos son intencionalmente estrechas: `.gitleaks.toml` excusa cadenas
placeholder **literales**, nunca archivos de documentación completos (permitir `README.md`
cegaría el escaneo el día que un secreto real se pegue en él) — así que un nuevo token
de ejemplo en la documentación debe añadirse allí. `.trivyignore` contiene una sola entrada,
`DS-0002`, argumentada en el archivo: el maestro de php-fpm debe iniciar como root para bajar
sus workers a `www-data` (uid 82).
### Verificación post-despliegue (`nc-selfcheck.sh`)
El CI puede bloquear la configuración *enviada* (el trabajo `deploy_selfcheck` de arriba levanta
el ruleset de nginx en un contenedor y lo sondea), pero no puede verificar el servidor
al que realmente desplegaste — host diferente, credenciales de Basic-auth, permisos
del sistema de archivos. `tools/nc-selfcheck.sh` cierra esa brecha. Es un script
bash independiente y de solo lectura (mismo modelo que `nc-audit.sh`) que ejecutas después de cada despliegue:```bash
# Black-box, no credentials: confirms Basic auth is enforced (401) and that
# sensitive files are blocked (cache/, servers.*, .git, config.php source).
bash tools/nc-selfcheck.sh https://monitoring.example.com
# + security headers behind Basic auth:
bash tools/nc-selfcheck.sh -u user:pass https://monitoring.example.com
# + filesystem checks (run ON the host): servers.json / config.php / CSRF-secret
# permissions, and a stray closing "?>" in config.php.
bash tools/nc-selfcheck.sh --webroot /var/www/ncstatuscheck https://monitoring.example.com
Sale con código de salida distinto de cero ante cualquier hallazgo crítico (fuga de código fuente, archivo secreto sin bloquear, almacén de tokens legible por cualquier usuario, autenticación Basic ausente), por lo que puede condicionar un despliegue: conéctalo a tu script de sincronización/despliegue como paso posterior. WARN/INFO nunca hacen fallar la ejecución.
// In config.php define('ENV', 'dev');
En modo de desarrollo, se muestra información adicional (versión de PHP, servidor web).
### Pruebas del servidor
Use la interfaz de administración para añadir un servidor por URL. El servidor será consultado en la siguiente actualización de datos.
### Registros de depuración
Revise los archivos de registro en `cache/`:
- `monitor.log` — Registros generales de la aplicación
- `cron.log` — Registros completos del script de recopilación (`cron-update.php`)
- `ping.log` — Registros ligeros de sondeo de estado activo/inactivo (`cron-ping.php`)
- `alerts.log` — Envío proactivo de alertas (webhook/correo electrónico), nunca registra secretos de webhook ni credenciales SMTP
### Suite de pruebas```bash
php tests/run.php # plain-PHP assertions, no framework — exit 0 = all green
Cubre la lógica de negocio pura (reglas de versión/aplicaciones, advertencias, máquina de estados de tiempo de actividad y disponibilidad, máquina de estados de deduplicación/rearme de alertas, constructores de correos electrónicos).
Abre una nueva incidencia con:
Este proyecto está licenciado bajo GNU AGPL v3.
NcStatusCheck es desarrollado por ézéo, una cooperativa digital especializada en soluciones de código abierto.
¿Necesitas ayuda? Consulta las incidencias o contacta con el equipo de ézéo.