Skip to main content
This page is the address for the questions a security review asks first.
This is a factual description of the software, not legal advice and not a compliance certification. Which of these facts matter under your regulator, and what you have to do about them, is a decision for you and your counsel.

Who holds what

Veriqa is delivered as code you run. In the self-hosted model that is the whole answer to “where does the data go”:
  • You are the controller. The database, the key material and the logs are on your infrastructure, under your connection strings. Nothing is provisioned for you.
  • Veriqa (the vendor) is neither controller nor processor of your users’ data. No user data reaches the Veriqa vendor: the product makes no calls home, ships no telemetry and has no vendor endpoint of any kind. The only outbound addresses in the code are the messenger APIs you configure and the SMTP server you point it at (see What leaves your perimeter).
  • The messenger providers are your counterparties, not the Veriqa vendor’s. Telegram, MAX and the WhatsApp Cloud API see the messages you send through your own bot credentials. Your relationship with each of them is direct.

Personal data inventory

Everything the shipped product touches, and nothing beyond it. Not in the list: message bodies from the channel (a channel adapter reads what it needs to identify the sender and confirm, and keeps nothing else) and any long-lived profile of the user beyond the collected claims above. The product ships no user directory — see Channel identities.

Collection defaults

These are on out of the box. Nothing here needs an action from you to start; each needs one to stop. Two qualifications that matter more than the flags: Geolocation is on by default but inert by default. The lookup needs the satellite package Veriqa.Core.AuthServer.MaxMind and a local .mmdb database, and the base packages carry neither. Without them the provider reports itself unavailable and the geo fields stay empty — so a stock installation collects no location, even though the flag says true. Adding the database is what starts the collection. The IP is collected but never shown and never logged. It goes into the snapshot inside the transaction and stops there: the confirmation message names only the application, browser, OS and region, and the diagnostic log for an untrusted forwarding configuration records a result code rather than the address. Turning all of it off is one section:
appsettings.json
With Enabled: false no snapshot is built, and the confirmation message simply carries no context line. You can also keep the context and narrow it — drop region from DisplayFields, or set CollectIpAddress: false (which also removes geolocation, since the lookup has no input). Details and the per-application narrowing are in Initiator context.

Where it is stored

Channel identities: nothing is stored by default

The port that would persist a channel identity — channel user id, phone, email, display name, as an upsert with neither TTL nor deletion — is IChannelIdentityRepository, and Veriqa ships no implementation of it and registers none. Sign-in works without one: the resolved identity travels with the transaction and is gone with it. Keeping that data indefinitely is an act you perform deliberately, by registering an implementation of your own; see Storage.

Retention

In short: the transaction store empties itself within about fifteen minutes of a sign-in, and the audit trail is the only Veriqa store that keeps anything for days. Everything with an indefinite lifetime is something you added — a channel identity repository, the collected claims store, or tokens with a lifetime you chose.
The OpenIddict database is pruned by the auth server. A background job deletes token and authorization records that are no longer valid (expired or revoked) and were created more than Veriqa:OpenIddict:Server:TokenPruneThresholdSeconds ago (24 h by default), every TokenPruneIntervalSeconds (1 h), on every store provider. Valid records are never deleted. Until a record is pruned, its payload — including an avatar image for clients with avatar — stays in the database, so a lower threshold shortens how long that data is kept.

The audit trail is off until you ask for it, twice

Two independent switches, and both are needed:
  1. The satellite is opt-in — a host that never calls AddVeriqaAuditTrail has no trail at all.
  2. Veriqa:Logging:Mode ships as System, and records are written only in Audit. Disabled and System are the same thing to the receiver: write nothing.
Retention, once records exist, is not a third switch: the sweep is registered automatically for either built-in sink and deletes anything older than RetentionDays. ConfigureRetention only tunes how often the sweep runs and how large its batches are — it does not turn retention on, and skipping the call does not leave records unbounded. A retention of zero or less is refused at startup rather than read as “keep nothing”. The one case where nothing sweeps: a custom sink you registered with UseSink<TSink>(). Records then live in your store, the built-in sweep is not registered, and the host says so at startup. Bounding that store is yours.
appsettings.json

What an audit record actually contains

A closed set of attributes, fixed in the schema — one column per attribute, no free-form payload: the timestamp, the actor, the action code, the target (the transaction id), the outcome and its reason code, plus the transaction type, channel type, correlation id, tenant, client_id, UI locale and time zone, the channel’s own safe details and its declared inbound verification level. The actor is masked. For an action the user performed in a channel it is {channel}:{masked id} — at most the last two characters of the channel user id stay readable. The unmasked value never leaves the transaction store. No IP, no User-Agent, no geolocation, no display name, no email reaches an audit record. The initiator context lives in the transaction and dies with it. The single exception you can switch on is the parameters of the confirmed operation — the declared action kind and its slot values. Those are your application’s subject data and may be anything, so they are off by default and copied into the record only when you ask (Hardening).

Encryption

There is no application-level encryption of stored personal data beyond that one protected field, and no key management of its own. If your threat model requires personal data encrypted at rest, that requirement is met at the storage layer you operate — encrypted volumes, TDE, an encrypted connection to the database — not by a setting in Veriqa. Secrets — client_secret, bot tokens, SMTP credentials — belong in environment variables or a secret store, never in config files in the repository. Passwords are never written to a log: where one has to be compared, the comparison is over a SHA-256 hash.

Keys

Three key materials, three different homes. Token signing and encryption certificates. In Development the product uses OpenIddict’s development certificates. Outside Development it loads X.509 certificates you supply, by path or base64 (Veriqa:OpenIddict:Server:SigningCertificate* / EncryptionCertificate*), and refuses to start without them.
Replacing a certificate is a session-invalidating operation. The configuration accepts exactly one signing and one encryption certificate, so JWKS carries a single signing key and old and new never overlap. Tokens signed with the previous key stop validating the moment you swap it. Plan key replacement as a maintenance event, not as a background rotation — the same warning appears in hardening.
The DataProtection key ring. Priority is Redis (Veriqa:DataProtection:RedisConnectionString), then a directory (Veriqa:DataProtection:KeysDirectory); with neither set, the keys live in memory and are lost on restart. That is fine for development and wrong everywhere else — after a restart the protected expectations of live transactions can no longer be read. Data Protection rotates its own keys, and values written under an earlier key stay readable while that key is still in the ring, so a ring on durable storage makes rotation a non-event. Channel and application secrets — bot tokens, webhook secret tokens, client_secret, SMTP credentials. Rotating them is a channel-side and client-side operation; nothing in Veriqa caches them past a configuration reload.

What leaves your perimeter

Only what you configured a channel for. Per confirmation, the outbound message carries:
  • the name of the application that started the sign-in;
  • the browser, OS and region of the initiator, where those are known and displayed;
  • a link or buttons to confirm, and how long the request is valid.
It does not carry the IP address, the raw User-Agent, the user’s own profile data, or anything your application declared about the transaction. Where it goes depends on the channel: api.telegram.org for Telegram, platform-api.max.ru for MAX, graph.facebook.com for the WhatsApp Cloud API, and your own SMTP server for Email. In the other direction, each channel returns the sender’s profile — the fields in the inventory table above. The messenger sees the message content and the recipient because that is what delivering a message means; the Email channel sees whatever your SMTP relay sees. The external event bus. If you wire the RabbitMQ publisher, transaction events leave the process carrying the safe attribution only — the masked channel identity among it. The parameters of a confirmed operation are explicitly excluded from serialization and never reach the bus, whatever the audit configuration says.

Logs

Application logs are not an audit trail and are not designed to be evidence. What matters for this page is what they must never contain, and what the code does about it: identifiers are written as a 16-character non-reversible fingerprint (the leading bytes of a SHA-256) rather than as themselves, so an operator can still stitch the steps of one sign-in together without the log holding the identity. Tokens, secrets and the initiator’s IP are not logged at all. That is the shipped behaviour of Veriqa’s own log statements. Your host, your reverse proxy and your platform’s request logging are outside it — an access log in front of the auth server records client IPs by default, and bounding that is part of the same deployment step that configures ForwardedHeaders:KnownProxies.

What is left for you to decide

Nothing on this list has a right answer the product can pick for you.
  • Whether to keep the initiator context at all, and whether to add a GeoIP database.
  • Whether to enable the audit trail, at what retention, and whether records should carry the parameters of confirmed operations.
  • Whether to persist channel identities — and, if so, how they get deleted.
  • Whether to turn on the collected claims store, and with what retention.
  • How long expired and revoked token records stay in the OpenIddict database before pruning.
  • Where the DataProtection key ring lives.
  • Encryption at rest, backups and their retention, at the storage layer.
  • How a deletion request from a user is executed across your stores. Veriqa deletes its transaction rows on its own schedule and its audit rows on retention; the collected claims store you reach by sub (endpoint or host call); anything you added is yours to reach.

Next steps

Production hardening

The checklist to run before going live.

Storage

Which store holds what, and how to wire each one.

Known limitations

What the product does not do.