← Blog
SecurityPHPPerfex CRM

Perfex CRM Security: Hardening a Self-Hosted Install

Self-hosting a CRM means owning its security. A practical, layer-by-layer pass over a Perfex CRM install: the host, the login endpoint, staff permissions and the data itself.

Self-hosting a CRM buys you two things a SaaS never will: the data stays on hardware you choose, and nobody can change the pricing, the terms or the feature set out from under you. The bill for that is security. There is no vendor security team quietly patching things at 3am on your behalf. The install is yours, and so is every way into it.

The good news is that the work is finite and mostly one-off. A self-hosted Perfex CRM has a small, well-understood attack surface, and almost all of it can be closed in an afternoon.

What actually gets attacked

Not your custom fields. Not your workflows. In practice, the traffic that finds a self-hosted CRM falls into three buckets:

  1. Automated login attempts. Your admin sign-in page is reachable from anywhere on the internet, and scanners find it within days of the domain resolving. They are not targeting you specifically. They are trying a credential list against every login form they can reach.
  2. Known paths and old software. Bots request predictable URLs looking for a version with a published vulnerability. An unpatched PHP release or a stale third-party library is worth far more to them than guessing a password.
  3. Anything you left readable. A backup .sql in the web root, a .env served as plain text, an open directory listing. These need no exploit at all - just a request.

Everything below is aimed at those three, in that order.

The three layers

LayerWhat it protectsWho owns it
HostThe server, PHP, TLS, file permissionsYou and your hosting provider
ApplicationLogin, sessions, staff accessPerfex plus whatever you add to it
DataBackups, database credentials, exportsYou

Most write-ups only cover the middle row. The rows above and below it are where the cheap wins are.

Layer 1: the host

Keep PHP current. This is the highest-value item on the list and the one most often skipped, because an old PHP version keeps working right up until it does not. A release that has left security support gets no fixes at all, however carefully the application on top of it is written. Check which version you are on and when it stops being supported, then plan the upgrade before that date rather than after an incident.

Serve everything over TLS, and only TLS. Redirect port 80 to 443, and add HSTS once you are confident nothing legitimate still uses plain HTTP. A CRM login posted over an unencrypted connection is readable by every hop in between.

Turn off directory listing and confirm that files outside the intended document root are not reachable. The quickest test is to request a path you know exists but should never be public, and see what comes back.

Get the file permissions right. The web server needs to write to upload directories and nowhere else. Configuration files should not be world-readable. If one Unix account owns the files, runs the web server and holds the database password, then a bug anywhere becomes a bug everywhere.

Rate limit at the edge if you can. Cloudflare, a reverse proxy or fail2ban will drop a chunk of hostile traffic before PHP is ever started. Work done at the edge costs your server nothing.

Layer 2: the application

This is where a security module earns its place, because the things worth doing here are tedious to build and easy to get subtly wrong.

Throttle failed logins. An attacker with unlimited attempts will eventually succeed against a weak password. An attacker who gets five attempts and then waits fifteen minutes will not. This is the highest-impact application-level control there is, and it is worth tuning rather than accepting a default: how many failures before a lockout, how long the lockout lasts, and how much longer it gets when the same account is locked repeatedly.

Our own PerfShield exists for exactly this. It adds configurable limits for maximum login retries, lockout duration, escalating extended lockouts after repeated offences, password-reset attempt caps, and a ceiling on outgoing security emails so the system cannot be turned into a relay. It runs on Perfex CRM 3.0 or later and needs PHP’s allow_url_fopen enabled - a setting most hosts already have on, but one worth confirming. The equivalent for RISE CRM is RiseGuard.

Block the obvious offenders outright. Waiting for repeated failures is reactive. If a range has never had a legitimate user on it and keeps appearing in your logs, denying it up front is cheaper than tolerating it. IP, IP-range and per-user blacklists turn a recurring nuisance into a non-event.

Restrict who can change the security settings. A protective layer that every staff member can switch off is decoration. Access to the security configuration should be its own permission, held by the few people who actually administer the system.

Protect against locking yourself out. Any lockout mechanism needs an answer for the administrator account, otherwise a bad afternoon turns into a support ticket against your own CRM. Check this before you need it, not during.

Layer 3: the data

Back up somewhere the web server cannot reach. A backup sitting in the document root is not a backup, it is a copy of your entire customer database available on request. Push it off the machine, and confirm the destination is not itself publicly listable.

Give the database its own restricted user. The CRM does not need DROP DATABASE. A least-privilege account limits what a successful injection anywhere in the stack can actually do.

Treat exports as live data. A CSV of every client, sitting in someone’s downloads folder or on a shared drive, is the same data with none of the protections. Decide who can export, and keep that permission narrow.

Test a restore. An untested backup is a hypothesis. Restore into a scratch environment once and you will find the missing piece - almost everyone does - while it is still cheap to find.

What to do this week

If you only have an hour, do these five, in order:

  1. Check your PHP version and its security-support end date.
  2. Confirm HTTPS is enforced and no plain-HTTP route remains.
  3. Look at your access log and count the failed login attempts from the last seven days. The number is usually a surprise.
  4. Put a limit on those attempts, with an escalating lockout.
  5. Verify your most recent backup is off the machine and actually restores.

None of this is exotic. It is the difference between a CRM that is self-hosted and a CRM that is merely unmanaged.