L'identité entre Fronds

Dans un seul process, un appel est un appel de fonction : rien ne traverse, rien n'est à prouver. Séparez une Frond et le même appel devient une charge utile — et une charge utile peut dire n'importe quoi. Cette page répond à ce qu'un récepteur établit au lieu d'accepter.

Loopback ou signé

Un récepteur refuse de démarrer quand il écoute au-delà du loopback sans moyen d'établir son appelant. Pas à chaque appel : au démarrage.

127.0.0.1                        démarre, rien à configurer
0.0.0.0 + FOUGERE_ROOT_KEY       démarre, vérifie chaque appel
0.0.0.0 sans racine              ne démarre pas, et dit quoi faire
0.0.0.0 + allowUnsigned: true    démarre — quelque chose en amont a déjà authentifié

L'adresse porte déjà la décision. Écouter au-delà du loopback est un acte délibéré — hosts est l'endroit où vous l'écrivez — et un récepteur atteignable depuis l'extérieur qui n'établit rien croit l'état qu'on lui tend. Refuser au démarrage plutôt qu'à chaque appel est tout l'intérêt : un récepteur qui démarre puis rejette tout se découvre en production, un récepteur qui ne démarre pas se découvre au déploiement.

Ce qui voyage

Chaque appel qui traverse un lien porte une enveloppe, signée par l'appelant :

{ "method": "post.list",
  "params": { "params": {}, "query": {}, "body": null,
              "identity": "eyJhbGciOiJFZERTQSIs…" } }

L'enveloppe porte l'état — qui est connecté, s'il y a quelqu'un — et le récepteur le prend de là plutôt que de la charge utile. Envoyer les deux laisserait chaque lecteur en aval choisir entre une preuve et une prétention sur la même chose.

Elle lie l'appel entier : entity, op, params et query par valeur, le corps par empreinte. Signer l'état seul prouverait qui sans prouver quoi, et une enveloppe capturée pourrait être rejouée contre une autre opération tant qu'elle reste valide — post.list renvoyé en post.delete, même signature, toujours bonne.

Deux signatures, une clé publique

GRANT      la racine dit « cette clé est blog »    — émis une fois, au déploiement
ENVELOPPE  blog dit « voici mon appel entier »     — signée à chaque appel

Un récepteur détient la clé publique de la racine et rien d'autre. Il admet une Frond qu'il n'a jamais vue, et une Frond dotée demain est admise par un récepteur déployé aujourd'hui sans qu'on touche à sa configuration. C'est la différence entre une autorité et une liste d'appelants connus : la liste doit être mise à jour partout, et elle se périme.

La vérification est hors ligne — aucun service n'est joint, ni à un appel, ni à un démarrage.

Émettre

fougere keys              # une fois par système — écrit .fougere/root.key, imprime FOUGERE_ROOT_KEY
fougere grant blog        # une fois par Frond qui APPELLE — imprime FOUGERE_KEY et FOUGERE_GRANT

fougere keys est une commande, pas un service : elle s'exécute au déploiement et sort. Rien ne reste vivant, rien n'est joint au démarrage. La clé privée de la racine est écrite dans .fougere/root.key (mode 600, ajoutée au .gitignore) et jamais imprimée : elle signe des grants, et un grant est la seule chose qui doive voyager.

fougere grant imprime et ne stocke rien. La relancer émet une nouvelle clé au lieu de montrer l'ancienne — c'est ce qu'est une rotation : relancez, redéployez cette Frond, et la configuration de personne d'autre ne bouge.

Ce qu'un déploiement injecte

Trois variables, aucune clé de configuration — une clé privée n'a pas sa place dans un fichier versionné. PEM ou base64, les deux sont lus.

Ce que c'est
FOUGERE_ROOT_KEYchaque Frond qui répondpublique — sans risque dans une image ou un manifeste
FOUGERE_KEYchaque Frond qui appellesecrète, montrée une fois
FOUGERE_GRANTà côtéla parole de la racine : cette clé est cette Frond

Une Frond peut détenir l'une des deux moitiés ou les deux : celle qui ne fait que répondre n'a pas de clé, celle qui ne fait qu'appeler ne fait confiance à aucune racine, celle du milieu fait les deux. Faire confiance à une racine c'est demander à refuser : il n'y a pas de second drapeau, parce qu'un déploiement qui nomme une autorité puis accepte des appels non signés a dit deux choses à la fois.

Ce qu'un handler voit

Rien de nouveau à écrire. L'état arrive là où il arrivait déjà, donc un collector qui lit ctx.state.user est inchangé — en process comme en séparé. Ce qui s'ajoute est un champ :

invocation.caller   // 'blog' — la Frond qui a signé, telle que la racine l'a nommée

Il est au premier niveau et non une clé de state, et la raison décide de comment le lire : state voyage non vérifié quand aucun vérificateur n'est câblé, donc un caller logé dedans serait forgeable exactement là où rien ne contrôle. Absent veut dire non établi, et il n'existe aucune orthographe pour une prétention. Un émetteur le purge à chaque saut, si bien qu'il nomme le dernier : shop → catalog → billing fait lire catalog à billing.

Ce que ceci ne fait pas

L'autorisation. Une signature dit qui appelle, jamais ce qu'il a le droit d'appeler. if (user.role !== 'admin') throw reste à votre charge.

Le travail d'un mesh. Sous un service mesh, le sidecar a terminé le mTLS avant que votre process ne voie la requête, et demander une seconde signature referait ce qui vient d'être fait un centimètre plus loin. C'est à ça que sert allowUnsigned: true — écrit séparément de tout le reste, pour qu'il ne puisse jamais vouloir dire autre chose que « j'y ai réfléchi ».

Les corps entre langages. Le corps est lié par une empreinte de son texte JSON, seul endroit où l'ordre des clés compte. Un émetteur écrit dans un autre langage doit émettre le corps comme il l'a haché. Tout le reste est lié par valeur, ce qui maintient une dépendance à du JSON canonique hors du fil.

Le navigateur n'est pas dans cette histoire

Un navigateur est en dehors de la topologie et ne détient aucune clé. Il ne signe jamais, et rien de ce qu'il envoie n'est pris pour une identité : l'hôte résout la session côté serveur depuis le cookie de la requête, et le state de la charge utile est jeté. C'était déjà vrai et rien n'a changé — cette page parle du lien entre deux process.

Construit avec Fougere — ce site tourne sur le framework qu'il documente.