Set-OPS-Public/docs/acces-administration.md
Daniel Allaire be8c3917e5 acces d'administration : un tunnel par locataire, declare par lui, borne a lui
plan/acces.yml chez le locataire (cles publiques), reseau/port/instance derives de l'index.
La garde du devis refuse qu'un tunnel de locataire vise autre chose que son supernet : sans
elle, un ecosysteme s'ouvrirait un acces chez un voisin depuis son propre plan.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 16:01:54 -04:00

117 lines
6.5 KiB
Markdown

# L'accès d'administration — un tunnel nominatif
> **Pour qui :** celui qui administre le site et ses écosystèmes, et celui qui se demande
> par où un humain entre, avec quelle clé, et ce qu'il peut atteindre une fois entré.
>
> Décidé le 2026-09-17, en réponse à une question : *le runner ne devrait-il pas être le
> rebond SSH des admins ?*
## Pourquoi pas le runner
Le runner du site détient **la clé de la voûte du site** et les clés SSH qui configurent
toutes les machines. En faire la porte des humains réunirait deux pouvoirs que tout le reste
du dépôt sépare :
- une session humaine compromise (un agent SSH transféré de trop) deviendrait le **plan de
contrôle** — pas seulement un rebond ;
- il **ne doit jamais entrer chez un locataire** (charte des responsabilités) ; en faire le
passage obligé des admins créerait ce chemin en fait ;
- il est **reconstructible par le code**, et c'est sa vertu. Un point d'entrée doit survivre à
la reconstruction de ce qu'il sert ;
- « qui est entré » et « qu'est-ce qui a été déployé » cesseraient de se raconter séparément.
## Ce qui a été construit
Un **second** tunnel WireGuard sur la frontière — l'instance `admins`, port 51821 — à côté du
tunnel site-à-site vers le site pair (instance `chezlePro`, port 51820), jamais mêlé à lui.
| | |
|---|---|
| déclaré | `SITE-<nom>/plan/10-intrants.yml`, clé `acces_admin_vpn` |
| réconcilié | `scripts/vpn_admin.py` (`make vpn-admin-plan`, `make vpn-admin-appliquer`) |
| réseau | `10.37.29.0/24`, la frontière en `.1` |
| un pair | **une personne ET un appareil** — révoquer l'appareil perdu ne coupe pas les autres |
| clés | seule la clé **publique** est au plan ; la privée ne quitte jamais l'appareil |
**Le réseau du tunnel est un réseau d'administration, et tout en dérive** : les pare-feux des
machines du site (`resoudre_flux`), le contrat vers les locataires (`site_intrants`, donc les
pare-feux de leurs machines) et les règles de la frontière (`devis_opnsense`). Rien à recopier
— une liste recopiée prend toujours du retard sur celle qu'elle suit.
## Ajouter un appareil
```bash
python3 scripts/vpn_admin.py pair-nouveau --nom prenom-appareil # tire la paire de clés
# coller le bloc `pairs:` rendu dans SITE-<nom>/plan/10-intrants.yml
make vpn-admin-plan # lire ce qui changerait
CONFIRMER=true make vpn-admin-appliquer # poser le pair
python3 scripts/vpn_admin.py config --nom prenom-appareil # la config de l'appareil
make flux && make deployer-groupe GROUPE=serveur_durci # les pare-feux des machines
```
La clé privée ne s'affiche **qu'une fois**, au moment où elle est tirée : elle appartient à
l'appareil. Perdue, on en tire une autre ; l'ancienne se révoque par `etat: absent`.
## Retirer un accès
`etat: absent` sur le pair, puis `CONFIRMER=true make vpn-admin-appliquer`. Le script retire
aussi tout pair **attaché à notre instance que le plan ne déclare plus** : un accès qui
survivrait à la décision de le retirer est exactement ce qu'on ne veut pas.
## Deux trous que seul l'usage a montrés (2026-09-17, une heure après la pose)
- **Le SSH des machines du site.** Le socle déclare son 22 en `pair: [flotte, externe]`, jamais
`admin` : la source dérivée est le site lui-même. L'exploitant y arrivait **par rebond sur la
frontière** — la connexion partait alors du boîtier, que pf laisse sortir sans règle. Par le
tunnel, il arrive comme une source extérieure, et plus rien ne l'autorisait. Les locataires,
eux, marchaient : leur règle SSH naît de `nftables_admin_ssh`, qui contient déjà le tunnel.
- **La frontière elle-même.** Aucun flux ne la désignait comme destination d'administration :
monter le tunnel faisait perdre sa console et son API — donc le moyen même de réparer la
règle manquante. *La panne se referme sur celui qui la répare* : il a fallu remettre
l'adresse d'avant pour poser le correctif.
Les deux règles sont désormais émises par `devis_opnsense` dès que `acces_admin_vpn` est
déclaré (`ports_frontiere`, par défaut 22 et 443). Une règle **par port** : OPNsense refuse
« 22,443 » dans un champ de port, et aucun flux jusque-là n'en portait deux.
## Un tunnel par locataire (2026-09-17)
Chaque écosystème a le sien, et **c'est lui qui décide qui entre chez lui**. Le site porte la
route, jamais la liste des gens — même partage que les zones DNS publiques.
| | |
|---|---|
| déclaré par | le **locataire**, dans `plan/acces.yml` (registre `acces_admin_vpn`) |
| réseau | `10.<index>.29.0/24` — **dérivé** de son index, hors de ses six zones (.16 à .21) |
| port | `52000 + index` — dérivé, donc sans collision : P21 garde déjà l'unicité des index |
| instance | `wg<index>` — un rang (2, 3, 4…) renommerait les interfaces le jour où un écosystème part |
| ce qu'il atteint | **son supernet, et rien d'autre** : SSH et consoles web |
**L'isolement est gardé, pas promis.** `devis_opnsense.verifier` refuse toute règle dont la
source est le tunnel d'un locataire et la destination autre chose que **son** supernet —
sinon, un écosystème obtiendrait un accès chez un voisin en ajoutant un pair dans son propre
plan. La garde est exécutée par P24, avant toute écriture. Mise en défaut vérifiée.
**Le pare-feu de ses machines suit tout seul** : `resoudre_flux` ajoute le réseau du tunnel aux
sources d'administration **dérivées de son plan**, et seulement s'il déclare au moins un pair
actif — déclarer un réseau que personne n'emprunte ouvrirait le SSH à un tunnel qui n'existe
pas.
```bash
python3 scripts/vpn_admin.py pair-nouveau --nom prenom-appareil --locataire OPS-<X>
# coller le bloc rendu dans OPS-<X>/plan/acces.yml
make vpn-admin-plan && CONFIRMER=true make vpn-admin-appliquer
python3 scripts/vpn_admin.py config --nom prenom-appareil --locataire OPS-<X>
```
## Ce que le tunnel donne, et ce qu'il ne donne pas
`AllowedIPs` est **dérivé** de la carte : zones du site, fabric, lien de transit et supernets
des locataires. Ce que la frontière laisse ensuite passer reste décidé par les flux déclarés —
SSH partout, et les consoles d'administration (Grafana, Icinga Web, la forge, le runner).
**Le seul port ouvert sur l'Internet par cette voie est le 51821/udp**, vers l'adresse publique
de la frontière. Tout le reste voyage dans le tunnel.
**Ce qui n'est pas fait** : l'enregistrement des sessions (un rebond dédié le permettrait, pas
un tunnel), et la double authentification — la possession de l'appareil fait foi.