Operator bootstrap
© Pi Soft, 2018-2026 · Tricryption Engine 8.1
This is the OPERATOR's half. The application developer's half is the starter's README. It sets up one Gateway (contract 1.7.1) and its key server so that one application -- the starter -- can log in by certificate, mint delegated sessions for its own users, and create and remove those users.
Two steps are not endpoints and are done by hand: the service registry is a file, and the
key server's XAuth auto-provisioning is a provision-time switch the starter does not need. Every
other step is an endpoint, and examples/python-agent-starter/bootstrap/bootstrap.py performs it and reads it back.
What you need first
- the Gateway's delegator certificate and key -- the identity it impersonates users with. It is
configured on the Gateway as
TE_GATEWAY_CLIENT_CERT/TE_GATEWAY_CLIENT_KEYand enrolled on the key server as a certificate principal; - the application's service certificate and its key, issued by a CA the key server already
trusts (
POST /api/v1/trusted-cas) and holding a per-issuer revocation policy; - the application's sender public key -- a SEPARATE keypair the application generated
(
openssl genrsa -out sender.key 2048;openssl pkey -in sender.key -pubout -out sender.pub), sent to you assender.pub. Pass that file as--sender-key:bootstrap.pyregisters its public half and, holding no private half, skips step 7's signed proof by name (the developer's first run is the proof). Given the private key instead, it runs step 7 in full. The Gateway refuses to register a service whose sender key is its certificate's key; - the key server administrator's password, in a file (
--admin-password-file) or inTE_STARTER_ADMIN_PASSWORD-- never on a command line.
Step 1 (a file) -- the service registry
The Gateway reads its registered services from the JSON file named by
TE_GATEWAY_SERVICE_REGISTRY and re-reads it every TE_GATEWAY_SERVICE_REGISTRY_POLL_MS
milliseconds (default 2000), comparing a content hash. A changed file is validated WHOLE: one bad
entry and the reload is refused, the registry already in force stays, and GET /api/v1/ready
reports the refusal. No restart is needed for an edit -- only for the first time the variable is
set, because the Gateway reads its environment once, at boot. Write the file whole and rename it
into place; never edit it in place.
{
"services": {
"<ksSystemId>:<loginId>": {
"serviceId": "python-starter",
"subjectHint": "CN=python-starter,O=Example",
"xauthPrefix": "starter:",
"majordomo": "default",
"senderKey": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n",
"operations": ["session:delegated"]
}
}
}
- the KEY is the
serviceKeythe Gateway reports for the service's certificate login -- never assembled by hand.bootstrap.pylogs the service in once and reads it; xauthPrefixis prepended to everylocalUserthe application sends, and the result is the XAuth id the key server sees. The application can never name an identity outside it;senderKeyis the SPKI PEM of the application's sender key;- the starter needs
session:delegatedonly. The MCP server beside it (mcp/README.md) also needssession:scoped: it gives an agent a five-minute session covering only the documents its task names, bound to a key the server generated for that task. Runbootstrap.pywith--allow-scopedand the entry reads"operations": ["session:delegated", "session:scoped"].
bootstrap.py --registry-file <the file> merges the entry into the file for you (whole file,
renamed into place) and waits for the Gateway to pick it up; without --registry-file it prints the
entry for you to place.
Step 2 (a switch, NOT used by the starter) -- XAuth auto-provisioning
A key server can create an XAuth principal the first time a delegated login names it: the
authentication policy AP03, enabled in the key server's configuration at provision time (the test
kit's provision-test-ks.sh --xauth-policy), together with the policy parameters
Policy.XAuth.CreatePrincipal = 1 and Policy.XAuth.Roles -- both read when the key server
STARTS, so a change needs a key-server restart.
The starter does not use it, deliberately. It creates each run's principals EXPLICITLY: a
delegated login for an unknown user answers 404 TARGET_NOT_FOUND naming the id the Gateway built
from the prefix, and the service then creates that id with POST /api/v1/principals/xauth, giving it
the role you name. That needs no key-server restart and no policy, and the service's role says
exactly what it may do. If your deployment already runs with AP03 on, the first delegated login
creates the principal itself, with Policy.XAuth.Roles rather than Starter Principal, and the
example looks it up instead of creating it -- and then gives it exactly Starter Principal
(PUT /api/v1/principals/{id}/roles, which Starter Service's Update Principal allows; read back),
so the deployment's auto-provisioning roles need not be the starter's. The workflow adapter does the
same, and bootstrap.py's step 7 takes that path too. The repository's control
(te_python_starter) grades BOTH paths: its main legs run with AP03 OFF (explicit creation), and
its last leg, AP03_AUTOPROVISION, re-provisions the same key server with AP03 on, sets
Policy.XAuth.CreatePrincipal = 1 and Policy.XAuth.Roles to a NARROW role of its own (a login and
nothing else, as a deployment's own role may be) through PUT /api/v1/policy-parameters, restarts
it, and runs the example again -- each run principal's record then reads "via": "key server (AP03)", and the five example legs and the clean-up hold.
Step 3 (endpoints) -- bootstrap.py
python3 bootstrap/bootstrap.py --endpoint https://gw.example.com --trust-file gw-ca.pem \
--openssl /usr/bin/openssl --admin-user admin --admin-password-file admin.pw \
--service-cert service.pem --service-login-key service.key --sender-key sender.key \
--delegator-cert delegator.pem --xauth-prefix "starter:" \
--registry-file /etc/pisoft/gateway/service-registry.json --config-out config.json
For the starter's MCP server (mcp/README.md), add --allow-scoped: the server
works through a scoped, sender-constrained session, and a registration without session:scoped
is refused when it asks for one.
Each step prints PASS or FAIL and is read back from the key server through the Gateway:
| step | what | read back |
|---|---|---|
| 1 | the administrator's password login | the session exists |
| 2 | role Starter Principal: Get Token, Release Token Session, Encrypt Data, Decrypt Data, Get Access Control List, Update Access Control List, Expire Key -- what the owner and the recipient do | GET /api/v1/roles: exactly that set |
| 2 | role Starter Service: Get Token, Release Token Session, Add Principal, Update Principal, Get List of Principal, Find Principal, Remove Principal -- what the service does | exactly that set |
| 3 | the delegator holds Starter Principal beside its own roles (the key server's delegation guard is a SUPERSET test) and the DELEGATION flag 0x400 | GET /api/v1/principals/{id}, the flags answer |
| 4 | the service certificate enrolled once, holding exactly Starter Service | GET /api/v1/principals/{id} |
| 5 | the service's certificate login | its serviceKey |
| 6 | the registration (step 1); session:scoped added with --allow-scoped | the file |
| 7 | the proof: a fresh login is the registered service; a delegated mint for a throwaway user is 404 TARGET_NOT_FOUND naming <prefix><user>; that user created with Starter Principal; a delegated session as it; the user removed. With a PUBLIC --sender-key: only "a fresh login is the registered service" -- every other request needs a proof signed with the private half | each answer |
| 8 | --config-out: the developer's config.json -- paths, the role id, the clean-up recipient, the XAuth prefix (a scoped mint is read back against it) | the file |
There is no role update or delete route, so a role is created once and then only read; a role of
the same name whose operations differ is refused by name rather than used.
Emergency Check In is deliberately not in Starter Service: removing a principal sends one first
and a refusal there is non-fatal (checkedIn: false) -- the run's principals never check a key out.
The clean-up recipient is a PASSWORD principal (the removal route resolves no other kind): a removed
principal's keys are reassigned to it.
bootstrap.py refuses a port or path that the repository's governance/protected-estate.conf
declares protected, when this copy sits inside the repository, and talks to the --endpoint it is
given and nothing else.
With --trust-ca <ca.pem> it first trusts the service certificate's issuing CA on the key server
(POST /api/v1/trusted-cas), and with --revocation-policy <policy> writes that issuer's revocation
policy (PUT /api/v1/revocation/issuers/{digest}/policy), each read back. With
--registry-push <command> it runs that command once --registry-file is rewritten -- for a Gateway
that cannot read the file where you write it -- and waits for the Gateway only after it exits 0.
Step 4 -- on the Compose evaluation stack
The Compose evaluation stack (deploy/compose/) can run a registered instance WITH the delegation
layer: its line in deploy/compose/instances.conf ends in delegation, and up then enrols a
delegator for the instance's Gateway (with the DELEGATION flag) and gives the Gateway a service
registry. The default stack never has it. What differs from a Gateway you installed yourself:
- the registry is in the stack's volume, not a file you can edit.
te-compose.sh operatorcopies the registry in force to your working directory;bootstrap.pymerges the service into that copy;--registry-pushhands it back withte-compose.sh registry --instance <name> --file <it>, which places it in the volume and restarts the Gateway, and returns once the Gateway is healthy again (reading[boot] service registry (<n>)from its log). Every other service in the file is kept. A registry change restarts the Gateway, so its sessions end: log in again afterwards; - the administrator's password and the delegator's certificate come from the stack too:
te-compose.sh operator --instance <name> --out <dir>writesadmin-password(mode 600; it is never printed),delegator.pemandservice-registry.jsoninto<dir>; - the ports: the instance publishes its Gateway on
127.0.0.1:<port base + 1>(the default stack on 29791) over plain HTTP -- the stack's Gateway terminates no TLS -- sobootstrap.pytakes--allow-http-loopbackand theconfig.jsonit writes reads"trust_file": nulland"allow_http_loopback": true.external_originstaysnull: the Gateway builds the DPoPhtuit checks from the host and port your client connects to, so the published port is the origin, though the Gateway listens on 18091 inside its container; - the service's CA is your own:
--trust-catrusts it and--revocation-policygives it a per-issuer policy. The command below opts that evaluation CA out of revocation evidence (evidence=none) -- the shipped default refuses a certificate whose CA holds none; a deployment imports its CA's CRL instead (POST /api/v1/revocation/crls).
Bring the instance up (bash deploy/compose/te-compose.sh up --instance <name> --debs <the release's packages>), then, from the engine's source tree, register one service and run the TypeScript
starter's preflight.ts and example.ts against it. Set the first three lines to your instance:
I=myeval # the instance: its instances.conf line ends in `delegation`
GW=http://127.0.0.1:29851 # its Gateway, 127.0.0.1:<port base + 1>
W=$HOME/tricryption-operator/myeval # the operator's working directory, outside the source tree
umask 077
mkdir -p "$W"
bash deploy/compose/te-compose.sh operator --instance "$I" --out "$W"
openssl genrsa -out "$W/service-ca.key" 2048
openssl req -x509 -new -nodes -key "$W/service-ca.key" -sha256 -days 825 -subj "/CN=Example Service CA/O=Example" -out "$W/service-ca.pem"
openssl genrsa -out "$W/service.key" 2048
openssl req -new -key "$W/service.key" -subj "/CN=typescript-starter/O=Example" -out "$W/service.csr"
openssl x509 -req -in "$W/service.csr" -CA "$W/service-ca.pem" -CAkey "$W/service-ca.key" -CAcreateserial -days 825 -sha256 -out "$W/service.pem"
openssl genrsa -out "$W/sender.key" 2048
python3 -B examples/python-agent-starter/bootstrap/bootstrap.py --endpoint "$GW" --allow-http-loopback \
--openssl "$(command -v openssl)" --admin-user admin --admin-password-file "$W/admin-password" \
--trust-ca "$W/service-ca.pem" --revocation-policy "evidence=none;max-age=7d;refresh=6h;fallback=allow" \
--service-cert "$W/service.pem" --service-login-key "$W/service.key" --sender-key "$W/sender.key" \
--delegator-cert "$W/delegator.pem" --service-id typescript-starter --xauth-prefix "starter:" \
--registry-file "$W/service-registry.json" \
--registry-push "bash deploy/compose/te-compose.sh registry --instance $I --file $W/service-registry.json" \
--config-out "$W/config.json"
node examples/typescript-integration-starter/preflight.ts --config "$W/config.json"
node examples/typescript-integration-starter/example.ts --config "$W/config.json" --json
Every bootstrap.py step prints PASS and it ends BOOTSTRAP: OK; preflight.ts ends
PREFLIGHT: OK; example.ts --json prints one line per verdict, each "ok": true --
OWNER_ENCRYPT, AGENT_READ_A, AGENT_DENIED_B (a 403), REVOKED_SAME_SESSION,
REVOKED_FRESH_SESSION and CLEANUP. Here the operator holds the sender key, so step 7's signed
proof runs in full; with only the developer's sender.pub it is skipped by name, as above. On an
instance without the delegation layer, te-compose.sh operator says so and writes no
delegator.pem, and bootstrap.py stops at its first check, no such file: <dir>/delegator.pem.