Security document for customers
FluxCRM security: a separate instance, encrypted transport, a full change log
The technical and organisational measures that actually work in the product, and what is not there yet.
Every company has its own instance and database, and every change in the data is visible in the audit log.
Rules of this document
We describe only what works in the code and in the deployment. A missing entry means we do not declare that measure. The document is reviewed once a quarter together with the rest of the documentation.
Isolation and hosting
- One company, one instance. Every customer has a separate container, its own SQLite database, its own API key and its own domain. There is no shared database with other customers.
- Server. Instances run on a VPS server at the OVH provider, in Docker containers, as a user without administrator rights (uid 10001). The instance data directory has restricted permissions, and files with secrets have mode 600.
- Transport. All traffic goes over HTTPS (Let's Encrypt certificates renewed automatically by Caddy). The session cookie has the
HttpOnlyandSameSite=Strictflags, and behind an HTTPS proxy alsoSecure. - No external dependencies in the code. The server uses only the Python standard library. There are no packages that could bring a supply chain vulnerability (list: library licences in the technical documentation).
- Encrypted files. Files uploaded by customers in the portal, files sent by the company, scans and photos are stored encrypted (an authenticated stream cipher, a key in a file outside the database, with rotation). A changed byte or a wrong key gives an error, never a silent wrong result.
Access and authentication
- Accounts and roles. Owner, salesperson, read only. The "read only" role cannot change anything: every change ends with a refusal.
- Passwords. At least 10 characters by default; the owner tightens the policy in the panel (length, capital letter, digit, special character, no login in the password). Passwords are stored only as a scrypt hash (N=2^14, r=8, p=1) and are never saved in clear or logged.
- 2FA. A one-time TOTP code (RFC 6238) enabled per account; the owner can enforce it for the owner role (without it the panel does not allow work). Access keys (passkeys, WebAuthn, ES256) replace the password together with the second factor: we require user verification (biometrics or PIN), and the signature counter detects a cloned key.
- Sign-in link from email. An account without 2FA and with an email address saved can request a one-time link (valid 15 minutes). The token is in the address fragment, so mail scanners do not use it up, and only its SHA-256 hash is in the database. Accounts with 2FA do not get such links.
- Sessions. A random token in a cookie. Only its SHA-256 hash is in the database, so a copy of the database does not give a signed-in session. A session expires after 30 days without use, and after a password change the other sessions of the account expire.
- Protection against guessing. Failed logins and wrong keys are limited per IP address: 10 attempts per minute, then a 429 response.
- API keys. A full key and a read-only key, renewed in Settings. The last active owner cannot be disabled or demoted.
- Integration keys. A separate key for each integration (Zapier, website): only selected modules, read or write mode, an expiry date and its own request limit. Only the SHA-256 hash is in the database, and the full value is visible once, at creation. A key can be revoked at any time.
- Custom roles and teams. The owner creates roles with permissions per module (read or write: opportunities, tasks, mail, finance, projects, reports, team, settings) on top of the base salesperson or read-only role. Teams have a lead who sees and changes the opportunities of their members.
- Request limits. Besides the login attempt limit there is a request limit per IP address (1200 per minute by default) and per account or integration key (600 per minute by default), with a 429 response and a Retry-After header.
- Login log and devices. Every login attempt (also a failed one) leaves an entry with the device and IP address. A login from a new device sends a notification (push and an instance alert). Every user sees their devices and signs them out remotely, and the owner does this for any account.
- Masking in logs. Server logs, error traces and reports pass through masking: email addresses, phone numbers, national ID numbers, account numbers, tokens, keys and passwords do not reach the logs or the error database.
- Secret rotation. Secrets are in the instance directory (not in the repository); diagnostics in the panel remind about keys older than 90 days.
- Webhooks. Input from the form requires a secret, and outgoing notifications are signed with HMAC-SHA256.
- Push notifications. The payload is encrypted with the device key (RFC 8291), and the server sends it only to the known notification services of Apple, Google, Mozilla and Microsoft (https).
Protection against browser attacks
Content-Security-Policy: default-src 'self': the panel does not load scripts or styles from outside and does not use inline code.- Headers
X-Content-Type-Options: nosniffandReferrer-Policy: no-referrer. - Content from customers (emails, forms) is always treated as text, never as HTML. API responses have
Cache-Control: no-store. - Panel files are served with path validation (no
.., hidden files or links leading outside the directory).
Change log (audit)
Every change in the data (POST, PUT, PATCH, DELETE) leaves an entry: who (login or key level), what (method and path), and the state before and after. Export of all data and export of one person's data are audited separately and available only to the owner. The log contains no passwords or secrets. The server access log records the time, method, path without parameters, response code and IP address, without keys, secrets or email addresses.
Backups and continuity
- A consistent copy of the database every hour: 48 hourly and 14 daily per instance.
- Once every 7 days a read test of the newest copy (an integrity check and a record count), and the result is visible in the panel.
- A daily copy outside the container, on the server (30 days of retention), and a mirror on a separate operator device.
- Monitoring: the instance state (ok, warning, outage) is calculated in the product, and an external script checks availability every 5 minutes and sends an email alert without personal data.
- Restoring from a copy and outages: the emergency runbook in the administrator documentation.
Error and outage handling
- Error identifier. Every server error (5xx) gets a number
E-XXXXXXXXthat returns in the response and in the panel; the owner opens it by number (a message and a trace without customer data). - One-click report. The Report a bug button sends the screen, resolution, error numbers and short JS messages; the contents of opportunities and customer data are not part of the report.
- Errors in the panel. JavaScript errors from the browser go to a set with counters (one entry per kind of error, with a request limit per IP address).
- Status page. The public address
/status(and/api/status) shows only component states: panel, database, imports, backups, emergency mode; it also works when the database is unavailable. - Diagnostics. The owner sees in the panel what does not work and why, with advice on what to do (database, disk, consistency, backups, each import, model, mail, secret rotation).
- Emergency mode. With one button the owner switches the panel to read-only for the time of the repair; logging in and reporting errors keep working.
- Copy before migration. When an update changes the database schema, a separate copy of the file is made before the migration (the last five are kept).
- Import retries. An import with an error is retried by itself with a growing interval (5, 10, 20, 40, 80 minutes); after the attempts run out an alert remains.
- Graceful degradation. An unavailable database gives 503, and an unavailable model, mail or network gives 502 with a message; the rest of the panel keeps working. Chaos tests switch off the database, the model and the network and check this at every change.
Data minimisation and deletion
- Anonymisation of personal data after a period without activity (retention) while keeping the numbers for statistics, switched on per instance.
- Export of a person's data at their request.
- GDPR consents with a date, source and proof.
- Process details: GDPR for customers.
Access to sensitive data and location
- Access log. Every successful read of a deal card with a sensitive field (income, liabilities, credit bureau, income type: the list in
[zgodnosc] pola_wrazliwe), of documents, attachments, recordings and exports leaves an entry: who, when, from which address, which deal and which category, without the data itself. Retention is 730 days ([zgodnosc] logi_dni), with an alert on bulk reads (more than 50 different deals a day by one account). Only the owner sees it:GET /api/zgodnosc/dostep. - Data location. The declared region and hosting (
[zgodnosc]) are compared with the active features that send content off the server:GET /api/zgodnosc/lokalizacjaanswers whether the data stays in the EU and what leaves it. - Email tracking. Off by default (
[sledzenie] wlaczone). When on, every tracked email carries a notice for the recipient and an opt-out link; we only count opens and clicks per email (no IP address or User-Agent), and redirects go only to addresses stored at sending time. - Client portal. Uses one strictly necessary session cookie (HttpOnly, SameSite=Strict, up to 3 days); the cookie notice is shown in three languages, with no advertising or analytics cookies.
Language models
Features based on a language model are off by default and are switched on by the instance owner in agreement with FluxLab. The model gets only the data needed for the task (the conversation of one opportunity), works in read-only mode, and its result is always approved by a person. Customer content is treated as data, not as a command. The instance can require a customer's consent before their data is processed by the model, every use of AI has an entry in the ethical review register, and drafts for customers can carry an AI note. The provider is Anthropic, see sub-processors.
What is not there yet
We say it plainly so there is no gap with expectations:
- the SQLite database is not encrypted at application level (files and secrets are); protection relies on system permissions and the security of the server provider,
- sending copies to object storage (S3, B2) is prepared in the configuration but is not switched on,
- we have no ISO 27001 certification and no SOC 2 report,
- sign-in with Google and Microsoft (SSO) is not available; there are a password with TOTP and access keys (passkeys, ES256, without verifying the key manufacturer),
- we have not carried out an independent penetration test, and the security code review is protected by regression tests.
Reporting vulnerabilities
Report vulnerabilities to pawel@fluxlab.pl. We acknowledge a report within 2 working days, and we deploy critical fixes to all instances immediately. Incidents involving personal data are handled according to the 72-hour procedure (see GDPR for customers).