L'arbre source des politiques, fichier par fichier
Écrire et exploiter les politiques explique
comment penser un bundle de politiques. Cette page est la référence : chaque
fichier que lit le plan de contrôle, chaque champ qu’il accepte, chaque valeur
qu’admet une énumération, et ce que fait obsign-control compile quand le
champ est mauvais.
La règle qui gouverne toute cette organisation : tout ce qui décide d’une issue est signé, et tout ce qui est signé est revu sous forme de diff. C’est pourquoi il n’y a ici aucun champ qu’un serveur en fonctionnement puisse changer, et pourquoi une faute de frappe est une erreur de compilation en CI plutôt qu’une surprise sur une passerelle.
Conventions
Section intitulée « Conventions »- Obligatoire — la compilation échoue si le champ est absent.
- Défaut — la valeur utilisée quand le champ est absent. Un champ doté d’une valeur par défaut peut être omis sans risque ; c’est cette valeur que la signature couvrira.
- Refusé à la compilation signifie que
obsign-control compilesort avec un code non nul et affiche le chemin du fichier. Rien n’est écrit, rien n’est publié.
Les champs inconnus sont ignorés, à une exception près. L’analyse JSON est
tolérante partout sauf dans deployment/origin-keys.json, qui rejette
purement et simplement les champs inconnus. Ainsi "destructve": true dans
tools.json compile sans broncher et l’outil n’est pas destructeur. Revoyez
les diffs de ces fichiers comme vous revoyez du code, et préférez recopier les
exemples ci-dessous plutôt que d’écrire de mémoire.
En un coup d’œil
Section intitulée « En un coup d’œil »| Chemin | Obligatoire | Analysé comme | Compilé dans | Absent signifie |
|---|---|---|---|---|
policies/*.cedar | oui | source Cedar | policy-bundle.json | refusé — aucun jeu de politiques du tout |
tools.json | oui | tableau d’objets outil | policy-bundle.json | refusé — chaque outil serait refusé |
fail-mode.json | non | objet de mode de défaillance | policy-bundle.json | {"default":"closed"} |
identity/provider.json | avec identity/ | objet fournisseur | identity-bundle.json | pas de bundle d’identité : la passerelle démarre en mode déclaré et le dit |
identity/jwks.json | avec identity/ | JWKS | identity-bundle.json | idem |
deployment/origin-keys.json | avec deployment/ | tableau d’entrées de clé | deployment-bundle.json | aucune passerelle enrôlée |
deployment/attestation.json | non | tableau d’attestations | deployment-bundle.json | pas d’enrôlement TPM (v3 inutilisée) |
identity/ et deployment/ fonctionnent en tout ou rien : créez le
répertoire et son fichier obligatoire le devient aussi : une demi-configuration
d’identité ne vérifie rien, et un deployment/ vide mérite un fichier
explicite plutôt qu’un ensemble vide silencieux.
Tout autre fichier de l’arbre (un README.md, une configuration de CI, une
fixture de test) n’est jamais lu. Seuls les sept chemins ci-dessus atteignent
un bundle.
1. policies/*.cedar
Section intitulée « 1. policies/*.cedar »| Règle | Détail |
|---|---|
| Quels fichiers | *.cedar, fichiers réguliers, directement dans policies/. Les sous-répertoires ne sont pas parcourus — policies/finance/10-x.cedar est silencieusement invisible. |
| Ordre | Triés par nom de fichier, dans l’ordre lexicographique (ordre des octets), puis concaténés. 00-base.cedar avant 10-finance.cedar ; 9-x.cedar après 10-x.cedar — complétez vos préfixes par des zéros. |
| Provenance | Chaque fichier est préfixé dans le bundle par un commentaire // ─── <name> ───, si bien qu’une règle de l’artefact compilé peut être retracée jusqu’à son fichier. |
| Minimum | Au moins un fichier .cedar. Un jeu de politiques vide refuse tout, ce qui mérite un fichier explicite qui le dise. |
@id | Obligatoire sur chaque règle, et unique sur toute la concaténation. L’identifiant est le motif d’audit. |
Il n’y a aucun fichier de schéma à écrire à la main : le modèle d’entités
(User, Group, Tool, Resource, Prompt, Server, les six actions et
les attributs de contexte) est fixé par la passerelle, documenté dans
le modèle que voient vos règles,
et généré depuis votre propre tools.json par obsign-control schema.
compile vérifie le typage de chaque règle contre ce modèle et refuse de
signer une règle qui lit un attribut que la passerelle n’expose pas.
2. tools.json — le catalogue
Section intitulée « 2. tools.json — le catalogue »Le niveau supérieur est un tableau JSON, pas un objet. Un outil qui en est absent est refusé avant que Cedar ne s’exécute.
[ { "name": "delete_production_db", "server": "mcp://db", "destructive": true, "required_scope": "db:admin" }]| Champ | Type | Obligatoire | Défaut | Signification |
|---|---|---|---|---|
name | chaîne | oui | — | Le nom de l’outil MCP, comparé exactement à celui de tools/call. Également la ressource Cedar : Tool::"delete_production_db". Doit être non vide et unique dans le fichier. |
server | chaîne | oui | — | Quel serveur sous-jacent possède l’outil. Exposé à Cedar sous la forme resource.server. Libre ; la convention est d’utiliser la même URI que celle avec laquelle la passerelle est démarrée (--server-id mcp://db). Purement descriptif — aucun verdict n’est indexé dessus. |
destructive | bool | non | false | Votre jugement : un effet qui ne peut pas être annulé (suppression, virement, envoi externe). Exposé sous la forme resource.destructive. Marquer un outil comme destructeur ne coûte rien et permet à une seule règle de protéger tous les outils dangereux, y compris ceux ajoutés plus tard. |
required_scope | chaîne | non | absent | Scope de délégation dont l’outil a besoin. Exposé sous la forme resource.required_scope, comme la chaîne vide en son absence — Cedar n’a pas d’optionnel ergonomique, écrivez donc resource.required_scope != "" plutôt qu’un test de nullité. Le déclarer ne l’applique pas : il faut qu’une règle le lise (voir allow_scoped dans Écrire des règles). |
policy_args | tableau | non | [] | Les arguments que la politique peut lire. Toute liste non vide fait passer le bundle en obsign-policy/2 — voir l’avertissement sur l’ordre plus bas. |
Refusé à la compilation : un name vide ; le même name deux fois (le
moteur indexe par nom, un doublon masquerait donc une définition et laquelle
l’emporte dépendrait de l’ordre des fichiers) ; du JSON malformé.
policy_args[] — la liste d’autorisation des arguments
Section intitulée « policy_args[] — la liste d’autorisation des arguments »Cette liste est une frontière de confidentialité : un argument qui n’y est
pas déclaré n’atteint jamais le moteur, et le journal d’audit conserve
args_hash, jamais les valeurs.
{ "name": "send_message", "server": "mcp://chat", "policy_args": [ { "name": "channel", "kind": "string" }, { "name": "amount_cents", "kind": "long", "default": 0 }, { "name": "labels", "kind": "string_set", "default": [] }, { "name": "target", "kind": "string", "at": "/recipient/id" } ]}| Champ | Type | Obligatoire | Défaut | Signification |
|---|---|---|---|---|
name | chaîne | oui | — | La clé sous context.args. Non vide, unique au sein de l’outil. |
kind | énumération | oui | — | string | long | bool | string_set. Voir la table de coercition. |
at | chaîne | non | /<name> | Pointeur JSON RFC 6901 dans l’objet arguments de l’appel, pour une valeur imbriquée ou nommée autrement. Doit commencer par /. Lorsqu’il est dérivé de name, le nom est échappé (~→~0, /→~1), si bien qu’un argument littéralement nommé path/glob lit la clé path/glob et non arguments.path.glob. |
default | quelconque | non | absent | Injecté quand l’appel omet l’argument. Doit lui-même se coercer vers kind. Un argument sans valeur par défaut est obligatoire : un appel qui l’omet est refusé avant que Cedar ne s’exécute, avec un message explicite. |
Un null JSON explicite dans l’appel compte comme absent : les SDK clients
MCP sérialisent couramment un champ optionnel omis sous la forme null, et le
traiter comme présent refuserait un appel que l’omission complète de la clé
aurait autorisé.
Les valeurs de kind
Section intitulée « Les valeurs de kind »kind | JSON accepté | Type Cedar | Refusé |
|---|---|---|---|
string | chaîne, ≤ 4096 octets | String | tout ce qui n’est pas une chaîne ; une chaîne plus longue (les arguments pertinents pour une politique sont des identifiants, pas des charges utiles) |
long | nombre entier, plage i64 | Long | tout flottant — même 2.0 — et tout entier hors plage. Jamais arrondi : un contrôle de montant qui arrondit est un contrôle de montant troué. Les règles monétaires déclarent des unités mineures (centimes). |
bool | true / false | Bool | les chaînes "true" / "false", 0, 1 |
string_set | tableau de chaînes, ≤ 64 éléments, chacun ≤ 4096 octets | Set of String | ce qui n’est pas un tableau ; un tableau contenant autre chose que des chaînes |
| Limite | Valeur |
|---|---|
| Arguments déclarés par outil | 16 |
| Longueur de chaîne (et de chaque élément d’ensemble) | 4096 octets |
| Éléments d’un ensemble | 64 |
Refusé à la compilation : policy_args sur un bundle qui n’est pas
obsign-policy/2 (impossible depuis un arbre source, le plan de contrôle
choisit le format automatiquement) ; plus de 16 arguments ; un nom d’argument
vide ou en double ; un at qui ne commence pas par / ; un default qui ne
se coerce pas vers son kind.
Le contrôle à blanc
Section intitulée « Le contrôle à blanc »Dès qu’un outil déclare policy_args, la compilation évalue en plus chaque
règle une fois par outil contre une requête synthétique construite à partir de
vos valeurs par défaut déclarées (ou de la valeur zéro : "", 0, false,
[]). Un context.args.chanel mal orthographié échoue donc en CI, en nommant
la règle, au lieu de refaire surface des mois plus tard comme un événement de
mode de défaillance sur une passerelle en production.
Les arbres qui ne déclarent aucun argument continuent de compiler sans ce contrôle : bloquer leur publication sur une entrée synthétique reviendrait à passer outre le choix de mode de défaillance qu’ils ont déjà fait.
Avertissement sur l’ordre
Section intitulée « Avertissement sur l’ordre »Le plan de contrôle émet obsign-policy/2 dès qu’un outil déclare
policy_args, et une passerelle non mise à niveau refuse un bundle /2 au
démarrage plutôt que d’appliquer en silence moins que ce que le bundle
prescrit. Mettez d’abord à niveau toutes les images de passerelle, publiez
ensuite le bundle qui déclare des arguments.
3. fail-mode.json
Section intitulée « 3. fail-mode.json »Ce qui se passe quand le moteur ne peut pas décider, qu’il s’agisse d’un bundle qu’il ne sait pas lire ou d’une règle qui lève une erreur d’évaluation. Ce n’est pas le chemin du refus : une règle qui refuse est une décision, et le mode de défaillance ne s’y applique jamais.
{ "default": "closed", "tools": { "search_docs": "open", "list_tickets": "open" }}| Champ | Type | Obligatoire | Défaut | Signification |
|---|---|---|---|---|
default | énumération | oui, si le fichier existe | — | Comportement pour tout ce qui n’a pas de surcharge. |
tools | objet | non | {} | Surcharges par outil : nom d’outil → comportement. |
Omettre le fichier entier donne {"default": "closed", "tools": {}}. Mais un
fichier contenant {} est refusé. default n’a pas de valeur par défaut
à l’intérieur du fichier, et c’est délibéré : écrire le fichier, c’est
déclarer une position.
Les valeurs de default / tools.*
Section intitulée « Les valeurs de default / tools.* »| Valeur | Comportement | Enregistré comme |
|---|---|---|
closed | Refuser en cas de doute. | Deny, motif evaluation failed, fail-closed: … |
open | Laisser passer l’appel. | AllowFailOpen — jamais un Allow franc. La dégradation reste visible à la relecture du journal deux ans plus tard. |
Par outil, parce qu’il n’existe pas de bonne réponse universelle : bloquer une recherche en lecture seule casse la production pour rien, laisser passer une suppression est indéfendable.
Refusé à la compilation : une surcharge nommant un outil qui n’est pas dans
tools.json. Une faute de frappe n’échouerait pas là, elle appliquerait
silencieusement la valeur par défaut à l’outil que l’auteur voulait
surcharger : la pire issue possible pour une déclaration en défaillance
ouverte.
Les surcharges ne valent que pour les outils du catalogue. Les actions de
capacité (resource_read, prompt_get, sampling, elicitation, notify)
sont indexées par nom d’action, et un nom d’action n’est pas un nom d’outil :
la compilation le refuse donc comme outil inconnu. Ces actions suivent par
conséquent toujours default. Gardez default: "closed" sauf si vous avez
décidé autrement pour toute la surface.
4. identity/provider.json
Section intitulée « 4. identity/provider.json »Qui peut forger des identités, et où les lire à l’intérieur du jeton. Ce
fichier plus jwks.json devient le bundle d’identité signé, signé parce que
quiconque peut l’écrire peut se forger une identité, et parce que déplacer un
chemin de claim change quels groupes sont attribués, donc quelles règles Cedar
s’appliquent.
{ "issuer": "https://idp.example.com/realms/corp", "audience": "obsign-gateway"}| Champ | Type | Obligatoire | Défaut | Signification |
|---|---|---|---|---|
issuer | chaîne | oui | — | Claim iss attendu, comparé exactement. Doit être non vide. |
audience | chaîne | oui | — | Claim aud attendu. Doit être non vide — une audience vide accepterait des jetons forgés pour n’importe quel autre service. |
claims | objet | non | les valeurs par défaut ci-dessous | Table de claims. Tout ou rien, voir l’avertissement. |
Piège Keycloak. Les jetons d’accès Keycloak portent aud: "account" par
défaut. Configurez un mapper d’audience dans le realm pour qu’il émette
l’audience de votre passerelle. N’assouplissez jamais le contrôle de ce côté-ci.
claims — la table de claims
Section intitulée « claims — la table de claims »Les valeurs par défaut couvrent Keycloak, Entra ID et Okta sans aucune
configuration. Omettez complètement claims à moins que votre IdP ne place
quelque chose ailleurs.
claims est en tout ou rien pour ses quatre premiers champs. subject,
scopes, groups et client_id n’ont pas de valeur par défaut individuelle
à l’intérieur de l’objet : dès l’instant où vous écrivez "claims": { … },
les quatre doivent être présents ou le fichier est refusé. Copiez le bloc par
défaut complet ci-dessous et éditez-le, plutôt que d’écrire une surcharge
partielle. labels et machine, eux, ont bien une valeur par défaut
individuelle.
{ "issuer": "https://idp.example.com/realms/corp", "audience": "obsign-gateway", "claims": { "subject": "/sub", "scopes": ["/scope", "/scp"], "groups": ["/groups", "/roles", "/realm_access/roles", "/resource_access/*/roles"], "client_id": ["/client_id", "/azp"], "labels": ["/preferred_username", "/email", "/name"], "machine": { "subject_is_client": true, "equals": [{ "path": "/idtyp", "value": "app" }], "prefixes": [{ "path": "/preferred_username", "value": "service-account-" }] } }}| Champ | Type | Obligatoire dans claims | Défaut | Résolution |
|---|---|---|---|---|
subject | chaîne (un chemin) | oui | /sub | L’identifiant stable. Devient User::"<subject>" dans Cedar et l’identité dans chaque enregistrement d’audit. |
scopes | tableau de chemins | oui | ["/scope", "/scp"] | Le premier chemin qui répond l’emporte. Les scopes ne s’accumulent pas d’une représentation à l’autre : scope et scp sont deux orthographes d’une même chose, les fusionner produirait des doublons trompeurs. Parvient à Cedar sous la forme context.scopes. |
groups | tableau de chemins | oui | ["/groups", "/roles", "/realm_access/roles", "/resource_access/*/roles"] | Union de tous les chemins, triée et dédoublonnée : un utilisateur peut légitimement porter à la fois des groupes d’annuaire et des rôles applicatifs. Chacun devient un parent Group::"<name>" du principal, si bien que principal in Group::"dba" fonctionne. |
client_id | tableau de chemins | oui | ["/client_id", "/azp"] | Premier chemin qui répond. Utilisé par le marqueur machine subject_is_client. |
labels | tableau de chemins | non | ["/preferred_username", "/email", "/name"] | Nom d’affichage lisible par un humain, la première réponse non vide l’emporte. Enregistré à côté du sujet, jamais à sa place — un nom d’affichage peut être renommé, une piste d’audit a besoin de l’identifiant qui ne le peut pas. Le claim d’où il vient est enregistré avec lui. |
machine | objet | non | le bloc ci-dessus | Ce qui marque un jeton comme celui d’une machine. Voir plus bas. |
Les valeurs sont lues avec souplesse à la feuille : une chaîne séparée par
des espaces ("scope": "a b c") et un tableau ("scp": ["a","b"]) se
ramènent tous deux à la même liste, récursivement.
Syntaxe des chemins
Section intitulée « Syntaxe des chemins »Pas tout à fait RFC 6901 : des segments séparés par /, plus * signifiant
chaque enfant d’un objet ou d’un tableau.
| Chemin | Lit |
|---|---|
/sub | le sub de premier niveau |
/realm_access/roles | imbriqué — les rôles de realm Keycloak |
/resource_access/*/roles | les roles de chaque client — les rôles client de Keycloak, dont le segment intermédiaire dépend du client et ne peut pas être écrit en dur |
/ext/*/login | n’importe quelle imbrication d’un niveau |
Il n’y a aucun échappement ~0/~1 ici (contrairement à
policy_args.at) : un claim dont le nom contient littéralement / n’est pas
adressable. Un chemin qui ne résout rien n’est pas une erreur : il ne répond
simplement pas, et le chemin suivant est essayé.
machine — les marqueurs machine
Section intitulée « machine — les marqueurs machine »Ce sont eux qui décident du PrincipalKind, donc des règles Cedar qui
s’appliquent, et c’est pourquoi ils voyagent à l’intérieur du bundle signé
plutôt que comme une simple option de fichier. Chaque marqueur ne fait
qu’ajouter un verdict Machine, si bien qu’élargir ce bloc ne peut jamais
rétrograder un vrai humain en robot, seulement l’inverse, qui est le sens sûr.
| Champ | Type | Obligatoire | Défaut | Signification |
|---|---|---|---|---|
subject_is_client | bool | non | true | sub == client_id : la forme client_credentials de manuel. Mettez false si votre IdP réutilise azp == sub pour des connexions humaines de première partie. |
equals | tableau de {path, value} | non | [{"path":"/idtyp","value":"app"}] | Un claim égale exactement une valeur. La valeur par défaut est le marqueur « application seule » d’Entra ID. |
prefixes | tableau de {path, value} | non | [{"path":"/preferred_username","value":"service-account-"}] | Un claim commence par un préfixe. La valeur par défaut est le préfixe de connexion réservé aux comptes de service de Keycloak, qu’aucun humain ne porte. |
path utilise la même syntaxe que plus haut, joker compris. value est
comparé comme une simple chaîne.
Pourquoi trois marqueurs et pas seulement le premier : un vrai jeton
client_credentials Keycloak ou Entra a un sub réglé sur l’identifiant
propre du principal de service, distinct de l’identifiant de client. À lui
seul, sub == client_id laisserait un robot sans clé se classer comme Human
et satisfaire une règle « exige un humain ».
La classification obtenue :
context.principal_kind | Quand |
|---|---|
machine | un marqueur quelconque se déclenche |
delegated_human | aucun marqueur, et une chaîne act est présente (échange de jetons RFC 8693) |
human | aucun marqueur, aucune chaîne act |
L’imbrication des act est suivie jusqu’à une profondeur de 8 et parvient à
Cedar sous les formes context.actor_chain et context.delegation_depth.
Voir Identité & délégation.
Format de bundle
Section intitulée « Format de bundle »compile émet toujours le format courant, obsign-identity/3, dont la
signature couvre les chemins de libellés et les marqueurs machine. Les bundles
plus anciens (/1, /2) se vérifient toujours, mais chacun refuse exactement
ce que sa propre signature ne couvre pas : un bundle /1 ou /2 portant des
labels personnalisés, ou un bundle /1 portant des marqueurs machine
personnalisés, est rejeté au chargement. Resigner au format courant est à un
obsign-control compile de distance.
5. identity/jwks.json
Section intitulée « 5. identity/jwks.json »Les clés publiques de l’IdP, chargées depuis un fichier, jamais depuis le
réseau : la passerelle est sur le chemin critique et ne fait aucun appel
sortant. Récupérez-le depuis le jwks_uri de votre fournisseur et committez-le
tel quel, comme une règle :
curl -s https://idp.example.com/realms/corp/protocol/openid-connect/certs \ > identity/jwks.json{ "keys": [ { "kty": "RSA", "kid": "rMx8k…", "alg": "RS256", "n": "0vx7agoebGcQSuu…", "e": "AQAB" } ]}| Champ | Type | Obligatoire | Défaut | Signification |
|---|---|---|---|---|
keys | tableau | oui | — | Le jeu de clés. Doit se résoudre en au moins une clé utilisable. |
keys[].kty | énumération | oui | — | RSA | EC | OKP. Tout le reste est refusé. |
keys[].kid | chaîne | oui | — | Comparé à l’en-tête kid du jeton. Obligatoire dans le jeton aussi : sans lui, il faudrait essayer chaque clé, ce qui masque une rotation ratée et brouille le diagnostic. |
keys[].alg | chaîne | non | selon kty, ci-dessous | Algorithme de signature. |
keys[].crv | chaîne | non | — | Transportée et signée, sans autre interprétation ; la courbe est impliquée par alg pour EC et par kty pour OKP. |
keys[].n, keys[].e | base64url | pour RSA | — | Module et exposant. |
keys[].x | base64url | pour EC et OKP | — | Coordonnée X (EC) ou la clé publique brute (OKP/Ed25519). |
keys[].y | base64url | pour EC | — | Coordonnée Y. Ignorée pour OKP. |
Valeurs de alg acceptées
Section intitulée « Valeurs de alg acceptées »kty | alg par défaut | Acceptés |
|---|---|---|
RSA | RS256 | RS256, RS384, RS512, PS256, PS384, PS512 |
EC | ES256 | ES256, ES384 |
OKP | EdDSA | EdDSA (le champ n’est pas consulté — une clé Ed25519 signe en EdDSA) |
Les algorithmes HMAC (HS256 …) et none sont délibérément absents. Une
clé symétrique dans un JWKS publié est une vulnérabilité.
Refusé à la compilation — les mêmes contrôles que ceux que fait la
passerelle, déplacés en CI : un jeu de clés vide ou entièrement inutilisable ;
un kid en double (deux clés pour un même identifiant, et on ne peut plus dire
laquelle a signé) ; un composant manquant pour le kty déclaré ; un kty ou
un alg non pris en charge.
La rotation n’exige pas de redémarrage. Le bundle d’identité est relu à
l’exécution, et chaque rechargement, appliqué ou rejeté, entre dans le
journal sous la forme d’un enregistrement config_reload. C’est l’inverse du
bundle de politiques. Publiez le nouveau JWKS avant que l’IdP ne commence à
signer avec la nouvelle clé, et gardez l’ancienne clé dans le fichier jusqu’à
l’expiration de tous les jetons émis.
6. deployment/origin-keys.json
Section intitulée « 6. deployment/origin-keys.json »Quelles passerelles ont le droit d’écrire dans le journal. Enrôler une passerelle, c’est committer son entrée publique ici ; la révoquer, c’est supprimer l’entrée et republier.
[ { "key_id": "origin-3f19c2a8b4d70e51", "algo": "ed25519", "public_key": "3f19c2a8b4d70e51…", "role": "origin" }]| Champ | Type | Obligatoire | Défaut | Signification |
|---|---|---|---|---|
key_id | chaîne | oui | — | Doit correspondre octet pour octet à l’identifiant avec lequel la passerelle signe. Les passerelles le dérivent sous la forme origin-<16 premiers caractères hexadécimaux de la clé publique>, donc n’en inventez pas un. |
algo | chaîne | oui | — | ed25519 — la seule valeur acceptée aujourd’hui. |
public_key | chaîne | oui | — | La clé publique brute de 32 octets, en hexadécimal, sans 0x. |
role | énumération | en pratique oui | seal | origin | seal. Doit valoir origin ici, et la valeur par défaut est seal : l’omettre est donc refusé. |
Ce fichier rejette les champs inconnus. Une clé mal orthographiée est une erreur d’analyse franche. C’est le seul endroit de l’arbre où cette protection existe, parce que ces entrées décident qui peut écrire dans le journal.
Pourquoi role n’est pas optionnel en pratique : les clés d’origine
authentifient l’auteur de l’écriture (la passerelle signe chaque
enregistrement au moment où elle l’écrit), les clés de scellement certifient le
journal (le ledger signe des points de contrôle dessus). Une clé de
scellement qui certifierait son propre auteur d’écriture est exactement la
cohabitation que les deux rôles empêchent, si bien que le bundle de déploiement
ne porte que des clés d’origine.
D’où viennent les valeurs
Section intitulée « D’où viennent les valeurs »La passerelle affiche sa propre entrée publique au démarrage, sur stderr. Recopiez l’objet JSON tel quel :
[obsign] origin key origin-3f19c2a8b4d70e51 — every record signed directly — public entry: {"key_id":"origin-…","algo":"ed25519","public_key":"…","role":"origin"}Les déploiements à deux niveaux (--identity-key, ou --identity-hsm-module
en production) affichent la même ligne pour la clé d’identité : [obsign] identity key … — certifies a session key per chain — public entry: {…}.
Enrôlez cette entrée-là : la clé d’identité est ce à quoi le bundle doit
faire confiance, et les clés par session qu’elle certifie en sont dérivées et
vérifiées.
Un fichier absent alors que deployment/ est présent est refusé ; un tableau
vide [] est accepté et signifie « aucune passerelle de confiance pour
l’instant », légitime avant le premier enrôlement, et honnête plutôt que
silencieux. Sous l’exigence par défaut, le ledger refusera alors de sceller
quoi que ce soit, ce qui est correct : pas d’auteur d’écriture de confiance,
pas de preuve.
7. deployment/attestation.json (optionnel, v3)
Section intitulée « 7. deployment/attestation.json (optionnel, v3) »Les enrôlements TPM : la preuve que la clé d’identité d’une passerelle réside dans du matériel réel exécutant un logiciel mesuré. Optionnel, et n’a de sens qu’avec un TPM 2.0 ; voir HSM (PKCS#11) et TPM.
Le niveau supérieur est un tableau, une entrée par clé enrôlée.
| Champ | Type | Obligatoire | Défaut | Signification |
|---|---|---|---|---|
key_id | chaîne | oui | — | Quelle clé d’origine enrôlée cette attestation lie. Doit nommer une entrée de origin-keys.json. |
ak_pub | hex | oui | — | La clé d’attestation TPM qui a signé le quote et le certify. 32 octets pour une AK Ed25519 ; 65 octets (04 ‖ x ‖ y, point non compressé) pour une AK ECDSA-P256 — le repli pour les nombreux TPM qui n’implémentent aucun EdDSA. L’algorithme est lu sur le matériel de clé, jamais sur un champ déclaré. |
ek_cert | hex (DER) | oui | — | Le certificat de clé d’endossement. Transporté de façon opaque : il chaîne jusqu’à la racine du fabricant du TPM, qui est validée hors bande, jamais ici. Peut être une chaîne vide — le rapport de vérification le signale alors comme invérifiable plutôt que de faire semblant. |
certify | hex | oui | — | Sortie de TPM2_Certify : un TPMS_ATTEST marshalé suivi de la signature de 64 octets de l’AK. Lie la clé d’identité à l’AK. |
quote | hex | oui | — | Sortie de TPM2_Quote, de même forme. Rapporte les valeurs des PCR. |
expected_pcrs | tableau | oui | — | Les valeurs de PCR que le quote doit rapporter — c’est cela, la politique, et c’est sous la signature ops. |
expected_pcrs[].index | entier | oui | — | Index de PCR. La mesure du binaire de la passerelle va par convention dans le 16. |
expected_pcrs[].digest | hex | oui | — | Valeur attendue de ce PCR. |
identity_pub | hex | non | absent | Le TPMT_PUBLIC marshalé de la clé d’identité. Présent dans tout ce qu’émet un vrai TPM : le vérificateur recalcule le Name auquel le certify doit correspondre (alg ‖ H(ces octets)) et en extrait la clé publique brute, qui doit être égale à l’entrée enrôlée — si bien que entrée → zone publique → Name → signature de l’AK se referme sans trou. Absent, on retombe sur l’ancienne liaison synthétique, conservée pour que les attestations antérieures au matériel continuent de se vérifier. |
Tous les champs hexadécimaux sont en minuscules, sans préfixe 0x.
Le générer
Section intitulée « Le générer »obsign-tpm-enroll \ --tpm /dev/tpmrm0 \ --key-id origin-3f19c2a8b4d70e51 \ --binary-hash "$(sha256sum ./obsign-proxy | cut -d' ' -f1)" \ --pcr 16 \ --ek-cert-file ./ek.der \ --out ./one-attestation.jsonDeux choses à savoir :
--outécrit un objet nu ; le fichier doit être un tableau. Enveloppez-le (jq -s '.' one-attestation.json > deployment/attestation.json) ou collez l’objet dans le tableau existant.- Si l’outil vous avertit que votre TPM a produit une clé d’identité
ecdsa-p256, ne collez pas sonidentity_entrydansorigin-keys.json. Les bundles de déploiement n’acceptent aujourd’hui que des clés d’origine Ed25519, et une entrée P-256 fait tomber tout le bundle endeployment_bundle_invalid. L’attestation elle-même est valable ; l’entrée attend la prise en charge des clés d’origine P-256.
Refusé à la compilation : une attestation dont le key_id n’est pas une
clé d’origine enrôlée, un dérapage de copier-coller qu’il vaut mieux attraper
en revue qu’au vérificateur.
8. Un arbre complet
Section intitulée « 8. Un arbre complet »my-policies/├── policies/│ ├── 00-base.cedar│ └── 10-finance.cedar├── tools.json├── fail-mode.json├── identity/│ ├── provider.json│ └── jwks.json└── deployment/ └── origin-keys.jsonpolicies/00-base.cedar
@id("forbid_destructive_prod")forbid (principal, action == Action::"tool_call", resource)when { resource.destructive && context.env == "prod" };
@id("forbid_robot_destructive")forbid (principal, action == Action::"tool_call", resource)when { resource.destructive && !context.has_human_delegation };
@id("allow_scoped")permit (principal, action == Action::"tool_call", resource)when { resource.required_scope != "" && context.scopes.contains(resource.required_scope)};policies/10-finance.cedar
@id("forbid_large_transfers")forbid (principal, action == Action::"tool_call", resource == Tool::"transfer_funds")when { context.args.amount_cents > 100000 };
@id("allow_finance_transfers")permit (principal in Group::"finance", action == Action::"tool_call", resource == Tool::"transfer_funds");tools.json
[ { "name": "search_docs", "server": "mcp://docs" }, { "name": "transfer_funds", "server": "mcp://ledger", "destructive": true, "required_scope": "finance:write", "policy_args": [ { "name": "amount_cents", "kind": "long" }, { "name": "account", "kind": "string", "at": "/destination/iban" } ] }]fail-mode.json
{ "default": "closed", "tools": { "search_docs": "open" } }identity/provider.json
{ "issuer": "https://idp.example.com/realms/corp", "audience": "obsign-gateway"}identity/jwks.json — committé tel quel depuis le jwks_uri de l’IdP.
deployment/origin-keys.json — l’objet public entry: recopié depuis la ligne
de démarrage de chaque passerelle.
Puis :
obsign-control compile --source . \ --key ./ops-key.hex --key-id ops-2026 --out ./out9. Ce que la compilation refuse, et pourquoi
Section intitulée « 9. Ce que la compilation refuse, et pourquoi »| Message | Fichier | Cause |
|---|---|---|
no policies/ directory | — | le répertoire obligatoire est absent |
no .cedar file in policies/ | policies/ | un jeu de politiques vide refuse tout ; dites-le explicitement |
rule "…" has no @id annotation | un .cedar | règle anonyme — l’identifiant est le motif d’audit |
conflicting or invalid @id values | un .cedar | deux règles partagent un identifiant, tous fichiers confondus |
missing tools.json | — | le catalogue fait foi ; sans lui, chaque outil est refusé |
a tool with an empty name | tools.json | — |
tool "x" is declared twice | tools.json | laquelle des définitions l’emporte dépendrait de l’ordre des fichiers |
policy_args: N declared args, maximum is 16 | tools.json | — |
policy_args: duplicate arg "x" / an arg has an empty name | tools.json | — |
arg "x": "…" is not a JSON pointer (must start with '/') | tools.json | at malformé |
arg "x": default: expected an integer (i64 range, floats refused) | tools.json | un default qui ne correspond pas à son kind |
attribute … not found | un .cedar | une règle lit quelque chose que le modèle n’expose pas — le contrôle de typage refuse de la signer |
tool "x": smoke evaluation: … | un .cedar | typiquement une faute de frappe dans context.args.<name> |
missing field \default“ | fail-mode.json | le fichier existe mais ne déclare aucune position |
fail-mode override for "x", which is not in the catalogue | fail-mode.json | une faute de frappe appliquerait silencieusement la valeur par défaut |
identity/ exists but …/provider.json is missing | identity/ | une demi-configuration d’identité ne vérifie rien |
issuer and audience must both be set | provider.json | une audience vide accepte des jetons forgés pour n’importe quel autre service |
missing field \subject`(ouscopes, groups, client_id`) | provider.json | une surcharge claims partielle — fournissez les quatre |
unsupported key type: oct | jwks.json | une clé HMAC dans un JWKS — refusée par conception |
duplicate kid / empty JWKS / malformed JWK / unsupported algorithm | jwks.json | les contrôles du magasin de clés de la passerelle, exécutés à la compilation |
deployment/ exists but …/origin-keys.json is missing | deployment/ | un répertoire de déploiement vide mérite un fichier explicite |
key "x" has role "seal" | origin-keys.json | role omis ou faux — le bundle ne porte que des clés d’origine |
origin key id "x" is declared twice | origin-keys.json | — |
key "x" unusable: … | origin-keys.json | algo n’est pas ed25519, ou public_key n’est pas 32 octets hexadécimaux valides |
unknown field \…“ | origin-keys.json | le seul fichier qui rejette les champs inconnus |
attestation for "x", which is not an enrolled origin key | attestation.json | dérapage de copier-coller |
… is not inside a git repository — pass --label | — | la compilation estampille un sha de commit, ou un libellé explicite |
| refus d’estampiller un arbre modifié | — | une citation policies@<sha> doit désigner les octets que ce commit contient |
Voir aussi Modes de défaillance à connaître pour les défaillances qui surviennent à l’exécution plutôt qu’à la compilation.