
Monitoring centralisé pour instances Nextcloud multiples
Surveillance centralisée pour plusieurs instances Nextcloud
NcStatusCheck est un outil de surveillance qui vous permet de suivre l'état de santé de plusieurs serveurs Nextcloud depuis une interface web unique. Il analyse les versions de Nextcloud et de PHP et fournit des recommandations de mise à jour.

< 15d / < 7d)occ app:list avec l'App Store Nextcloud pour signaler les applications à vérifier — bloquant (bloquant la mise à jour, incompatible, applications de test en production) plus signaux informatifs de maturité/turbulence (récemment publié, pré-1.0, version alpha/bêta/rc, rafale de versions, version fraîchement publiée)📦) et un bloc jumeau « Conteneurs à surveiller » (🐳) agrègent chaque application/Docker image signalée sur toutes les instances en une seule entrée, avec des filtres de type par groupe et une popup listant les instances impactées et leurs versionsALERT_WEBHOOK_URL), et/ou un email récapitulatif par lot (ALERT_EMAIL_TO) envoyé par soumission SMTP directe à votre serveur de messagerie (PHPMailer intégré ; bascule vers le MTA local si aucun relais SMTP n'est configuré). Couvre également les signaux lents (ALERT_CHECKS) : expiration du certificat SSL (par paliers), version Nextcloud vulnérable/dépréciée, sonde Push obsolète, rapport d'audit critique, application bloquante — une alerte par nouvelle condition, pas de spam de rappelnc_update_grace_days), pour éviter de courir après une version buggée du jour⚠️ avertissements, 📦 applications, 🔄 Docker, 🔒 SSL, 🔴 hors ligne, 🚧 maintenance) que lorsqu'il y a quelque chose à signalernc-audit.shlocalStoragechmod 700) pour l'instance Nextcloud distante, mis à jour automatiquement en fonction des optionsncstatuscheck/ ├── 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 │ ├── 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
## 🔌 Modes de collecte
Les modes ne sont pas mutuellement exclusifs — un serveur peut être en mode Étendu et Push simultanément.
| Mode | Badge | Source | Données collectées |
|------|-------|--------|----------------|
| **Basique** | *(aucun)* | `/status.php` + en-têtes HTTP | Version Nextcloud (PHP/serveur web si exposé) |
| **Étendu** | `⚡ Étendu` (violet → orange en cas d'erreur/données obsolètes) | `/ocs/v2.php/apps/serverinfo/api/v1/info` avec `NC-Token` | Version NC, PHP, serveur web, OPcache, Redis, DB, utilisateurs actifs… |
| **Push** | `📡 Push` (bleu → orange en cas d'erreur/données obsolètes) | POST vers `push-api.php` | Données poussées par l'instance NC distante via le script cron |
Le **jeton NC serverinfo** est disponible dans **Paramètres Nextcloud → Administration → Système**.
Le **jeton Push** est généré depuis l'interface d'administration ; l'administrateur fournit un script cron bash prêt à l'emploi (`chmod 700`) à déployer sur l'instance surveillée.
Les données du mode Étendu sont fournies par l'application [nextcloud/serverinfo](https://github.com/nextcloud/serverinfo), qui doit être installée et activée sur l'instance surveillée.
**Comportement de secours** : si l'API Étendue est inaccessible (erreur de connexion, jeton invalide, application non installée), NcStatusCheck revient automatiquement à `/status.php` pour récupérer au moins la version Nextcloud.
**Seuil d'obsolescence Push** : un serveur Push est considéré obsolète si aucune donnée n'a été reçue dans `auto_push_interval + 30 minutes`. L'intervalle Push par défaut est de 12 heures.
### Colonnes du tableau de bord
Le tableau de bord principal affiche 5 colonnes : **Serveur** | **Version NC** | **PHP** | **Sondes** | **Santé**
La colonne **Sondes** affiche les modes de collecte actifs pour chaque serveur :
- Badge `⚡ Étendu` (violet, devient orange en cas d'erreur de connexion ou de données obsolètes)
- Badge `📡 Push` (bleu, devient orange lorsque aucune donnée n'est reçue dans le délai)
- Les deux badges peuvent apparaître simultanément si les deux modes sont actifs
- Pas de badge = mode Basique uniquement
### Colonne Santé
La colonne Santé n'affiche quelque chose que lorsqu'il y a quelque chose à traiter :
| Indicateur | Badge | Signification |
|-----------|-------|---------|
| Hors ligne | `🔴 Hors ligne` | Instance inaccessible (sonde HTTP a échoué), avec « hors ligne depuis X » |
| Avertissements actifs | `⚠️ N` | N problèmes de configuration |
| Audit des applications | `📦 N` | N applications installées à examiner (bloquant mise à jour/incompatible) |
| Expiration SSL | `🔒 N j` | Certificat expire bientôt — orange `< 15 j`, rouge `< 7 j` ou expiré |
| Mises à jour Docker | `🔄 M` | M mises à jour de conteneurs disponibles |
| Tout OK | *(vide)* | Rien à signaler |
| Aucune donnée | `?` | Mode Basique sans données Push |
#### État haut/bas et expiration SSL
NcStatusCheck conserve un état haut/bas **minimal** par serveur (état actuel + date de dernier changement uniquement — pas de séries temporelles, pas de page d'historique). « Haut » signifie que la sonde HTTPS sortante a atteint l'instance ; un badge rouge **Hors ligne** n'apparaît que lorsque c'est bas. Lors de cette même sonde HTTPS, **l'expiration du certificat SSL** est lue gratuitement (`CURLOPT_CERTINFO`) et signalée lorsqu'elle approche. Les deux sont visibles en détail sur la page de détail. *Remarque : ces contrôles sortants ne s'appliquent pas aux instances Push uniquement que le moniteur ne contacte jamais.*
#### Audit des applications (`📦`)
Lorsqu'un serveur Push signale ses applications installées (`occ app:list`, script push v3+), NcStatusCheck les recoupe avec le catalogue de l'app store Nextcloud et signale les applications dignes d'examen. **Signalement uniquement** — l'outil ne désactive jamais rien ; il remonte des candidates (il ne peut pas savoir si une application est réellement utilisée). Seuls des signaux **factuels et binaires** sont utilisés. Le badge `📦 N` compte les constats bloquants (pas de version compatible pour la version NC actuelle, pas de version pour NC N+1 → bloque la mise à niveau, ou une application de test/développement laissée activée en production). Les constats informatifs (application obsolète sur l'instance, abandonnée en amont, incompatible PHP) sont affichés uniquement sur la page de détail. Les constats peuvent être masqués via le même mécanisme d'accusé de réception que les avertissements.
### Format `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** : le dépôt comprend des fichiers `.htaccess` reflétant les règles de `deny` ci-dessus (root : bloque `*.log`/`*.json`/`*.txt` et `.git` ; `cache/`, `tools/`, `deploy/`, `tests/` : `Require all denied`). Ils ne fonctionnent que si le vhost définit `AllowOverride FileInfo AuthConfig` (ou `All`) — la valeur par défaut de Debian pour `/var/www` est `AllowOverride None`, auquel cas il faut répliquer les règles directement dans le vhost. L'authentification HTTP Basic doit de toute façon être configurée dans le vhost.
### Déploiement
1. **Cloner le dépôt**```bash
git clone https://gitlab.com/jp.louvel/ncstatuscheck.git
cd ncstatuscheck
Modifiez `config.php` et ajustez les chemins et l'URL à votre environnement :```php
define('MONITOR_PATH', '/var/www/ncstatuscheck');
define('MONITOR_URL', 'https://monitoring.your-domain.com'); // your public URL
define('CACHE_DIR', MONITOR_PATH . '/cache');
Les serveurs sont gérés directement depuis l'interface d'administration (bouton ⚙️ Admin).
Vous pouvez également créer servers.json manuellement :```json
[
{"url": "https://cloud.example.com"},
{"url": "https://nextcloud.mycompany.org", "serverinfo_token": "your_token_here"}
]
> **Migration depuis `servers.txt`** : si un fichier `servers.txt` existe, il est automatiquement converti en `servers.json` lors du premier accès. Vous pouvez ensuite supprimer `servers.txt`.
4. **Définir les permissions**
nginx/PHP-FPM s'exécutent sous leur propre utilisateur (`www-data` sur Debian/Ubuntu, `nginx`/`apache` sur la famille RHEL — ajustez ci-dessous) — si vous avez cloné en tant que votre propre utilisateur de connexion, cet utilisateur ne sera presque certainement pas `www-data` ou dans son groupe, donc `chmod` seul laisse le serveur web **sans aucun accès**, même pas en lecture (chaque requête 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.jsonn'existe pas encore (vous laissez l'interface d'administration le créer plutôt que de suivre l'étape manuelle ci-dessus), la commandechmod 600ci-dessus n'a simplement rien sur quoi agir — ce n'est pas grave :ServersStore::save()exécute elle-même unchmodvers0600à chaque écriture, donc un fichierservers.json(re)créé via l'interface d'administration ne reste jamais lisible par le groupe/monde avec des jetons serverinfo/push à l'intérieur.
6. **Tâches planifiées (optionnelles)**
Deux crons complémentaires — installez les deux **dans le même appel `crontab -`** :
`crontab -` installe une toute nouvelle crontab depuis l'entrée standard, il n'ajoute pas, donc
l'exécuter deux fois (une fois par ligne) ne laisse que le *deuxième* job — le premier
disparaît silencieusement, sans erreur. Cela préserve également tout ce qui se trouve déjà dans votre
crontab (en redirigeant d'abord `crontab -l`) au lieu de l'effacer :```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-running this appends duplicates if these lines are already present — check
with crontab -l first if unsure.
cron-ping.php est volontairement minimal : il ne fait que vérifier le
status.php de chaque instance et mettre à jour l'état up/down (cache/uptime_state.json), afin de pouvoir s'exécuter fréquemment sans charge. Une instance n'est marquée down qu'après UPTIME_FAIL_THRESHOLD sondes consécutives échouées (par défaut 2 → ~10 min avec une cadence de 5 min) ; le retour à up est immédiat. Le cron-update.php complet reste inchangé pour tout le reste.
Au lieu des étapes 1 à 6 ci-dessus, NcStatusCheck peut également fonctionner comme une petite stack docker compose (PHP-FPM + nginx + un conteneur cron) — le dépôt est monté en bind tel quel, sans étape de build ni Composer, donc il reflète exactement l'agencement bare-metal, simplement conteneurisé. Sert uniquement du HTTP simple (port 8080 par défaut) — placez votre propre proxy inverse de terminaison TLS devant.
L'installation complète — configuration, les pièges de permissions (uid 82, pré-création de servers.json), l'authentification HTTP Basic, cron, les mises à jour et les sauvegardes — se trouve entièrement dans deploy/docker/README.md. Commencez par là ; cette section est intentionnellement juste un pointeur, pour éviter de maintenir deux copies des mêmes étapes en synchronisation.
https://monitoring.votre-domaine.comAccessible en cliquant sur n'importe quel nom de serveur ou son indicateur de santé.
Pour les serveurs Basic (pas de sonde Extended ou Push), une page simplifiée affiche les données disponibles (version NC, serveur web, protocole HTTP) avec un avis et une suggestion pour activer une sonde.
Pour les serveurs Extended / Push, la page détaillée complète affiche des sections séparées :
NcStatusCheck expose plusieurs points de terminaison REST :
API principale (api.php)
GET ?action=get_data — Récupérer les données (cache ou rafraîchissement)POST ?action=refresh_data — Forcer la mise à jour de tous les serveursAPI Push (push-api.php)
POST avec en-tête push_token — Recevoir les données push d'une instance NC distantePOST ?action=request_push_all — Demander un push immédiat de tous les serveurs push configurés (définit un indicateur de déclenchement consommé par le script cron distant)Le script cron généré par l'interface d'administration est divisé en deux : un noyau générique
/usr/local/bin/ncstatuscheck-push.sh— identique sur chaque serveur (toute la logique) — piloté par une petite configuration par instance/etc/ncstatuscheck/<slug>.conf(SERVER_URL,SLUG,OCC_CMD,DOCKER_ENABLED,SKOPEO_ENABLED). Il est invoqué sous la formencstatuscheck-push.sh /etc/ncstatuscheck/<slug>.conf [--test]. Le noyau refuse de sourcer une configuration accessible en écriture par le groupe/tout le monde (anti-injection de code).Il est multi-cible (fan-out) : les données sont collectées une fois et poussées vers chaque moniteur listé dans
/etc/ncstatuscheck/targets-<slug>.conf(une ligneurl|push_token[|http_user|http_pass]par moniteur). L'admin de chaque moniteur émet une commande idempotente pour s'enregistrer.Plusieurs instances Nextcloud sur un même hôte : les chemins par instance sont suffixés par un dérivé de l'URL surveillée (ex. → ) : , , , , état . Seul le noyau est partagé, donc les instances colocalisées n'entrent jamais en collision.
API de détail (detail-api.php)
GET ?server=<url> — Données complètes serverinfo + avertissements calculés pour un serveur Extended/PushAPI d'administration
admin-api.php — Configuration des versionsservers-admin-api.php — Gestion des serveurs (get_servers, add_server, remove_server, update_server_token, generate_push_token, remove_push_token)nextcloud-versions-api.php — Versions officiellesnc-audit.sh)Un sous-système séparé de la surveillance : un script bash autonome et en lecture seule
(tools/nc-audit.sh) exécuté en tant que root sur un serveur Nextcloud pour un audit
ponctuel / mensuel du réglage du web + PHP + base de données, recoupé avec la capacité
physique de la machine (RAM, CPU, type de disque). Destiné à une offre de supervision gérée :
le client l'installe, le moniteur ne reçoit que des rapports — aucun accès machine/réseau
requis. Le script ne fait que lire la configuration (aucune modification), imprime
un rapport coloré et en écrit une copie dans /tmp.
Ce qu'il vérifie : capacité du serveur (RAM/CPU/SSD-HDD, swappiness, détection de serveur
partagé) · Nextcloud (versions, cron, cache, environnement Redis runtime, type de base de données, journaux) ·
PHP/PHP-FPM (SAPI de service réel, environnement OPcache, mémoire multi-pool) · Apache
(mémoire worker sensible au MPM) · Nginx · PostgreSQL · MariaDB · hygiène de sécurité
(fail2ban ou CrowdSec + bouncer + blocklist communautaire ; mises à jour en attente /
redémarrage / services sur bibliothèques obsolètes) · réconciliation du budget RAM (InnoDB +
FPM + Apache vs RAM réelle) · analyse approfondie avec des outils optionnels s'ils sont déjà
présents (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
**Hôtes multi-instances** (plusieurs Nextclouds + une base de données partagée). `NC_RAM_BUDGET_PCT`
est alors le budget **total** de la pile ; `NC_PHP_SHARE_PCT`% de celui-ci (par défaut 60, le reste
couvre la base de données, le serveur web et le système d'exploitation — à réduire sur les serveurs lourds en base de données) est la part PHP, répartie entre
les pools FPM par **poids** (une importance relative — pas un pourcentage, pas des Mo) pour obtenir une
cible `pm.max_children` par pool :```
target = PHP_share × (weight / Σ weights) / ~50 MB per process
La cible est un plafond que le budget permet, pas une valeur que vous devez définir (n'augmentez qu'un pool qui sature effectivement). Les poids sont votre affaire — l'outil ne les devine jamais.```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
**Rapport push-back** (optionnel, réutilise l'infrastructure Push) : `nc-audit.sh --push
/etc/ncstatuscheck/<slug>.conf` lance l'audit et envoie le rapport (POST) au(x)
moniteur(s), qui le stockent et l'affichent sur la page détaillée du serveur
(« 🩺 Server audit »). Généralement un cron mensuel. La page web (admin, bêta) à
l'adresse `audit.php` distribue le script (téléchargement + en ligne + GitLab
one‑liner) et affiche sa version.
> **Les outils d'analyse avancée ne sont jamais installés** par le script – ils
> ne s'exécutent que s'ils sont déjà présents (pas de `curl | bash`, pas
> d'installation automatique), chacun étant limité par `timeout`.
## 🔧 Configuration avancée
### Personnalisation des règles de version
Les règles d'évaluation sont configurables via l'interface d'administration :
**Statuts Nextcloud :**
- `dev` — Version de développement
- `stable` — Version stable actuelle
- `oldstable` — Version stable précédente prise en charge
- `deprecated` — Version obsolète
**Statuts PHP :**
- `recommended` — Version recommandée
- `supported` — Version prise en charge
- `deprecated` — Version obsolète
### Variables de configuration
Modifiez `config.php` pour adapter la configuration :```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', '');
Voir
config-example.phppour la liste complète et commentée des options (y comprisDEMO_MODEetPUSH_SCRIPT_VERSION).
lib/csrf-client.js + csrf_require()).htaccess fournis pour Apache ; servers.json avec chmod 0600 automatiquement (jetons à l'intérieur)X-Frame-Options, nosniff, Referrer-Policy) sur chaque page servie par PHPconfig.php est dans .gitignore et édité manuellement par serveur, donc il dérive — silencieusement, puisque presque chaque constante a une valeur par défaut dans le code. Une bannière en haut de la page d'administration signale ce qui ne va pas réellement, et uniquement quand quelque chose ne va pas : un PUSH_SCRIPT_VERSION laissé par une mise à jour, aucun transport d'alerte configuré, une balise de fermeture finale émettant un octet avant tout header(), un répertoire de cache non inscriptible, des constantes absentes qui tombent silencieusement sur les valeurs par défaut.
En lecture seule par conception et sans action de sauvegarde, pour la même raison que l'onglet Notifications : config.php appartient à root et contient des secrets. Le contenu du fichier ne voyage jamais — seuls des faits à son sujet — et aucun secret n'est lu.
Tout ce qui précède est au niveau de l'application : n'importe qui sur Internet peut encore atteindre le moniteur et le sonder, seul le mot de passe les arrête. L'onglet Filtrage IP génère les règles qui placent une liste d'autorisation devant l'application, de sorte que les hôtes inconnus ne peuvent pas du tout lui parler. C'est une défense en profondeur, pas un remplacement de l'authentification Basic ou des jetons de push — et cela ne produit que du texte à vérifier et coller, cela n'écrit jamais une configuration de serveur web ou de pare-feu.
Deux classes de source, délibérément inégales, de sorte qu'un serveur surveillé compromis ne puisse pas atteindre l'administration :
| Classe | Qui | Peut atteindre |
|---|---|---|
push | instances surveillées en mode Push uniquement | /push-api.php, rien d'autre |
admin | bastion / VPN / adresse de bureau fixe | tout |
Les instances interrogées en mode Basique/Étendu n'ouvrent aucune connexion entrante et n'obtiennent aucune entrée de liste d'autorisation.
Les adresses proviennent de deux sources, et la différence est importante : l'enregistrement DNS d'un domaine surveillé est son adresse d'entrée, tandis que son push part de son adresse de sortie. Lorsqu'elles diffèrent, seule la seconde fonctionne. push-api.php enregistre donc l'adresse source réelle de chaque push (source_ip dans le cache des pushes), et l'onglet l'autorise, signalant l'incohérence. Tant qu'un serveur n'a pas poussé une fois, il revient sur le DNS A+AAAA et le mentionne.
Trois sorties :
conf.d autonome (geo + map) plus une seule ligne if ($ncsc_forbidden) { return 403; } dans le vhost. Pas besoin de dupliquer le bloc fastcgi, les fichiers statiques sont également couverts (admin.html en fait partie), et /.well-known/acme-challenge/ reste ouvert pour que le renouvellement du certificat ne casse pas silencieusement.<LocationMatch> avec une anticipation négative plus un <Location> pour le point de terminaison push, de sorte que les deux sections ne puissent pas se chevaucher et que rien ne dépende de l'ordre de fusion d'Apache. Toutes les adresses d'une règle vont sur une seule ligne Require ip : plusieurs lignes dans <RequireAll> sont en ET, ce que personne ne peut satisfaire.Le générateur refuse d'émettre lorsqu'aucune adresse d'administration n'est donnée, avertit lorsque l'adresse de l'opérateur lui-même n'est pas couverte, et avertit lorsque la requête est passée par un proxy (car geo et Require ip lisent le pair de transport, donc derrière un proxy, chaque client se ressemble). L'extrait ufw généré place la règle SSH en premier, maintient le port 80 ouvert pour le défi HTTP-01, et explicite le piège IPv6 : contrairement à nginx, qui refuse une adresse v6 non listée, ufw ne filtre pas le v6 du tout sauf si IPV6=yes est défini — un hôte double pile serait autrement grand ouvert sur IPv6.
Limite connue, signalée dans la page elle-même : une fois les règles appliquées, cet onglet ne découvre rien de nouveau. Un push refusé est rejeté par le serveur web avant d'atteindre PHP, donc l'adresse enregistrée reste la dernière qui est passée — et semble toujours vérifiée. Deux conséquences : ajouter un serveur Push signifie régénérer et réappliquer les règles, sinon son premier push est refusé ; et si l'adresse d'une instance change, la nouvelle n'est lisible que dans le journal d'accès du serveur web (grep 'push-api.php' access.log | grep ' 403 '). L'onglet affiche donc la date de dernière observation de chaque adresse et la signale dès qu'elle est plus ancienne qu'un cycle de push manqué complet — le même seuil que l'alerte push_stale, qui couvre le même angle mort depuis l'autre côté.
Distinguer un problème de filtrage d'un autre : un GET nu sur le point de terminaison push sépare proprement les couches, sans effet secondaire et sans jeton nécessaire — exécutez-le depuis la machine concernée, puisque ce qui est jugé est l'adresse de sortie de cette machine :```bash
curl -sS -o /dev/null -w '%{http_code}\n' https://your-monitor/push-api.php
| Réponse | Signification |
|---|---|---|
| `403` | bloqué par le filtrage IP |
| `401` | filtrage passé, Basic auth répond — le problème est ailleurs |
| `405` | la requête a atteint l'application (GET n'est pas une méthode acceptée ici) |
| rien / timeout | pas le filtrage : un filtre répond, il ne devient pas silencieux |
Relancez avec `-u user:password` pour lever un doute sur un `403` : si le code ne bouge pas, c'est bien le filtrage. Vérifié sur nginx et Apache (y compris avec `Require valid-user` activé), le filtre répond *avant* l'authentification — et un `403` provenant de l'application elle-même contient toujours du JSON dans le corps.
La logique des snippets se trouve dans `lib/hardening-rules.php`, qui est pure et couverte par `tests/run.php` : les snippets sont le produit ici, et un mauvais snippet soit bloque l'opérateur, soit laisse une brèche. Les sorties nginx et Apache ont été vérifiées comportementalement (serveurs réels, adresses sources réelles, y compris des tentatives de path-traversal depuis la classe `push`).
### Analyse automatisée (étape CI `security`)
L'analyse des dépendances (`npm/pnpm audit`, Snyk Open Source, Dependabot) est une non-opération ici : il n'y a pas de `package.json` ni de `composer.json` — rien à analyser. Le risque réside dans le code personnalisé (~15k lignes de PHP, ~6k de JS) et dans les scripts shell qui s'exécutent **en tant que root** sur les instances surveillées (`tools/*.sh`). Le pipeline est ciblé là-dessus :
| Job | Outil | Bloquant | Périmètre |
|---|---|---|---|
| `secrets_scan` | gitleaks | oui | secrets commités (arbre de travail) |
| `sast_semgrep` | semgrep (`p/php`, `p/javascript`, `p/owasp-top-ten`) | oui | SSRF, authz/CSRF manquants, XSS |
| `shellcheck` | shellcheck (`--severity=warning`) | oui | `tools/*.sh` — root sur les hôtes clients |
| `dockerfile_misconfig` | trivy misconfig | oui | `deploy/docker/` |
| `container_cve` | trivy image | non (`allow_failure`) | l'image que `deploy/docker` construit, plus `nginx:alpine` |
| `ui_tests` | node (sans dépendances) | oui | invariants d'échappement de `lib/ui-common.js` (régressions XSS passées) |
| `phpmailer_freshness` | API GitHub | non (`allow_failure`) | pin vendu vs version amont |
| `deploy_selfcheck` | nc-selfcheck.sh | oui | le jeu de règles nginx livré (règles de refus + en-têtes de sécurité) déployé dans un conteneur jetable |
Tous les jobs bloquants ont une **base de zéro constatation**, donc toute nouvelle alerte est un signal réel. Deux choix délibérés, documentés en ligne dans `.gitlab-ci.yml` :
- **`php.lang.security.injection.echoed-request` est exclue** de semgrep : elle signale chaque `echo json_encode()` comme XSS, ce que fait légitimement chaque point d'API ici (réponses JSON, pas HTML). Cela représentait 10 des 10 constatations au premier passage, toutes fausses. La conserver habituerait tout le monde à ignorer le job.
- **Les deux jobs `allow_failure` rapportent des faits amont** (une CVE dans `nginx:alpine` ou une dont le correctif n'a pas encore atteint la branche Alpine, une nouvelle version de PHPMailer) qu'une demande de fusion ne peut pas corriger. Rouge mais toléré est le signal précis — "il est temps de reconstruire / rafraîchir le vendoring" — pas une raison pour bloquer un travail non lié. `phpmailer_freshness` signale une API GitHub inaccessible ou limitée comme un *skip*, jamais comme "obsolète".
- **`container_cve` analyse l'image qu'il construit, pas le tag `FROM`.** Le Dockerfile durcit la base avec `apk --no-cache upgrade` (l'image PHP officielle est en retard sur les dépôts Alpine — elle a livré c-ares 1.34.6-r0 alors que 1.34.8-r0, corrigeant CVE-2026-33630, était déjà publiée). Analyser le tag de base rapporterait donc des CVE que l'image livrée n'a plus : un job orange permanent que personne ne lit.
Les listes blanches sont intentionnellement restreintes : `.gitleaks.toml` excuse les chaînes de substitution **littérales**, jamais des fichiers de documentation entiers (mettre `README.md` en liste blanche aveuglerait l'analyse le jour où un vrai secret y est collé) — donc un nouveau jeton d'exemple dans la documentation doit y être ajouté. `.trivyignore` contient une seule entrée, `DS-0002`, argumentée dans le fichier : le maître php-fpm doit démarrer en tant que root pour descendre ses workers à `www-data` (uid 82).
### Vérification post-déploiement (`nc-selfcheck.sh`)
La CI peut verrouiller la configuration *livrée* (le job `deploy_selfcheck` ci-dessus met le jeu de règles nginx dans un conteneur et le sonde), mais elle ne peut pas vérifier le serveur sur lequel vous avez réellement déployé — hôte différent, identifiants Basic-auth, permissions du système de fichiers. `tools/nc-selfcheck.sh` comble cette lacune. C'est un script bash autonome et en lecture seule (même modèle que `nc-audit.sh`) que vous exécutez après chaque déploiement :```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
Il retourne un code de sortie non nul en cas de découverte critique (fuite de source, fichier secret non verrouillé, stockage de jetons lisible par tous, authentification Basic manquante), ce qui permet de bloquer un déploiement — intégrez-le dans votre script de synchronisation/déploiement en tant qu'étape postérieure. WARN/INFO ne font jamais échouer l'exécution.
// In config.php define('ENV', 'dev');
En mode développement, des informations supplémentaires sont affichées (version PHP, serveur web).
### Test du serveur
Utilisez l'interface d'administration pour ajouter un serveur par URL. Le serveur sera interrogé lors du prochain rafraîchissement des données.
### Journaux de débogage
Consultez les fichiers journaux dans `cache/` :
- `monitor.log` — Journaux généraux de l'application
- `cron.log` — Journaux complets du script de collecte (`cron-update.php`)
- `ping.log` — Journaux légers des sondes up/down (`cron-ping.php`)
- `alerts.log` — Envoi proactif des alertes (webhook/email), ne journalise jamais les secrets webhook ni les identifiants SMTP
### Suite de tests```bash
php tests/run.php # plain-PHP assertions, no framework — exit 0 = all green
Couvre la logique métier pure (règles version/apps, avertissements, machine d'état de disponibilité et uptime, machine d'état de déduplication/réarmement des alertes, générateurs d'emails).
Ouvrez un nouveau ticket avec :
Ce projet est sous licence GNU AGPL v3.
NcStatusCheck est développé par ézéo, une coopérative numérique spécialisée dans les solutions open source.
Besoin d'aide ? Consultez les issues ou contactez l'équipe ézéo.
| Section | Champs |
|---|
| Système Nextcloud | Version, mode debug, memcache local/distribué, file locking, espace disque |
| PHP | Version, memory_limit, upload_max_filesize, max_execution_time, FPM, OPcache |
| Serveur web | Nom + version, protocole HTTP |
| Base de données | Type, version, taille |
| Cache | Redis, taux de hit APCu |
| Utilisateurs actifs | 5 dernières min, 1 h, 24 h, 7 jours |
<slug>latest.ezeo.cooplatest_ezeo_coop<slug>.conf/etc/cron.d/ncstatuscheck-<slug>targets-<slug>.confncstatuscheck-push-<slug>.log…-<slug>.<md5>.lastNextcloud fonctionnant dans Docker (image officielle, compose, AIO) : entièrement pris en charge — le
script est installé sur l'hôte (cron root + accès au démon Docker), jamais à l'intérieur du
conteneur, et occ passe par docker exec :
OCC_CMD=docker exec -u www-data <conteneur> php occ (conteneur AIO : nextcloud-aio-nextcloud).
Le générateur de script d'administration a un type d'installation prédéfini qui préremplit cela. N'ajoutez jamais
-t (pas de TTY sous cron) ; gardez -u www-data (l'image officielle refuse occ en tant que root).
Déploiement / mises à jour de flotte : comme le noyau est un seul fichier identique, mettre à jour
la logique sur de nombreux serveurs = remplacer ce seul fichier (le marqueur ↑ signale les serveurs
exécutant une version plus ancienne). Voir deploy/ansible/ pour un
playbook prêt à l'emploi (ou une simple boucle scp). Le moniteur reste passif — il n'envoie jamais de code
vers la flotte ; l'ancre de confiance est votre propre accès SSH, pas le moniteur.
Migration depuis une installation pré-v4 (script monolithique par instance) : supprimez l'ancien
/usr/local/bin/ncstatuscheck-push-<slug>.sh et /etc/cron.d/ncstatuscheck-<slug>
avant d'installer le noyau + la configuration (le targets-<slug>.conf est réutilisé tel quel),
sinon vous ferez un double push.
hash_equals()request_pushrequest_push_alltargets.conf copie silencieusement chaque push vers un tiers sinon<>"'& à l'ingestion, en plus de l'échappement au rendutry/catch ne peut attraper, ce qui tuerait la boucle de collecte en cours et, avec elle, toutes les alertes pour l'ensemble de la flotte