Zum Hauptinhalt springen

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
  • The Stripe secret key and the Stripe webhook signing secret

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

The Stripe pay route rides the same token rather than taking a public URL of its own, so revoking the share link revokes the ability to pay through it, and there is no second public path to one invoice with different rules.

The webhook endpoint​

/api/stripe/webhook is the one unauthenticated route in the system that can write money, so it is guarded four ways:

  • HMAC-SHA256 over the raw request bytes, compared in constant time against the endpoint's signing secret. Without a signing secret configured, every event is refused; there is no mode in which an unverified event is acted on.
  • A five-minute timestamp window, so a genuine event captured once cannot be posted again for ever.
  • livemode checked against the mode of the stored key, so a test event cannot settle a real invoice.
  • A unique event id and a unique payment reference in the database, so Stripe's three days of retries cannot produce a second ledger row.

Refused events are logged and raise a single escalating alert rather than writing to the audit trail, so an unauthenticated caller cannot fill it.

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