Async Job Content Encryption#
AI-Q can encrypt sensitive async job content before it is persisted by the AI-Q async jobs API. This is application-level envelope encryption for final report output and selected artifact event payload fields.
Content encryption is disabled by default. Operators must explicitly set
AIQ_CONTENT_ENCRYPTION=key or vault on every API and worker process to
enable it.
This feature is intentionally narrow in its first milestone. It protects the
serialized final output payload returned by
GET /v1/jobs/async/job/{job_id}/report, such as {"report": "..."}, and
the content field of artifact.update events for output and file
artifacts.
Scope and Limitations#
Encrypted when enabled:
job_info.outputfor jobs submitted through/v1/jobs/async/submitoraiq_api.jobs.submit.submit_agent_job.job_events.event_datafieldartifact.update.data.contentwhen the artifactdata.typeisoutputorfile.
Still plaintext:
Other
job_events.event_datafields, including artifact metadata, tool events, heartbeat events, cancellation events, error events, citation source artifacts, citation use artifacts, and todo artifacts.Job status, ownership metadata, timestamps, event type, and other control plane fields.
job_info.error.PostgreSQL notification payloads.
summaries.summary.LangGraph checkpoints in
aiq_checkpoints.Historical
job_info.outputrows written before encryption was enabled.Inline CLI and local NeMo Agent Toolkit runs that do not use the AI-Q async API job runner.
Because checkpoints, errors, citations, todos, and other event fields can contain equivalent research content, this phase does not provide full database-level job-content confidentiality.
This feature protects the configured fields from storage readers and detects ciphertext tampering within the authenticated job and field context. It is not a database integrity or event-freshness control. A principal with database write access can still delete or reorder rows, replay an entire encrypted row, or swap encrypted artifact content between events in the same job when the authenticated field path is identical.
Modes#
Set AIQ_CONTENT_ENCRYPTION on every API and worker process.
Mode |
Behavior |
|---|---|
|
Default. Preserves existing plaintext behavior and never attempts to decrypt |
|
Uses one operator-managed static 32-byte key to wrap per-job data encryption keys. Intended only for development, testing, or deployments that cannot use Vault. |
|
Uses HashiCorp Vault Transit to generate and wrap per-job data encryption keys. Recommended for production. |
Encrypted values are stored as aiqenc: envelopes. Encrypted event fields are
stored as marker objects containing the envelope so the surrounding event JSON
remains inspectable. The envelope contains non-secret metadata, the wrapped data
encryption key, nonce, ciphertext, tag, algorithm, key id, and an AAD hint that
binds the value to either job_info.output:{job_id} or the encrypted
job_events.event_data field path for that job.
Static Key Configuration#
Static key mode requires a base64 or base64url value that decodes to exactly 32 raw bytes.
AIQ_CONTENT_ENCRYPTION=key
AIQ_CONTENT_ENCRYPTION_KEY=<base64url-encoded-32-byte-key>
AIQ_CONTENT_ENCRYPTION_KEY_ID=<operator-managed-key-id>
AIQ_CONTENT_ENCRYPTION_KEY_ID is optional, but it is cryptographic identity,
not cosmetic metadata: it is authenticated while wrapping each data encryption
key. If omitted, envelopes use static-key as the key id. Keep the configured
key id unchanged while encrypted jobs are retained. The first implementation
supports one active static key only; changing the key or key id makes existing
envelopes unreadable unless those jobs have expired or a future rewrap process
has migrated them.
Invalid static-key configuration fails startup.
Vault Transit Configuration#
Vault mode uses AppRole authentication and Transit data keys. Token fallback is not supported in the first implementation.
AIQ_CONTENT_ENCRYPTION=vault
VAULT_ADDR=<vault-address>
VAULT_ROLE_ID=<approle-role-id>
VAULT_SECRET_ID=<approle-secret-id>
AIQ_ENCRYPTION_TRANSIT_KEY=<transit-key-name>
VAULT_TRANSIT_MOUNT=<transit-mount>
AIQ_CONTENT_ENCRYPTION_KEY_ID=<logical-key-id>
VAULT_TRANSIT_MOUNT defaults to transit if omitted.
AIQ_CONTENT_ENCRYPTION_KEY_ID is optional; if omitted, envelopes use
<transit-mount>/<transit-key-name>.
Set VAULT_NAMESPACE=<vault-namespace> only when your Vault deployment
requires a namespace.
Missing Vault configuration fails startup. If Vault configuration is present but Vault is temporarily unreachable, unauthorized, or otherwise operationally unready, the API starts unhealthy instead of exiting.
The application relies on Vault Transit versioned ciphertext for decrypting after Transit key rotation. Do not disable or destroy old Transit key versions until corresponding encrypted jobs have expired or have been rewrapped by a future migration process.
Vault Client and Retry Behavior#
Vault mode uses the synchronous hvac.Client. API routes that may call Vault
offload that work to worker threads so Vault latency does not block the FastAPI
event loop. Each API or worker process keeps one process-local Vault client and
guards client creation, AppRole login, and Transit calls with a lock. This
serializes Vault Transit operations per process and avoids concurrent mutation
of the shared hvac.Client.
Vault Transit operations use bounded retries for transient failures only:
request connection failures and timeouts,
Vault rate limiting,
Vault 5xx-style operational failures, and
unauthorized responses after forcing one AppRole re-login.
The retry policy does not retry missing configuration, invalid requests,
permission-denied responses, missing Transit paths, malformed Vault responses,
or decrypt denials. Tune VAULT_TIMEOUT_SECONDS to bound individual Vault
requests and AIQ_CONTENT_ENCRYPTION_READINESS_TTL_SECONDS to control how
often health and submit readiness recheck Vault.
Rollout Behavior#
The feature defaults to off, so upgrading AI-Q does not change persisted job
content unless an operator explicitly enables encryption. The first
implementation is forward-only after enablement:
New
job_info.outputwrites are encrypted after enablement.New
artifact.update.data.contentevent fields are encrypted after enablement foroutputandfileartifacts.Existing plaintext
job_info.outputrows are intentionally unreadable whileAIQ_CONTENT_ENCRYPTION=keyorvault.Existing plaintext event rows remain readable in encrypted modes.
No historical plaintext backfill is included.
No rewrap tooling is included.
Migration, backfill, application-managed key rotation, and decrypt-on-rollback
tooling are outside the scope of this release. Enable encryption only for a new
or empty job history, after existing jobs have expired, or after operators
accept that old plaintext final-report rows in job_info.output cannot be read
in encrypted modes.
Keep the encryption mode, static key and key id, or Vault Transit mount and key
name stable while encrypted jobs are retained. Switching the mode to off
does not decrypt existing values: report reads can expose the stored aiqenc:
value and encrypted event fields remain marker objects. Restore the original
encryption configuration to read those jobs.
Health and Failure Behavior#
/live reports process liveness without calling Vault or the database. /health
reports dependency readiness and includes encryption status. When encryption is
configured but unready, /health returns HTTP 503 and new async submissions are
rejected with HTTP 503, while /live remains successful so an orchestrator does
not restart an otherwise live API process during a dependency outage.
Workers independently validate encryption before marking a job RUNNING. If
encryption is unavailable at worker startup, the job is marked FAILURE and
the agent does not run.
Each submission also carries a non-secret encryption policy identity from the
API process to the worker. The identity covers the mode and key id, a static-key
fingerprint in key mode, or the Vault address, namespace, Transit mount, and
Transit key in vault mode. The worker fails the job before RUNNING if its
local policy does not match, including key or vault submissions received by
a worker configured with off.
If final-report encryption, artifact event-content encryption, or encrypted
persistence fails after an agent has completed, the job is marked FAILURE.
Timer-triggered event flush failures are surfaced before the worker can mark
the job SUCCESS. The worker does not fall back to writing plaintext output.
When encryption is off, event persistence retains its legacy best-effort
behavior: database write failures are logged without changing the job status.
Report reads fail closed:
Vault or crypto unavailability returns HTTP 503.
Plaintext, malformed, or undecryptable
job_info.outputin encrypted mode returns HTTP 500.Job access is authorized before decryption is attempted.
Cache#
Decrypt paths use an in-memory plaintext data encryption key cache per process.
The default TTL is 15 minutes with a maximum of 1024 entries. Set
AIQ_CONTENT_ENCRYPTION_DEK_CACHE_TTL_SECONDS=0 to disable this cache.
The readiness cache defaults to 60 seconds. Health checks and submit requests
reuse the cached state until it becomes stale. Set
AIQ_CONTENT_ENCRYPTION_READINESS_TTL_SECONDS to override the default.