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.
| Où | Ce que c'est | |
|---|---|---|
FOUGERE_ROOT_KEY | chaque Frond qui répond | publique — sans risque dans une image ou un manifeste |
FOUGERE_KEY | chaque Frond qui appelle | secrè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.