Skip to main content

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_KEY and 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 as sender.pub. Pass that file as --sender-key: bootstrap.py registers 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 in TE_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 serviceKey the Gateway reports for the service's certificate login -- never assembled by hand. bootstrap.py logs the service in once and reads it;
  • xauthPrefix is prepended to every localUser the application sends, and the result is the XAuth id the key server sees. The application can never name an identity outside it;
  • senderKey is the SPKI PEM of the application's sender key;
  • the starter needs session:delegated only. The MCP server beside it (mcp/README.md) also needs session: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. Run bootstrap.py with --allow-scoped and 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:

stepwhatread back
1the administrator's password loginthe session exists
2role 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 doGET /api/v1/roles: exactly that set
2role Starter Service: Get Token, Release Token Session, Add Principal, Update Principal, Get List of Principal, Find Principal, Remove Principal -- what the service doesexactly that set
3the delegator holds Starter Principal beside its own roles (the key server's delegation guard is a SUPERSET test) and the DELEGATION flag 0x400GET /api/v1/principals/{id}, the flags answer
4the service certificate enrolled once, holding exactly Starter ServiceGET /api/v1/principals/{id}
5the service's certificate loginits serviceKey
6the registration (step 1); session:scoped added with --allow-scopedthe file
7the 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 halfeach 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 operator copies the registry in force to your working directory; bootstrap.py merges the service into that copy; --registry-push hands it back with te-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> writes admin-password (mode 600; it is never printed), delegator.pem and service-registry.json into <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 -- so bootstrap.py takes --allow-http-loopback and the config.json it writes reads "trust_file": null and "allow_http_loopback": true. external_origin stays null: the Gateway builds the DPoP htu it 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-ca trusts it and --revocation-policy gives 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.