Securing CloudPanel on Ubuntu 24.04 Part 3

15/11/2024 15/11/2024 security 7 mins read
Table Of Contents

Part 3: Web Security and Application-Level Protection #

I’ll explain how to implement comprehensive web security measures specifically tailored for CloudPanel. Let’s break this down into manageable components that work together to create a robust security layer.

1. Enhanced Nginx Security Configuration

First, let’s create a security-focused Nginx configuration that aligns with CloudPanel. CloudPanel uses a dual Nginx configuration approach, with core settings in /home/clp/services/nginx/ and site-specific configurations in /etc/nginx/:

Terminal window
# Create a security-focused Nginx configuration
sudo nano /etc/nginx/conf.d/security-headers.conf

Let’s implement strong security headers while maintaining CloudPanel’s functionality:

# Enhanced Security Headers Configuration
# These headers protect against common web vulnerabilities while ensuring CloudPanel operates correctly
# Basic security headers
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Content Security Policy tailored for CloudPanel
add_header Content-Security-Policy "
default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self' data:;
frame-ancestors 'self';
form-action 'self';
" always;
# HSTS configuration - Enable only after ensuring SSL is properly configured
# add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# Protection against various attacks
client_body_buffer_size 10K;
client_max_body_size 10M;
client_body_timeout 12;
client_header_timeout 12;
# Rate limiting configuration
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
limit_conn_zone $binary_remote_addr zone=addr:10m;

2. PHP Security Hardening

CloudPanel manages multiple PHP versions, so we need to secure each version while maintaining compatibility. Let’s create a comprehensive PHP security configuration:

Terminal window
# Create a function to apply security settings to all PHP versions
sudo nano /usr/local/bin/cloudpanel-php-secure.sh

Here’s a script that automatically secures all PHP versions used by CloudPanel:

#!/bin/bash
# CloudPanel Security Manager
# This script implements security measures for CloudPanel installations
# focusing on PHP hardening and firewall configurations.
set -euo pipefail
IFS=$'\n\t'
# Configuration variables
readonly CP_HOME="/home/clp"
readonly SITES_ROOT="/home"
readonly DB_PATH="${CP_HOME}/htdocs/app/data/db.sq3"
readonly LOG_DIR="/var/log/cloudpanel_monitoring"
readonly LOG_FILE="${LOG_DIR}/security_manager.log"
readonly ALERT_LOG="${LOG_DIR}/alerts.log"
readonly BACKUP_DIR="${LOG_DIR}/backups"
# Color definitions
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly BLUE='\033[0;34m'
readonly NC='\033[0m'
# Security settings
readonly DEFAULT_MEMORY_LIMIT="256M"
readonly DEFAULT_UPLOAD_SIZE="10M"
# Initialize logging
setup_logging() {
mkdir -p "${LOG_DIR}" "${BACKUP_DIR}"
if [[ ! -f "${LOG_FILE}" ]]; then
echo "=== CloudPanel Security Manager Log Started $(date '+%Y-%m-%d %H:%M:%S') ===" > "${LOG_FILE}"
fi
if [[ ! -f "${ALERT_LOG}" ]]; then
echo "=== CloudPanel Security Alerts Started $(date '+%Y-%m-%d %H:%M:%S') ===" > "${ALERT_LOG}"
fi
# Set up log rotation
cat > "/etc/logrotate.d/cloudpanel-security" << 'LOGROTATE'
/var/log/cloudpanel_monitoring/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 0640 root root
}
LOGROTATE
log "INFO" "System" "Logging initialized"
}
# Log function with component tracking
log() {
local level=$1
local component=$2
shift 2
local message="$*"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
# Select color based on level
local color=""
case "${level}" in
"ERROR") color="${RED}" ;;
"WARN") color="${YELLOW}" ;;
"INFO") color="${GREEN}" ;;
"DEBUG") color="${BLUE}" ;;
esac
# Log to file
echo "${timestamp} | ${level} | ${component} | ${message}" >> "${LOG_FILE}"
# Console output
echo -e "${color}[${timestamp}] [${level}] ${component}: ${message}${NC}"
# Log alerts
if [[ "${level}" == "ERROR" || "${level}" == "WARN" ]]; then
echo "${timestamp} | ${level} | ${component} | ${message}" >> "${ALERT_LOG}"
fi
}
# Backup function
backup_file() {
local file=$1
local backup="${BACKUP_DIR}/$(basename "${file}").$(date +%Y%m%d_%H%M%S)"
if [[ -f "${file}" ]]; then
cp "${file}" "${backup}"
log "INFO" "Backup" "Created backup: ${backup}"
else
log "WARN" "Backup" "File not found: ${file}"
return 1
fi
}
# Configure firewall
configure_firewall() {
log "INFO" "Firewall" "Configuring firewall rules"
sqlite3 "${DB_PATH}" << 'FWEND'
BEGIN TRANSACTION;
DELETE FROM firewall_rule;
INSERT INTO firewall_rule (created_at, updated_at, port_range, source, description)
VALUES
(datetime('now'), datetime('now'), '80,443', '0.0.0.0/0', 'Web Traffic'),
(datetime('now'), datetime('now'), '22', '0.0.0.0/0', 'SSH Access'),
(datetime('now'), datetime('now'), '8443', '127.0.0.1,::1', 'CloudPanel Local Access');
COMMIT;
FWEND
log "INFO" "Firewall" "Rules updated successfully"
}
# Configure PHP security
configure_php_security() {
local site_id=$1
local domain=$2
log "INFO" "PHP" "Configuring security for ${domain}"
sqlite3 "${DB_PATH}" << PHPEND
UPDATE php_settings SET
memory_limit = '${DEFAULT_MEMORY_LIMIT}',
max_execution_time = 30,
max_input_time = 60,
max_input_vars = 1000,
post_max_size = '${DEFAULT_UPLOAD_SIZE}',
upload_max_file_size = '${DEFAULT_UPLOAD_SIZE}',
additional_configuration = '
expose_php = Off
allow_url_fopen = Off
allow_url_include = Off
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = "Strict"
session.use_strict_mode = 1
display_errors = Off
log_errors = On
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
disable_functions = exec,passthru,shell_exec,system,proc_open,popen
max_execution_time = 30
memory_limit = ${DEFAULT_MEMORY_LIMIT}'
WHERE site_id = ${site_id};
PHPEND
log "INFO" "PHP" "Settings updated for ${domain}"
}
# Process site
process_site() {
local site_id=$1
local domain=$2
log "INFO" "Site" "Processing ${domain}"
configure_php_security "${site_id}" "${domain}"
}
# Main function
main() {
local start_time=$(date +%s)
setup_logging
log "INFO" "System" "Starting security configuration"
backup_file "${DB_PATH}"
configure_firewall
while IFS='|' read -r site_id domain; do
process_site "${site_id}" "${domain}"
done < <(sqlite3 "${DB_PATH}" "SELECT id, domain_name FROM site;")
log "INFO" "System" "Restarting PHP-FPM"
systemctl restart "php*-fpm"
local end_time=$(date +%s)
log "INFO" "System" "Completed in $((end_time - start_time)) seconds"
}
# Run main with error handling
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
trap 'log "ERROR" "System" "Failed on line $LINENO"' ERR
main "$@"
fi

3. Web Application Firewall Integration

Let’s implement a WAF configuration that works with CloudPanel’s structure:

Install Naxsi WAF for Nginx #

Here’s a CloudPanel-specific Naxsi WAF configuration:

Examples for CloudPanel

# Include in the server block of your WordPress site
server {
# Standard CloudPanel configuration remains unchanged
listen 80;
listen [::]:80;
server_name example.com;
# Enable Naxsi base rules
include /etc/nginx/naxsi/naxsi-base.conf;
# WordPress-specific whitelist rules
include /etc/nginx/naxsi/wordpress.rules;
location / {
# Standard WordPress handling
try_files $uri $uri/ /index.php?$args;
# WordPress-specific Naxsi configurations
BasicRule wl:1315,1101 "mz:$BODY_VAR:post_title";
BasicRule wl:1315,1101 "mz:$BODY_VAR:content";
BasicRule wl:1315 "mz:$BODY_VAR:excerpt";
BasicRule wl:1101 "mz:$BODY_VAR:cat";
# Allow WordPress admin actions
BasicRule wl:1000,1015 "mz:$URL:/wp-admin/|$BODY_VAR:action";
}
# Special handling for wp-admin
location /wp-admin {
# Allow file uploads in wp-admin
BasicRule wl:1500 "mz:$URL:/wp-admin/|$BODY_VAR:attachment";
BasicRule wl:1310,1311 "mz:$URL:/wp-admin/|BODY";
# Standard CloudPanel proxy configuration
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}