Notarizing documentation
Operator guide
Developer preview. Not yet production ready. This page is rendered from docs/operator-guide.md of the Notarizing repository at revision d23579e737f9d1e16e07b3c6b21a51282d64027e. It describes the behavior of that revision.
Contents
This guide is for the person who runs a notarizing workspace. It covers the owner process, backups, retention, evidence bundles, the optional ka2a adapter, the optional semantic search provider and MCP for agents.
Authorities
Notarizing keeps these authorities apart:
| Authority | Credential | Can do |
|---|---|---|
| Browser viewer | One-time viewer secret of serve, then a session cookie | Read the workspace. |
| Browser reviewer | A viewer session with an expiring grant from the CLI | Append review records inside the grant scope. |
| CLI control | Bearer token in control.token of the workspace | Imports, bindings, policy, purges, grants, backups. |
| MCP | Standard input and output of notarizing mcp | Read-only queries. |
| ka2a principal | A verified ka2a endpoint with a source binding | evidence.submit, evidence.lookup, change.report as configured. |
No authority can get the rights of another authority. The browser has no route that imports, purges, sets a policy or changes a source.
Run the owner process
notarizing --workspace /srv/notarizing/ws serve --port 7373
serveowns the workspace until you stop it with Ctrl-C. A second owner gets the exit code 3 (workspace busy).servelistens on 127.0.0.1 only. Do not put it behind a proxy that changes theHostorOriginheaders. The server refuses a foreignHostand a cross-origin request.servewritescontrol.jsonandcontrol.token(mode 0600) into the workspace directory. Other notarizing commands use them to send imports to the owner. Keep the workspace directory private.- A stop ends every browser session and grant.
servewrites log records to standard error: server errors, a background service that stops with an error, and a shutdown that does not finish in 10 seconds.--log-levelselects the lowest level:debug,info(the default),warnorerror.--log-format jsonwrites one JSON object per line for a log collector; the default istext. A record names error codes, routes and operations. It never holds a secret, a token, a request body, a query or source text. The viewer secret and the browser URL are separate lines on standard error, not log records.
To see the open browser sessions:
notarizing review session list
Back up and restore
Make a backup while serve runs or while it is stopped:
notarizing backup --output /backups/ws-2026-10-02
The backup directory must not exist. It holds workspace.db (a consistent copy of the
live pages) and backup.json (the manifest with digests).
Check a backup. The check needs no workspace and changes nothing:
notarizing backup verify /backups/ws-2026-10-02
To restore:
- Stop
serve. - Copy the backup directory to a new location, for example
/srv/notarizing/ws-restored. - Run
notarizing --workspace /srv/notarizing/ws-restored initonce. The command takes ownership and applies pending migrations. - Use the new location as the workspace.
A backup from a newer notarizing version has a newer schema. An older binary refuses it with
unsupported_schema and changes nothing.
Backups and purged content
A backup holds the content that the workspace held when the backup was made. A later purge
does not change the backup. If you restore a backup that is older than a purge, the purged
content comes back. After such a restore, preview and apply the same purge again. Keep the
retention plans (retention preview --output) for this reason. To remove purged content
for good, also delete the backups that are older than the purge.
Schedule backups
The parent directory of the backups must exist. backup does not create it. Create it once
with mode 0700, as the workspace account:
mkdir -m 0700 /backups/notarizing
This script makes one backup with a UTC time in its name, checks it and removes backups
older than 14 days. Install it as /usr/local/bin/notarizing-backup:
#!/bin/sh
set -eu
ws=/srv/notarizing/ws
root=/backups/notarizing
dir="$root/ws-$(date -u +%Y%m%dT%H%M%SZ)"
notarizing --workspace "$ws" backup --output "$dir"
notarizing backup verify "$dir"
find "$root" -mindepth 1 -maxdepth 1 -type d -name 'ws-*' -mtime +14 -exec rm -rf {} +
- The backup works while
serveruns. It then goes through the control API ofserve. - If another notarizing command owns the workspace at that time,
backupstops with exit code 3 (workspace busy). Run it again later. - The script removes old backups only after a new backup passed
backup verify. - Run the script as the account that owns the workspace. Keep its output, for example in the system journal.
Run it every day at 02:15 with cron (crontab -e as the workspace account):
15 2 * * * /usr/local/bin/notarizing-backup
Or with a systemd timer. /etc/systemd/system/notarizing-backup.service:
[Unit]
Description=Back up the notarizing workspace
[Service]
Type=oneshot
User=notarizing
UMask=0077
ExecStart=/usr/local/bin/notarizing-backup
/etc/systemd/system/notarizing-backup.timer:
[Unit]
Description=Daily backup of the notarizing workspace
[Timer]
OnCalendar=*-*-* 02:15:00
Persistent=true
[Install]
WantedBy=timers.target
Enable the timer:
systemctl daemon-reload
systemctl enable --now notarizing-backup.timer
Copy the backups to a second location, for example another disk, as your backup policy requires. Keep mode 0700 on the copies: a backup holds all workspace content and is not encrypted.
Rotate backups
- Keep daily backups for a short period (14 days in the script), and weekly or monthly copies for longer if your policy requires them.
- Keep the backup that you made before an upgrade until the upgraded workspace works (see the upgrade notes).
- Each backup removed by rotation also removes content that a purge removed from the workspace. The rotation period is therefore the longest time that purged content stays on disk.
Practice a restore
Test a restore regularly, for example each month, on a copy. The drill does not touch the workspace:
Check the backup:
notarizing backup verify /backups/notarizing/ws-20261002T021500ZCopy it to a new directory:
cp -a /backups/notarizing/ws-20261002T021500Z /tmp/restore-drillTake ownership and apply pending migrations:
notarizing --workspace /tmp/restore-drill initCheck the copy and compare it with the workspace:
notarizing --workspace /tmp/restore-drill doctor notarizing --workspace /tmp/restore-drill change listDelete the copy:
rm -rf /tmp/restore-drill
Record the date, the backup and the result of each drill.
Retention and purge
A purge removes retained evidence content from every query surface. Preview it first:
notarizing retention preview --output plan.json source:ci before:2026-01-01T00:00:00Z
A selector term is receipt:ID, artifact:SHA256, source:NAMESPACE or
before:RFC3339-TIME. Every term must match. The preview changes nothing.
Apply the plan:
notarizing retention apply --plan plan.json
- The command computes the plan again in its write transaction. If the workspace changed
after the preview, the result is
stale_revisionand nothing changes. Preview again. - A purge keeps a tombstone without content for each receipt.
- A purge invalidates every search generation, graph projection, vector and snippet that could hold the content. A rebuild after a purge says that it is partial.
- A purge never touches a repository, a source snapshot, a target or a review record.
- A purge does not erase earlier backups, file system copies or copies in external services. Purge or delete those copies separately. See Backups and purged content.
Source access
Revoke the source access of a registered repository when the workspace may no longer show its content, for example after the repository moved to a restricted host:
notarizing repo access revoke payments --reason "The repository moved to a restricted host."
- The command needs a reason of at most 500 bytes. Only the CLI can change source access.
While
serveruns, the command goes through its control API. - The change invalidates every search index of the workspace and drops the cached graph and review views. A running semantic build cannot store its late vectors.
- While access is revoked, the browser, MCP, the change report and the CLI refuse every view
that reads the stored source of the repository with
forbidden. Search finds none of its text.change import,change list --atandsearch indexrefuse to read it. Asearch indexbuild that read the source before the revocation stores nothing.bundle export --targetrefuses a target of the repository, because the review events of the bundle quote its source. A bundle export by receipt IDs or by source still works. repo listshows the access. The browser lists the repository without its changes.- Snapshots, receipts and review records stay. A revocation is not a purge: it erases no content, and copies in backups, bundles, exports and external services stay.
Restore the access, then rebuild the search indexes:
notarizing repo access restore payments --reason "The host is approved."
notarizing search index --target TARGET_ID
The same command again changes nothing. Search indexes of other repositories are invalidated too; rebuild them as well.
Evidence bundles
A bundle (notarizing.bundle/1) moves retained evidence to another workspace.
notarizing bundle export --target tgt_ID --output evidence.bundle
notarizing --workspace /other/ws bundle import evidence.bundle
- The export holds the exact report and artifact bytes, the receipts as attributed historical records, the source target references and explicit omissions.
- The import checks the archive, the manifest, every digest and every report before it writes. Then it stores all receipts or nothing.
- The JSON Schema
bundle-manifest.schema.jsondescribesmanifest.jsonin the archive. The import checks more, for example the canonical bytes, the order of the lists and that each file is carried. - An imported receipt gets the attribution
imported_bundleand a new local receipt ID. A trust policy for the original source does not apply to it. - Review events and corrections in the bundle stay claims of the exporting workspace. They change no local review state, assessment or policy.
Optional ka2a adapter
Local use needs no broker and no ka2a configuration. Enable the adapter only to accept evidence from ka2a producers:
notarizing serve --ka2a-config /etc/notarizing/ka2a.json
Without --ka2a-config, serve opens no broker connection.
Configuration file
The file is strict JSON with the schema notarizing.ka2a-adapter/1. Relative paths are
relative to the directory of the file. An unknown field is an error. The JSON Schema
ka2a-adapter.schema.json describes the structure;
serve --ka2a-config checks more, for example the files and the broker addresses.
{
"schema_version": "notarizing.ka2a-adapter/1",
"node": {
"domain": "example.org",
"endpoint": "notarizing",
"state_dir": "ka2a-state",
"catalog_file": "catalog.json",
"signing_key_file": "notarizing.key",
"durability": "qualified",
"kafka": {
"brokers": ["kafka-1.example.org:9093", "kafka-2.example.org:9093"],
"tls": {
"ca_file": "ca.pem",
"cert_file": "client.pem",
"key_file": "client-key.pem",
"crl_file": "broker-crl.pem",
"server_name": "kafka.example.org"
},
"sasl": {
"mechanism": "SCRAM-SHA-512",
"username": "notarizing",
"password_file": "kafka-password"
}
}
},
"sources": [
{
"domain": "example.org",
"endpoint": "ci-runner",
"source_namespace": "ci",
"operations": ["evidence.submit", "evidence.lookup", "change.report"],
"repositories": ["payments"]
}
]
}
Node fields:
| Field | Rule |
|---|---|
domain, endpoint | The local ka2a endpoint. The trusted catalog must list it. |
state_dir | The state directory of the ka2a node. |
catalog_file | The trusted ka2a catalog. It binds each signing key to one endpoint. |
signing_key_file | The signing key of the local endpoint. |
durability | Required: qualified or development. |
kafka.brokers | 1 to 16 bootstrap brokers. |
kafka.allow_plaintext | Optional. Only with the development durability profile. |
kafka.tls | Optional: ca_file, cert_file, key_file, crl_file, server_name. cert_file and key_file go together and turn on mutual TLS. The certificate files are at most 1 MiB, the key file at most 64 KiB. crl_file holds certificate revocation lists of the broker chain, at most 8 MiB, and needs ca_file. See "Mutual TLS and broker ACLs" and "Broker certificate revocation lists". |
kafka.sasl | Optional: mechanism (PLAIN, SCRAM-SHA-256 or SCRAM-SHA-512), username, password_file. PLAIN needs TLS. |
kafka.client_id, kafka.consumer_group | Optional names. |
kafka.request_timeout, delivery_timeout, session_timeout, heartbeat_interval, rebalance_timeout | Optional Go durations, positive and at most 1h. |
max_dispatch | Optional: 1 to 32 requests at the same time. The default is 4. |
drain_timeout | Optional: at most 5m. The default is 10s. |
Top-level handler_timeout is optional. It bounds the application work of one request.
The value is 1s to 10m. The default is 2m.
Source bindings (1 to 1000, one for each principal):
| Field | Rule |
|---|---|
domain, endpoint | The verified ka2a principal of the producer. |
source_namespace | The notarizing source namespace of its requests. Only the binding selects it; a request never does. It cannot start with bundle:. |
operations | One or more of evidence.submit, evidence.lookup, change.report, each at most once. |
repositories | The repositories that change.report can read, at most 1000. Give repositories only with change.report, and give change.report only with repositories. |
share_local_namespace | Optional. Permits the namespace local of local file imports. The namespace local needs it. |
Keep the password file and the key files private (mode 0600). Notarizing reads the password file once at the start. Logs and error messages never show the password.
Mutual TLS and broker ACLs
When the broker requires a client certificate, set cert_file and key_file in
kafka.tls. Notarizing checks the files when serve starts, like ka2a --tls-cert and
--tls-key:
cert_fileis PEM: the leaf certificate first, then the intermediate CAs.key_fileis an unencrypted PEM key. It must be a regular file (no symbolic link) of the user ofserve, with mode 0600 or 0400.- The key must match the certificate.
- A certificate with extended key usages must allow client authentication (
clientAuth).
If a check fails, serve does not start, and the error names the field and the cause. The
error never shows file content. After you renew the certificate, reload it without a restart
(see "Rotate the broker credentials").
An example kafka member for mutual TLS without SASL:
"kafka": {
"brokers": ["kafka-1.example.org:9093"],
"tls": {
"ca_file": "ca.pem",
"cert_file": "notarizing-client.pem",
"key_file": "notarizing-client-key.pem",
"server_name": "kafka-1.example.org"
}
}
The broker maps the certificate subject to the principal (ssl.principal.mapping.rules),
for example User:notarizing. With SASL on a SASL_SSL listener, the SASL user is the
principal, and the certificate only authenticates the connection.
Give the principal only the ACLs that ka2a topics acl-plan prints. The command reads the
catalog and contacts no broker:
ka2a topics acl-plan --catalog catalog.json --principal User:notarizing \
--domain example.org --endpoint notarizing
The plan has four kinds of entries:
| Resource | Operation | Why |
|---|---|---|
| The own mailbox topic | Read | Consume the requests. |
| The own mailbox topic | DescribeConfigs | Check the mailbox against the durability profile. |
The group ka2a.owner.<domain>.<endpoint> | Read | Join the consumer group and commit offsets. |
| The mailbox topic of each producer | Write | Send the responses. |
Apply the printed kafka-acls.sh commands with an administrative principal after review.
Provision the mailbox with an administrative principal too (ka2a topics provision); the
plan does not grant Create.
If an ACL is missing, the broker denies the request. The adapter health then shows
degraded with the reason authorization_failed and transport.broker.denial_scope:
| Scope | Missing ACL |
|---|---|
topic_write | Write on the mailbox topic of a producer. |
topic_read | Read on the own mailbox topic. |
topic_describe | Describe or DescribeConfigs on the own mailbox topic. |
group_read | Read on the consumer group. |
cluster | A cluster permission, for example IdempotentWrite before Kafka 3.0. |
Local work continues. Add the missing ACL. ka2a does not send a denied response again; the
producer sends a lookup to get its receipt. The state returns to ready at most 5 minutes
after the last denial.
Broker certificate revocation lists
Set kafka.tls.crl_file to refuse a broker certificate that its CA revoked, for example
after a broker key leaked. The file holds certificate revocation lists (CRL): PEM blocks
X509 CRL or one DER list, at most 64 lists and 8 MiB. Notarizing parses and checks them
with ka2a, like ka2a --tls-crl:
crl_fileneedsca_file. A CA certificate of theca_filebundle must have signed each list. The list of an intermediate CA needs that intermediate in the bundle.- Each list must be current: not before its this update, not after its next update, and it must have a next update. A stale list is refused.
- The file must be a regular file. A symbolic link to a regular file is allowed, as for
ca_file; the CA signature, not the file owner, makes the list authentic. - Notarizing never fetches a CRL distribution point and never asks OCSP. Distribute the lists like the CA bundle.
If a check fails, serve does not start, and the error names the field and the cause. Each
TLS handshake with a broker then checks the broker chain, the leaf and the intermediate CAs.
A revoked certificate fails the connection with the class tls_failed.
Publish the next lists before the earliest next update and reload them (see "Rotate the
broker credentials"). The health shows transport.revocation_lists: state is none,
current, due (the earliest next update is within 24 hours) or stale, with
next_update. notarizing doctor warns when the state is due or stale. A running node
keeps checking with stale lists, so a late publication does not stop the adapter. But a
reload or a restart of serve refuses the stale file until it holds current lists.
Rotate the broker credentials
serve can read the TLS and SASL files of the adapter again while it runs. Use this to
renew the client certificate, to rotate the key or the CA bundle, to adopt new revocation
lists, or to change the SASL password.
- Write the new files over the files that the configuration names (
ca_file,cert_file,key_file,crl_file,password_file). Keep the key file mode 0600. Write each new file next to the old one and rename it over the old one, so a reload never reads a partial file. A reload between the renames of the certificate and the key finds no key pair; it is refused and the adapter keeps the previous material. - Request a reload. Do one of these:
- Run
notarizing ka2a reloadwith the workspace ofserve. - Send
SIGHUPtoserve(for examplesystemctl reloadwithExecReload=/bin/kill -HUP $MAINPID).servewrites the result to standard error.
- Run
- Read the result.
reloadedmeans that the ka2a node checked the new material with one broker request and uses it for each new broker connection. Work in flight continues, and open connections keep their material until they close.
Notarizing checks the files with the same rules as at the start. The ka2a node then checks
them with the broker before it uses them. If a check fails, the result is refused with a
class, and the adapter keeps the previous material and keeps working:
| Class | Cause | Action |
|---|---|---|
invalid_files | A file cannot be read or fails a check of the start. The message names the field. | Fix the file and reload again. |
invalid_credentials | The certificate is not valid now, or the SASL settings are not valid. | Use a valid certificate. |
tls_failed | The broker refused the TLS handshake, for example a certificate of another CA, or the new revocation lists revoke the certificate of the broker. The result has the hint of ka2a. | Use a certificate that the broker trusts, or renew the broker certificate. |
authentication_failed | The broker refused the SASL user or password. | Check the password. |
authorization_failed | The new principal lacks Describe on the own mailbox. | Apply the ACL plan for the new principal. |
incompatible_credentials | TLS or SASL would turn on or off. | Restart serve with the new configuration. |
node_not_ready, connection_refused, timeout and other outage classes | No node or broker could check the material. | Reload again when the adapter is ready. |
notarizing ka2a reload exits with 0 for reloaded and staged. It exits with 1 and the
code invalid_input for refused material, or unavailable when you must try again later.
The reload reads only these files. The configuration file itself stays as it was at the
start: brokers, server name, SASL mechanism and user name, endpoint, sources, catalog and
signing key change only with a restart of serve. A running node never adopts a changed
catalog. The health shows a changed catalog file as transport.catalog.disk differs;
restart serve to adopt it.
If the client certificate is expired, the adapter starts no node. A reload with a valid new
certificate then gives staged: the adapter starts a node with the new certificate at once.
Adapter health
A broker outage degrades the adapter only. serve, the browser and local imports stay ready.
The adapter tries again with a backoff from 1 second to 1 minute.
Read the health through the control route ka2a-status. Only the CLI control authority can
read it. Without --ka2a-config, the route does not exist, and the answer is not_found:
WS=/srv/notarizing/ws
ADDR=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["control_addr"])' "$WS/control.json")
curl -s -X POST \
-H "Authorization: Bearer $(cat "$WS/control.token")" \
-H 'Content-Type: application/json' \
--data '{}' "http://$ADDR/control/v1/ka2a-status"
The answer has these fields:
state:starting,ready,degraded,unavailableorstopped.reasonandlast_error: why the state is notready.transport: the live broker state (reachable,unreachableorunknown) with the class of the last failure, and the result of the mailbox check at the start of the node.transport.brokeralso haslast_denial_at,denial_scopeanddenialsof broker authorization denials.transport.client_certificatehas thestateof the client certificate (none,valid,expiringwithin 30 days,expiredornot_yet_valid) and itsnot_beforeandnot_aftertimes.transport.revocation_listshas thestateof the broker revocation lists (none,current,duewithin 24 hours,stale) and their earliestnext_update.transport.credentialshas theresultof the last credential reload (none,reloaded,stagedorrefused), itsclass, its time and the countsreloadsandrefusals.transport.catalogcompares the catalog file on disk with the running catalog:diskissame,differs,refusedornot_checked. The health never shows a path, a key or a password.requests: the request counters of the node.
A degraded state with the reason broker_unreachable means that no broker answers. Local
work continues. Fix the broker connection; the adapter recovers without a restart.
If the client certificate is expired or not yet valid, the adapter opens no broker
connection. The state is unavailable with the reason client_certificate_expired or
client_certificate_not_yet_valid. Renew the certificate and run notarizing ka2a reload.
notarizing doctor also reads this health while serve runs. It shows the check
ka2a adapter and, with a client certificate, the check ka2a client certificate. With
crl_file it shows ka2a revocation lists, and after a broker connection that failed with
tls_failed it shows ka2a broker TLS. After a reload it shows ka2a credentials, and after
a catalog check ka2a catalog. An adapter
problem is a warning, never an error.
Semantic search provider
Hybrid search is off by default. It uses a local Ollama server only. Notarizing never downloads a model and never calls a hosted service.
Install the model in Ollama yourself. Read its digest from
/api/tags.Configure the provider. The command makes no network call:
notarizing search provider set --endpoint http://127.0.0.1:11434 --model nomic-embed-text --digest SHA256 --dimension 768Check the provider:
notarizing doctor --check-providerBuild the semantic index of a target:
notarizing search index --target tgt_ID --semantic
If the provider fails, returns bad vectors or has another model digest, search gives lexical results with a warning. To turn hybrid search off:
notarizing search provider disable
MCP for agents
notarizing mcp serves read-only tools on standard input and output. It opens the
workspace read-only, so it works while serve runs. It has these tools: list_changes,
get_change_report, get_requirement_history, get_assumption, get_graph_neighborhood,
get_graph_suggestions, compare_snapshots, search_specs and get_receipt. It has no tool
that writes. get_graph_suggestions returns the similar-wording suggestions (may relate) of a
target; a suggestion is inert until a reviewer accepts it.
An example client configuration:
{
"mcpServers": {
"notarizing": {
"command": "notarizing",
"args": ["--workspace", "/srv/notarizing/ws", "mcp"]
}
}
}
- Each response is at most 512 KiB and names its target and coverage.
search_specsis lexical only. It never contacts the embedding provider.- At the end of its input, the server answers the requests that it already read and then exits with 0. If the client does not read the answers within 2 s, the server exits with 1.
- Repository, report and review text in a response is untrusted data. Configure the agent to treat it as data, not as instructions.