Panellicense

CloudLinux SecureLinks: stop symlink attacks on shared cPanel hosting

Configure CloudLinux SecureLinks to block cross-account symlink and hardlink attacks on shared cPanel servers — kernel sysctls, Apache directives, and verification.

7 min readUpdated 2026-05-19cloudlinux · securelinks · symlinks · security
schema: HowToschema: FAQPageschema: BreadcrumbList

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.

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.

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.

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

  1. Setting fs.symlinkown_gid = 0. Breaks WHM and cPanel internals. Always 48 on EL, 33 on Debian/Ubuntu. Check with getent group apache if you're not sure.
  2. 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.
  3. Trusting .htaccess over kernel sysctls. A customer can override .htaccess settings; they can't override the kernel. The sysctls are the load-bearing layer.
  4. 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 stale Options FollowSymLinks that nobody noticed.

FAQ

Does SecureLinks work without CloudLinux?+
No — the kernel patches that enforce symlink owner match across all processes only ship in the CloudLinux kernel. Vanilla AlmaLinux or Rocky give you Apache's SymLinksIfOwnerMatch directive only, which leaves FTP, PHP-FPM workers running outside Apache, and cron jobs exposed.
Will enabling fs.enforce_symlinksifowner break existing customer sites?+
Rarely — but legitimate cross-account setups will fail. The two real-world cases are shared WordPress media libraries (one account's plugin symlinks into another's wp-content) and old multi-domain setups where one cPanel account hosts symlinks into a sibling account. Both are misconfigurations you want to find.
What's the difference between fs.protected_symlinks and fs.enforce_symlinksifowner?+
fs.protected_symlinks is the upstream Linux protection — it blocks following a symlink in a world-writable sticky directory (think /tmp) unless the follower owns the symlink. fs.enforce_symlinksifowner is the CloudLinux extension — it blocks any process in the configured GID from following a symlink whose target owner doesn't match the symlink owner, anywhere on the filesystem.
How do I check if SecureLinks is currently active?+
Run `sysctl fs.enforce_symlinksifowner fs.symlinkown_gid` — you want `1` and `48` (or your distro's apache GID). Then do the two-user curl test from step 4. A 403 or empty response means it's working; the file contents means it's not.
Does SecureLinks slow down disk operations?+
The overhead is one extra credential comparison per `open()` of a symlink, so it's measurable in microbenchmarks but invisible under real web traffic. Shared hosts running 1,000+ accounts per server report no measurable PHP latency change after enabling it.

Next steps

Switch in an afternoon

Switch from your current reseller — free.

We migrate active cPanel, Plesk, LiteSpeed and CloudLinux licenses from any reseller. We prorate the first month so you never pay twice, and your customers see zero downtime during the swap.