Skip to content
Registry Stack Docs Latest

Operator configuration reference

View as Markdown

This reference describes how to assemble a deployable Registry Notary configuration. It is written for teams adopting the service, not for contributors changing the implementation.

Registry Notary is config driven. The YAML file describes which claims can be evaluated, which upstream registries are contacted, how callers authenticate, how credentials are signed, and which operational stores are used. Secrets should stay in environment variables or a secret manager; config fields name the environment variable to read.

Before editing YAML, decide these items:

DecisionUse this whenMain config
Machine-to-machine APIA backend service calls Notary for evaluation or issuanceauth.mode: api_key
Citizen or wallet flowsA user-held OIDC token identifies the subjectauth.mode: oidc, self_attestation
SD-JWT VC issuanceNotary signs credentials from evaluated claimsevidence.signing_keys, evidence.credential_profiles
OID4VCI wallet facadeA wallet requests credentials directlyoid4vci, self_attestation
Multi-instance deploymentMore than one Notary process serves trafficreplay.storage: redis, usually credential_status.storage: redis
Credential suspension or revocationVerifiers need a live status URLcredential_status.enabled: true
Audit retentionOperators need traceability without raw personal dataaudit
OpenFn sidecar readsA target system needs pinned adaptor execution or normalization outside Notaryconnector: openfn_sidecar, retry_on_5xx: false
OpenFn sidecar assuranceNotary must fail closed unless it is talking to the approved sidecar runtimesource_connections.<id>.expected_sidecar
OpenFn batch matchingBatch evaluation should share one OpenFn sidecar read across compatible itemsbulk_mode: openfn_sidecar_batch, binding query_fields

Start with one narrow claim, one source connection, one signing key, and one credential profile. Add federation, wallet issuance, and batch evaluation after the basic path passes doctor.

BlockPurposeRequired for startup
serverBind address and process HTTP settingsNo, defaults are present
authCaller authentication and scope mappingYes
deploymentOperator-declared deployment profile and gate waiversNo, an undeclared profile binds no gates
auditRedacted audit envelope sink and HMAC secretRecommended for every deployable environment
config_trustDurable local state for governed config applyNo, only for signed governed config
evidenceClaims, sources, rules, formats, signing keys, and credential profilesYes
celOptional CEL worker policy, limits, and regex postureDefaults are present
replayOne-time-use store for federation request JWTs, OID4VCI nonces, and holder proof JWTsDefaults to in-process memory
credential_statusOptional storage-backed lifecycle status URL for issued credentialsNo
self_attestationOIDC-bound citizen request policyOnly for citizen or wallet flows
oid4vciWallet-facing OpenID4VCI facadeOnly for wallet flows
federationStatic-peer delegated evaluationOnly for federation

Unknown fields are rejected. That is intentional: a misspelled field should fail at config validation instead of becoming an accidental open policy.

The deployment block lets an operator declare the assurance shape of a deployment. The profile is always declared by the operator and is never inferred from environment name, hostname, or network position.

deployment:
profile: production # local | hosted_lab | production | evidence_grade
multi_instance: true # declares this instance shares a workload with others
waivers:
- finding: notary.source.private_network_escape
reason: "approved internal source for partner pilot, ticket OPS-123"
expires: 2026-09-30
FieldPurpose
profileThe declared assurance shape. Absent means undeclared.
multi_instanceOperator declaration that this instance runs active-active with peers, which makes shared, durable replay storage mandatory.
waiversPer-finding suppressions, each with a mandatory reason and expiry.

Profiles:

ProfileUse
localDevelopment, demos, tests, local pilots. Binds no gates.
hosted_labShared demos, partner evaluations, hosted validation.
productionReal integrations handling sensitive or operational data.
evidence_gradeDeployments where the evidence trail is itself part of the assurance claim.

An undeclared profile binds no gates and keeps current behavior. The posture report then carries a single deployment.profile_undeclared warning so the gap is visible without breaking the deployment. An invalid profile value fails startup, so a typo cannot silently disable enforcement.

Each gate evaluates to one of four severities under the declared profile:

SeverityEffect
startup_failThe process refuses to start. Never waivable.
readiness_failThe readiness endpoint reports not-ready; the process runs.
finding_errorA posture finding, error class.
finding_warnA posture finding, warn class.

The gates bound for Registry Notary:

Finding idConditionhosted_labproductionevidence_grade
notary.replay.in_memory_high_riskIn-memory replay while federation, OID4VCI pre-authorized code, holder proof, wallet traffic, or multi_instance is declarederrorreadiness_failstartup_fail
notary.audit.sink_missingNo durable, retained audit sinkerrorstartup_failstartup_fail
notary.source.insecure_urlSource connection over a plain http:// URL with no localhost or private-network allowanceerrorreadiness_failstartup_fail
notary.source.private_network_escapeA source enables the private-network escape hatchwarnerrorerror
notary.sidecar.expected_sidecar_missingAn OpenFn source omits expected_sidecarwarnerrorreadiness_fail
notary.admin.shared_exposureThe admin surface shares the public listenererrorreadiness_failstartup_fail
notary.openapi.publicOpenAPI is served without authenticationwarnerrorerror
notary.config.unsignedLocal YAML config rather than signed governed configwarnerrorstartup_fail

A waiver names exactly one finding id, a free-text reason, and a mandatory expires date (YYYY-MM-DD). While active, a waiver changes a triggered finding’s status to waived in posture instead of applying its severity effect.

  • startup_fail gates are never waivable. A waiver for one is rejected at config load, because running at all would falsify the declared profile.
  • An expired waiver stops suppressing its finding and additionally raises deployment.waiver_expired in posture, so lapsed approvals surface rather than silently persisting.
  • Waiver reasons appear in the restricted-tier posture for review. Never put a secret in a reason.

Active waivers and gate findings appear in the admin posture document under the deployment object, and the eight-field audit assurance vocabulary appears under the top-level audit object. See docs/security-assurance.md for the assurance vocabulary.

Config files should contain names, not secret values.

NeedConfig fieldEnvironment value
API key or bearer-token authauth.api_keys[].fingerprint, auth.bearer_tokens[].fingerprintsha256:<hex> fingerprint
Static upstream source tokenevidence.source_connections.<id>.token_envRaw upstream bearer token
OAuth2 client credential source authsource_auth.client_id_env, source_auth.client_secret_envOAuth client id and secret
Local JWK signing keyevidence.signing_keys.<id>.private_jwk_envPrivate Ed25519 JWK JSON
Watched local JWK signing keyevidence.signing_keys.<id>.path with provider: file_watchPrivate Ed25519 JWK JSON in a host-local file
Publish-only JWKevidence.signing_keys.<id>.public_jwk_envPublic JWK JSON
Publish-only deadlineevidence.signing_keys.<id>.publish_until_unix_secondsOptional public metadata, not a secret
PKCS#11 PINevidence.signing_keys.<id>.pin_envHSM token PIN
Audit hashingaudit.hash_secret_envStable high-entropy HMAC secret
Redis storesreplay.redis.url_env, credential_status.redis.url_envRedis connection URL

Use registry-notary hash-api-key --print-secret to generate a local API key and its hash. Store only the hash in the environment variable referenced by config; give the plaintext key only to the caller.

registry-notary doctor --config <path> validates active PKCS#11 signing keys by loading the configured module, opening the token, checking the private-key lookup, and running the startup self-test. Run registry-notary build-info on the deployed artifact to confirm the pkcs11 capability is compiled in before debugging token or vendor module configuration.

For local no-restart key material refresh, use provider: file_watch with a host-local private JWK file path. The file is read at startup and re-read on signing use. A valid replacement with the same configured kid, alg, and public JWK identity is picked up without process restart; a malformed, missing, or different-public-key replacement marks the key degraded and keeps the last good signer serving. Use a new kid and governed config change for real key rotation.

For local development, the binary accepts --env-file. For shared environments, prefer the platform secret store and avoid checking dotenv files into the repository.

Most deployments can skip this section. config_trust is optional; it governs signed, threshold-approved config changes for high-assurance deployments. Simple local deployments omit it and keep using the local YAML loaded at startup.

This governed example is syntactically valid but illustrative. Generate the tuf_root_sha256 and targets-role signer key IDs from your own trusted TUF repository before using governed apply in an environment.

config_trust:
antirollback_state_path: /var/lib/registry-notary/config-antirollback.json
local_approval_state_path: /var/lib/registry-notary/config-local-approvals.json
break_glass_rate_limit:
max_accepted: 1
window_seconds: 3600
remote_tuf_repositories:
- root_path: /etc/registry-notary/tuf/metadata/1.root.json
metadata_base_url: https://config.example.gov/metadata
targets_base_url: https://config.example.gov/targets
datastore_dir: /var/lib/registry-notary/tuf
allow_dev_insecure_fetch_urls: false
accepted_roots:
- root_id: ops-root
production: false
tuf_root_sha256: sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
valid_from_unix_seconds: 1770000000
valid_until_unix_seconds: 1772592000
signers:
"1111111111111111111111111111111111111111111111111111111111111111":
kid: "1111111111111111111111111111111111111111111111111111111111111111"
enabled: true
roles:
- name: config-admin
threshold: 1
signer_kids: ["1111111111111111111111111111111111111111111111111111111111111111"]
allowed_change_classes: [public_metadata, root_transition]

config_trust is optional. Simple local deployments omit it and keep using the local YAML loaded at startup. Governed config apply requires antirollback_state_path and local_approval_state_path, which must point to durable local state such as a mounted volume. break_glass_rate_limit is the trusted local rolling-window policy for break-glass apply requests; when omitted it defaults to one accepted request per rate-limit identity per hour. accepted_roots uses the shared Registry trust-root shape. Standalone Registry Notary verifies local or remote signed TUF config targets against accepted_roots when the admin request provides a tuf source. Verified TUF targets-role signature key IDs, not target-declared custom metadata, satisfy the role threshold. Inline YAML remains available for verify/dry-run diagnostics. Local TUF sources use root_path, metadata_dir, targets_dir, datastore_dir, and target_name. Remote TUF sources keep the same root_path, datastore_dir, and target_name, and replace local repository directories with metadata_base_url and targets_base_url. Remote sources are recorded as signed_bundle_endpoint; local repository sources are recorded as signed_bundle_file.

remote_tuf_repositories is an operator-controlled allowlist of remote TUF sources that may be submitted in admin apply requests. An apply request whose remote TUF source does not exactly match one of the listed entries (comparing root_path, metadata_base_url, targets_base_url, and datastore_dir) is rejected before any TUF fetch is attempted. This prevents an attacker who can POST to the admin endpoint from directing the Notary to an arbitrary TUF server. When omitted the list is empty and all remote TUF apply requests are rejected. Each entry carries its own allow_dev_insecure_fetch_urls flag; the flag from the matching allowlist entry is always used, never the value in the incoming request. HTTP loopback remote repositories require allow_dev_insecure_fetch_urls: true and are intended only for tests and local development. Production entries must use HTTPS URLs and must set allow_dev_insecure_fetch_urls: false.

Governed bundle metadata may set previous_config_hash as either bare lowercase SHA-256 hex or sha256:<64 lowercase hex>. Notary normalizes both forms at the product boundary before anti-rollback comparison. The canonical form in verification reports, admin API responses, audit events, docs, and mismatch errors is sha256:<64 lowercase hex>. On a true chain mismatch, the error detail includes the expected canonical hash and the received value’s detected format.

For TUF root transition, apply a signed local TUF bundle whose target metadata includes root_transition, changes only config_trust.accepted_roots, keeps the antirollback and local approval paths unchanged, retains existing roots unchanged, and references a matching unexpired local approval. Add the new final tuf_root_sha256 as another local accepted_roots entry before applying bundles that verify through the rotated root. valid_from_unix_seconds and valid_until_unix_seconds are optional local bounds for overlap windows; expired or not-yet-valid roots fail authorization even when TUF verification and signer quorum otherwise succeed.

POST /admin/v1/config/apply can hot-apply governed signed signing-key rotations for credential issuer, pre-authorized access-token, eSignet (an OpenID Connect identity service) client-assertion, and federation response signing paths after TUF verification, trust-root authorization, and local anti-rollback acceptance. It can also hot-apply signing_key_cleanup for expired publish-only keys that are no longer active signing references. Inline config candidates are accepted only by verify and dry-run; apply rejects them with registry.admin.config.inline_apply_rejected. Other signed changes continue to reject with rejected_restart_required, so rejected signed targets do not advance anti-rollback state or change active posture provenance. This restart-required apply result is distinct from unsupported live reload: it means the signed candidate is valid but cannot be hot-applied. Use GET /admin/v1/capabilities with registry_notary:ops_read before automation invokes governed config or reload operations. Standalone Notary does not support resource, table, or runtime config reload; the mounted POST /admin/v1/reload route returns 501 registry.admin.capability.not_supported.

Break-glass apply is available only for signed targets whose target metadata includes the local approval’s emergency_change_class; the approval fields come from the admin request, the rolling-window policy comes from local config_trust.break_glass_rate_limit, and the audit record stores no raw reason text.

This is the smallest useful shape for a backend caller that evaluates one claim from one DCI source and can later issue a credential from that claim.

server:
bind: 127.0.0.1:8081
openapi_requires_auth: true
request_timeout: 30s
request_body_timeout: 10s
http1_header_read_timeout: 10s
max_connections: 1024
auth:
mode: api_key
api_keys:
- id: verifier-service
fingerprint:
provider: env
name: REGISTRY_NOTARY_API_KEY_HASH
commitment: sha256:0000000000000000000000000000000000000000000000000000000000000000
scopes:
- civil_registry:evidence_verification
- registry_notary:credential_issue
audit:
sink: stdout
hash_secret_env: REGISTRY_NOTARY_AUDIT_HASH_SECRET
evidence:
enabled: true
service_id: civil.registry-notary
api_base_url: https://notary.example.gov
source_connections:
civil_registry:
base_url: https://registry.example.gov
token_env: CIVIL_REGISTRY_TOKEN
dci:
search_path: /registry/sync/search
query_type: idtype-value
records_path: /message/search_response/0/data/reg_records
field_paths:
birth_date: /birth_date
signing_keys:
issuer-2026-05:
provider: local_jwk_env
private_jwk_env: REGISTRY_NOTARY_ISSUER_JWK
alg: EdDSA
kid: did:web:notary.example.gov#issuer-2026-05
status: active
issuer-file-watch:
provider: file_watch
path: /run/secrets/registry-notary/issuer.jwk
alg: EdDSA
kid: did:web:notary.example.gov#issuer-file-watch
status: active
credential_profiles:
birth_record_sd_jwt:
format: application/dc+sd-jwt
issuer: did:web:notary.example.gov
signing_key: issuer-2026-05
vct: https://notary.example.gov/credentials/birth-record/v1
validity_seconds: 31536000
allowed_claims:
- birth-record-exists
holder_binding:
mode: did
proof_of_possession: required
allowed_did_methods:
- did:jwk
disclosure:
allowed:
- value
- redacted
claims:
- id: birth-record-exists
title: Birth record exists
version: 2026-05
subject_type: person
value:
type: boolean
inputs:
- name: target.identifiers.national_id
type: string
source_bindings:
birth_record:
connector: dci
connection: civil_registry
required_scope: civil_registry:evidence_verification
dataset: civil_registry
entity: birth_registration
lookup:
input: target.identifiers.national_id
field: UIN
op: eq
cardinality: one
rule:
type: exists
source: birth_record
formats:
- application/vnd.registry-notary.claim-result+json
credential_profiles:
- birth_record_sd_jwt

server.openapi_requires_auth defaults to true. Set it to false only for local testing or controlled tooling environments that need unauthenticated access to /openapi.json.

auth.mode: api_key is for backend integrations. Configure at least one API key or bearer token. Each entry has an id, a committed fingerprint, and scopes. Use the smallest scope set each caller needs. Admin functions, including metrics and credential status mutation, require registry_notary:admin.

auth.mode: oidc is for citizen and wallet flows. When OIDC is selected, auth.api_keys and auth.bearer_tokens must be empty. Configure:

OIDC field names follow the shared Registry service runtime configuration conventions. Removed pre-convention names are rejected before deserialization with an error naming the replacement field.

  • issuer: expected token issuer.
  • jwks_url: HTTPS JWKS URL, or HTTP loopback only with allow_insecure_localhost: true.
  • audiences: accepted access-token audiences.
  • allowed_clients: optional client allow-list.
  • allowed_algorithms: explicit token signing algorithms accepted from the identity provider. Match the provider and do not mix unrelated algorithm families in one deployment.
  • scope_claim, scope_separator, and scope_map: how external token scopes map to Registry Notary scopes.
  • principal_claim: claim used for audit principal identity. The default is sub.

For citizen self-attestation, the OIDC token must also carry a binding claim that Registry Notary uses to derive the requester and target context.

Every source binding references one source_connections entry. A source connection defines the upstream base URL, the authentication method used to contact it, and connector-specific settings.

Use exactly one source authentication mechanism:

  • token_env for a static bearer token.
  • source_auth.type: oauth2_client_credentials for OAuth2 client credentials.

The OAuth2 client-credentials shape is:

source_auth:
type: oauth2_client_credentials
token_url: https://registry.example.gov/oauth2/client/token
client_id_env: DCI_CLIENT_ID
client_secret_env: DCI_CLIENT_SECRET
request_format: json
scope: registry.search

request_format is form by default and may be set to json for sources such as the OpenCRVS DCI demo endpoint.

For DCI sources, check these fields carefully:

  • search_path: path appended to base_url.
  • sender_id, receiver_id, registry_type, registry_event_type, and record_type: envelope values expected by the upstream DCI implementation.
  • query_type: idtype-value for one identifier lookup, or expression when the upstream supports fielded query expressions.
  • records_path: JSON Pointer to the records array in a single response.
  • field_paths: JSON Pointers for fields that the claim rule reads.
  • bulk_mode: leave none until the source contract has been tested. Use dci_batched_search or rda_in_filter only when the upstream supports that access pattern.

For any source binding, query_fields can replace the single-field lookup wire query when the source supports multi-field lookup. registry_data_api sends them as query parameters, and DCI expression sends them inside the DCI query envelope. For openfn_sidecar, Notary sends single reads through the sidecar’s Registry Data API-shaped read endpoint, and sends batch reads through the sidecar’s records:batchMatch endpoint. Leave query_fields empty for the legacy single-field lookup.

For production, leave allow_insecure_localhost and allow_insecure_private_network false unless the deployment review explicitly accepts the private network source. Local demos may use them for loopback or Docker Compose style setups.

Use connector: openfn_sidecar when a target system needs OpenFn adaptor execution, target credential handling, or output normalization outside Notary. The source connection must use static sidecar bearer auth through token_env. Do not configure target-service credentials in Notary; keep them in the sidecar environment or secret store.

For high-assurance deployments, pin the sidecar runtime that Notary is allowed to use with expected_sidecar. Notary reads the private sidecar assurance endpoint before source reads and fails closed when the product identity, environment, stream, config_hash, expression-hash verification, runtime verification, or smoke-check state does not match the pin.

Single-read OpenFn sidecar example:

evidence:
source_connections:
openfn_crvs:
base_url: http://127.0.0.1:9191
allow_insecure_localhost: true
token_env: OPENFN_SIDECAR_TOKEN
retry_on_5xx: false
bulk_mode: none
expected_sidecar:
product: registry-notary-openfn-sidecar
instance_id: civil-registry-sidecar
environment: production
stream_id: openfn-sidecar-runtime
config_hash: sha256:2222222222222222222222222222222222222222222222222222222222222222
require_expression_hashes_verified: true
require_runtime_verified: true
require_smoke_verified: true
claims:
- id: date-of-birth
title: Date of birth
version: 2026-06
subject_type: person
value:
type: date
inputs:
- name: target.identifiers.national_id
type: string
source_bindings:
crvs:
connector: openfn_sidecar
connection: openfn_crvs
required_scope: civil_registry:evidence_verification
dataset: civil_registry
entity: civil_person
lookup:
input: target.identifiers.national_id
field: national_id
op: eq
cardinality: one
fields:
birth_date:
field: birth_date
type: date
required: true
rule:
type: extract
source: crvs
field: birth_date

OpenFn sidecar batch matching example with query_fields:

evidence:
source_connections:
openfn_crvs:
base_url: http://127.0.0.1:9191
allow_insecure_localhost: true
token_env: OPENFN_SIDECAR_TOKEN
retry_on_5xx: false
bulk_mode: openfn_sidecar_batch
bulk_timeout_max_ms: 30000
expected_sidecar:
product: registry-notary-openfn-sidecar
instance_id: civil-registry-sidecar
environment: production
stream_id: openfn-sidecar-runtime
config_hash: sha256:2222222222222222222222222222222222222222222222222222222222222222
require_expression_hashes_verified: true
require_runtime_verified: true
require_smoke_verified: true
claims:
- id: birth-record-exists
title: Birth record exists
version: 2026-06
subject_type: person
value:
type: boolean
operations:
batch_evaluate:
enabled: true
max_subjects: 100
inputs:
- name: target.attributes.given_name
type: string
- name: target.attributes.family_name
type: string
- name: target.attributes.birthdate
type: date
source_bindings:
crvs:
connector: openfn_sidecar
connection: openfn_crvs
required_scope: civil_registry:evidence_verification
dataset: civil_registry
entity: civil_person
lookup:
input: target.attributes.birthdate
field: birthdate
op: eq
cardinality: one
query_fields:
- input: target.attributes.given_name
field: given_name
op: eq
- input: target.attributes.family_name
field: family_name
op: eq
- input: target.attributes.birthdate
field: birthdate
op: eq
matching:
policy_id: civil-person-name-birthdate-v1
method: exact_name_birthdate
target_type: Person
allowed_purposes:
- benefit_eligibility_check
sufficient_target_inputs:
- [target.attributes.given_name, target.attributes.family_name, target.attributes.birthdate]
allowed_target_inputs:
- target.attributes.given_name
- target.attributes.family_name
- target.attributes.birthdate
collapse_matching_errors: true
confidence: high
fields:
national_id:
field: national_id
type: string
required: true
birth_date:
field: birth_date
type: date
required: true
rule:
type: exists
source: crvs

For OpenFn sidecar connections:

  • Set retry_on_5xx: false. Notary does not retry OpenFn worker execution failures.
  • Use bulk_mode: openfn_sidecar_batch only after sidecar contract tests cover per-item not found, exact match, ambiguous match, missing response item, duplicate response item id, worker timeout, worker failure, and output projection.
  • In governed environments, set expected_sidecar on every OpenFn sidecar connection. Local demos may omit it only when the assurance boundary is not part of the test.

See the deployment hardening runbook for network isolation requirements, responsibility boundaries between Notary and the sidecar, and deployment security expectations.

CEL rules are evaluated out of process when the binary is built with registry-notary-cel. The default posture is production-oriented: worker mode, no queueing, bounded worker count, bounded frames, and regex disabled.

cel:
mode: worker
worker_count: 2
eval_timeout_ms: 2000
queue_max: 0
allow_regex: false
max_expression_bytes: 8192
max_binding_json_bytes: 65536
max_result_json_bytes: 16384
max_string_bytes: 16384
max_list_items: 1024
max_object_depth: 16
max_object_keys: 256
worker_memory_bytes: 134217728
worker_stderr_bytes: 1024

Set mode: disabled only when no configured claim uses rule.type: cel. queue_max must stay 0; saturation fails fast so callers can retry or shed load explicitly. Keep allow_regex: false unless the deployment has a reviewed reason to permit regex-capable CEL helpers such as matches, text.regex_extract, text.regex_replace, or validate.matches.

A claim is a public capability. It should describe one thing Notary can evaluate or issue, such as “birth record exists” or “farmer under four hectares”.

Important fields:

  • id: stable machine id used by clients and credential profiles.
  • title, version, subject_type, and value: operator and verifier metadata.
  • inputs: request lookup paths. Supported paths include target.id, target.identifiers.<scheme>, target.attributes.<name>, requester.id, requester.identifiers.<scheme>, requester.attributes.<name>, and relationship.attributes.<name>.
  • source_bindings: upstream reads, lookup fields, required caller scope, and extracted source fields.
  • rule: exists, extract, or cel.
  • depends_on: prerequisite claims for CEL rules that reuse earlier results.
  • operations: enable or cap evaluate and batch_evaluate.
  • disclosure: default and allowed response disclosure modes.
  • formats: response formats the claim can render.
  • credential_profiles: profiles allowed to issue from this claim.

Avoid broad source bindings. A claim should read only the fields needed to evaluate that claim. If two credentials need different fields, prefer two claims or a small dependency graph over one over-broad claim.

Each source binding has an optional matching block that gates and shapes how the request is resolved to a source record before the read runs. The block is the operator control behind identity and record matching; read that page for the concepts and the outcome model. With no matching block, a binding falls back to unrestricted, identifier-only behavior.

source_bindings:
person_record:
connector: registry_data_api
connection: civil_registry
dataset: people
entity: person
lookup:
input: target.attributes.birthdate
field: birthdate
matching:
policy_id: person-name-birthdate-v1
method: exact_name_birthdate
target_type: Person
allowed_purposes:
- benefit_eligibility_check
allowed_relationships:
- self
- guardian
relationship_purpose_scopes:
guardian:
- benefit_eligibility_check
sufficient_target_inputs:
- [target.attributes.given_name, target.attributes.family_name, target.attributes.birthdate]
allowed_target_inputs:
- target.attributes.given_name
- target.attributes.family_name
- target.attributes.birthdate
confidence: high

Fields:

FieldPurposeDefault
policy_idStable label for this policy, returned in the response and audit trailnone
methodStable label for the matching method, returned in the response and audit trailnone
target_typeIf set, the request target.type must equal this valueunenforced
requester_typeIf set, the request requester.type must equal this valueunenforced
allowed_purposesPurposes this binding may be used for; empty means no purpose restriction hereempty
allowed_relationshipsRelationship types this binding acceptsempty
relationship_purpose_scopesPer-relationship purpose allow-list; a scoped relationship used for any other purpose is rejected with granular code relationship.purpose_not_allowedempty
sufficient_target_inputsOR-of-AND groups of target paths; the request must satisfy at least one full groupempty
allowed_target_inputsAllow-list of target paths the binding may read; empty means unrestrictedempty
allowed_requester_inputsAllow-list of requester paths the binding may read; empty means unrestrictedempty
collapse_matching_errorsMap every matching error to public evidence.not_available, keeping the granular reason in audittrue
require_requester_reauthenticationRequire the requester to reauthenticate before this binding readsfalse
confidenceConfidence label returned with a successful matchnone

Notes:

  • sufficient_target_inputs is an OR of ANDs. Each inner list is a complete set of paths that, when all present, is enough to match; the request needs to satisfy any one group. For example, [[national_id], [given_name, family_name, birthdate]] accepts either a national id alone or the full name-and-birthdate triple.
  • allowed_target_inputs and allowed_requester_inputs are minimization controls. A request that supplies a path outside the allow-list is rejected, so a binding cannot over-collect by accident. Leave them empty only for identifier-only bindings that need no attribute minimization.
  • relationship_purpose_scopes narrows named relationships to specific purposes after the flat allowed_purposes and allowed_relationships checks. Each scoped relationship must also appear in allowed_relationships. Relationships with no entry in the map keep the unscoped behavior. When collapse_matching_errors is on, callers see evidence.not_available and the granular code is retained for audit.
  • collapse_matching_errors defaults to on. Turn it off only in a controlled environment where exposing not-found versus ambiguous versus rejected to the caller is acceptable, because those differences can be used as an existence oracle.
  • confidence is a fixed label for the source and method. It is returned verbatim on every successful match and does not measure how strong an individual match was.
  • Config validation rejects blank values: policy_id, method, target_type, and requester_type must be non-empty when present, and the purpose, relationship, relationship purpose scope, and input-path lists must not contain blank entries.

Credential profiles control SD-JWT VC issuance.

Required fields:

  • format: application/dc+sd-jwt.
  • issuer: DID issuer for the credential.
  • signing_key: key id from evidence.signing_keys.
  • vct: credential type URL.
  • allowed_claims: explicit allow-list. Empty allow-lists are rejected.
  • holder_binding: currently implemented holder binding is did:jwk.
  • disclosure.allowed: disclosure modes the profile may carry.

validity_seconds defaults to 600 and must be between 1 and evidence.max_credential_validity_seconds. Keep token, proof, offer, and evidence freshness windows short; set credential validity to the period the issuing agency wants verifiers to treat the wallet-held VC as fresh. For long-lived credentials, enable credential status or another revocation and lifecycle surface.

Signing keys are covered in detail in signing-key-provider.md.

replay.storage: in_memory is acceptable for a single process in local development. It is not acceptable for active-active serving because two processes cannot see the same nonce or proof replay decisions. When the in-memory backend is selected, /ready returns HTTP 503 with status: degraded so operators do not miss the single-process replay posture.

Use Redis for multi-instance deployments:

replay:
storage: redis
redis:
url_env: REGISTRY_NOTARY_REPLAY_REDIS_URL
key_prefix: registry-notary
connect_timeout_ms: 1000
operation_timeout_ms: 500

Key fields: storage is in_memory or redis. redis.url_env names the environment variable containing the Redis connection URL. redis.key_prefix scopes keys for shared clusters. connect_timeout_ms and operation_timeout_ms must both be greater than zero when Redis is configured. Notary fails to start when the named Redis URL environment variable is missing. /ready fails closed when the Redis replay backend is unavailable.

See deployment-hardening-runbook.md for operational expectations, alerting guidance, and when to prefer Redis over in-memory.

Credential status tracks the lifecycle of individual issued credentials so verifiers can check suspension or revocation after issuance. It is disabled by default. Enable it only when verifiers need a live status check beyond credential expiry. base_url must be the public HTTPS issuer origin verifiers can reach; retention_seconds should cover maximum credential validity plus verifier tolerance. Use Redis for any deployment where more than one process can issue credentials or where status records must survive a restart.

See credential-lifecycle-status.md for status semantics, the full config block with all Redis fields, the status payload shape, lifecycle state transitions, privacy boundary, and rollout checklist.

Self-attestation lets a citizen use their own OIDC token to evaluate or issue only the claims that policy allows for the subject bound to that token. It requires auth.mode: oidc. The subject binding is derived from a token claim at request time; conflicting caller-supplied identity context is rejected before any source read. All operations, claims, formats, disclosures, and credential profiles are explicit allow-lists. Batch evaluation is not supported. Credential profiles must use DID holder binding with proof of possession and did:jwk. In-process rate limits are guardrails; public deployments need gateway and identity-provider controls as well.

The config keys unique to this page are: subject_binding.token_claim, subject_binding.normalize (must be exact), subject_binding.allow_sub_as_civil_id, citizen_clients, token_policy ceilings, allowed_operations, allowed_purposes, allowed_claims, allowed_formats, allowed_disclosures, credential_profiles, scope_policy, required_scopes, allowed_wallet_origins, and rate_limits.

See the self-attestation operator guide for the full config blocks, identity-provider requirements, scope policy, wallet origin controls, rate-limit fields, and rollout checklist.

OID4VCI depends on self-attestation. Enable it when a wallet should retrieve Notary-issued credentials through OpenID4VCI-style metadata, offers, nonces, and credential requests. The facade is narrow: credential format is dc+sd-jwt, proof type is JWT with EdDSA, holder binding is did:jwk, and issuance is backed by self-attestation policy.

oid4vci:
enabled: true
credential_issuer: https://notary.example.gov
authorization_servers:
- https://idp.example.gov
accepted_token_audiences:
- registry-notary-wallet
credential_endpoint: https://notary.example.gov/oid4vci/credential
offer_endpoint: https://notary.example.gov/oid4vci/credential-offer
nonce_endpoint: https://notary.example.gov/oid4vci/nonce
nonce:
enabled: true
ttl_seconds: 300
authorization:
require_pkce_method: S256
proof:
max_age_seconds: 300
max_clock_skew_seconds: 60
pre_authorized_code:
enabled: true
pre_authorized_code_ttl_seconds: 300
tx_code:
required: true
input_mode: numeric
length: 6
esignet:
client_id: registry-notary-rp
client_signing_key_id: esignet-rp-key
redirect_uri: https://notary.example.gov/oid4vci/offer/callback
authorize_url: https://idp.example.gov/authorize
token_url: https://idp.example.gov/oauth/v2/token
issuer: https://idp.example.gov
jwks_uri: https://idp.example.gov/.well-known/jwks.json
scopes:
- openid
login_state_ttl_seconds: 300
credential_configurations:
birth_record_sd_jwt:
claim_id: birth-record-exists
credential_profile: birth_record_sd_jwt
format: dc+sd-jwt
scope: birth_record
vct: https://notary.example.gov/credentials/birth-record/v1
display_name: Birth record attestation

Public URLs must use HTTPS except for loopback development. Endpoint URLs must live under credential_issuer, include a path, and have no query string. Each vct must also be a public HTTPS URL and must match the referenced credential profile vct.

authorization.require_pkce_method pins the PKCE challenge method wallets must use. proof.max_age_seconds bounds how fresh a holder proof JWT must be, and proof.max_clock_skew_seconds is the only clock difference tolerated when checking that freshness.

pre_authorized_code.tx_code.required defaults to true. Set it to false only for wallets that cannot send a transaction code. That compatibility mode is reported as bearer_offer in admin posture and validates only when pre_authorized_code_ttl_seconds is at most 300, because the offer URI is then sufficient to redeem the code.

Each credential_configurations entry must be consistent with both the claim and the credential profile it references:

  • claim_id exists in evidence.claims.
  • claim_id is allowed by self_attestation.allowed_claims.
  • credential_profile exists in evidence.credential_profiles.
  • credential_profile is allowed by self_attestation.credential_profiles.
  • The claim references the credential profile.
  • The profile allows the claim.
  • format is dc+sd-jwt.
  • vct matches the credential profile vct.

See the OID4VCI wallet interop guide for the wallet flow sequence, authenticated pre-authorized-code flow details, nonce policy, Type Metadata serving, compatibility checklist, and troubleshooting.

Run config checks before exposing the service:

Terminal window
registry-notary explain-config --config registry-notary.yaml --env-file .env.local
registry-notary doctor --config registry-notary.yaml --env-file .env.local
registry-notary doctor --config registry-notary.yaml --env-file .env.local --live

Use --live only against a test target or a controlled integration environment. When live lookup values are supplied, the doctor output redacts target ids and tokens, but the upstream source still receives a real lookup.

For local VC smoke tests:

Terminal window
registry-notary doctor \
--config registry-notary.yaml \
--env-file .env.local \
--issue-demo-vc
  • Each caller has only the scopes required for its claims and operations.
  • Every source connection has exactly one auth method.
  • Insecure source or JWKS allowances are absent outside local demos.
  • Claims read only required upstream fields.
  • Credential profiles list explicit allowed_claims.
  • Signing keys are active only when they may sign; old public keys are publish_only until their configured publication window ends or verifiers no longer need them.
  • Multi-instance deployments use Redis replay storage.
  • Credential status, if enabled, uses the externally reachable issuer base URL and a shared store.
  • Audit has a stable high-entropy hash_secret_env value and off-host retention.
  • /metrics is scraped with a registry_notary:metrics_read credential and normal network controls.
  • doctor passes without --live, then passes with a controlled live subject.