NovaSeal Configuration Reference

Status: Works today (NovaSeal v0.1+, ADR-0041)

This document is the single source of truth for configuring NovaSeal: the novaseal.yaml config file, all environment variables, path resolution order, and deployment-specific patterns.

Related documents:


Table of contents

  1. Configuration file — novaseal.yaml
  2. Profiles
  3. Merkle log database (merkle_db)
  4. Environment variables — complete reference
  5. Discovery order for novaseal.yaml
  6. Troubleshooting

1. Configuration file — novaseal.yaml

NovaSeal is opt-in: if no novaseal.yaml is found, NovaSeal is silently disabled and nova capture proceeds without signing. This is the correct behavior for local development. nova init never writes this file; the quickest way to create one is nova seal init (§1.2, experimental, ADR-0301).

The file is discovered automatically (see §5). A minimal example for local use with a file-based ECDSA P-256 key:

# ~/.novafabric/novaseal.yaml
profile: local
key_path: ~/.novafabric/seal.key     # ECDSA P-256 private key (PEM, PKCS#8)
cert_path: ~/.novafabric/seal.crt    # X.509 certificate for the key
merkle_db: ~/.novafabric/novaseal-merkle.db  # SQLite Merkle log (optional)
# tsa_url: https://tsa.example.org/tsr  # RFC 3161 timestamp authority — opt-in

All path values are expanded with ~ resolution. profile defaults to local. cert_path is always required, plus the profile's key reference (key_path for local, kms_key_id for aws_kms, vault_url + key_name for azure_kv, key_version_name for gcp_kms); everything else is optional.

Where the file and the log live by default: both defaults sit under $NOVAFABRIC_HOME — $NOVAFABRIC_HOME/novaseal.yaml and $NOVAFABRIC_HOME/novaseal-merkle.db, i.e. ~/.novafabric/… only when NOVAFABRIC_HOME is unset. If NOVAFABRIC_HOME points elsewhere, a ~/.novafabric/novaseal.yaml is not read and sealing is silently off.

RFC 3161 timestamping is opt-in (since v0.103, ADR-0292). With no tsa_url (or tsa_urls) NovaSeal signs and logs every capsule but requests no timestamp and makes no network call; it logs one warning per process saying so, and nova verify reports Timestamp (RFC 3161): NOT PRESENT. To timestamp, name a TSA explicitly — your organisation's or a QTSP's. The public FreeTSA service (https://freetsa.org/tsr) works for development and demos; it is rate-limited, not eIDAS-qualified, and never contacted unless you configure it.

Behaviour change in v0.103. Through v0.102.x an omitted tsa_url silently meant https://freetsa.org/tsr. To keep timestamping exactly as before, add tsa_url: https://freetsa.org/tsr to novaseal.yaml.

1.1 TSA fallback list (tsa_urls)

Status: Works today (REG-ADR-007). When only tsa_url is set it is the only TSA tried; a tsa_urls list on its own also opts in to timestamping. For production and EU-regulated deployments where a single TSA being unreachable should not block sealing, configure an ordered fallback list instead:

# ~/.novafabric/novaseal.yaml
profile: local
key_path: ~/.novafabric/seal.key
cert_path: ~/.novafabric/seal.crt
tsa_urls:                                    # tried in order; first success wins
  - https://tsa.your-qtsp.example/tsr        # primary — e.g. a QTSP for eIDAS Art. 41(2)
  - https://freetsa.org/tsr                  # fallback — public, non-qualified (example)
merkle_db: ~/.novafabric/novaseal-merkle.db

If tsa_urls is omitted, it defaults to a single-entry list containing tsa_url — existing configs with only tsa_url set need no changes. When tsa_urls has more than one entry, each is tried in sequence; a TSA that returns an error, times out, or is unreachable is skipped and the next one is tried. Timestamping is already best-effort in NovaSeal (a capsule seals successfully without a timestamp token if the TSA request fails) — the fallback list only widens how many TSAs must fail before that happens.

tsa_urls applies identically across all four profiles (§2) — it only affects the timestamp request, not the signing key/certificate.

Not yet built: fallback is sequential with no configurable per-TSA timeout or retry count (each URL gets one attempt at the module's fixed 30-second HTTP timeout before moving to the next) — sufficient for a small fallback list, but there is no automated benchmark validating this against a specific deployment's real network conditions (e.g. an air-gapped site with a local relay TSA); that assessment is left to the operator.

ca_bundle — signer certificate chain validation (ADR-0055)

Status: experimental. An optional key, accepted by all four profiles:

ca_bundle: /etc/novaseal/ca-bundle.crt   # operator CA chain (PEM, concatenated)

When set, nova verify adds a Signer certificate chain (CA bundle) check: the certificate embedded in the capsule's DSSE envelope must chain, offline, to a certificate in the bundle (every bundle certificate is a trust anchor, so include the issuing intermediate). nova verify --ca-bundle PATH overrides the key for one run. A configured path that does not exist is a config error — it never silently disables the check. Omit the key and verification is unchanged. Validity is evaluated at the current time; no OCSP. Details: NovaSeal Key Management §2.3.

crl_dir / crl_strict — offline CRL revocation check (ADR-0070 §3)

Status: experimental. Optional, all four profiles; only takes effect together with a CA bundle (ca_bundle here or nova verify --ca-bundle):

crl_dir: /var/lib/novaseal/crl   # CRLs (DER or PEM) synced by an operator cron job
crl_strict: false                # true: a missing / stale / invalid CRL also fails

nova verify then checks every certificate of the validated signer chain against the locally synced CRLs — nothing is ever fetched. A revoked certificate always fails; by default a missing, stale or unverifiable CRL is a visible warning (soft-fail, ADR-0070 §3), and crl_strict: true (or --crl-strict) turns those into failures. --crl-dir DIR overrides crl_dir; --crl-strict can only tighten the config value. A crl_dir that does not exist, an empty string, a non-boolean crl_strict, or crl_dir / crl_strict set without ca_bundle (which would check nothing) is a config error (unless tsa_ca_certs is set — the CRLs then apply to the TSA chain). Directory limits, statuses and scope rules: NovaSeal Key Management §2.3.

tsa_ca_certs — RFC 3161 TSA trust chain (ADR-0070 §1/§5)

Status: experimental. Optional, all four profiles — a list of PEM files holding the TSA CA certificates the operator trusts (NIST SP 800-89: pre-provisioned anchors; no public bundle is shipped or downloaded):

tsa_ca_certs:
  - /etc/novaseal/tsa/freetsa-cacert.pem   # e.g. https://freetsa.org/files/cacert.pem

When set and a capsule carries manifest.dsse.tsr, nova verify adds a TSA certificate chain (RFC 3161, TSA CA bundle) check: the token's CMS signature, the TSA certificate's critical id-kp-timeStamping-only EKU, the ESSCertID binding, the message imprint (SHA-256 of the DSSE envelope) and the chain to these anchors at the token's genTime, all offline. A capsule with no (or an empty) token then fails — the token is outside the DSSE signature, so its absence cannot be told apart from its deletion. crl_dir / crl_strict also apply to this chain (revocation as of genTime, CRL freshness judged at verification time). nova verify --tsa-ca-bundle PATH overrides the key for one run. A listed file that does not exist, an empty list, a non-list value, or more than 32 entries is a config error. Omit the key and verification is unchanged. Details: NovaSeal Key Management §2.5.

Without this key (or --tsa-ca-bundle) a timestamp is never trusted time: a verified token is reported UNANCHORED (timestamp_trust: unanchored), because anyone can mint a TSA certificate and sign any genTime. There is no built-in default TSA trust bundle, and tsa_url anchors nothing — it only names where seals request tokens. With the key set, POST /api/runs/{id}/verify, the Evidence Bundle verify endpoint and the trust radar anchor the token too.

The ADR's crl_cache_dir key is served by the existing crl_dir (one operator-synced directory for both chains); it has no default path — an unset crl_dir means no revocation check, never a silently empty one.

1.2 Generated by nova seal init (local, self-asserted)

Experimental (ADR-0301). nova seal init writes a ready-to-use local profile and the key material it points at, offline:

# managed-by: nova seal init
# Local sealing identity (ADR-0301). SELF-ASSERTED: ...
profile: local
key_path: $NOVAFABRIC_HOME/keys/novaseal/signing.key.pem    # ECDSA P-256, mode 600
cert_path: $NOVAFABRIC_HOME/keys/novaseal/signing.crt.pem   # issued by the local seal CA
merkle_db: $NOVAFABRIC_HOME/novaseal-merkle.db

(The real file carries absolute paths.) The first line marks the file as managed: nova seal init --force may archive and replace it; a file without that line — an operator's KMS or CA-issued profile — is never replaced. The local seal CA is keys/novaseal/ca.crt.pem (ca.key.pem, mode 600, beside it); both certificates carry O=NovaFabric local seal identity (self-asserted).

Trust level nova verify reports How
self-asserted default — proves the key holder signed, identifies nobody
local-ca-pinned nova verify --ca-bundle $NOVAFABRIC_HOME/keys/novaseal/ca.crt.pem — continuity with this installation across --force rotations
ca-anchored only with a certificate from a CA the verifier trusts (§2, your own cert_path)

You may add tsa_url (opt-in timestamps) or ca_bundle to the generated file; it stays managed as long as the first line is kept. Rotation: nova seal init --force (new key, same CA, old files archived to keys/novaseal/archive/<UTC>/, a key_rotation entry in the Merkle log); --force --new-ca also replaces the CA.


2. Profiles

The four profiles below are configured through novaseal.yaml's profile: field. Two further signing backends exist outside this file, selected instead via a CLI flag on nova seal sign/nova verify — a keyless --backend sigstore (ADR-0071) and the ADR-0055 x509 cert-pinned identity (currently a library API, not yet a CLI flag); see NovaSeal Key Management §2.3–2.4 for both.

2.1 local — ECDSA P-256 file key

The default for on-premises and laptop deployments. The private key is an unencrypted PEM file (PKCS#8) on disk — there is no keychain or secrets-manager integration, so protect it with file permissions (0600) and disk encryption. ECDSA P-256 is the default (what nova seal init generates); an Ed25519 key with a matching certificate is also accepted by the local profile.

profile: local
key_path: /path/to/seal.pem      # required — unencrypted PKCS#8 PEM, ECDSA P-256 (or Ed25519)
cert_path: /path/to/seal.crt     # required — PEM certificate
tsa_url: https://tsa.example.org/tsr  # optional, opt-in; omitted = no timestamp, no network call
merkle_db: /path/to/merkle.db    # optional; see §3

nova seal init (§1.2) generates a key, a local CA and a CA-issued certificate for this profile in one step. To bring your own instead, generate a key and self-signed certificate (nova verify reports a self-signed signer as self-asserted, and --ca-bundle cannot pin a self-signed end-entity certificate):

openssl ecparam -name prime256v1 -genkey -noout | \
  openssl pkcs8 -topk8 -nocrypt -out ~/.novafabric/seal.key

openssl req -new -x509 -key ~/.novafabric/seal.key \
  -out ~/.novafabric/seal.crt \
  -days 3650 -subj "/CN=NovaSeal-Local"

2.2 aws_kms — AWS KMS asymmetric key

Uses an AWS KMS ECDSA_SHA_256 key. Requires the [seal-aws] optional extra: pip install novafabric[seal-aws].

profile: aws_kms
kms_key_id: arn:aws:kms:us-east-1:123456789012:key/mrk-abc123  # required
aws_region: us-east-1             # optional; default us-east-1
cert_path: /path/to/cert.pem      # required — certificate for the KMS public key
tsa_url: https://tsa.example.org/tsr  # optional, opt-in (no default TSA)
merkle_db: /path/to/merkle.db     # optional; see §3

2.3 azure_kv — Azure Key Vault

Uses an Azure Key Vault EC P-256 key. Requires [seal-azure].

profile: azure_kv
vault_url: https://myvault.vault.azure.net/  # required
key_name: my-ec-key                          # required
cert_path: /path/to/cert.pem                 # required
tsa_url: https://tsa.example.org/tsr         # optional, opt-in
merkle_db: /path/to/merkle.db               # optional; see §3

2.4 gcp_kms — GCP Cloud KMS

Uses a GCP Cloud KMS EC P-256 asymmetric signing key. Requires [seal-gcp].

profile: gcp_kms
key_version_name: projects/P/locations/L/keyRings/R/cryptoKeys/K/cryptoKeyVersions/1
cert_path: /path/to/cert.pem  # required
tsa_url: https://tsa.example.org/tsr  # optional, opt-in
merkle_db: /path/to/merkle.db    # optional; see §3

3. Merkle log database (merkle_db)

NovaSeal maintains a tamper-evident SQLite Merkle log of all seal operations. The merkle_db field in novaseal.yaml controls where this file lives.

3.1 Path resolution order

Works today. Every NovaSeal caller resolves the Merkle log through one rule (novafabric.trust.novaseal.config.resolve_merkle_db_uri(); the loaded profile's merkle_db is computed by the same rule). That covers capture sealing, nova serve sealing, nova verify, the trust radar and dashboard Seal tab, POST /api/runs/{id}/verify, the dashboard /api/doctor novaseal_db row, the SealTab and nova server policy-store endpoints, the CLI policy-store defaults (nova policy sign / list, nova seal propose / verify, nova export-model-independence --policy-db, POST /api/policy/sign; changed 2026-10-23, see the troubleshooting note below), nova seal log verify, and the key_rotation entry nova seal init --force appends. So the log a capsule is sealed into is the log every verifier reads.

Diagram source (Mermaid)
flowchart TD
    subgraph SEAL["Sealing and verification"]
        A["nova capture · serve sealing<br/>nova verify · trust radar"] --> Y["merkle_db in novaseal.yaml"]
        Y -- "not set" --> H["$NOVAFABRIC_HOME/novaseal-merkle.db"]
    end
    subgraph DIAG["Diagnostics and policy store"]
        B["nova doctor · SealTab<br/>nova seal log verify"] --> E{"NOVAFABRIC_SEAL_DB_PATH<br/>set?"}
        E -- yes --> ENV["that path or DSN"]
        E -- no --> Y2["merkle_db in novaseal.yaml"]
        Y2 -- "not set" --> H2["$NOVAFABRIC_HOME/novaseal-merkle.db"]
    end
    subgraph PROMO["Promotion workflow"]
        P["nova seal propose · approve · bypass"] --> DB["--db<br/>default ~/.local/share/novafabric/merkle.db"]
    end

Precedence (highest to lowest):

Priority Source When to use
1 NOVAFABRIC_SEAL_DB_PATH env var Process-wide override: CI/test environments, or a job that must seal into a specific log. A path or a Postgres DSN (§3.4)
2 merkle_db: field in novaseal.yaml Production config — set this for non-default paths
3 $NOVAFABRIC_HOME/novaseal-merkle.db (default ~/.novafabric/novaseal-merkle.db) Default for local laptop use

If novaseal.yaml is not found, or is not valid YAML, priority 2 is skipped and priority 3 applies. A merkle_db (or NOVAFABRIC_SEAL_DB_PATH) that is neither a file path nor a supported DSN is a config error (§3.4); it is never used as a file name. Loading the config creates nothing on disk. The SQLite file and its directory are created the first time something appends to the log.

nova seal init writes merkle_db: $NOVAFABRIC_HOME/novaseal-merkle.db. nova seal init --force keeps a merkle_db you set in the managed file, and records the rotation in the log this rule resolves.

Before 2026-10-22 capture sealing, nova verify and the radar ignored NOVAFABRIC_SEAL_DB_PATH, while the SealTab and nova seal log verify honoured it. With the variable set, capsules were sealed into one log and checked against another.

3.2 Docker and container deployments

In Docker the default home directory and NOVAFABRIC_HOME are typically different from the host. Set merkle_db explicitly in novaseal.yaml so the path always resolves to the mounted volume:

# /data/nova/novaseal.yaml  (mounted into the container)
profile: local
key_path: /data/nova/keys/seal.key
cert_path: /data/nova/keys/seal.crt
merkle_db: /data/nova/novaseal-merkle.db
# docker-compose.yml (excerpt)
services:
  nova-serve:
    environment:
      NOVAFABRIC_SEAL_CONFIG: /data/nova/novaseal.yaml
    volumes:
      - nova-data:/data/nova

Setting NOVAFABRIC_SEAL_CONFIG ensures the config is found regardless of where the container's home directory or NOVAFABRIC_HOME is.

3.3 SLURM and shared-filesystem deployments

⚠ Do not put the SQLite Merkle log on a shared network filesystem (Lustre, GPFS, NFS) or share one log between concurrent jobs. SQLite's locking and WAL mode do not work reliably over a network filesystem. Use one log per node (a node-local merkle_db), for example:

# /home/$USER/.novafabric/novaseal.yaml
profile: local
key_path: /lustre/project/novafabric/keys/seal.key
cert_path: /lustre/project/novafabric/keys/seal.crt
merkle_db: /tmp/$USER/novafabric/novaseal-merkle.db   # node-local, one writer

Several processes on one host may append to one local SQLite log: each append takes the write lock before it picks its leaf index, and waiters queue on SQLite's busy timeout. Before 2026-10-22 two captures sealing at the same moment could pick the same index, and one capsule was left unsealed with only a warning (measured: 10 of 200 appends from 8 processes). That does not make a network filesystem safe. Shared, concurrent appends across nodes are what the experimental Postgres backend (§3.4) is for.

3.3a previous_merkle_dbs — moving a host to v2 roots (ADR-0313)

Status: experimental. A log keeps one root version for life, so moving a host from v1 to v2 roots means starting a new merkle_db file. List the retired log so verification can still check the capsules it sealed:

merkle_db: ~/.novafabric/novaseal-merkle-v2.db
previous_merkle_dbs:
  - ~/.novafabric/novaseal-merkle.db      # the retired v1 log

Verification only — nothing is ever appended to a previous log. Entries are SQLite paths or Postgres DSNs; a listed SQLite file that does not exist, a non-list value, or more than 32 entries is a config error.

Without it, a v1 capsule verified on a host whose log (holding this signer's seals) is v2 keeps its carried-proof result but reports sealer_log: unverifiable-other-version, and nova verify --strict fails on it. The root_version that says "v1" is a field of log-entry.json, which whoever wrote the capsule controls, so a key holder's forgery claiming v1 looks exactly like a genuine pre-migration capsule until the v1 log is consulted. A v2 capsule checked against a v1 sealer's log fails: v2 logs came later.

3.4 Postgres backend (experimental, Scale-S4)

Experimental — implemented (ROADMAP Scale-S4). SQLite remains the default; the Postgres backend is opt-in via a Postgres DSN. It needs the seal-postgres extra (psycopg 3).

merkle_db: postgres://user:pass@pghost/novafabric_seal

Accepted schemes: postgres://, postgresql://, and the SQLAlchemy spelling postgresql+psycopg:// (rewritten to postgresql://, the form psycopg connects with). A DSN stays a string from the config to the connection. Any other scheme:// value — postgresql+psycopg2:// (a driver NovaFabric does not ship), mysql://, or a DSN missing a slash (postgres:/…) — is a SealConfigError, never a file path. The NOVAFABRIC_SEAL_DB_PATH env var accepts the same values.

Secrets. NovaFabric never writes the DSN to disk (no directory or file is created from it) and never prints its password: error messages, nova seal log verify and the /api/doctor row show user:***@host. Prefer keeping the password out of the URL altogether, in ~/.pgpass or the PGPASSWORD environment variable, which libpq reads.

Before 2026-10-22, a DSN in merkle_db was wrapped in a file path, which turned // into /. Loading the config then created a directory tree named after the DSN, password included, in the current directory. Capture sealing silently wrote to a SQLite file in that tree instead of Postgres. If you configured a DSN before that, look for and delete a postgres: or postgresql: directory in the directories you ran nova from.

What Postgres does and does not buy you. It removes SQLite's single-writer limit and its sequential table scan, which is the reason to switch at high write volume. It does not make nova seal log verify sub-linear: the Merkle root is recomputed from every stored leaf hash on both backends, so verification is O(N) either way — roughly 1.9 s at 1M entries, dominated by ~1M SHA-256 calls in Python rather than by the database.

Full recompute is the guarantee, and linear cost is its documented price (ADR-0268, decided 2026-10-09). A clean nova seal log verify means every stored leaf hash was folded back into the recorded root — no signer or cached interior node is trusted. Measured: ~1.9 s end to end at 1M entries (GitHub-hosted runner + Postgres 16, 2026-09); 1361 ms of that is in-process hashing with no database, and the in-process pass scales at roughly 1.2 µs per leaf (re-measured 2026-10 on a laptop). Plan on roughly 1.2–2 s per million entries plus the time to read the leaf hashes from the backend, and run verification at that scale as a scheduled audit, not an interactive check.

⚠ Earlier releases documented a p99 < 200 ms verification at 1M entries. That figure was never achieved and is withdrawn by decision, not deferred — see the correction in docs/releases/v0.38.0.md. A faster path (checking sampled leaves against a signed tree head) would detect less and add a trusted signer, so it is not planned.


4. Environment variables — complete reference

Variable Default Description
NOVAFABRIC_SEAL_CONFIG $NOVAFABRIC_HOME/novaseal.yaml Path to novaseal.yaml. Takes precedence over the default location. Set this in Docker/K8s to point to the config on the mounted volume.
NOVAFABRIC_SEAL_DB_PATH — Explicit Merkle log path (or a Postgres DSN for the experimental Postgres backend, §3.4). Overrides merkle_db in novaseal.yaml for every caller — sealing, verification, the dashboard and nova seal log verify (§3.1). Intended for CI/test environments.
NOVAFABRIC_HOME ~/.novafabric Base directory for the default novaseal.yaml and novaseal-merkle.db.
NOVA_BYPASS_NOTIFY_FILE — Append one JSON line per maker-checker bypass to this file (§4.1).
NOVA_BYPASS_NOTIFY_WEBHOOK — POST one JSON event per maker-checker bypass to this http(s):// URL (§4.1).
NOVAFABRIC_PROMOTE_CA_BUNDLE — CA bundle that nova seal verify trusts for maker-checker signers; without it every signer is refused.
NOVAFABRIC_PROMOTE_INSECURE_SELF_SIGNED — Dev-only: accept self-signed maker-checker certificates. Never in production.
NOVA_REKOR_URL — When set, approval and export envelopes are also published to this Rekor transparency log (network call).
NOVASEAL_KMS_ENDPOINT — NovaSeal KMS endpoint for collector batch signing (cluster-scale, experimental).

Interaction between NOVAFABRIC_SEAL_CONFIG and NOVAFABRIC_SEAL_DB_PATH:

NOVAFABRIC_SEAL_DB_PATH wins over the YAML merkle_db for every caller (§3.1). nova seal init writes $NOVAFABRIC_HOME/novaseal.yaml (it warns when NOVAFABRIC_SEAL_CONFIG points elsewhere, ADR-0301); nova seal init --force records the key rotation in the log §3.1 resolves, including the variable.

4.1 Maker-checker bypass notifications

Works today. Every maker-checker bypass — nova seal bypass and the dashboard's POST /api/seal/{capsule_id}/bypass — is announced on the channels these variables configure, read when the bypass is created:

Variable Channel
NOVA_BYPASS_NOTIFY_FILE appends one JSON line (event_type: bypass_created, capsule_id, bypass_uuid, valid_until, requester_keyid, recipients, timestamp)
NOVA_BYPASS_NOTIFY_WEBHOOK POSTs the same JSON object (Content-Type: application/json, no retry). The 5 s limit is a total deadline for the whole delivery, not a per-read timeout

Set both to use both.

A bypass is refused unless it is audited (fail closed). A bypass skips the two-person rule, and the audit trail is its compensating control. Before the bypass is stored, a seal.bypass.create entry goes into the hash-chained audit log. It records the actor (cli-user, or serve:<token fingerprint> for the dashboard), the signer's certificate name (authorized_by), bypass_uuid, valid_until, the SHA-256 of the signed envelope, the configured notification_channels and the surface (cli or serve). If that entry cannot be written, the bypass is refused and nothing is stored or announced: nova seal bypass exits 1, and the route answers 503. The dashboard route also writes a seal_bypass record to the dashboard audit log (result ok or error).

A bypass that cannot be stored is withdrawn (works today; review r8). The entry is written before the bypass file is renamed into place. If that rename fails (disk full, I/O error), the staging file is removed, a seal.bypass.abort entry with the same bypass_uuid (reason: store_failed, the error type, the surface) is appended, and the bypass is refused: nova seal bypass exits 1, the route answers 503 and records the attempt. If even the abort entry cannot be written, the refusal says so and an ERROR is logged; the log then shows a seal.bypass.create for a bypass that does not exist, which verification never honours (below).

A bypass counts only while the audit log vouches for it (works today; review r8). nova seal verify, nova seal approve, the nova serve / nova server seal routes and nova export-model-independence honour a stored bypass only when the audit log (NOVAFABRIC_AUDIT_LOG_PATH, as resolved where verification runs) holds a seal.bypass.create entry for the same capsule, bypass_uuid and envelope SHA-256 whose own entry_hash recomputes, and no seal.bypass.abort for that bypass_uuid. This is on top of ADR-0315's checks (trusted signer, policy approver, this capsule, unexpired). A bypass file planted in the store without nova seal bypass — so with no audit entry and no notification — does not count, and neither does one whose envelope was swapped after it was audited. An unreadable audit log vouches for nothing.

Variable Effect
NOVAFABRIC_PROMOTE_BYPASS_AUDIT_OPTIONAL=1 skip the audit-entry check, for verifying a bundle store copied without the audit log it was written to. Not the default; the message then says audit entry not checked

Upgrading: a bypass created by a release before 2026-10-23 has no seal.bypass.create entry, so it stops counting after the upgrade. A bypass lasts at most 168 hours; create a new one, or set the variable above until the old one expires.

Concurrent writers keep the audit chain intact (works today; review r8). Every append to the hash-chained audit log reads the last entry_hash, writes and fsyncs under an exclusive fcntl.flock on the log file plus an in-process lock, so threads (the dashboard runs bypasses in worker threads) and processes chain onto the true predecessor. Before, 8 threads creating 40 bypasses produced 12 prev_hash mismatches. flock is advisory and not honoured by every network filesystem (NFS without lock support, Lustre mounted without -o flock): keep the audit log on a local filesystem, or one that implements flock.

The signature covers the configuration, not the delivery. The bypass is signed before anything is sent, so the signed predicate cannot honestly say "sent". It records what was configured when it was signed:

Field Value
notification_sent_to the configured channels (file, webhook:scheme://host:port), then each --notify address as mailto:<addr>
notification_status pending when at least one channel is configured, not_configured otherwise. Never sent

The outcome of the delivery that follows is a seal.bypass.notify audit entry with outcome delivered, failed (with failed_channels) or not_configured. A failing channel never blocks the bypass, because the bypass is already stored and audited by then; the failure is logged as a warning (bypass notification failed). A webhook is recorded as scheme://host:port only, because its path or query may carry a token. Bypasses signed before 2026-10-23 may carry the legacy values sent (from --notify, although no email was ever sent) or not_configured (always, from the dashboard). For those, trust the audit log over the predicate.

Not done: email delivery. nova seal bypass --notify <email> records the addresses in the signed predicate and in the event's recipients, but NovaFabric sends no email. A --notify address does not make the status pending; use the webhook to reach a mail or chat relay.

Before 2026-10-22 neither variable had any effect: nothing read them, and no bypass notified anyone.


5. Discovery order for novaseal.yaml

load_signing_profile() searches in this order:

  1. NOVAFABRIC_SEAL_CONFIG env var — if set and the file exists, use it. If set but the file is missing, raise SealConfigError (fail fast; do not silently fall through to the default).
  2. $NOVAFABRIC_HOME/novaseal.yaml (default ~/.novafabric/novaseal.yaml) — if it exists, use it.
  3. No config found — NovaSeal is disabled; load_signing_profile() returns None.

This means NovaSeal is completely opt-in at the individual user level. A system administrator can pre-create $NOVAFABRIC_HOME/novaseal.yaml (or set NOVAFABRIC_SEAL_CONFIG) for the user (e.g., via SLURM prologue or dotfiles management) to enable NovaSeal transparently.


6. Troubleshooting

The dashboard doctor (/api/doctor) shows novaseal_db FAIL

The diagnostic check resolves the log with the §3.1 rule and tests whether the file exists. A Postgres log is reported with its password redacted and is not connected to by this check. (nova doctor on the command line has no NovaSeal check.) Common causes:

Symptom Cause Fix
Path shown is ~/.novafabric/novaseal-merkle.db but the file isn't there No capsule has been sealed yet — the DB is created by the first sealed nova capture (or any NovaSeal seal operation) Run a seal operation, or pre-create the file: touch ~/.novafabric/novaseal-merkle.db
novaseal_db FAIL after setting NOVAFABRIC_SEAL_DB_PATH The env var path doesn't exist yet Create the parent directory and touch the file, or run a seal operation

SealConfigError: NOVAFABRIC_SEAL_CONFIG points to missing file

The env var is set but the file at that path doesn't exist. Either create the file or unset the env var to fall back to $NOVAFABRIC_HOME/novaseal.yaml.

NovaSeal silently disabled — no seal directory in capsule

load_signing_profile() returned None (no config found). Run nova seal init, create $NOVAFABRIC_HOME/novaseal.yaml yourself, or set NOVAFABRIC_SEAL_CONFIG.

⚠ NovaSeal sealing failed … does not match the public key

The key_path key is not the key in cert_path, so any seal would fail verification; capture writes the capsule without .seal/ instead (ADR-0301). Re-issue the certificate — for a nova seal init identity, nova seal init --force.

nova init --force refuses: "signs capsules with … signing_key.pem"

Your novaseal.yaml points key_path at the nova init Ed25519 key. Rotating that key would orphan the certificate, so nova init --force stops. Rotate the sealing key with nova seal init --force, or point key_path at a different key first.

Merkle log differs between two shells or jobs

Every code path uses the §3.1 rule, so within one process sealing and verification agree. Two processes disagree only when their environment differs: check for a stale NOVAFABRIC_SEAL_DB_PATH or NOVAFABRIC_SEAL_CONFIG exported in one shell or job script but not another. The promotion policy store follows the same rule. nova policy sign, nova policy list, nova seal propose / verify, nova export-model-independence and POST /api/policy/sign default to the §3.1 Merkle log file, which is what the dashboard SealTab and server mode read; an explicit --db still wins. Up to v0.106.0 the CLI defaulted to ~/.local/share/novafabric/merkle.db, so a policy signed on the command line was invisible to the dashboard. Default changed: a policy signed before the change stays in that old file — pass --db ~/.local/share/novafabric/merkle.db or re-sign it without --db. nova seal propose and nova policy list name the old file when they find no policy and it exists. A Postgres Merkle log cannot hold the policy store (it is SQLite); pass --db <file> then.

TSA timeout or tsa_url errors

There is no default TSA (ADR-0292): NovaSeal contacts only the tsa_url / tsa_urls you configure. If you configured the public https://freetsa.org/tsr, note it is a free service with rate limits — for production use an organizational or commercial TSA. To disable RFC 3161 timestamps entirely, omit the field (or leave it empty) — NovaSeal still signs with ECDSA, without a timestamp token and without any network call. If a single TSA being occasionally unreachable is the actual problem, configure a fallback list instead of (or as well as) switching providers — see §1.1 TSA fallback list.