Last active
January 29, 2026 14:19
-
-
Save cPFence/041ef1ba10a41d62bd2a4d6cee2dae41 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # | |
| # cPFence OpenLiteSpeed Freeze - v2 (Enhance CP / Ubuntu 24) | |
| # Version adapted for Enhance v12 | |
| # Copyright (C) 2023 - 2026 Linkers Gate LLC. | |
| # | |
| # Usage: | |
| # ./cPFence_ols_freeze.sh freeze -> store current top-of-config and freeze + auto-install systemd manager | |
| # ./cPFence_ols_freeze.sh unfreeze -> remove freeze marker and stored files + disable systemd manager | |
| # ./cPFence_ols_freeze.sh -> run enforcement (intended for systemd path/timer or manual) | |
| # ./cPFence_ols_freeze.sh install-systemd -> install/enable cpf_ols_freeze.service + cpf_ols_freeze.path + cpf_ols_freeze.timer | |
| # ./cPFence_ols_freeze.sh uninstall-systemd -> disable/remove cpf_ols_freeze units | |
| # | |
| # [WARNING] !!! | |
| # If the configuration is frozen, you **must unfreeze** before making changes | |
| # to cPFence WAF settings (e.g., turning WAF on or off). Failure to unfreeze | |
| # will cause the cron job to enter a loop and continuously restart OpenLiteSpeed. | |
| # Always unfreeze before making changes, and freeze again afterward. | |
| # | |
| # | |
| # Disclaimer: | |
| # This script is provided "as is" without any warranties of any kind, express or implied. | |
| # It is recommended to thoroughly test this script in a non-production environment prior to | |
| # deployment on any live or critical systems. cPFence and Linkers Gate LLC are not liable for | |
| # any damage or data loss resulting from the use of this script. | |
| # | |
| # | |
| ################################################################################ | |
| set -euo pipefail | |
| IFS=$'\n\t' | |
| # ----------------------------- | |
| # Timer controls (defaults) | |
| # ----------------------------- | |
| # systemd timer frequency (default 1 minute) | |
| TIMER_INTERVAL="1min" | |
| # delay after boot before first run | |
| TIMER_BOOT_DELAY="2min" | |
| # ----------------------------- | |
| # Paths / settings (Enhance CP) | |
| # ----------------------------- | |
| CONFIG_PATH="/usr/local/lsws/conf/httpd_config.conf" | |
| BACKUP_DIR="/usr/local/lsws/conf/" | |
| FREEZE_DIR="/usr/local/src" | |
| FROZEN_TOP_FILE="$FREEZE_DIR/ols_top_config_frozen.conf" | |
| FROZEN_MD5_FILE="$FREEZE_DIR/ols_config_frozen.md5" | |
| FREEZE_MARKER="$FREEZE_DIR/.ols_frozen" | |
| RETENTION_DAYS=7 | |
| # OpenLiteSpeed systemd service name (Enhance CP) | |
| SERVICE_NAME="lshttpd" | |
| # systemd manager unit base name | |
| UNIT_NAME="cpf_ols_freeze" | |
| # lock file to prevent concurrent runs (timer/path/manual) | |
| LOCK_FILE="/run/${UNIT_NAME}.lock" | |
| # temp vars (avoid "unbound variable" with set -u) | |
| TMP_VHOSTS="" | |
| TMP_NEW_CONF="" | |
| # --------------------------------------------------------------------------- | |
| log() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*"; } | |
| display_welcome() { | |
| echo "**********************************************************************************************" | |
| echo "* cPFence Web Security *" | |
| echo "* cPFence OpenLiteSpeed Freeze v2 *" | |
| echo "* Copyright (C) 2023 - 2026 Linkers Gate LLC. *" | |
| echo "**********************************************************************************************" | |
| } | |
| require_root() { | |
| if [ "${EUID:-$(id -u)}" -ne 0 ]; then | |
| echo "ERROR: Must run as root." | |
| exit 1 | |
| fi | |
| } | |
| ensure_dirs() { | |
| mkdir -p "$BACKUP_DIR" "$FREEZE_DIR" | |
| chmod 0750 "$FREEZE_DIR" 2>/dev/null || true | |
| } | |
| check_config_exists() { | |
| if [ ! -f "$CONFIG_PATH" ]; then | |
| echo "ERROR: OLS config file not found at: $CONFIG_PATH" | |
| exit 1 | |
| fi | |
| } | |
| cleanup_old_backups() { | |
| log "Cleaning up backup files older than $RETENTION_DAYS days in $BACKUP_DIR..." | |
| find "$BACKUP_DIR" -name "httpd_config_backup-*.conf" -type f -mtime +"$RETENTION_DAYS" -print -exec rm -f {} \; || true | |
| } | |
| backup_current_config() { | |
| local TIMESTAMP BACKUP_FILE | |
| TIMESTAMP="$(date +'%d%m%y-%H%M%S')" | |
| BACKUP_FILE="${BACKUP_DIR}httpd_config_backup-${TIMESTAMP}.conf" | |
| cp -a "$CONFIG_PATH" "$BACKUP_FILE" | |
| log "Backed up current config to: $BACKUP_FILE" | |
| } | |
| # Freeze: backup full config, store top portion, store md5, create marker, install systemd manager | |
| freeze_config() { | |
| require_root | |
| ensure_dirs | |
| check_config_exists | |
| cleanup_old_backups | |
| backup_current_config | |
| log "Extracting top portion (everything before 'virtualhost Example {')..." | |
| awk '/virtualhost[[:space:]]+Example[[:space:]]*{/{exit}{print}' "$CONFIG_PATH" > "$FROZEN_TOP_FILE" | |
| if [ ! -s "$FROZEN_TOP_FILE" ]; then | |
| echo "ERROR: Frozen top file ended up empty. Pattern 'virtualhost Example {' not found?" | |
| exit 1 | |
| fi | |
| log "Computing MD5 of the full config..." | |
| md5sum "$CONFIG_PATH" | awk '{print $1}' > "$FROZEN_MD5_FILE" | |
| touch "$FREEZE_MARKER" | |
| log "CONFIG FROZEN. Future runs will enforce the stored top-of-config settings." | |
| log "WARNING: Unfreeze before making changes to OLS settings or cPFence WAF settings." | |
| install_systemd_units | |
| } | |
| # Unfreeze: remove marker and stored files + disable systemd manager | |
| unfreeze_config() { | |
| require_root | |
| ensure_dirs | |
| rm -f "$FREEZE_MARKER" "$FROZEN_TOP_FILE" "$FROZEN_MD5_FILE" 2>/dev/null || true | |
| log "Unfrozen. Future runs will not revert configuration changes." | |
| disable_systemd_units | |
| } | |
| # Enforce freeze: if frozen and MD5 differs, restore frozen top portion + vhosts, restart service | |
| enforce_freeze_if_needed() { | |
| require_root | |
| ensure_dirs | |
| # Safe cleanup (no "unbound variable") | |
| trap 'rm -f "${TMP_VHOSTS:-}" "${TMP_NEW_CONF:-}" 2>/dev/null || true' EXIT | |
| # prevent concurrent runs (flock optional) | |
| exec 9>"$LOCK_FILE" | |
| if command -v flock >/dev/null 2>&1; then | |
| if ! flock -n 9; then | |
| log "Another ${UNIT_NAME} run is in progress. Exiting." | |
| exit 0 | |
| fi | |
| else | |
| log "WARNING: flock not found; running without lock." | |
| fi | |
| if [ ! -f "$FREEZE_MARKER" ]; then | |
| log "Not frozen. No action taken." | |
| exit 0 | |
| fi | |
| check_config_exists | |
| if [ ! -f "$FROZEN_TOP_FILE" ] || [ ! -f "$FROZEN_MD5_FILE" ]; then | |
| echo "ERROR: Freeze marker present but frozen files are missing!" | |
| exit 1 | |
| fi | |
| local CURRENT_MD5 STORED_MD5 NEW_MD5 | |
| CURRENT_MD5="$(md5sum "$CONFIG_PATH" | awk '{print $1}')" | |
| STORED_MD5="$(cat "$FROZEN_MD5_FILE" 2>/dev/null || echo "")" | |
| if [ "$CURRENT_MD5" = "$STORED_MD5" ]; then | |
| log "No changes detected (MD5 match). Exiting." | |
| exit 0 | |
| fi | |
| # Small debounce helps when Enhance writes config multiple times quickly | |
| sleep 2 | |
| CURRENT_MD5="$(md5sum "$CONFIG_PATH" | awk '{print $1}')" | |
| if [ "$CURRENT_MD5" = "$STORED_MD5" ]; then | |
| log "Change disappeared after debounce (MD5 match). Exiting." | |
| exit 0 | |
| fi | |
| log "Changes detected. Reverting to frozen top-of-config..." | |
| cleanup_old_backups | |
| backup_current_config | |
| local MODE | |
| TMP_VHOSTS="$(mktemp /tmp/ols_vhosts.XXXXXX)" | |
| TMP_NEW_CONF="$(mktemp /tmp/ols_new_conf.XXXXXX)" | |
| log "Extracting virtualhost section (from 'virtualhost Example {' to EOF)..." | |
| awk '/virtualhost[[:space:]]+Example[[:space:]]*{/{found=1}found{print}' "$CONFIG_PATH" > "$TMP_VHOSTS" | |
| if [ ! -s "$TMP_VHOSTS" ]; then | |
| echo "ERROR: Could not extract vhost section. Pattern 'virtualhost Example {' not found?" | |
| exit 1 | |
| fi | |
| log "Combining frozen top-of-config with current virtualhosts..." | |
| cat "$FROZEN_TOP_FILE" "$TMP_VHOSTS" > "$TMP_NEW_CONF" | |
| MODE="$(stat -c '%a' "$CONFIG_PATH")" | |
| log "Installing new config and preserving permissions (mode $MODE)..." | |
| install -m "$MODE" -o lsadm -g www-data "$TMP_NEW_CONF" "$CONFIG_PATH" | |
| log "Restarting OpenLiteSpeed service ($SERVICE_NAME)..." | |
| systemctl restart "$SERVICE_NAME" | |
| NEW_MD5="$(md5sum "$CONFIG_PATH" | awk '{print $1}')" | |
| echo "$NEW_MD5" > "$FROZEN_MD5_FILE" | |
| log "Reversion complete. MD5 updated." | |
| } | |
| # ---------------- systemd install (service + path + timer) ---------------- | |
| install_systemd_units() { | |
| require_root | |
| ensure_dirs | |
| local SRC DST | |
| SRC="$(readlink -f "$0")" | |
| DST="/usr/local/sbin/${UNIT_NAME}" | |
| install -m 0755 "$SRC" "$DST" | |
| local SERVICE_FILE TIMER_FILE PATH_FILE | |
| SERVICE_FILE="/etc/systemd/system/${UNIT_NAME}.service" | |
| TIMER_FILE="/etc/systemd/system/${UNIT_NAME}.timer" | |
| PATH_FILE="/etc/systemd/system/${UNIT_NAME}.path" | |
| cat > "$SERVICE_FILE" <<EOF | |
| [Unit] | |
| Description=cPFence OLS Freeze enforcement | |
| After=${SERVICE_NAME}.service | |
| Wants=${SERVICE_NAME}.service | |
| [Service] | |
| Type=oneshot | |
| ExecStart=${DST} | |
| StandardOutput=journal | |
| StandardError=journal | |
| EOF | |
| # Run immediately when config changes | |
| cat > "$PATH_FILE" <<EOF | |
| [Unit] | |
| Description=Watch OLS config and enforce cPFence freeze | |
| [Path] | |
| PathChanged=${CONFIG_PATH} | |
| Unit=${UNIT_NAME}.service | |
| [Install] | |
| WantedBy=multi-user.target | |
| EOF | |
| # Timer fallback | |
| cat > "$TIMER_FILE" <<EOF | |
| [Unit] | |
| Description=Run cPFence OLS Freeze enforcement periodically | |
| [Timer] | |
| OnBootSec=${TIMER_BOOT_DELAY} | |
| OnUnitActiveSec=${TIMER_INTERVAL} | |
| Persistent=true | |
| [Install] | |
| WantedBy=timers.target | |
| EOF | |
| systemctl daemon-reload | |
| systemctl enable --now "${UNIT_NAME}.path" "${UNIT_NAME}.timer" >/dev/null | |
| # restart to apply any interval changes immediately | |
| systemctl restart "${UNIT_NAME}.path" "${UNIT_NAME}.timer" >/dev/null 2>&1 || true | |
| log "systemd manager installed/enabled: ${UNIT_NAME}.service + ${UNIT_NAME}.path + ${UNIT_NAME}.timer" | |
| } | |
| disable_systemd_units() { | |
| systemctl disable --now "${UNIT_NAME}.timer" "${UNIT_NAME}.path" 2>/dev/null || true | |
| systemctl daemon-reload 2>/dev/null || true | |
| } | |
| uninstall_systemd_units() { | |
| require_root | |
| disable_systemd_units | |
| rm -f "/etc/systemd/system/${UNIT_NAME}.service" \ | |
| "/etc/systemd/system/${UNIT_NAME}.timer" \ | |
| "/etc/systemd/system/${UNIT_NAME}.path" 2>/dev/null || true | |
| rm -f "/usr/local/sbin/${UNIT_NAME}" 2>/dev/null || true | |
| systemctl daemon-reload | |
| log "systemd manager removed: ${UNIT_NAME}" | |
| } | |
| # MAIN | |
| display_welcome | |
| MODE="${1:-}" | |
| case "$MODE" in | |
| freeze) | |
| freeze_config | |
| ;; | |
| unfreeze) | |
| unfreeze_config | |
| ;; | |
| install-systemd) | |
| install_systemd_units | |
| ;; | |
| uninstall-systemd) | |
| uninstall_systemd_units | |
| ;; | |
| *) | |
| enforce_freeze_if_needed | |
| ;; | |
| esac | |
| exit 0 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment