rescriptum · an answer written for this machine
FR
Sur cette page

L’API d’administration

Avec RESCRIPTUM_STORE=sqlite, les réponses peuvent être gérées en HTTP plutôt qu’en éditant des fichiers. Elle est désactivée sauf si vous la configurez, et elle tourne sur son propre listener.

$ export RESCRIPTUM_STORE=sqlite RESCRIPTUM_DB_PATH=/srv/answers.db
$ export RESCRIPTUM_ADMIN_ADDR=127.0.0.1:8001
$ export RESCRIPTUM_ADMIN_TOKEN=$(openssl rand -hex 24)
$ rescriptum
2026-08-24T08:52:30Z - admin API listening on 127.0.0.1:8001
2026-08-24T08:52:30Z - rescriptum 0.1.0 listening on 0.0.0.0:8000 — store=sqlite:/srv/answers.db …

Trois propriétés porteuses

1. Son propre listener. L’endpoint de réponse n’est pas authentifié par nécessité — l’installateur n’a aucun identifiant à offrir. Cette API décide du mot de passe root et des clés SSH de chaque machine installée ensuite. Elle ne partage jamais ce port.

2. SQLite uniquement. Au-dessus d’un répertoire de fichiers il y aurait deux façons de changer la même configuration, à la main et par le réseau, en concurrence.

3. Une écriture ne peut jamais laisser le jeu de réponses cassé. Chaque écriture prend un instantané des problèmes courants, s’applique, puis compare. Tout ce qui est nouvellement cassé est annulé et répondu 409.

Le serveur refuse de démarrer — en erreur, pas en avertissement — si vous pointez l’API d’administration sur le store fichiers, omettez le jeton, ou définissez un jeton de moins de 16 caractères.

Endpoints

RequêteRôle
GET /machines, GET /groupslister les identifiants
GET /machines/{id}, GET /groups/{name}, GET /defaultle document stocké, tel qu’écrit — commentaires et mise en forme intacts
PUT /machines/{id}, PUT /groups/{name}, PUT /defaultstocker un document (le corps est le document)
DELETE /machines/{id}, DELETE /groups/{name}, DELETE /defaulten supprimer un
GET /resolve/{id}la réponse fusionnée que cette machine recevrait
GET /checkles problèmes actuels, le même jeu que la sous-commande check
GET /healthvivacité — le seul endpoint sans jeton, et jamais bloqué

Chaque endpoint nommant un document prend ?format= — l’extension du document. Il vaut toml par défaut, ce que ce serveur servait à ses débuts :

$ curl -H "$AUTH" -X PUT --data-binary @base.preseed \
    'http://127.0.0.1:8001/groups/base?format=preseed'

Comme la clé d’un document est (identifiant, format), un identifiant apparaît dans GET /machines une fois par format dans lequel il existe — une machine qui est à la fois un nœud Proxmox et un nœud Debian est listée deux fois.

Exemples

$ AUTH="Authorization: Bearer $RESCRIPTUM_ADMIN_TOKEN"

$ curl -s -H "$AUTH" http://127.0.0.1:8001/groups
{"group":["base","example-rack","rhel-compute","ubuntu-web"]}

$ curl -s -H "$AUTH" -X PUT --data-binary @rack-a.toml \
    http://127.0.0.1:8001/groups/rack-a
{"status":"stored","problems":[]}

$ curl -s -H "$AUTH" http://127.0.0.1:8001/resolve/98:fa:9b:50:d8:10
[global]
country = "fr"
keyboard = "fr"

GET /resolve renvoie aussi l’en-tête de réponse X-Answer-Source, portant la même description que la ligne de log :

x-answer-source: format=toml machine=98fa9b50d810 group=example-rack

Répéter une vraie requête

GET /resolve accepte les mêmes étiquettes que porterait une vraie requête, ce qui permet de répéter une URL particulière — la différence entre /user-data et /meta-data, par exemple :

$ curl -s -H "$AUTH" 'http://127.0.0.1:8001/resolve?path=/rhel/ks&serial=7ABC123'

Quand une query string est présente, l’identifiant du chemin est ignoré — les faits viennent de la query seule. Donc GET /resolve/98:fa:9b:50:d8:10?format=toml ne résout rien, parce que format=toml n’est pas une identité. Utilisez la forme sans query, ou mettez l’identité dans la query : ?mac=98:fa:9b:50:d8:10.

Elle ne vous laissera pas casser le parc

Chaque écriture est vérifiée après application. Si elle a introduit un problème — un cycle entre groupes, un document référençant un groupe qui n’existe plus — l’écriture est annulée et vous recevez un 409 disant ce que vous avez cassé :

$ curl -s -H "$AUTH" -X DELETE 'http://127.0.0.1:8001/groups/base?format=preseed'
{"error":"refused: this would break the answer set (rolled back)",
 "problems":["machine \"98fa9b50d810\": extends unknown group \"base\""]}

Deux choses découlent de ce fonctionnement :

  • Une écriture réussie signale quand même les problèmes préexistants, dans le tableau problems. Une réponse propre n’implique jamais que tout le jeu est sain — seulement que vous ne l’avez pas aggravé.
  • C’est pourquoi un extends de machine pointant sur un groupe manquant est détecté au chargement plutôt qu’au moment où cette machine demande. Le garde-fou ne peut attraper que ce que le rapport de problèmes connaît.

Les documents malformés sont également refusés à l’écriture, plutôt que de devenir un 500 la prochaine fois qu’une machine les demande :

$ curl -s -H "$AUTH" -X PUT --data-binary 'x = = 1' http://127.0.0.1:8001/machines/aa-bb-cc-dd-ee-01
{"error":"document: invalid TOML: TOML parse error at line 1, column 5 …"}

Identifiants

Lettres, chiffres et - _ . : uniquement. Ils deviennent des noms de répertoires sous export et dans le store fichiers, donc tout ce qui pourrait traverser un répertoire est rejeté — à la frontière de l’API et dans les deux stores. groups et default sont réservés comme identifiants de machine pour la même raison : ce sont les répertoires que l’agencement garde pour lui, et une base qui en accepterait un exporterait vers un répertoire incapable de le contenir.

Codes de statut

CodeSignifie
200fait
400document malformé, identifiant invalide, ou corps non-UTF-8
401jeton manquant ou faux
404document ou endpoint inexistant ; rien ne se résout pour cet identifiant
409l’écriture aurait cassé le jeu de réponses (annulée), ou un resolve qui n’a pas pu rendre
413document de plus de 256 Ko
429cette adresse est bloquée après des échecs d’authentification répétés
500le store n’a pas pu être lu ou écrit

Veiller sur le jeton

Le jeton constitue toute l’authentification, et ce qu’il protège mérite d’être dit franchement : les documents de réponse portent root-password-hashed et root-ssh-keys, donc quiconque peut écrire sur cette API décide des identifiants root de chaque machine que vous installerez ensuite.

Générez-en un vrai — pas un mot auquel vous avez pensé :

$ openssl rand -hex 24        # ou : head -c 24 /dev/urandom | base64

Ne le mettez pas sur une ligne de commande. Tout ce qui se trouve dans les arguments d’un processus est visible par tous les autres utilisateurs via ps, ce qui inclut le mettre directement dans une tâche planifiée DSM. Gardez-le dans un fichier réservé à root et sourcez-le — voir Sécurité.

Ce que le serveur fait de son côté :

  • Compare le jeton en temps constant, pour qu’il ne puisse pas être récupéré octet par octet par qui chronomètre les réponses.
  • Exclut une adresse qui insiste. Cinq échecs en une minute valent un blocage, doublant à chaque récidive jusqu’à un maximum de quinze minutes, et chaque tentative est journalisée. Le blocage s’applique aussi à un jeton correct venant de cette adresse — sinon deviner jusqu’à tomber juste ne coûterait rien.
  • Borne sa propre comptabilité à 4096 adresses suivies, pour que le garde-fou ne puisse pas lui-même être transformé en fuite de mémoire.
  • Laisse GET /health non authentifié et non bloqué, pour que la supervision ne s’éteigne pas pendant une attaque.
2026-08-24T08:52:32Z - admin: 10.0.0.9 failed authentication 5 times — blocked for 60s

Deux limites à prévoir

  • Elle parle HTTP en clair, donc le jeton traverse le réseau en clair. Sur la boucle locale c’est sans objet. Ailleurs, mettez un reverse proxy terminant TLS devant.
  • Le blocage par adresse n’arrête pas un attaquant disposant de nombreuses adresses. C’est la longueur du jeton qui rend la devinette sans espoir — d’où le plancher de 16 caractères au démarrage.

Binder au-delà de la boucle locale est votre choix, et le serveur le dit dans le log quand vous le faites :

2026-08-24T08:52:30Z - warning: the admin API is not bound to loopback — it rewrites what gets installed on every machine, so restrict it to a management network

127.0.0.1 plus un tunnel SSH est le défaut sûr.

Voir aussi

Mis à jour le Modifier cette page