On this page0
Unfamiliar word? See Glossary: words used in these docs. Looking for the tasks in order? Begin at Start Here.
Reference
Transport & Service Trust
How a request reaches your instance, where it is encrypted and where it is not, how the services behind the gateway know a request is genuine, how passwords and tokens are stored, which outside systems your server connects to, and what software it runs.
How A Request Is Protected
- Your browser always talks to your instance over an encrypted connection. The encryption ends at the network edge in front of your server, and the request then travels to the server through a private tunnel that the server itself opens outwards.
- Only one program on the server, the gateway, accepts requests from outside. It checks who is calling and passes a signed note about the caller to the program that does the work. A request that tries to skip the gateway is refused.
- Passwords are never stored. Only a one-way scrambled form of each password is kept, so nobody, including us, can read a password back.
- Your server also makes connections outwards: to Zonitix for the license check-in, and to the systems you connect it to, such as your identity provider, your backup storage and your mail server. The full list is under Technical detail.
- An administrator has nothing to switch on here. None of this can be changed from the app.
Sign-in itself, session tokens and cookies are on API Authentication; encryption of stored data and the keys behind it are on Data Isolation.
If something here is not working as you expect, start with When something goes wrong.
The plain explanation is above. This part is for IT and developers: the exact settings, permission names, API addresses and data fields behind it. Closed: select the heading above to open it. While it is closed, your browser’s find-on-page (Ctrl+F or Cmd+F) does not search this part.
Request Path And TLS
The server publishes no port for the application. A tunnel client runs as one of the containers in the stack and dials out to the Cloudflare edge; every request for your instance’s hostnames comes back down that connection.
# One request, hop by hop
[ Browser / script ] --HTTPS--> [ Cloudflare edge ] TLS ends here
|
| tunnel, opened outwards by the server
v
[ tunnel container ] --HTTP--> [ web server :80 ] the app pages
--HTTP--> [ API gateway :8000 ] every API call
|
+--HTTP--> [ internal services ] private container network
- TLS terminates at the edge, not on your server. Nothing in the stack listens with a certificate: the web server listens on plain port 80 and the gateway is a plain HTTP server on port 8000. A request is therefore decrypted at the edge and carried to the server inside the tunnel.
- Behind the tunnel everything is plain HTTP between containers on the instance’s own container network. What protects those hops is described under Gateway-to-service trust below.
- No inbound application port. The stack definition binds no host port for the gateway, the web server, the database or any service. The signing page for people outside your company is a separate static web server reached the same way, on its own hostname.
- The edge sees requests and responses in clear. Because TLS ends at the Cloudflare edge, the edge decrypts each request before it enters the tunnel and encrypts each response after it leaves. It is the one outside party in the request path, and it is listed as a sub-processor for that reason.
- The stack definition still carries an older direct-TLS entry (a web server on ports 80 and 443 with a certificate client). It is switched off by default and only starts when the profile
legacy-direct-tlsis selected.
The gateway closes a connection that has not sent its headers within 15 seconds and allows 5 minutes for a whole request, which is what bounds a long upload.
Who Hosts What, And What Is Not Published
Two facts here come from the site’s published sub-processor list in the Trust Center, not from the instance’s code: your dedicated server is hosted by Contabo GmbH, in the region agreed with you when it is provisioned, and the edge in front of it is Cloudflare. By default your instance is reached through the tunnel.
The following are set outside the instance, so its code and stack definition cannot show them, and this documentation does not state a value for any of them:
- The minimum TLS version and the cipher suites accepted at the edge. They are settings of the edge account. The instance itself only tells browsers to use HTTPS, through the
Strict-Transport-Securityheader described below. - The list of hosting regions you can choose from. The region is agreed with you; no list is published.
- Whether the server’s disk is encrypted by the hosting provider. What the application encrypts itself is on Data Isolation.
- How we monitor the server beyond the heartbeat. The one signal the instance sends us by itself is the five-minute license heartbeat, whose fields are on Heartbeat (License Check-In). The stack definition contains no monitoring agent that reports to us. No uptime commitment is published.
HSTS And Security Headers
Every public host of your instance sends one HSTS value: max-age=31536000; includeSubDomains (one year). It does not carry preload. On the web servers it is set with nginx’s always flag on the page, asset and API-docs locations, so it is also sent on error statuses from those locations. The small runtime config file (/config.js) and the app web server’s own 5xx page are served without it.
| Header | App pages | Signing page | API gateway |
|---|---|---|---|
| Strict-Transport-Security | max-age=31536000; includeSubDomains | Same value | Same value |
| X-Content-Type-Options | nosniff | nosniff | Set by the gateway’s Helmet defaults |
| X-Frame-Options | SAMEORIGIN | DENY | Set by the gateway’s Helmet defaults |
| Referrer-Policy | strict-origin-when-cross-origin | no-referrer | Set by the gateway’s Helmet defaults |
| Permissions-Policy | Geolocation, microphone, camera, payment, USB, Bluetooth, serial, HID, MIDI and the motion sensors are all switched off | Not set | The same list as the app pages |
| Content-Security-Policy | Enforced. See below | Enforced; default-src ’none’, frame-ancestors ’none’ | Set by the gateway’s Helmet defaults |
| Cache-Control | no-store on pages; content-hashed assets are cached for a year | no-store on the page | no-store on every response |
Content Security Policy On The App Pages
script-src 'self' https://static.cloudflareinsights.com. There is no'unsafe-inline'and no'unsafe-eval'for scripts. The app’s own code does not load anything from the second origin; it is allowed so that a script added at the edge is not blocked.style-src 'self' 'unsafe-inline': inline styles are allowed.connect-src 'self' https: wss:: the API host differs per instance and is read at run time, so it is not pinned in the policy. Plainhttp:is not allowed.frame-ancestors 'self',base-uri 'self',form-action 'self',object-src blob:.- No report endpoint is configured, so a violation is blocked in the browser and not reported to the server.
What The Gateway Removes
X-Powered-Byis disabled on the gateway, andx-powered-by,serverandstrict-transport-securityare deleted from every response an internal service returns, so one HSTS value applies and no framework name is disclosed.- An unknown path answers JSON
404with{ "error": "Not found" }, andGET /answers{ "status": "ok" }with no version string. - Cross-origin requests are answered with CORS headers only for the origins the instance is configured to trust; a request with any other
Origingets noAccess-Control-Allow-Origin. The CSRF rule for cookie sessions is on API Authentication.
Gateway-To-Service Trust
The gateway is the only service that accepts a session cookie or a personal API token. Once it has validated the caller it forwards the request with three headers it sets itself. An internal service authorises on those headers, never on anything the client sent.
| Header | What it carries | How it is protected |
|---|---|---|
| x-user | A JSON object describing the caller: id, email, roles, groups, a permission list and the MFA state. The permission list is the caller’s own (for a personal API token, that token’s narrower list), cut down to the service being called: see What The Gateway Forwards below. | Not secret. Its integrity comes from the signature in the next row. |
| x-user-sig | A base64 Ed25519 signature over the exact bytes of the x-user header value (UTF-8). Nothing else is signed: not the path, the method or the body. | Made with a private key generated on the server when the instance is set up. The private key is mounted as a secret file into the gateway and the six services that make calls on the system’s own behalf; every service holds the public key. |
| x-internal-service-token | A JWT with iss: "zonitix_m2m", sub set to the calling service (api-gateway for a forwarded request) and iat. It expires 60 seconds after it is issued, and a new one is made for every request. | Signed with HS256. The services share one per-instance secret, with one exception: the document parser is not given that secret. It holds a separate per-instance secret of its own, and the gateway signs a request bound for the parser with that one. Verification accepts HS256 only. |
What A Service Checks, In Order
- The internal service token. Missing, expired, wrongly signed or with another issuer:
401with{ "error": "Internal service trust failure" }. This is what a request sent around the gateway gets. - The
x-userheader is present. Otherwise401with{ "error": "Authentication context missing" }. - The signature. A service that holds the public key verifies
x-user-sigagainstx-user. A missing or wrong signature answers401with{ "error": "Identity signature invalid" }and writesAUTHZ_XUSER_FORGEDto the audit log. - MFA. A caller whose account requires MFA and has not completed it is refused with
403MFA Required. - The permission the route names, read from the permission list in
x-user, which the gateway has already narrowed (next heading). The administrator role passes every permission check.
What The Gateway Forwards: A Narrowed Permission List
Before it builds x-user, the gateway cuts the caller’s permission list down to the service the request is bound for. It takes the request path without /api/v1, finds the first route prefix in a fixed table that the path starts with, and keeps only the permissions whose name contains one of that prefix’s keywords, plus view_dashboard, which is always kept. The match is a plain case-insensitive substring test on the permission name.
- Which prefixes have a keyword list.
/auth,/recruitment,/user-management,/workflow,/integration,/security-lab,/compliance,/logging,/device,/financeand/signatureare among them. Two examples: a request under/loggingis forwarded with only the permissions whose name containslogs, and one under/devicewith only those containingdevices. - One exemption. A request to
/auth/meis never narrowed. - A path under no listed prefix is forwarded with the whole list.
- Roles are not narrowed. The administrator role passes every permission check from the role list, so narrowing does not affect an administrator.
What this means for an integration: a permission that an account or a token holds can be absent from the list a service receives. A 403 with Access Denied on a route can therefore come from this narrowing and not from the role: the service accepts the permission, the caller holds it, and the gateway did not forward it on that prefix. When every permission a route accepts is removed this way, only the administrator role passes it. The instance’s own OpenAPI file is built with the same rule: an operation’s x-required-permissions lists only the permissions that survive the narrowing, and the removed ones are written to x-permissions-stripped-by-gateway. See Machine-Readable Spec (OpenAPI).
When A Key Or Secret Is Not Set
- No shared service secret: the service does not start. It logs a fatal error and exits. The document parser applies the same rule to its own secret: in production it refuses to start when that secret is missing or shorter than 32 characters.
- No public key on a service: that service skips step 3 and relies on the internal service token alone. The set-up script generates the key pair on a new server, and on every later run it restores the public key from the private key if the public key is missing from the configuration.
- No private key on the gateway: the gateway forwards
x-userwithout a signature, and every service that holds the public key then refuses the request at step 3.
What A Client Cannot Do
- Send these headers itself. Before authentication the gateway deletes
x-user,x-user-sig,x-internal-service-token,x-directory-tokenandx-correlation-idfrom every incoming request. When one of the first four was present it recordsSPOOFED_TRUST_HEADER_STRIPPEDin the audit log, with the header names and never their values. - Reach a service-to-service route. The gateway refuses any path that contains an
internalsegment, and those routes also reject an internal service token whosesubisapi-gateway, which is the only kind a forwarded browser or script request can carry. - Pass a personal API token to a service. For a token-authenticated call the
Authorizationheader is removed before the request is forwarded.
The limit of this design, stated plainly: the signature covers the identity only, and the shared service secret is held by every service except the document parser. A process on the instance that holds both that secret and a signing key could therefore mint a request as any user. The document parser, which reads uploaded files, holds neither: it is given its own separate secret and the identity public key only. The error bodies above are listed with their causes on API Error Troubleshooting.
How Passwords And Tokens Are Stored
No password or bearer token is stored in a form that can be read back. Random high-entropy tokens are stored as a plain SHA-256 hash; passwords and the short codes a person types are stored with bcrypt.
| Secret | Stored as | Notes |
|---|---|---|
| Account password | bcrypt, cost factor 10 | Only the hash is stored. An account created by directory sync, SCIM or single sign-on gets the hash of a random value nobody knows, so it has no usable password. One exception to the cost: the first administrator’s password, set in the setup wizard, is hashed at cost 12, and at cost 10 from the next time that password is changed. |
| Earlier passwords (history) | bcrypt, cost factor 10 | The hashes of the last passwords are kept so a reused one can be refused. They are never returned by the API. |
| MFA recovery codes | bcrypt, cost factor 10 | Each code is hashed separately and marked used after one use. |
| Trusted-device token | bcrypt, cost factor 10 | The browser holds the token; the server holds its hash. |
| Personal API token | SHA-256 | The token is zx_ followed by 32 random bytes (base64url). It is shown once; the server keeps the hash and a short prefix for display. |
| SCIM bearer token | SHA-256 | Shown once; the server keeps the hash and a short prefix. |
| Invitation link token | SHA-256 | 32 random bytes. The hash is cleared when the link is used or replaced. |
| First-run setup key | SHA-256 | The server is given only the hash, and compares in constant time. |
Password History
The passwordHistoryCount setting decides how many earlier passwords may not be reused: 5 on a new instance, any whole number from 0 (off) to 24. A new password that matches one of them is refused with the code PASSWORD_REUSED. A forced password change always refuses the current password, even when the setting is 0. An administrator changes the number in Security Settings; the steps are in Password policy and sessions.
Account Lockout
- The fifth wrong password in a row locks that account for 15 minutes and writes
LOGIN_LOCKED_OUTto the audit log. Each earlier failure writesLOGIN_FAILURE. - While the account is locked, sign-in answers
403withAccount is locked. Please try again in N minute(s).and writesLOGIN_LOCKED, whether or not the password was right. - A successful sign-in sets the counter back to zero. So does the end of the 15 minutes, at the next attempt.
- The numbers are fixed in the product; there is no setting for them. The per-address limits that apply on top are on Rate, Size & Time Limits.
Outbound Connections
Your server accepts no inbound connection for the application, but it does open connections outwards. The first table lists the destinations that are fixed in the product. The second lists the ones that exist only because you configured them. Use both when you write a firewall or data-flow record.
Fixed In The Product
| Destination | Purpose and what is sent | When | Optional? |
|---|---|---|---|
| Cloudflare edge | The tunnel that carries every request for your instance to the server. | All the time, opened by the tunnel container. | No. It is how the instance is reached by default. |
| Zonitix licensing server (the store) | The license heartbeat, and the one-time registration of a new instance. The fields are on the Heartbeat page. | Every 5 minutes; registration once, when the instance is first provisioned. | No. |
| Zonitix application repository (GitHub) | The update agent on the server host reads the list of releases and fetches the one being installed, whether an administrator applies it or the automatic schedule does. If that agent has not reported, the instance asks the GitHub API to compare the deployed version with the latest one; that request names the deployed commit and nothing else about your instance. | When the host agent checks for releases, and when the Updates status is read. | No. |
| Package and image registries | Base images and software packages needed to build a release on the server. | While a release is being built on the host. | No. |
| Zonitix Apple MDM service (mdm.zonitix.com) | Registers your tenant and carries Apple MDM requests. See Design Philosophy. | At each deploy; every 15 minutes, by a background sync that reads the enrolled-Mac list, collects any new FileVault recovery key held there and, where a FileVault profile is in use, asks a Mac with no key yet for its security information; and when an administrator uses an Apple MDM screen. While a server holds a token but no tenant key, each of these also retries the registration. | Yes. Only on a server that was given an MDM token. |
| www.cisa.gov and api.first.org | The Security Lab vulnerability feed: the public list of exploited vulnerabilities is downloaded, and exploit-likelihood scores are requested for a list of public CVE numbers. Nothing about your systems is sent beyond those CVE numbers. An administrator can point the feed at other addresses. | On the feed schedule (every 24 hours unless changed) and when an administrator refreshes it. | Yes. Only when Security Lab is on your plan and the feed is enabled. |
| crt.sh | Security Lab asset discovery can look up certificate names for a domain. The request names that domain. | When a discovery that uses it is run. | Yes. Security Lab only. |
| timestamp.digicert.com (plain HTTP) | Zonitix Sign asks a public time-stamping service to time-stamp the seal on a signed document. If the request fails the document is sealed without that timestamp. | When a signed document is sealed. | Yes. Only when Zonitix Sign seals a document. |
| The model publisher’s registry | The local AI model runtime downloads a model when the one selected is not on the server yet. Prompts and your data are not sent anywhere: the model runs on your server. | The first time a model is needed, or after a different allowed model is selected. | Only when the private AI features are used. |
Only Because You Set Them Up
| Destination | What it is | When |
|---|---|---|
| Your identity provider | OIDC: the discovery document, the signing keys and the token endpoint. SAML: the metadata address, if you give one to be read. | When someone signs in with SSO, and when you save or test the SSO settings. |
| Directories and apps you connect | The API of each service you add under Identity Hooks: Google Workspace, Microsoft Entra ID, Okta, AWS, your LDAP server, and the connected apps such as Slack, GitHub, Zoom, Dropbox, Salesforce, Notion, Zendesk, Figma and BambooHR. | On the sync schedule you set, when you test a connection, and when a workflow creates or changes an account there. |
| Cloud accounts you connect | The AWS, Azure and Google Cloud management APIs, with the read access you granted. | On cloud-mapping and Security Lab discovery runs. |
| Scan targets you put in scope | Hosts and web addresses in a Security Lab scope you attested. | When a scan is run. |
| Your backup destination | The S3-compatible bucket or Azure Blob Storage container you set under System → Backups & Recovery. | After each backup, and for the audit-log archive and anchor files. |
| Your mail server | The SMTP server you set under Security Settings → Email / SMTP. A new instance has none. | Whenever the instance sends mail. |
| Your SIEM | Each log-forwarding destination: an HTTPS webhook, Splunk, Datadog or syslog over TLS. | Continuously, in batches, while a destination is enabled. |
| Webhook addresses you enter | Scheduled-report deliveries to a webhook. | When a scheduled report runs. |
| Your calendar | Google Calendar, when interview scheduling is connected to it. | When an interview is scheduled or changed. |
- Addresses you type are checked before they are called. The SSO, backup, log-forwarding, directory and scan-discovery code paths refuse an address that resolves to a private, loopback or cloud-metadata network.
- Country lookup for the geo rules makes no outbound call. It uses a database of address ranges that is packaged with the gateway.
- The private AI features make no call to an outside AI service. The only model endpoint the code calls is the model runtime container on your own server.
- This list was built by reading the outbound HTTP, SMTP and storage clients in each service. It covers the standard product; a destination added for one customer by arrangement would not appear here.
Related: Heartbeat (License Check-In), Design Philosophy, Identity Hooks.
Components And Versions
The instance is a set of containers on one server. These are the versions the stack definition names in the current release; a later release can change them.
| Component | Software | Notes |
|---|---|---|
| Database | PostgreSQL 18.4 | Pinned to that exact version. One database for the instance; each service connects with its own database role. |
| Cache | Redis 6 (Alpine image) | Pinned to the major version 6. What it holds is listed below. |
| Application services | Node.js 22 on Alpine Linux | The gateway and the identity, user-management, workflow, integration, logging, compliance, device, recruitment, signature and AI services, and Security Lab on plans that include it. Built from the release source on the server. |
| Document parser | Python 3.10 (slim image) | Reads uploaded documents. Started only on plans that use it. |
| Web servers | nginx (stable, Alpine) | One serves the app pages, a second serves the signing page for people outside your company. |
| Local AI model runtime | Ollama 0.30.6 | Pinned by image digest. Runs the model on your server. |
| Tunnel client | cloudflared 2026.5.1 | Pinned by image digest. Started with its own self-update switched off. |
| Container-status reader | nginx 1.27 (Alpine) | The only container that can see the container engine’s control socket, mounted read-only, and it answers only a short allow-list of read requests for the System Health page. |
Container Hardening That Is Configured
- Capabilities. Every application service container drops all Linux capabilities and adds back three:
SETUID,SETGIDandCHOWN. They are used once, by the start-up script. Security Lab, on plans that include it, also keepsNET_RAWandNET_ADMIN. - Process user. The start-up script runs as root only long enough to read the secret files, then starts the service as the unprivileged user
node. The document parser drops to its own unprivileged user,appuser, in the same way. The one exception is the short-lived database migration job that runs at each deploy, which stays root. - No new privileges. The application services, the migration job and both web servers set
no-new-privileges. - Memory limits. Every long-running container has one: 384 MB for most services, 512 MB for user management, 768 MB for the document parser, 1 GB for the database and for Security Lab, 128 MB for Redis and for the tunnel client, 256 MB and 64 MB for the two web servers, 32 MB for the container-status reader, and 8 GB by default for the model runtime. A limit is a ceiling, not memory set aside: the only container with memory reserved for it is the model runtime, at 2 GB. The limits do not change with the size of the server; how they add up against each size is under Services And Internal Ports on the installation page.
- CPU limits. Three containers have one: the model runtime (4 cores), the document parser (3) and Security Lab (2).
- Swap and core files. The gateway and the tunnel client run with core files switched off and a swap limit equal to their memory limit, so their memory is not written to swap.
- Container logs are rotated at 20 MB, five files per container.
- Secrets reach a container as mounted files, not as values in its environment configuration. The tunnel token is the exception: the tunnel client can only take it from its environment.
How Services Reach The Database
- Where. Each service connects to PostgreSQL by its container name on port 5432, over the instance’s own container network. That network exists only on your server, so a database connection starts and ends on the same machine. The stack definition publishes no host port for the database.
- As whom. Each service signs in with its own database role and its own password. The password is mounted as a secret file and put into the connection address when the container starts. Which role may touch which tables is on Data Model & ERD.
- Connection pool. A service holds at most 5 database connections at once. Two are set higher in the stack definition: the identity service (12) and user management (10). A pool keeps no connection open when idle, closes one that has been idle for 10 seconds, and a request waits up to 30 seconds for a free connection before it fails. The database itself is started with a ceiling of 100 connections.
- Statement timeout. Every connection opened by those services carries a 60-second limit on a single SQL statement; a statement that runs longer is cancelled by the database and the request fails.
- Document parser. It keeps its own connection pool with the library’s default size, tests a connection before each use and gives up on a new connection after 10 seconds.
What Redis Holds
Redis holds short-lived security state and no business records. The gateway and the identity service keep the items below in it; user management also opens a connection for a background job queue that nothing in the current release writes to. It has no published port and no dedicated data volume in the stack definition; it is reachable only from the instance’s own container network, at its container name on port 6379, so a connection to it also starts and ends on your server.
- Gateway: rate-limit counters and the counters behind automatic address blocking.
- Signed-out session tokens, each until the token’s own expiry, and the one-time marks that stop an emailed sign-in link or a password-change token being used twice.
- Each person’s computed roles, groups and permissions, cached for 15 seconds.
- Used authenticator codes (90 seconds) and wrong-code counters for the second factor.
- Passkey challenges and the short-lived state of a single sign-on in progress.
If Redis is unreachable, the identity service keeps the same state in its own memory until Redis is back; that copy does not survive a restart of the service. See Failure Modes.
Clocks And Scheduled Jobs
- Jobs inside the app (for example the daily audit-chain anchor at 02:30) are scheduled without a time zone, and the stack definition sets no time zone for any container. They therefore run by the container’s clock, which is UTC unless a zone has been set on the server.
- Jobs on the server host (the backup, restore-test and recovery-drill timers) are system timers written without a time zone, so they follow the host’s own zone. The installation scripts do not set that zone. Which zone a given server uses is not published here; ask us if you need it for your records.
- Dates that mean a calendar day to your company, such as a due date or a month boundary, use the time zone in your instance’s Regional settings, and fall back to UTC when none is set.
- Timestamps in the API, the audit log and exports are ISO 8601 in UTC.