Technical & organisational measures
This page lists the technical and organisational measures (TOMs) that the ZentraLink platform supports out of the box. The list reflects what the codebase actually implements; operators add their own physical and procedural measures on top.
Access control
- Self-hosted deployment on operator-owned infrastructure; physical access controlled by the host provider's policies (Hetzner / IONOS / on-prem).
- Argon2id password hashing (memoryCost 19 MiB).
- Two-factor authentication via TOTP with single-use backup codes; backup codes are themselves stored only as argon2id hashes.
- Server-side opaque session tokens (no JWT in cookies), 14-day TTL, secure + httpOnly + sameSite cookies.
- Role-based access control (Owner, Admin, Supporter) plus per-tenant memberships.
- Account-enumeration-safe password reset: /forgot-password returns the same neutral response whether the address exists or not.
Authorisation
- Tenant-scoped data access enforced on every API route via a single requireTenant helper.
- Per-route role-based access checks.
- Plan-feature gates (requireFeature) on every gated endpoint and sidebar entry; a plan downgrade transparently revokes access to features the new tier does not include (e.g. Public API).
- Configurable approver count and approver roles for DNS change requests.
- Tenant separation verified by automated tests on every release.
DNS write protection (fail-closed)
- Provider-account mode: MONITORING_ONLY (no writes attempted), LIVE_READONLY (writes blocked at the executor), LIVE_WRITE (writes allowed).
- Per-domain DomainAccessMode override that can only tighten the provider mode, never loosen it (default READ_ONLY).
- A connection test must succeed before a provider can be flipped to LIVE_WRITE.
- All DNS writes go through one change-request executor — there is no second code path that could bypass the mode check.
- Every write captures a pre-execution and a post-execution snapshot for diffing and rollback.
Pseudonymisation / minimisation
- Audit log stores user id + structured metadata, not raw payloads.
- Login fingerprints are stored as SHA-256 hashes (not raw IP/UA pairs).
- Backup codes stored as argon2id hashes only.
- Notification payloads persist structured codes (titleKey + reasonCodes), not the rendered free text — translated on read for each recipient.
- DENIC WHOIS for .de domains transmits only the bare domain name (no contact data); the rest of the registry already strips PII post-GDPR.
Encryption
- TLS for all transport (operator-issued certificates, e.g. via Let's Encrypt).
- Provider API credentials for all 11 supported provider types (Cloudflare, Hetzner DNS, DigitalOcean, AWS Route53, PowerDNS, IONOS, Gandi, GoDaddy, Namecheap, OVH, Porkbun), 2FA secrets, API-key secrets, webhook signing keys and per-tenant SMTP passwords are encrypted at rest with AES-256-GCM using PLATFORM_ENCRYPTION_KEY.
- Database access via Unix socket or localhost TCP recommended for single-host installs.
- Security response headers set globally: X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: strict-origin-when-cross-origin, Permissions-Policy locks down camera/microphone/geolocation; X-Powered-By is suppressed.
Integrity
- Every DNS write produces a DnsChange row + audit event.
- Pre- and post-execution snapshots captured automatically by the change-request executor.
- Provider operations report per-op success/failure into the change request.
- Defence in depth at the protocol boundary — the DENIC port-43 WHOIS lookup validates the domain as a bare hostname before writing it to the raw socket, blocking CRLF/whitespace query-line injection.
- Outbound webhooks are HMAC-signed and pass an SSRF guard that refuses private / loopback / link-local IPs.
Availability
- PostgreSQL standard backup tooling via scripts/backup.sh (pg_dump).
- systemd-supervised services with automatic restart on failure.
- Healthcheck endpoint exposed at /api/healthz.
- Cron worker isolated behind CRON_TOKEN bearer auth, idempotent loops; cron-job catalogue and TASKS map are linked by a module-load assertion so a future addition cannot silently 404.
- External lookups (RDAP, DENIC WHOIS, provider APIs) all enforce strict timeouts; a hung registry cannot wedge the orchestrator. WHOIS_DISABLE=1 disables the port-43 fallback per deployment.
Public API & outbound webhooks
- Per-tenant API keys with scope set, per-key IP allow-list and per-key rate limit; the plaintext key is shown exactly once on creation (one-time modal).
- Public-API requests check the tenant's plan capability (api.publicAccess) on every call, so a downgrade revokes existing keys without revoking the rows.
- Outbound webhooks ship an HMAC-signed envelope (X-ZentraLink-Signature) with consecutive-fail counter and exponential retry; Slack / Teams / Discord webhooks are auto-rewritten into each platform's expected shape (those platforms authenticate via the unguessable URL).
- Webhook signing secret is also shown exactly once at creation.
Auditability
- AuditEvent rows for every privileged action (filtered viewer at /dashboard/audit).
- GovernanceActivity rows for governance-relevant overrides (provider-trust health override, scheduled plan change, Reset-Center category wipes).
- CronRun rows for every scheduled job invocation.
- MailMessage rows for every outbound email attempt (QUEUED/SENT/FAILED).
- DnsChange and DnsChangeRequest rows form the complete DNS history.
- Public-API requests are logged with key id, route and outcome.
Data subject support
- Owner-only Reset Center selectively wipes data per category (customers, domains, tickets, billing, audit, ...) while preserving the operator's own user, sessions and memberships; every wipe is logged.
- CSV export endpoints for the full domain portfolio, the latest zone snapshots, open recommendations and customer-visible audit events.
- CSV bulk import for the domain portfolio (plan-gated).
Vendor management
- Eleven DNS / registrar provider integrations (Cloudflare, Hetzner DNS, DigitalOcean, AWS Route53, PowerDNS, IONOS, Gandi, GoDaddy, Namecheap, OVH, Porkbun); each is engaged only when the operator connects it.
- Provider list and roles documented on the /integrations page.
- Subprocessor list maintained at /subprocessors with current vendors and their role in the data flow.
- Provider Trust Center exposes computed and operator-overridden health per provider account with audit.
Domain sales
- Live availability check, registration, renewal, AuthCode and DNSSEC operations against the upstream DomainRobot reseller API. The operator's API key + password are stored AES-256-GCM-encrypted in the DomainSalesSettings table (or, optionally, in environment variables); the customer-facing surface never sees credentials.
- Registration only runs AFTER Mollie has confirmed payment. The order processor uses an optimistic status lock to prevent two concurrent workers from registering the same domain twice.
- Failed registrations after a successful payment trigger an automatic Mollie refund and a customer notification; the order is recorded as REFUNDED in the audit log.
- AuthCodes are AES-GCM-encrypted at rest, revealed exactly once via the dashboard's one-shot reveal endpoint, and the encrypted blob is wiped after the read. Co-admins of the workspace receive a security mail on every reveal.
- Customer-facing payment method picker is curated by the operator: each Mollie method has a separate toggle for subscriptions vs domain orders, so methods like Klarna or Apple Pay can be offered for one-off purchases without affecting recurring billing.
- Master kill-switch in DomainSalesSettings.enabled. When off, every buy surface (marketing search, dashboard search, public TLD list, availability check, order creation) returns 503 SALES_DISABLED and the sidebar entry hides itself.
Incident response
- Statuspage rendered from the StatusIncident table at /status.
- Predictive incident notifications fire from a scheduled cron based on detected risk signals; the notification payload carries structured reason codes, not raw scan output.
- Security disclosure address: security@zentralink.net (operator can override per deployment).
- Operator is responsible for time-bound notification flows (incident comms).