Registry stack documentation: machine-readable Markdown.
Index of all pages: https://docs.registrystack.org/llms.txt
Full corpus: https://docs.registrystack.org/llms-full.txt

# Registry Render configuration reference

> Generated fields, accepted values, defaults, and constraints for a Registry Render bundle manifest, its label tables, and the serve runtime configuration.

import ConfigurationFormatChecks from '../../../components/ConfigurationFormatChecks.astro';
import ConfigurationReference from '../../../components/ConfigurationReference.astro';
import reference from '../../../data/generated/render-configuration.json';

## Scope

This grammar is not frozen.

`manifest.yaml` declares the bundle's document types: each one's template, data schema, label
locales, version, and PDF standard. Each `labels/<locale>.yaml` holds one label table, and every
table must carry the same keys. `runtime.yaml` is what `registry-render serve` reads: listener,
package, secret providers, API key, limits, and audit.

The JSON Schema a document type names for its data is yours to write; Render applies it to every
request and has no schema of its own for it. A schema-valid file still needs `registry-render
check`, because rules across files, such as a label key the template reads, are not in these
tables. [Configuration files](../configuration-files/) describes the YAML rules and environment
substitution that apply when serve reads `runtime.yaml`.

Two rules are written as exclusions the tables do not show: each document's `entryFile` and
`schemaFile` is relative to the bundle root and contains no `..`, and `package.root` has no `.` or
`..` segment.

{/* Evidence: crates/registry-render/src/manifest.rs, Manifest;
    crates/registry-render/src/labels.rs;
    crates/registry-render/src/runtime.rs, RenderRuntime;
    crates/registry-render/src/check.rs, run;
    docs/site/scripts/generate-render-configuration.mjs. */}

## Read the tables

`documents[]` means an array item; `labels.*` means a map entry whose name you choose. Required
fields inside an optional object apply when that object is present. Conditional fields depend on
another value, such as `audit.path` on `audit.destination`. `audit.rotateBytes` defaults to
100 MiB and `audit.retentionDays` to 90.

<ConfigurationReference contracts={reference.contracts} prefix="render" />

{/* Generated from src/data/generated/render-configuration.json by
    docs/site/scripts/generate-render-configuration.mjs. Run npm run generate from docs/site. */}

{/* Evidence: crates/registry-platform-audit/src/writer.rs, DEFAULT_AUDIT_ROTATE_BYTES and
    DEFAULT_AUDIT_RETAIN_DAYS. */}

## Seal documents

The tables above list the `signing.seals.<id>` keys and the manifest's per-document `seal`. These
rules are not in the tables.

- Omit `signing` to leave sealing off. With it, at least one seal is required, and `serve` mounts
  `POST /v1/seal/{type}`. Without it, that path is an unknown route and `/ready` has no `signing`
  member.
- `signing` needs `authentication.oidc`. Each seal has exactly one of `timestampAuthority` (B-T) and
  `withoutTimestamp: seal-stops-validating-when-certificate-expires` (B-B).
- `timestampAuthority.url` is `https`; `http` is accepted only for `localhost`, `127.0.0.0/8`, and
  `[::1]`. `attemptTimeoutMilliseconds` is 1000 to 60000 and defaults to 10000.
- `timestampAuthority.caBundleRef` is a secret reference to a PEM CA bundle that verifies the
  authority's TLS certificate in place of the platform's trusted roots. Omitted, the platform's
  trusted roots verify it.
- The timestamp authority must answer in DER, as RFC 3161 specifies; a BER answer, such as one
  with indefinite lengths, is refused as `malformed-response`. Its token must be signed with
  RSA PKCS#1 v1.5 or ECDSA over SHA-256, SHA-384, or SHA-512; RSA-PSS and SHA-1 are refused as
  `token-signature-invalid`. Its signer must carry an ESS signing-certificate attribute (v1 or
  v2, RFC 5816) naming the authority's certificate, or the token is refused as
  `tsa-certificate-not-named-by-token`. Seal one test document through each authority before you
  rely on it for B-T.
- `certificateChainFile` is an absolute path with no `.` or `..` segment. The file is PEM, the
  seal certificate first.
- `assuranceProfile` is a top-level runtime member, `local` or `production`, the same member and
  values Evidence uses. It is required when `signing.seals` declares a seal, and unused otherwise.
  Under `production` every seal signs through a `transit` signer; under `local` every seal signs
  with a `local-jwk` key file. A seal with the other signer is refused as
  `render.runtime.signer-assurance-mismatch`, at its `signer.type`. A runtime without seals
  needs no `assuranceProfile`.
- `signer` names how the seal key signs. `type: transit` signs through a Vault or OpenBao Transit
  signing proxy on the Render host: `unixSocketPath` is the proxy's socket, an absolute path with no
  `.` or `..` segment; `mount` and `keyName` are local identifiers naming the key; `keyVersion`
  pins one version of it; and `attemptTimeoutMilliseconds` (1 to 30000, required) bounds each call.
  The key is ECDSA P-256, so the seal signs ES256, and it must be non-exportable, not derived, and
  without plaintext backup. The proxy holds the Transit token; Render sends none.
- `type: local-jwk` is for `assuranceProfile: local`: `privateKeyRef` is a file-provider secret reference
  (`secret:file/<name>`) to one ES256, ES384, RS256, or RS384 private JWK, and EdDSA is refused. An
  environment reference is refused as `render.runtime.seal-key-requires-file-provider`.
- `registry-render keygen --key <file> --csr <file> --common-name <name>` writes a seal key in the
  `local-jwk` form, owner-only, and a PKCS #10 certificate request for it. `--organization` and
  `--country` add to the request's subject, and `--algorithm` picks `es256` (the default), `es384`,
  `rs256`, or `rs384`. `--from-pem <file>` converts an unencrypted PKCS #8 key instead of
  generating one; encrypted PKCS #8, SEC 1, PKCS #1, and PKCS #12 keys are refused with the
  `openssl` command that converts each. In place of `--key`, `--transit-unix-socket-path <socket>
  --transit-mount <mount> --transit-key-name <key> --transit-key-version <version>` names a Transit
  key, as a `transit` signer does: keygen reads its public key, has Transit sign the request, and
  writes the request alone. Keygen never overwrites a file and never prints a key.
- `allowedClients` is a list of at least one named client. `unrestricted` and any item containing
  `*` are refused. When `authentication.oidc.allowedClients` lists clients, each seal client must be in it.
- `assuranceProfile: local` sets `signing.testOnly` in the template payload, and every answer about the
  seal says so: `testOnly: true` in the JSON `seal` member, the `/ready` entry, and each audit entry
  of the call, and `X-Registry-Seal-Test-Only: true` on the PDF answer. `serve` logs one `test-only
  seal` warning per such seal at startup. Under `assuranceProfile: production` a seal carries none of these.
- A document type's `seal` must name a seal in `signing.seals`. A package that names an unbound
  seal is refused at `serve`; `check` reports it, and warns about a runtime seal no document type
  names.
- The seal is a certification signature (DocMDP permission 2) in an invisible field named
  `RegistrySeal`. The space it reserves is the certificate chain, twice the seal certificate's
  issuer name and serial number, and 2112 bytes of structure and signature allowance, plus 16 KiB
  when timestamped. A chain that needs more than 256 KiB is refused.

### Runtime and check problem codes

Serve and `check` report these as runtime problems at the position of the key.

| Code | Meaning |
|---|---|
| `render.runtime.signing-requires-oidc` | `signing` is declared with the legacy API key. |
| `render.runtime.no-seals` | `signing.seals` is empty. |
| `render.runtime.relative-path` | `certificateChainFile`, or a `transit` signer's `unixSocketPath`, is relative or has a `.` or `..` segment. |
| `render.runtime.seal-timestamp-undecided` | Neither `timestampAuthority` nor `withoutTimestamp` is declared. |
| `render.runtime.seal-timestamp-conflict` | Both are declared. |
| `render.runtime.insecure-timestamp-url` | The timestamp URL is `http` and not loopback. |
| `render.runtime.seal-clients-not-listed` | `allowedClients` is missing, empty, or not a list. |
| `render.runtime.seal-client-wildcard` | A client is `unrestricted` or contains a wildcard. |
| `render.runtime.seal-client-not-admitted` | A seal client is not in `authentication.oidc.allowedClients`. |
| `render.runtime.seal-key-requires-file-provider` | `signer.privateKeyRef` names the environment provider; a seal key is read through the file provider only. |
| `render.runtime.signer-assurance-mismatch` | A seal's signer does not suit `assuranceProfile`: `local` signs with `local-jwk`, `production` signs through `transit`. |
| `render.runtime.seal-not-bound` | A document type names a seal `signing.seals` does not bind (error). |
| `render.runtime.seal-unused` | A runtime seal is named by no document type (warning). |
| `render.runtime.seals-not-served` | Document types name seals and the runtime has no `signing` block: they render unsealed (warning). |

`check --environment` and `serve` read the chain file and report:

| Code | Severity | Meaning |
|---|---|---|
| `render.runtime.seal-chain-unreadable` | error | The chain file cannot be read. |
| `render.runtime.seal-chain-invalid` | error | The file is not a usable certificate chain: its certificates do not link in order, or its first certificate, taken as the seal certificate, is a CA certificate (`basicConstraints` `CA:TRUE`), which validators refuse as a document's signer. Seal with a leaf issued with `CA:FALSE`; for a test seal, a test root of your own issues it. |
| `render.runtime.seal-certificate-usage` | error | The seal certificate has neither the digitalSignature nor the nonRepudiation key usage. |
| `render.runtime.seal-chain-too-large` | error | The chain exceeds the signature space a seal may reserve. |
| `render.runtime.seal-certificate-expired` | warning | The certificate is expired; the seal is unavailable. |
| `render.runtime.seal-certificate-not-yet-valid` | warning | The certificate is not yet valid; the seal is unavailable. |
| `render.runtime.seal-certificate-expiring` | warning | The certificate expires within 30 days. |
| `render.runtime.seal-certificate-purpose` | warning | The certificate's extended key usage names none of document signing (RFC 9336 or Microsoft's), Adobe Authentic Documents, email protection or anyExtendedKeyUsage; validators such as Acrobat flag its seals. `serve` logs it at startup too. |

Each sealed document type is checked against its `pdfStandard`, at its `seal` member in
`manifest.yaml`:

| Code | Severity | Meaning |
|---|---|---|
| `render.bundle.seal-space-exceeds-standard` | error | The seal reserves more signature space than the standard's string limit holds: 65535 bytes under PDF/A-1, 32767 under PDF/A-2 and PDF/A-3. `check --environment` reports it, and `serve` refuses to start. |

Without `--environment`, `check` reads no chain file and says so: the human output ends with
`seal chains not read: pass --environment to check them`, and the JSON report has
`"sealChains": "not-read"`.

`serve` alone also reads the key and the timestamp authority's CA bundle, and reports
`render.runtime.seal-key-unreadable` (with the secret provider's reason; a key file must be a
regular file owned by the serving user, with mode 0600 or 0400 and one link),
`render.runtime.seal-key-invalid`, `render.runtime.seal-key-unsupported`,
`render.runtime.seal-key-mismatch` (the key file, or the Transit key version, is not the key the
certificate certifies), `render.runtime.seal-transit-unavailable` (the Transit proxy does not
answer, refuses the key read, or names a key that is not a non-exportable, non-derived
`ecdsa-p256` key without plaintext backup, or a version outside its live versions; checked by
reading the key and signing once),
`render.runtime.seal-timestamp-ca-bundle-unreadable` (with the secret provider's reason),
`render.runtime.seal-timestamp-ca-bundle-invalid` (the bundle is not PEM, holds no certificate,
or is too large), and `render.runtime.seal-timestamp-client` (the timestamp client cannot be built
from the URL).

A seal that cannot load refuses `serve` at startup as `runtime-invalid` (exit code 20), whichever
piece failed, with each error at its member's line in the runtime file, or, for
`render.bundle.seal-space-exceeds-standard`, at the document type's `seal` member in the
package's `manifest.yaml`. `seal-certificate-invalid` (27) and `signing-unavailable` (26) answer
seal requests only.

### The seal route

`POST /v1/seal/{type}` takes the render route's request body and returns the document sealed. It
requires the `render:seal` scope, which `render:render` does not imply, from a client the seal's
`allowedClients` names. The legacy API key never seals. The signing time is the server's clock,
truncated to seconds. `GET /v1/documents` names the seal of each document type this route takes
as `seal`; a document type without it is refused here with `document-not-sealable`.

The default answer is `application/pdf` with these headers. Every digest is labelled,
`sha256:<hex>`:

| Header | Value |
|---|---|
| `X-Registry-Document-Digest` | SHA-256 of the first revision, the document Render rendered. |
| `X-Registry-Signed-File-Digest` | SHA-256 of the whole signed file. |
| `X-Registry-Data-Digest` | SHA-256 of the canonical request data. |
| `X-Registry-Document-Version` | Document type and version. |
| `X-Registry-Package-Digest` | The package digest, in the form `package.expectedDigest` takes. |
| `X-Registry-Warning-Count` | Number of render warnings. |
| `X-Registry-Seal-Id` | The seal id. |
| `X-Registry-Seal-Level` | `B-T` or `B-B`, the baseline level names of ETSI EN 319 142-1. |
| `X-Registry-Certificate-Digest` | SHA-256 of the seal certificate's DER. |
| `X-Registry-Timestamped-At` | The timestamp authority's `genTime`, RFC 3339 at the authority's precision; B-T only. |
| `X-Registry-Seal-Test-Only` | `true`; only for a seal under `assuranceProfile: local`. |

With `Accept: application/json` the body is `pdfBase64`, `documentDigest`, `signedFileDigest`,
`dataDigest`, `documentVersion`, `packageDigest`, `warnings`, and `seal` (`id`, `level`,
`certificateDigest`, `timestampedAt` for B-T, and `testOnly: true` for a test-only seal). A
client granted only `render:seal` pins the package from `packageDigest` without reading
`/v1/documents`. There is no `pdfDigest`. Because the payload carries the `signing` member, a
sealed render's `dataDigest` differs from a plain render of the same request.

The problems specific to this route, from a closed vocabulary shared with the render route:

| Problem | HTTP | Exit code | Meaning |
|---|---|---|---|
| `forbidden` | 403 | 23 | The credential lacks `render:seal`, or is the legacy API key. |
| `seal-client-not-allowed` | 403 | 30 | The verified client is not in the seal's `allowedClients`. |
| `document-not-sealable` | 400 | 29 | The document type names no seal. |
| `signing-unavailable` | 503 | 26 | The seal's key provider is not ready. |
| `seal-certificate-invalid` | 503 | 27 | The seal certificate is expired, not yet valid, or cannot sign. |
| `timestamp-failed` | 502 | 28 | The timestamp authority gave no usable token. No file is returned and no retry is made. |
| `rate-limited` | 429 | 18 | As many seals as `limits.maximumConcurrentRenders` are already in flight. The call is refused, not queued. |

`/ready` adds `signing`, one member per seal: `{"status": "ready"}` or `{"status": "not-ready",
"reason": ...}` with `certificate-expired`, `certificate-not-yet-valid`, or `signer-not-ready`,
plus `"testOnly": true` for a test-only seal. It never changes the status code. A `transit`
seal turns `signer-not-ready` when a signature fails; `/ready` calls and seal requests then start
probes that have Transit sign one probe message, and the seal can sign again once a probe
succeeds, with no restart. A seal has at most one probe in flight, and after a failed probe the
next starts no sooner than one second after it ended. `/ready` starts the probe and never waits
for it, so a recovery shows on a later call; a seal request waits for the probe it starts. At
startup, `serve` logs a `seal unavailable` warning with `seal_id` and the same `reason` for each
seal that cannot sign, and a `seal certificate expiring` warning for one whose certificate expires
within 30 days.

Each seal call adds `sealId` to its audit entries, and `testOnly: true` when the seal is
test-only. The response entry adds `documentDigest`,
`signedFileDigest`, `certificateDigest`, and, for B-T, `timestampTokenDigest`, each labelled
`sha256:<hex>`.

A `timestamp-failed` response entry adds `reason`, the failing check, and the log has one
`seal refused` warning with the same `reason`. Neither carries a value from the exchange.

| `reason` | Piece |
|---|---|
| `connection-failed`, `timed-out` | The authority could not be reached within the attempt timeout. |
| `http-status`, `not-granted` | The authority answered with an HTTP error, or a status that grants no token. |
| `not-a-timestamp-reply`, `malformed-response`, `missing-token`, `not-tst-info`, `malformed-tst-info` | The answer is not a DER timestamp reply carrying a token. |
| `message-imprint-mismatch`, `nonce-mismatch` | The token does not answer this request. |
| `token-signature-invalid`, `tsa-certificate-not-named-by-token`, `tsa-certificate-not-for-timestamping`, `tsa-certificate-not-valid-at-gen-time` | The token does not verify under a timestamping certificate it names and that is valid at its time. |
| `token-not-embeddable`, `gen-time-not-rfc3339` | The token cannot be added to the signature. |
| `invalid-token` | Any other refusal of the token. |
| `response-too-large`, `token-too-large` | The answer exceeds 64 KiB, or the token does not fit the space the seal reserves. |

The key tables above are generated from the runtime schema by
`docs/site/scripts/generate-render-configuration.mjs`; this section is hand-written.

{/* Evidence: crates/registry-render/src/runtime.rs, SigningRuntime, SealRuntime, signing_findings
    and seal_binding_findings;
    crates/registry-render/src/seal.rs, SIGNATURE_ALLOWANCE, STRUCTURE_ALLOWANCE,
    TIMESTAMP_TOKEN_ALLOWANCE, contents_capacity, check_chain and Seal::load;
    crates/registry-render/src/pdf_signature.rs, MAX_CONTENTS_CAPACITY;
    crates/registry-render/src/server.rs, seal_route, respond_sealed and ready;
    crates/registry-render/src/problem.rs, ProblemKind, exit_code and http_status;
    crates/registry-render/src/openapi.rs, OPENAPI_JSON;
    crates/registry-render/src/audit.rs, RenderAuditEvent::for_seal. */}

## Check commands

<ConfigurationFormatChecks product="render" />

## Next

- [Render your first document](../../tutorials/first-render-document/)
- [Deploy Registry Render](../../operate/registry-render/)