A common arrangement for an organisation running a portfolio of web properties: one server, one hosting account, one directory per site, and all of it executing as the same operating system user. It is the default nearly everywhere, because it is what control panels produce and because it works fine right up until it does not.
The failure is well understood and still routinely underestimated. A vulnerability in any component of any one site — an out-of-date plugin, an unpatched library, a file upload that does not validate what it accepts — gives an attacker the ability to execute code as that shared user. And that user can write to every other site's files, because from the operating system's point of view they are all the same tenant. One weak dependency on the least important property in the portfolio is a write primitive against the most important one.
This is not a hypothetical. It is the standard progression in mass-compromise incidents, and the reason such incidents take weeks to clean rather than hours: once the attacker has reached every document root, every site has to be treated as compromised, including the ones with nothing wrong with them.
The boundary that matters is execution, not file permissions
The intuitive response is to tighten directory permissions. That is usually the wrong lever, and teams lose days to it.
Web servers need to read static files, so the directories have to stay readable by the web server process. Lock them down past that and the site starts returning 403s on its own stylesheets while the files are plainly present on disk — a confusing failure that sends people down a permissions rabbit hole. Meanwhile the actual exposure is untouched, because the exposure is not about who can read the files. It is about which identity the application code executes as.
The boundary worth building is therefore at the process level:
- One operating system user per site. Not per customer, not per environment — per site.
- One application worker pool per site, running as that user, listening on its own socket. Code from site A physically cannot execute as site B's user.
- A filesystem restriction per pool confining the interpreter to that site's directory tree plus a scratch area, so path traversal has nowhere to go.
- Dangerous interpreter functions disabled — process execution, shell access — so a successful injection is contained to file operations inside an already-confined tree.
- Database credentials scoped per site, so a leaked configuration file exposes one database rather than the estate.
With that in place, the blast radius of a compromised plugin is one site. Cleanup is hours, and the other properties can be evidenced as untouched rather than assumed clean.
The costs, stated honestly
Per-tenant isolation is not free and it is worth being straight about where it bites.
Memory. Each pool holds its own worker processes. The headline figure looks alarming and is misleading — most of a worker's resident memory is shared library and cache pages — but the genuine incremental cost per worker is real and it multiplies by the number of sites. Pool sizing stops being a default and becomes a decision you have to make with numbers.
Shared subsystems leak. Isolation at the process level does not extend to everything. An opcode cache, for example, is typically one segment for the whole server: every site consumes it, and when it fills there is often no error at all — caching silently stops and every site slows down at once. Isolation buys you a security boundary, not a performance boundary, and the two are easy to confuse.
Configuration becomes a fleet problem. Fifty pools means fifty configuration files, and a syntax error in any one of them can prevent the whole worker service from starting — which takes down every site, including the forty-nine that were fine. Provisioning has to be scripted, the configuration has to be validated before it is loaded, and the reload has to be gated on that validation passing. Doing this by hand is how a routine addition becomes a fleet-wide outage.
Retrofitting
You do not have to do the whole estate at once, and you should not. Move the highest-value property first, prove the pattern, then script it. The script is the deliverable — anything done by hand fifty times will be done differently at least twice.
The test of whether it worked is simple and worth running deliberately: from inside one site's execution context, attempt to read another site's configuration file. If that succeeds, the boundary is decorative.