---
tags: [VPS-IA, securite, ssh, ntfy, pam, Securite]
date: 2026-10-19
univers: PRO
description: "Notification ntfy à chaque connexion SSH réussie via pam_exec, token lu par runuser."
nature: documentation
projects:
  - [[03_PROJETS/VPS-IA]]
type: note
---

# VPS-IA — Alerte SSH sur connexion (PAM + ntfy)

## Objectif

Notifier sur **ntfy** (topic `vps-admin`) à chaque ouverture de session SSH réussie — détection d'une clé volée ou d'un accès inattendu. Script appelé par **pam_exec** après authentification.

## Problèmes identifiés sur le script initial (19/10/2026)

1. **Token obligatoire** : publication sur `vps-admin` sans token → **403** (testé). Le topic est protégé (serveur ntfy local `127.0.0.1:8080`). Le script initial envoyait donc en silence.
2. **`seteuid` casse l'accès au coffre** : avec `seteuid`, pam_exec exécute le script en tant qu'utilisateur SSH → ne peut pas lire le store pass d'opencode (mode 700). Sans `seteuid`, le script tourne en root, mais **root ne peut pas déchiffrer** le store GPG (clé dans le trousseau d'opencode) → token lu via `runuser -u opencode -- pass show opencode/ntfy-token`.
3. **URL** : utiliser `http://127.0.0.1:8080/vps-admin` (convention existante, cf. `ntfy-alert.sh`) plutôt que le public — pas de dépendance au reverse proxy/TLS.

## Mise en place

- Script : `/usr/local/sbin/ssh-notify.sh` (root:root, 700) — envoi en tâche de fond, `exit 0` systématique, ne ralentit jamais la connexion.
- PAM : ligne ajoutée dans `/etc/pam.d/sshd` (fin de section session, avant `@include common-password`) :
  ```
  session optional pam_exec.so quiet /usr/local/sbin/ssh-notify.sh
  ```
- `optional` + `quiet` : une panne du script ne bloque jamais une connexion.

## Script (résumé)

- Filtres : `PAM_TYPE=open_session` et `PAM_SERVICE=sshd`.
- Token lu **à l'exécution** via `runuser -u opencode -- /var/lib/opencode/.local/bin/pass show opencode/ntfy-token` — jamais en dur, jamais dans l'environnement.
- Message : utilisateur, IP source (`PAM_RHOST`), date, service — titre « Connexion SSH sur <hostname> », tag `ssh`.

## Tests

- Publication avec token → **200** (sans token → 403).
- Test manuel en conditions PAM (root) : OK.
- Test réel : connexion SSH → notification reçue ✅ (validé par GRK).

## Points d'attention

- **Bruit** : notification à **chaque** connexion (y compris scp/sftp, qui passent aussi par sshd). Intérêt principal : détecter une clé volée / accès inattendu. Évolution possible : filtrer par IP connue.
- **Échecs non couverts** : pam_exec en `session` ne se déclenche qu'après authentification réussie. Pour les tentatives échouées : watcher sur `/var/log/auth.log` ou fail2ban (complémentaire, moins critique car `PasswordAuthentication no`).
- Contexte SSH : `PermitRootLogin no`, `PubkeyAuthentication yes`, `PasswordAuthentication no`.

## Fichiers en place

| Fichier | Rôle |
|---|---|
| `/usr/local/sbin/ssh-notify.sh` | script d'alerte (root:root, 700) |
| `/etc/pam.d/sshd` | ligne `session optional pam_exec.so quiet /usr/local/sbin/ssh-notify.sh` |

## Filtre loopback anti-bruit (03/11/2026, option 2)

- **Constat** : chaque `git push` vers Forgejo (remote `forgejo@forgejo.datagrandest.net:222`, résolu en `::1`) ouvre une session PAM `sshd` pour user `forgejo` → 1 alerte ntfy par push (~15/jour), sans valeur de sécurité (source loopback, clé locale `/var/www/forgejo/.ssh/authorized_keys`). Traçabilité : 129 sessions `forgejo` dans `auth.log*`, **100 % depuis `::1`**.
- **Choix (GRK)** : option 2 — filtrer les connexions **loopback uniquement** ; les connexions externes restent notifiées avec l'utilisateur dans le message.
- **Patch** : garde insérée juste après le shebang de `/usr/local/sbin/ssh-notify.sh` :
  ```bash
  # Anti-bruit 2026-11-03 : aucune alerte pour les connexions loopback
  case "${PAM_RHOST:-}" in
    ""|127.0.0.1|::1|localhost) exit 0 ;;
  esac
  ```
- **Déploiement** : one-shot `~/.opencode/bin/oneshot-20261103-ssh-notify-antibruit.sh` (root, idempotent, `bash -n` + restauration auto si erreur, backup `.bak-20261103`).
- **Tests hors prod** (copie + `curl`/`runuser` stub) : `PAM_RHOST=::1` → 0 envoi ; `PAM_RHOST=203.0.113.7` → alerte complète envoyée.
- **Validation v1 (03/11)** : push réel 07:13:15 (session forgejo ::1) → aucune alerte ; dernière alerte légitime à 07:05:26 (Guacamole).
- **Affinage v2 (03/11, décision GRK)** : v1 silentait TOUTE la loopback, y compris web-SSH Guacamole (shizendo, ::1) — non voulu (une intrusion web-SSH arriverait masquée). v2 ne silencie que les pushes Forgejo (PAM_USER=forgejo + loopback) ; Guacamole et toutes IP externes restent notifiées. Déploiement : ~/.opencode/bin/oneshot-20261103-ssh-notify-cible-user.sh (backup .bak-20261103-v2).
- **À valider** : prochain push → plus d'alerte ; prochaine connexion SSH externe → alerte reçue.
