The round
1
Draft
POST /policy/drafts with the whole document. The answer carries document: the exact
bytes to sign.2
Check
Parse
document and make sure it says what you sent. Sign only bytes you have read.3
Sign
Sign the bytes of
document with your policy key (key B), in your engine’s OpenBao.4
Publish
POST /policy/publish with version and the signature. The change is pending: it waits
for approval.5
Approval
People approve it in the console. Citadel then sends it to your engine.
GET /policy/drafts/{version} reads published, and GET /policy reads the document in force.policy.write scope. Reading needs read. A key limited to vaults
can do neither: one document rules every vault. No call here takes network: one document rules
every network.
Draft the whole document
The body is the whole document: the rules of each chain you use, the ceilings and the per-reference limits. A section you leave out is empty, not kept from the version in force. An empty whitelist lets your engine pay no external address, and an asset without a ceiling cannot be sent.GET /policy returns the document in force, so a change can start from it.
version, and each chain’s
chain_module_version, the address format the chain’s rules are written in. Both are inside the
signed bytes. Your engine keeps the highest version it has seen, so an old document can never
come back.
Citadel refuses the draft with 422 before it replaces anything when it is not a document your
engine can read. That covers a field the document does not have, an address its chain cannot
parse, an amount that is not an integer string, and a window that is not a duration such as
24h.
There is one draft per organization. A new draft replaces the one waiting to be signed, whether
it was made here or in the console.
Check before you sign
Your engine trusts your policy key’s signature and nothing else. Read the bytes before you sign them. The document is your body plusversion and each chain’s chain_module_version. Nothing
else is in it:
Sign with key B
Key B is thepolicy key in your engine’s OpenBao, and it never leaves it. Sign the bytes of
document exactly as answered: UTF-8, the trailing newline included. Write them to a file
rather than a shell variable, which drops the trailing newline:
-
Through your policy engine’s image.
citadel policy-engine sign-policyreads the document on stdin and prints the signature in hex, on the line aftersignature. Run it inside the engine’s container, which already holds the OpenBao login: -
OpenBao directly. Ed25519 signs the whole message, so
digestcarries the document’s bytes themselves, hex-encoded, and not a hash of them. The answer’sdata.signatureis the signature in hex:
Publish and wait for approval
sign-policy prints it, or in standard base64. Citadel checks it
against the policy key your engine registered when it joined, and answers 202 with
status: pending.
The change does not reach your engine yet. An approval request opens in the console with the bar
of your approval rules in force. It needs as many approvals as their strictest rule asks, from
that rule’s group. When no approval rules are in force, or they hold nothing, one member of your
organization approves. Every approver confirms with a passkey. Approvers see every whitelist
address added and removed, in full, and the other parts of the document the change touches.
Follow the change with GET /policy/drafts/{version}:
A rejected version number is never used again. The next draft takes the one after it.
Keep it in step
A backend that syncs its address book on a schedule can run the same round every time:201: the rules changed. Check, sign, publish.200withstatus: pending: these rules already wait for approval. Nothing to sign; the draft is left as it was.422withcode: nothing_changed: these are the rules in force. Nothing to do.
version and signature again answers the change as it stands. A retry after a
lost answer is safe.
When it is refused
Nothing changes on a refusal. See Errors for the codes every endpoint shares.