A compromised WordPress plugin doesn't always deface your site. Sometimes it just opens a socket, sends your database credentials to a command-and-control server, and waits. Inbound firewalls see nothing. File scanners might miss it if the code is obfuscated. But the outbound connection — PHP phoning home — is visible if you're watching the right layer. A local Squid proxy forces all PHP outbound traffic through one monitored choke point, and a Python correlation script matches every external connection back to the exact script and site that made it.
The blind spot
By default, PHP-FPM workers can open outbound connections to any host on any port. A malicious actor who injects code into a theme or plugin can exfiltrate your database, POST your wp-config.php to a remote server, or register the machine as a node in a botnet — all without triggering a single inbound firewall rule. Your Nginx access logs show what came in. They show nothing about what PHP did after the request completed.
Putting a proxy in front of PHP's outbound traffic changes that. Every connection — cURL requests, file_get_contents() calls, stream wrappers, SOAP clients — routes through Squid. Squid logs it. You audit it periodically. The built-in threat-intelligence blocklist stops connections to known C2 domains before they leave the machine. What's left in the log after filtering out trusted traffic is a short list of connections worth investigating.
Step 1: Install Squid
sudo apt install squid
Squid binds to localhost only — it's not a public proxy. Nothing outside the server can reach it. Only processes running on the same machine (PHP-FPM, cron jobs, your audit scripts) can use it.
Step 2: Configure Squid
Replace /etc/squid/squid.conf:
# Bind to localhost only — not reachable from outside
http_port 127.0.0.1:3128
# Allow only standard web ports
acl Safe_ports port 80
acl Safe_ports port 443
acl SSL_ports port 443
acl CONNECT method CONNECT
http_access deny !Safe_ports
http_access deny CONNECT !SSL_ports
# Only localhost and PHP-FPM can use this proxy
acl php_local src 127.0.0.1 ::1
http_access allow localhost
http_access allow php_local
http_access allow php_local to_localhost
# Threat intelligence blocklist — prevents connections to known C2 domains
acl malicious_sites dstdomain "/etc/squid/malicious_domains.txt"
http_access deny malicious_sites
# Block connections to internal/LAN addresses
http_access deny to_localhost
http_access deny to_linklocal
include /etc/squid/conf.d/*.conf
http_access deny all
# Lightweight caching — reduces repeat outbound requests for static assets
cache_mem 64 MB
maximum_object_size_in_memory 512 KB
cache_dir ufs /var/spool/squid 100 16 256
maximum_object_size 10 MB
refresh_pattern -i \.(css|js|woff|woff2|png|jpg|jpeg|ico)$ 4320 100% 43200 override-expire override-lastmod reload-into-ims
refresh_pattern ^ftp: 1440 20% 10080
refresh_pattern ^gopher: 1440 0% 1440
refresh_pattern -i (/cgi-bin/|\?) 0 0% 0
refresh_pattern . 0 20% 4320
A few things worth calling out:
http_access deny !Safe_ports— PHP can only connect to ports 80 and 443. No SMTP (port 25), no SSH (port 22), no arbitrary high ports. A compromised script can't use your server as a spam relay or open a raw socket to a C2 server on a non-standard port.http_access deny malicious_sites— Connections to domains on the blocklist are dropped before they leave the machine. The update script in Step 5 keeps this list current.http_access deny to_localhost— After explicitly allowing PHP to connect to localhost services, this catch-all blocks everything else from accessing internal addresses. Defense in depth.