The classic shared-hosting attack hasn't changed in fifteen years: one compromised account
drops a PHP shell, the shell creates a symlink to /home/victim/public_html/wp-config.php,
Apache serves the file because it has read access, and the attacker walks away with a
database password they can use against every other account on the box. SecureLinks is
CloudLinux's answer — a small set of kernel patches and Apache directives that make these
cross-account reads fail at the syscall level, not just in .htaccess.
This guide covers the configuration that ships with every CloudLinux license, what each sysctl actually does, and how to verify the protection is working before you trust it in production.
What CloudLinux SecureLinks blocks
CloudLinux extends the Linux kernel with three protections that activate together under the SecureLinks name:
- Symlink owner match — a process running as a given group cannot
open()a symlink whose target is owned by a different user. The check happens in the kernel, so Apache, PHP-FPM, FTP, and anything else hitting the same path all benefit. - Hardlink owner enforcement — a user cannot create a hardlink to a file they don't own. This closes the loophole where symlink protection is bypassed by hardlinking to another user's config file.
- Race-condition-safe link resolution — patches against the TOCTOU window where an
attacker swaps a directory for a symlink between Apache's permission check and its
open()call.
The plain-vanilla Apache directive SymLinksIfOwnerMatch does the first check, but only
inside Apache's request flow. Without the kernel patches, anything that bypasses Apache —
a PHP script, an FTP client, a cron job, the JetBackup restore agent — can still follow a
cross-owner symlink. CloudLinux makes the check mandatory for every process whose group
matches the configured GID.
Step 1 — Confirm you're running the CloudLinux kernel
The patches only exist in the CloudLinux kernel. If you're on a vanilla AlmaLinux or CentOS kernel, none of this works.
uname -r
Look for lve in the version string — for example 4.18.0-553.16.1.lve.el8.x86_64. If
you don't see it, the CloudLinux install guide walks
through cldeploy and the post-reboot kernel switch.
Step 2 — Set the symlink owner match sysctls
The two sysctls that drive SecureLinks live in /etc/sysctl.d/90-cloudlinux.conf on most
installs:
fs.enforce_symlinksifowner = 1
fs.symlinkown_gid = 48
enforce_symlinksifowner = 1 turns the check on. symlinkown_gid = 48 restricts the
check to processes whose effective GID is 48 — the apache group on EL-family systems.
That means Apache, PHP-FPM workers, and anything else running under the web user are
covered, while root processes (backups, package managers, the cPanel daemons) are not.
Apply without rebooting:
sysctl --system
sysctl fs.enforce_symlinksifowner fs.symlinkown_gid
The second command should echo 1 and 48 back. If symlinkown_gid returns 0, the
restriction applies to every user including root, which usually breaks WHM transfers
and cPanel's own scripts — set it to 48 explicitly.
Step 3 — Turn on hardlink protection
Symlink protection alone is bypassable. An attacker who can write to /tmp (or any
shared directory) can create a hardlink to /home/victim/public_html/wp-config.php,
then the hardlink — being a real directory entry, not a pointer — gets read by anyone
with permission on the path.
CloudLinux extends the standard kernel fs.protected_hardlinks sysctl with an
owner-match variant:
fs.protected_hardlinks = 1
fs.protected_hardlinks_create = 1
fs.protected_symlinks = 1
Add those to the same 90-cloudlinux.conf and re-run sysctl --system. The combination
prevents a user from creating a hardlink to a file unless they own the target — the
standard Linux protection — plus blocks the create call entirely if the source directory
isn't owned by the same user, which is CloudLinux's stricter variant.
Step 4 — Verify with a real cross-owner read
Sysctls are easy to set and hard to be sure about. A two-account test takes 30 seconds and proves the protection is actually doing something.
Create a "victim" file as one user:
useradd cltest1 && useradd cltest2
echo "SECRET" > /home/cltest1/secret.txt
chmod 644 /home/cltest1/secret.txt
Try to read it through a symlink as the other user:
su - cltest2
ln -s /home/cltest1/secret.txt ~/cross.lnk
curl -s http://localhost/~cltest2/cross.lnk
With SecureLinks active and Apache serving the request, you should get a 403 or an empty
body, not the contents of secret.txt. If you see SECRET in the response, the chain
is broken — most often because the Apache config still has Options +FollowSymLinks set
on the relevant <Directory> block. Switch it to SymLinksIfOwnerMatch and test again.
Clean up:
userdel -r cltest1 && userdel -r cltest2
Step 5 — Force Apache to honour the owner match
EasyApache 4 on CloudLinux defaults to SymLinksIfOwnerMatch, but customer .htaccess
files often re-enable plain FollowSymLinks. WHM has a global override:
WHM → Apache Configuration → Global Configuration → Directory "/" — set the Options
field to SymLinksIfOwnerMatch -FollowSymLinks and rebuild Apache. After that, even an
Options +FollowSymLinks in a user .htaccess is ignored, because EasyApache 4 emits
AllowOverride Options=Indexes,Limit,Nonfatal=All rather than the unrestricted form.
If you want to take it further and block Options overrides entirely, see
Apache MPM and AllowOverride tuning under EasyApache 4
for the directive matrix.
Step 6 — Add the same protection to FTP and Pure-FTPd
A symlink-aware web server still leaves you exposed if FTP and SSH happily resolve
cross-owner links. CloudLinux's CageFS handles this for
shell access — every user gets a private / view, so cross-owner paths literally don't
exist from inside the cage. For FTP, make sure Pure-FTPd has ChrootEveryone yes in
/etc/pure-ftpd.conf so users can't cd out of their home directory in the first place.
Common mistakes
- Setting
fs.symlinkown_gid = 0. Breaks WHM and cPanel internals. Always 48 on EL, 33 on Debian/Ubuntu. Check withgetent group apacheif you're not sure. - Forgetting to rebuild Apache after the global Options change. WHM saves the
directive but the running config doesn't pick it up until
/usr/local/cpanel/scripts/rebuildhttpdconf && service httpd restart. - Trusting
.htaccessover kernel sysctls. A customer can override.htaccesssettings; they can't override the kernel. The sysctls are the load-bearing layer. - Skipping the verification step. I have seen servers where the sysctls were set,
Apache was rebuilt, and the cross-owner curl test still leaked the file — because the
Directory /block had a staleOptions FollowSymLinksthat nobody noticed.
FAQ
Does SecureLinks work without CloudLinux?+
Will enabling fs.enforce_symlinksifowner break existing customer sites?+
What's the difference between fs.protected_symlinks and fs.enforce_symlinksifowner?+
How do I check if SecureLinks is currently active?+
Does SecureLinks slow down disk operations?+
Next steps
- For the resource-isolation half of shared-hosting hardening, see CloudLinux LVE tuning without angry customers
- The CageFS custom binaries guide covers per-user filesystem virtualisation that complements SecureLinks at the shell level
- If you haven't activated CloudLinux yet, the CloudLinux license activation page has single-server and bulk options