Skip to main content

Security

Authentication

  • Two-factor is mandatory. The first login on an unsecured account routes straight to setup: TOTP, or a passkey.
  • Passkeys via WebAuthn, multiple per user, bound to the site hostname.
  • JWT sessions, signed with JWT_SECRET, with a minimum-length check at boot so a short secret cannot be used by accident.
  • Passwords hashed with bcrypt.
  • Login rate limiting per IP, RATE_LIMIT_LOGIN per minute.
  • No email password reset. Root access to the server is the recovery factor, so an attacker who compromises a mailbox does not thereby get a billing system.
  • Forced password change on admin-created accounts.
  • Super admin seeded from .env on every boot; an existing account is left alone.

Encryption at rest

These are encrypted with AES-256-GCM under ENCRYPTION_KEY:

  • PBX administrator passwords
  • Xero client secrets, access tokens and refresh tokens
  • QuickBooks Online client secrets, access tokens and refresh tokens
  • SMTP passwords
  • SFTP credentials for export delivery

None of them are ever returned to the browser. Saving a form with a blank password field keeps the stored value rather than clearing it.

ENCRYPTION_KEY is unrecoverable

There is no key escrow and no recovery. If .env is lost, every credential above must be re-entered and every accounting connection re-authorised. Back .env up off the server.

Network

  • HTTPS with automatic certificates via Caddy and Let's Encrypt
  • Reverse proxy — the API port is not published to the host; only Caddy is reachable from outside
  • UFW firewall — rules for SSH, 80 and 443 added at install, additively, so a co-hosted application's rules survive
  • Gzip compression on responses

Port 443 must be reachable from all sources for Let's Encrypt's multi-perspective validation.

Authorisation

  • Role-based access. viewer is enforced at the API by method, not hidden in the interface, with authentication endpoints exempted.
  • Users and the Audit log are restricted to super_admin and admin. A navigation entry that would return 403 is not shown at all.
  • Cost and margin are partner-only and cannot reach an invoice, an export file, or an invoice share link.

Invoice share links are:

  • Scoped to one invoice
  • Revocable at any time from the invoice
  • Audited on creation and revocation
  • Free of cost and margin data by construction

PBX access

Vodia Billing needs an administrator account on each PBX to run discovery, take snapshots and, if armed, disable an extension.

Restrict the PBX account

Create a dedicated administrator account on the PBX for Vodia Billing, and whitelist the Vodia Billing server's IP address on the PBX so that only this server can authenticate with those credentials.

Automatic fraud response defaults to report only. Disabling an extension must be armed per tenant, and a live-armed tenant is flagged in red on its row so the state is never a surprise.

Data handling

  • All money is integer micro-units. No floating-point currency arithmetic.
  • Invoice detail is frozen at finalization, so an issued invoice cannot be altered by a later configuration change.
  • Rate sheet uploads supersede rather than rewrite, so historical rating is reproducible.
  • Export files are immutable; a retry re-sends the stored bytes.
  • Call reclassification preserves the original rating and is audited.

Audit trail

Administrative and financial actions are logged with actor, timestamp and detail. See Settings, Users and Roles.

  • Back up .env off the server, and the whole VM on a schedule
  • Restrict SSH to key authentication
  • Whitelist the Vodia Billing server's IP on each PBX
  • Keep automatic fraud response in report mode until thresholds are proven
  • Use a dedicated Xero or QuickBooks user for the OAuth connection so its access can be revoked independently