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).