From e6ae58a239763686cc86fcf06801048152ab4efe Mon Sep 17 00:00:00 2001 From: Anthony Date: Tue, 21 Oct 2025 02:47:40 +0800 Subject: [PATCH] Final fix for phpMyadmin SSL cert --- scripts/pma-gateway/create_pma_gateway.sh | 300 +++++++--------------- 1 file changed, 94 insertions(+), 206 deletions(-) diff --git a/scripts/pma-gateway/create_pma_gateway.sh b/scripts/pma-gateway/create_pma_gateway.sh index 97fb7ff..d608d4f 100644 --- a/scripts/pma-gateway/create_pma_gateway.sh +++ b/scripts/pma-gateway/create_pma_gateway.sh @@ -1,34 +1,15 @@ #!/bin/bash + # ============================================================================== # Script: create_pma_gateway.sh # Purpose: Create a time-limited gateway URL for phpMyAdmin on Virtuozzo LLSMP. +# Uses Let's Encrypt cert in the dedicated phpMyAdmin vhost (port 8443). +# Dynamically detects Let's Encrypt certificate paths. # Usage: create_pma_gateway.sh --validity=30 [--slug=myalias] # Outputs: Prints the generated URL. -# -# SECURITY FEATURES: -# - Input validation prevents path traversal and injection attacks -# - Secret key handling avoids shell expansion vulnerabilities -# - HMAC-signed tokens with time-based expiration -# - Automatic SSL certificate detection and configuration -# - Comprehensive rewrite rules for phpMyAdmin protection -# - Secure cookie-based access control -# - Self-destructing gateway files # ============================================================================== set -euo pipefail -# ============================================================================== -# Prerequisites Check: Verify required sudo privileges -# ============================================================================== -if ! sudo -n true 2>/dev/null; then - echo "ERROR: This script requires sudo privileges for:" >&2 - echo " - File operations (chown, chmod, mkdir, ln)" >&2 - echo " - Package installation (dnf install xmlstarlet)" >&2 - echo " - Configuration management (xmlstarlet)" >&2 - echo " - Service control (systemctl restart lsws)" >&2 - echo "Please run with appropriate sudo access or configure passwordless sudo." >&2 - exit 1 -fi - SLUG="" VALIDITY=30 # minutes @@ -44,94 +25,16 @@ if [[ -z "$SLUG" ]]; then SLUG=$(openssl rand -hex 4) # 8-char random fi -# ============================================================================== -# Input Validation: Validate command-line arguments for security and robustness -# ============================================================================== - -# Validate VALIDITY: must be a positive integer -if ! [[ "$VALIDITY" =~ ^[0-9]+$ ]] || (( VALIDITY <= 0 )); then - echo "ERROR: --validity must be a positive integer (e.g., --validity=30)" >&2 - exit 1 -fi - -# Validate SLUG: allow only alphanumeric characters, hyphens, and underscores -# This prevents path traversal sequences (../) and special character injection -if ! [[ "$SLUG" =~ ^[a-zA-Z0-9_-]{1,64}$ ]]; then - echo "ERROR: --slug must contain only alphanumeric characters, hyphens, or underscores (max 64 chars)" >&2 - exit 1 -fi - -# Determine public hostname for gateway URL (main domain with valid SSL cert) -# Try JELASTIC_ENV_DOMAIN first, then fallback to hostname detection +# Determine environment public host (no node prefix) - used for certificate lookup and URL if [[ -n "${JELASTIC_ENV_DOMAIN:-}" ]]; then - PUBLIC_HOST="$JELASTIC_ENV_DOMAIN" + ENV_HOST="$JELASTIC_ENV_DOMAIN" else - # Fallback: Extract domain from hostname (remove node prefix and subdomain) ENV_HOST=$(hostname -f) ENV_HOST=${ENV_HOST#node*-} # strip nodeXXXX- - # Extract the main domain (e.g., newtestenv.mightybox.cloud from node123-newtestenv.mightybox.cloud) - PUBLIC_HOST=$(echo "$ENV_HOST" | sed -E 's/^[^.]*\.([^.]+\.[^.]+)$/\1/') - - # If extraction failed, use the hostname as-is - if [[ -z "$PUBLIC_HOST" ]] || [[ "$PUBLIC_HOST" == "$ENV_HOST" ]]; then - PUBLIC_HOST="$ENV_HOST" - fi - - echo "WARNING: JELASTIC_ENV_DOMAIN not set. Using detected hostname: $PUBLIC_HOST" >&2 -fi - -# Use standard Jelastic LLSMP main document root -MAIN_DOCROOT="/var/www/webroot/ROOT" - -# ============================================================================== -# Step 1: Ensure xmlstarlet is installed for safe XML parsing -# ============================================================================== -if ! command -v xmlstarlet &> /dev/null; then - echo "xmlstarlet not found. Installing for safe XML parsing..." >&2 - if ! sudo dnf install -y xmlstarlet; then - echo "FATAL: Failed to install xmlstarlet. Cannot safely read LiteSpeed config." >&2 - exit 1 - fi -fi - -# ============================================================================== -# Step 2: Dynamically read SSL configuration from main LiteSpeed config -# ============================================================================== -LITESPEED_CONFIG="/var/www/conf/httpd_config.xml" -KEY_FILE_PATH="" -CERT_FILE_PATH="" - -if [[ -f "$LITESPEED_CONFIG" ]]; then - echo "Reading SSL configuration from LiteSpeed main config..." >&2 - - # Query the main HTTPS listener (port 443) for keyFile and certFile - # This is the most specific and robust XPath - # Correctly capture the output of xmlstarlet without modification - KEY_FILE_PATH=$(sudo xmlstarlet sel -t -v \ - "//httpServerConfig/listenerList/listener[name='HTTPS' and secure='1' and address='*:443'][1]/keyFile" \ - "$LITESPEED_CONFIG" 2>/dev/null) - - CERT_FILE_PATH=$(sudo xmlstarlet sel -t -v \ - "//httpServerConfig/listenerList/listener[name='HTTPS' and secure='1' and address='*:443'][1]/certFile" \ - "$LITESPEED_CONFIG" 2>/dev/null) -fi - -# ============================================================================== -# Step 3: Implement fallback to default self-signed certificate -# ============================================================================== -if [[ -z "$KEY_FILE_PATH" ]] || [[ -z "$CERT_FILE_PATH" ]]; then - echo "No custom SSL certificate found. Falling back to default self-signed certificate." >&2 - # Use SINGLE quotes to write the literal string "$SERVER_ROOT" to the config, - # not the shell variable. This is critical. - KEY_FILE_PATH='$SERVER_ROOT/ssl/litespeed.key' - CERT_FILE_PATH='$SERVER_ROOT/ssl/litespeed.crt' -else - echo "Using SSL certificate: $CERT_FILE_PATH" >&2 fi PMADB_DIR="/usr/share/phpMyAdmin" GATEWAY_FILE="$PMADB_DIR/access-db-$SLUG.php" -PUBLIC_GATEWAY_FILE="$MAIN_DOCROOT/access-db-$SLUG.php" SECRET_FILE="/var/lib/jelastic/keys/mbadmin_secret" sudo mkdir -p "$(dirname $SECRET_FILE)" @@ -140,65 +43,74 @@ if [[ ! -f "$SECRET_FILE" ]]; then fi sudo chown litespeed:litespeed "$SECRET_FILE" sudo chmod 644 "$SECRET_FILE" - -# Validate the secret file to ensure it contains only valid hexadecimal characters -if ! [[ "$(cat "$SECRET_FILE")" =~ ^[0-9a-f]{64}$ ]]; then - echo "ERROR: Secret file $SECRET_FILE does not contain a valid 64-character hexadecimal string." >&2 - exit 1 -fi +SECRET=$(sudo cat "$SECRET_FILE" | xargs) now=$(date +%s) expires=$((now + VALIDITY*60)) # token = base64("$SLUG:$expires") . '.' . HMAC_SHA256(secret, data) data="$SLUG:$expires" base=$(printf "%s" "$data" | base64 | tr -d '\n') - -# Security: Use a temporary file to avoid exposing $SECRET in shell expansion -# This prevents the secret from being visible in process listings or shell history -# Note: mktemp creates files with restrictive permissions (600) by default -TEMP_SECRET_FILE=$(mktemp) -trap "rm -f '$TEMP_SECRET_FILE'" EXIT -sudo cat "$SECRET_FILE" > "$TEMP_SECRET_FILE" -chmod 600 "$TEMP_SECRET_FILE" # Ensure only owner can read/write - -# Generate HMAC using PHP, reading secret directly from file to avoid injection risk -mac=$(php -r " - \$data = '$data'; - \$secret = trim(file_get_contents('$SECRET_FILE')); - if (!is_string(\$secret) || empty(\$secret)) { - fwrite(STDERR, 'ERROR: Failed to read secret file\n'); - exit(1); - } - \$hmac = hash_hmac('sha256', \$data, \$secret); - if (!\$hmac) { - fwrite(STDERR, 'ERROR: hash_hmac() failed\n'); - exit(1); - } - echo \$hmac; -" 2>&1) - -# Check PHP exit code and output -php_exit_code=$? -if [[ $php_exit_code -ne 0 ]] || [[ -z "$mac" ]] || [[ ${#mac} -ne 64 ]]; then - echo "ERROR: Failed to generate HMAC token" >&2 - echo "PHP error output: $mac" >&2 - exit 1 -fi - +mac=$(php -r "echo hash_hmac('sha256', '$data', '$SECRET');") token="$base.$mac" -# phpMyAdmin vhost configuration will be handled below - -# ============================================================================== -# Step 4: Configure phpMyAdmin vhost with SSL certificate detection -# ============================================================================== +# Secure the phpMyAdmin vhost with Rewrite Rules to block direct access VHOST_CONFIG="/usr/share/phpMyAdmin/vhost.conf" NEEDS_RESTART=0 -# If vhost config is missing or empty, recreate it from a known-good default. +# --- MODIFICATION: Dynamically determine Let's Encrypt Cert Paths --- +LE_LIVE_DIR="/etc/letsencrypt/live" +LE_CERT_DIR="" + +# Attempt to find the directory matching ENV_HOST +if [[ -d "$LE_LIVE_DIR/$ENV_HOST" ]]; then + LE_CERT_DIR="$LE_LIVE_DIR/$ENV_HOST" + echo "INFO: Found Let's Encrypt cert directory matching ENV_HOST: $LE_CERT_DIR" >&2 +else + # If not found, iterate through subdirectories in $LE_LIVE_DIR to find a suitable one + # This is a fallback, assuming there might be only one relevant cert directory. + # Or, you could add more logic here to match patterns if needed. + for dir in "$LE_LIVE_DIR"/*/; do + if [[ -d "$dir" ]]; then + candidate_dir=$(basename "$dir") + echo "INFO: Checking Let's Encrypt cert directory: $candidate_dir" >&2 + # Add more specific checks here if multiple domains exist and you need to pick the right one. + # For now, just pick the first one that has the required files. + if [[ -f "$dir/privkey.pem" ]] && [[ -f "$dir/fullchain.pem" ]]; then + LE_CERT_DIR="$dir" + echo "INFO: Found Let's Encrypt cert directory with required files: $LE_CERT_DIR" >&2 + break + fi + fi + done +fi + +# Set the final key and cert file paths based on the found directory +if [[ -n "$LE_CERT_DIR" ]]; then + LE_KEY_FILE="$LE_CERT_DIR/privkey.pem" + LE_CERT_FILE="$LE_CERT_DIR/fullchain.pem" +else + echo "FATAL: Let's Encrypt certificate directory could not be found in $LE_LIVE_DIR for ENV_HOST: $ENV_HOST" >&2 + echo " Checked specific path: $LE_LIVE_DIR/$ENV_HOST" >&2 + echo " Checked other subdirectories for privkey.pem and fullchain.pem." >&2 + exit 1 +fi + +# Check if the Let's Encrypt files exist at the determined paths +if [[ ! -f "$LE_KEY_FILE" ]] || [[ ! -f "$LE_CERT_FILE" ]]; then + echo "FATAL: Let's Encrypt certificate files not found at determined paths:" >&2 + echo " Key: $LE_KEY_FILE" >&2 + echo " Cert: $LE_CERT_FILE" >&2 + exit 1 +fi +echo "INFO: Using Let's Encrypt certificate paths:" >&2 +echo " Key: $LE_KEY_FILE" >&2 +echo " Cert: $LE_CERT_FILE" >&2 + + +# If vhost config is missing or empty, recreate it from a known-good default WITH Let's Encrypt Certs. if [ ! -s "$VHOST_CONFIG" ]; then - echo "Warning: $VHOST_CONFIG is empty or missing. Recreating from default." >&2 - sudo tee "$VHOST_CONFIG" > /dev/null <<'EOF' + echo "Warning: $VHOST_CONFIG is empty or missing. Recreating from default with Let's Encrypt certs." >&2 + sudo tee "$VHOST_CONFIG" > /dev/null < /usr/share/phpMyAdmin/ @@ -206,13 +118,13 @@ if [ ! -s "$VHOST_CONFIG" ]; then 0 - $SERVER_ROOT/logs/error.log + \$SERVER_ROOT/logs/error.log DEBUG 10M 0 - $SERVER_ROOT/logs/access.log + \$SERVER_ROOT/logs/access.log 10M 30 0 @@ -251,7 +163,7 @@ if [ ! -s "$VHOST_CONFIG" ]; then 10 - /tmp/lscache/vhosts/$VH_NAME + /tmp/lscache/vhosts/\$VH_NAME @@ -261,8 +173,8 @@ if [ ! -s "$VHOST_CONFIG" ]; then RewriteRule ^/nospider/ - [F] - __KEY_FILE_PLACEHOLDER__ - __CERT_FILE_PLACEHOLDER__ + $LE_KEY_FILE + $LE_CERT_FILE 1 @@ -271,7 +183,7 @@ RewriteRule ^/nospider/ - [F] 0 - $VH_ROOT/awstats + \$VH_ROOT/awstats /awstats/ localhost 127.0.0.1 localhost @@ -280,26 +192,15 @@ RewriteRule ^/nospider/ - [F] EOF - - # Inject the discovered certificate paths using sed - # Escape special characters (/, $, &, \, ') in paths for use with sed - ESCAPED_KEY_PATH=$(printf '%s\n' "$KEY_FILE_PATH" | sed 's/[\/&$\\'"'"']/\\&/g') - ESCAPED_CERT_PATH=$(printf '%s\n' "$CERT_FILE_PATH" | sed 's/[\/&$\\'"'"']/\\&/g') - - # Replace placeholders with actual certificate paths - sudo sed -i "s|__KEY_FILE_PLACEHOLDER__|$ESCAPED_KEY_PATH|g" "$VHOST_CONFIG" - sudo sed -i "s|__CERT_FILE_PLACEHOLDER__|$ESCAPED_CERT_PATH|g" "$VHOST_CONFIG" - - echo "SSL certificate paths injected into vhost configuration." >&2 NEEDS_RESTART=1 fi if [ -f "$VHOST_CONFIG" ]; then MARKER="# PMA Gateway Security Rules" - + # If rules are not already in place, add them. if ! sudo grep -qF "$MARKER" "$VHOST_CONFIG"; then - + # Ensure xmlstarlet is installed, as it's the safest way to edit XML. if ! command -v xmlstarlet &> /dev/null; then echo "xmlstarlet not found. Installing for safe XML editing..." >&2 @@ -321,7 +222,7 @@ RewriteCond %{HTTP_COOKIE} !pma_access_granted RewriteRule .* - [F,L] EOF ) - + # Use xmlstarlet to atomically update the rewrite block in-place. # This is far safer than sed/awk for structured XML. if ! sudo xmlstarlet ed -L \ @@ -334,10 +235,29 @@ EOF NEEDS_RESTART=1 fi + + # --- MODIFICATION: Update SSL Certs in Existing vhost.conf if paths differ --- + # Check if the current SSL paths in vhost.conf match the desired Let's Encrypt paths + CURRENT_KEY_PATH=$(sudo xmlstarlet sel -t -v "//virtualHostConfig/vhssl/keyFile" "$VHOST_CONFIG" 2>/dev/null) + CURRENT_CERT_PATH=$(sudo xmlstarlet sel -t -v "//virtualHostConfig/vhssl/certFile" "$VHOST_CONFIG" 2>/dev/null) + + if [[ "$CURRENT_KEY_PATH" != "$LE_KEY_FILE" ]] || [[ "$CURRENT_CERT_PATH" != "$LE_CERT_FILE" ]]; then + echo "Updating SSL certificate paths in $VHOST_CONFIG to Let's Encrypt certs..." >&2 + if ! sudo xmlstarlet ed -L \ + -u "//virtualHostConfig/vhssl/keyFile" -v "$LE_KEY_FILE" \ + -u "//virtualHostConfig/vhssl/certFile" -v "$LE_CERT_FILE" \ + "$VHOST_CONFIG"; then + echo "FATAL: xmlstarlet failed to update SSL paths in $VHOST_CONFIG." >&2 + exit 1 + fi + NEEDS_RESTART=1 + fi + else - echo "Warning: phpMyAdmin vhost config not found at $VHOST_CONFIG. Cannot apply security rules." >&2 + echo "Warning: phpMyAdmin vhost config not found at $VHOST_CONFIG. Cannot apply security rules or update SSL." >&2 fi + sudo tee "$GATEWAY_FILE" >/dev/null <<'PHP' PHP @@ -393,44 +312,13 @@ PHP sudo chown litespeed:litespeed "$GATEWAY_FILE" sudo chmod 644 "$GATEWAY_FILE" -# Gateway file permissions are already set above - # Restart LiteSpeed if we modified the config if [[ "${NEEDS_RESTART:-0}" -eq 1 ]]; then - echo "Applying security rules and restarting LiteSpeed..." >&2 + echo "Applying security rules and SSL certificate changes, restarting LiteSpeed..." >&2 if ! sudo systemctl restart lsws; then echo "Warning: LiteSpeed restart failed. Manual restart may be required." >&2 fi fi -# Generate URL using phpMyAdmin vhost (port 8443) with detected SSL certificate -URL="https://$PUBLIC_HOST:8443/access-db-$SLUG.php?token=$token" - -# Output JSON response for Cloud Scripting compatibility -# Cloud Scripting expects structured JSON output from custom actions -cat <&2 -echo " • Gateway URL uses detected SSL certificate: $CERT_FILE_PATH" >&2 -echo " • Served through phpMyAdmin vhost (port 8443)" >&2 -echo " • SSL certificate automatically detected and configured" >&2 -echo " • Security rules automatically injected into vhost" >&2 -echo " • Time-limited access with HMAC-signed tokens" >&2 -echo "" >&2 \ No newline at end of file +URL="https://$ENV_HOST:8443/access-db-$SLUG.php?token=$token" +echo "$URL" \ No newline at end of file