Trust and security
What is built, how it was tested, and what is not yet done. Written for the person who has to approve a purchase.
Where we are
AddressIQ is not yet production-ready. The engine, tenant separation, HTTP hardening, data governance (retention, export, erase) and plugins are built and tested, mostly on in-memory sqlite. The data is synthetic. Nothing has run against a real identity provider, a real production host or a real customer.
There is no independent penetration test, no SOC 2, no ISO 27001 and no IRAP assessment. None is held and none has been started.
This page lists only what exists in the product and has an automated test behind it (one exception is marked as untested). Where a test ran on a test database, or a feature has a known limit, the limit is stated next to the claim. If a claim has no evidence, it is not here.
Tenant separation
- Every tenant-owned row carries a tenant id. Separation is enforced in one module, not in each query. Tenant A cannot read, change or delete tenant B’s rows; a foreign row answers “not found”.
- The tenant comes only from verified identity (the login token claim) or from the authenticated API client. A tenant sent in a request body, query or header is ignored.
- A user with no tenant and no platform role is refused.
- Cross-tenant references are rejected, for example a routing step pointing at another tenant’s provider configuration.
- Caches are keyed by tenant: zones and the stored-address cache. One tenant’s cached result is never served to another.
- Usage, billing summaries and metrics are per tenant.
- A suspended tenant’s API keys and admin access stop at once, and other tenants are unaffected.
- The isolation test has one row per tenant-owned entity, and fails if a new entity has no row.
Limit: these tests ran on sqlite only. Not yet proven on PostgreSQL or HANA, and not against a real identity provider token.
API keys, scopes and origins
- API keys are random (128 bits), shown once at issue, and stored only as a SHA-256 hash plus a 13-character display prefix. The admin API does not expose the hash, and the change log records it as redacted.
- Each API route checks a key and a scope. A key without the scope gets a 403 that gives no hint about what exists.
- Keys can be pinned to web origins, so a key copied from a web page does not work from another site.
- Only active, non-deleted keys are accepted, so a suspended key stops working. Admin changes to a client are change-logged.
Limits: no test targets key suspension alone (tenant suspension is tested). Keys have no expiry field today. Key hashing is unsalted SHA-256, accepted because keys are random.
Rate limits and abuse controls
- Each key has a per-minute rate limit. An over-limit call gets 429.
- Each tenant is on a plan with a monthly call quota, counted across all its keys, bulk job rows included. Over quota, the call gets 429
quota_exceededwith the reset time, and every authenticated response carriesX-Quota-*headers. One tenant using up its quota does not affect another. A key cannot be issued with a per-minute limit above its tenant’s plan. - Batches are capped at 100 records and need a
bulkscope. Request bodies are capped (default 1 MB); a larger body gets 413. Road-routing matrices are capped at 10 by 10 with a 5 second upstream timeout. - Coordinates and polygons are validated: non-numeric coordinates are rejected and polygon rings are closed. Route points must be inside latitude and longitude ranges.
Limits: the per-key rate limit is counted in memory in each pod, so with several pods the effective limit is the pod count times the limit; the monthly quota is the real cap. The quota is read from the database and cached for 5 seconds in each pod, so several pods can overshoot by about 5 seconds of traffic. Tested on sqlite and one process; no test runs more than one pod. Plan numbers are defaults, not prices.
Admin access, change log and soft delete
- Admin access requires login. Roles (Admin, Operator, Viewer, Platform Admin) are enforced per entity and action. A viewer cannot write.
- Every create, update and delete on admin entities writes a change-log entry with who, when, a before image and an after image. The log is insert-only, except that address copies in it are blanked when an address is deleted by retention or erase.
- An ordinary admin delete does not remove data. It sets an archive flag and logs the before and after. Permanent deletion of personal data goes only through the erase actions below.
Licence-aware storage
- Results from a provider whose licence does not allow storage are not saved as canonical addresses.
- The stored-address cache is off by default. When on, it serves only licence-eligible results, only to the tenant that resolved them unless the provider allows sharing, and only inside the provider’s time limit.
- The raw answer of a provider whose licence bars storage is not kept. The evidence row holds only the provider code, a SHA-256 hash of the answer and a time.
- A switch can require legal sign-off per provider before anything is stored. It is off by default.
- A provider that bars resale is refused on a shared key.
Limits: the sign-off switch is off in the seed, so a provider marked storable by research but not yet signed off by legal still has its answer stored until the switch is turned on. Counsel has not confirmed that a hash stub is acceptable under each provider’s contract. “Own key” routing is a label today; live adapters still read the deployment key. Not tested on PostgreSQL.
Retention, export and erase
- Retention is per tenant. A job masks address text in the usage ledger, request inputs and stored provider answers, and deletes stored addresses and their enrichment, once each tenant’s window has passed (the tenant’s own setting, else the deployment defaults of 90, 30 and 180 days by data type). It is safe to re-run and logs one entry per tenant with counts.
- A tenant administrator can export all of the tenant’s data as NDJSON (API key hashes and secret references left out), and can erase one stored address or one record.
- A platform administrator can erase a whole tenant after typing a confirmation text. The tenant row stays as an archived tombstone and its keys stop working. Another tenant’s id answers “not found”.
- Bulk jobs, idempotency keys and webhook delivery logs are in the same retention job, the export and the erase. Finished bulk jobs and their input addresses are deleted at the request window (30 days by default); one job can be erased by its id.
- Evidence rows (ledger, requests, provider answers) are written just after the response through a bounded queue that is emptied on a clean shutdown. A crash can lose rows still queued.
Limits: the scheduled run is a Kubernetes CronJob that is off by default, so whether retention runs on a schedule depends on how a deployment is set up, and no alert ships for a failed run. Tested on sqlite only; not run on PostgreSQL or a real cluster. Erase does not reach backups (kept up to 30 days). Other requests for the same street address made separately are not matched by text. Default windows await owner and counsel sign-off.
HTTP hardening, request ids and logs
- Security headers on every API response:
X-Content-Type-Options: nosniff,Referrer-Policy: no-referrer,X-Frame-Options: DENYand a deny-all Content-Security-Policy.Strict-Transport-Securityis sent only when set for a deployment where TLS ends upstream. - CORS is an allow-list. A browser origin is allowed only if it is in the platform list or in an active client’s allowed origins. A blocked origin gets no allow-origin header, and credentials are never allowed.
- Every response carries
X-Request-Id. A caller’s id is kept if it is 8 to 128 safe characters; otherwise a new one is issued. - One JSON access-log line per API request holds time, method, route pattern, status, duration, request id, tenant id and client id. It does not hold API keys, the Authorization header, query, body or address text.
- The Prometheus
/metricsroute is for the platform operator only. Anonymous callers, tenant administrators and tenant API keys are refused. Labels are route patterns, with no tenant or client label. A tenant reads only its own counters. - Two probes: a liveness check that does not query the database, and a readiness check that does and returns 503 while a pod is draining. On shutdown the pod stops accepting and waits for in-flight requests up to a deadline.
- Every response says whether data is synthetic or live.
- An unexpected internal error on an API route answers 500 with
{ "error": "internal error", "requestId": "…" }and nothing else: no SQL, file path, stack line or provider URL. The real error goes to the server log, which can be searched by that request id. A caller’s own mistake (a 400) keeps its message.
Limits: the generic error body is tested on the API routes named in the test (autocomplete, resolve, enrich, classify, capture, route distance, authentication, connect, batches); the admin OData surface uses the framework’s own error output and is not covered. The access log is the only log with an automated check; other application log lines have no check and no independent review. Headers, CORS and request ids were tested in-process, not behind a real load balancer or TLS terminator. The admin-page policy is weaker than the API one (it allows inline scripts and one script host). No dashboard exists and no alert has been fired in a drill. A rolling restart under load has not been run. The public demo’s edge worker has its own CORS code, kept in step by a parity test.
AI features and MCP
- The geocoding and zone path never calls a language model.
- The copilot never writes. It proposes, and a person confirms through the normal permission-checked actions.
- The copilot is opt-in per tenant and off by default. No text goes to the model provider unless a deployment key is set and a platform administrator has switched it on for that tenant. Platform-wide requests never send.
- When on, the geo-assistant sends at most 20 ledger entries with each address cut to suburb, state and postcode (coordinates to two decimal places), plus the tenant’s zone list and field-mapping names. Text a user types is sent as typed. Each call’s token cost is logged to the tenant’s ledger.
- The copilot treats ledger text as untrusted data, and its prompt tells the model to ignore instructions found in data.
- The MCP server has eight tools, all read-only. The key comes only from the environment, is never returned, and a plain-http remote URL is refused. Upstream failures give generic errors that do not leak the key or host.
Limits: copilot tests used a simulated network call; no real call to the model provider has been made. The model key is one deployment-wide key, not per tenant. Masking is pattern based and written for Australian address shapes. There is no dedicated prompt-injection test. The MCP server has not been run against the live service and has no hosted transport.
Build and operations
- A CI workflow is defined to run the test suite on sqlite and PostgreSQL, a dependency audit, a secret scan, a software bill of materials (CycloneDX) and an image scan, and to build the container image. Dependency update automation is configured.
- Provider keys come from the environment or a secret reference, not from the repository or browser code.
- A runbook covers nightly backup, restore, key rotation and the first moves in an incident.
- The deployment chart no longer creates the well-known public demo key by default. Outside the chart (the single-instance Docker file, local runs) the demo key is still created unless a setting turns it off.
- The database schema is deployed by one Helm hook job before the pods roll out, not by every pod at start-up, so pods starting together no longer race.
- Webhook signing secrets are encrypted at rest (AES-256-GCM, bound to the endpoint, with key rotation) and are never shown by a read or an export. Production refuses to start with webhooks and no key. Webhook deliveries go only to public HTTPS addresses the customer registers, and carry ids and counts, not address text.
- The admin login validates OIDC tokens (signature, exact issuer, audience, expiry, allowed algorithms only) and was tested with real Keycloak tokens.
- On 2026-11-06 we ran an internal red-team pass over the newer surfaces (bulk jobs, webhooks, quotas, export and erase, login tokens, tenant isolation). It was done by us, with automated tests on a local test environment. It is not independent and is not a penetration test. It found no critical, high or medium issue and two low ones, both since fixed.
Limits: the audit, secret scan, bill of materials and image scan have not yet run on a CI runner. The runbook has never been drilled. The deployment chart has not been rendered with Helm and the schema job has not run on a cluster or on PostgreSQL. The webhook secret key is an environment variable, not held in a key management service. The login validator was tested with Keycloak only; Entra ID, Okta and XSUAA on BTP are not tested. The chart value that turns the demo key off has no automated test.
Not yet
This list matters as much as the one above.
- Independent penetration testNot started.
- CertificationNo SOC 2, ISO 27001, IRAP or other assessment held or started.
- Single sign-on with groups, and SCIMNot built. The admin login validates OIDC tokens and was tested with Keycloak only; Entra ID, Okta, XSUAA and browser sign-in are not tested.
- Key management service and per-tenant provider key vaultThe webhook secret key and the provider keys are deployment variables. “Own key” routing is a label.
- Limits shared across podsPlan quotas exist, but the quota count can overshoot by about 5 seconds of traffic across pods and the per-key rate limit is counted in memory in each pod. No test runs several pods together.
- Tested disaster recoveryBackups are documented, never restored in a drill. The targets of 24 hours (data) and under 1 hour (restore) are targets only.
- Live dataAll data is synthetic. No live provider contract is signed. Match rates and accuracy are unmeasured.
- Full log reviewOnly the access log is tested. No review of all application logs for secrets and personal data.
- First CI run of the scansDependency audit, secret scan, bill of materials and image scan are written, not yet run on a runner.
- Key expiryKeys have no expiry field today.
- Encryption in transit and at restLeft to the hosting setup. The software does not terminate TLS. No production host has been chosen.
- Incident response plan, status page, on-callNot in place.
- Export and erase in practiceBuilt but tested on sqlite only. Not run on PostgreSQL or by a real customer. They do not reach backups, and counsel has not said whether they satisfy deletion duties.
- Scheduled retentionThe CronJob is off by default and has not been run on a real cluster. Default windows await owner and counsel sign-off.
- Data residencyA region label exists on a tenant, but nothing enforces it.
- Load test against targetsOnly local laptop runs on sqlite exist. The 99.9% availability and 200 ms targets are unproven, and PostgreSQL has not been tested under load.
- HANA and XSUAA runtimeCompile-checked only.
- Independent assessment of any kindThe red-team pass above was internal.
- Staff securityVetting, training and access reviews are not documented.
Legal documents
Our terms, privacy policy and data processing addendum are in legal review and will be published when approved.
The service-level targets are targets only; none has been measured. Open legal questions include provider licence terms for own-key routing and cascading, caching and storage rights per provider, and how privacy law applies to overseas disclosure.
Contact
Security contact: [security contact] (a placeholder: a named security contact has not been set yet).
Vulnerability reports follow the acceptable use policy, which is in review. Until a security contact is published, write to addressiq@vrdigital.solutions.