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_LOGINper 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
.envon 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.
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.
vieweris enforced at the API by method, not hidden in the interface, with authentication endpoints exempted. - Users and the Audit log are restricted to
super_adminandadmin. 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.
Share links
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.
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.
Recommended hardening
- Back up
.envoff 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