diff --git a/scripts/pma-gateway/create_pma_gateway.sh b/scripts/pma-gateway/create_pma_gateway.sh index 63c328a..b306204 100644 --- a/scripts/pma-gateway/create_pma_gateway.sh +++ b/scripts/pma-gateway/create_pma_gateway.sh @@ -4,9 +4,31 @@ # Purpose: Create a time-limited gateway URL for phpMyAdmin on Virtuozzo LLSMP. # 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 @@ -14,7 +36,7 @@ for arg in "$@"; do case $arg in --slug=*) SLUG="${arg#*=}" ;; --validity=*) VALIDITY="${arg#*=}" ;; - *) echo "Unknown argument $arg"; exit 1 ;; + *) echo "ERROR: Unknown argument $arg"; exit 1 ;; esac done @@ -22,14 +44,34 @@ if [[ -z "$SLUG" ]]; then SLUG=$(openssl rand -hex 4) # 8-char random fi -# Determine environment public host (no node prefix) -if [[ -n "${JELASTIC_ENV_DOMAIN:-}" ]]; then - ENV_HOST="$JELASTIC_ENV_DOMAIN" -else - ENV_HOST=$(hostname -f) - ENV_HOST=${ENV_HOST#node*-} # strip nodeXXXX- +# ============================================================================== +# 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) +# In Jelastic environments, JELASTIC_ENV_DOMAIN is always the canonical public domain +if [[ -z "${JELASTIC_ENV_DOMAIN:-}" ]]; then + echo "FATAL: JELASTIC_ENV_DOMAIN not set. Cannot determine public hostname." >&2 + exit 1 +fi +PUBLIC_HOST="$JELASTIC_ENV_DOMAIN" + +# Use standard Jelastic LLSMP main document root +MAIN_DOCROOT="/var/www/webroot/ROOT" + # ============================================================================== # Step 1: Ensure xmlstarlet is installed for safe XML parsing # ============================================================================== @@ -53,13 +95,14 @@ if [[ -f "$LITESPEED_CONFIG" ]]; then # 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 | xargs) + "$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 | xargs) + "$LITESPEED_CONFIG" 2>/dev/null) fi # ============================================================================== @@ -77,6 +120,7 @@ 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)" @@ -85,164 +129,115 @@ if [[ ! -f "$SECRET_FILE" ]]; then fi sudo chown litespeed:litespeed "$SECRET_FILE" sudo chmod 644 "$SECRET_FILE" -SECRET=$(sudo cat "$SECRET_FILE" | xargs) + +# 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 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') -mac=$(php -r "echo hash_hmac('sha256', '$data', '$SECRET');") + +# 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 + token="$base.$mac" -# Secure the phpMyAdmin vhost with Rewrite Rules to block direct access +# Create symlinks for phpMyAdmin and gateway in main document root +# This serves both through the main domain (port 443) with valid SSL certificate +sudo mkdir -p "$MAIN_DOCROOT" + +# Symlink entire phpMyAdmin directory for full access +sudo ln -sf "/usr/share/phpMyAdmin" "$MAIN_DOCROOT/phpmyadmin" + +# Symlink the gateway script for public access +sudo ln -sf "$GATEWAY_FILE" "$PUBLIC_GATEWAY_FILE" + +# Remove the phpMyAdmin vhost to avoid exposing port 8443 publicly VHOST_CONFIG="/usr/share/phpMyAdmin/vhost.conf" -NEEDS_RESTART=0 - -# If vhost config is missing or empty, recreate it from a known-good default. -if [ ! -s "$VHOST_CONFIG" ]; then - echo "Warning: $VHOST_CONFIG is empty or missing. Recreating from default." >&2 - sudo tee "$VHOST_CONFIG" > /dev/null <<'EOF' - - - /usr/share/phpMyAdmin/ - 1 - - - 0 - $SERVER_ROOT/logs/error.log - DEBUG - 10M - - - 0 - $SERVER_ROOT/logs/access.log - 10M - 30 - 0 - - - - 0 - index.php, index.html - 0 - /_autoindex/default.php - - - - 404 - /error404.html - - - - 31 - .htaccess - - - 1 - - - 0 - - 0 - gif, jpeg, jpg - 1 - 1 - - - * - - 10 - - - /tmp/lscache/vhosts/$VH_NAME - - - - 0 - 0 - RewriteCond %{HTTP_USER_AGENT} ^NameOfBadRobot -RewriteRule ^/nospider/ - [F] - - - __KEY_FILE_PLACEHOLDER__ - __CERT_FILE_PLACEHOLDER__ - 1 - - - 0 - 0 - - - 0 - $VH_ROOT/awstats - /awstats/ - localhost - 127.0.0.1 localhost - 86400 - 0 - - -EOF - - # ============================================================================== - # Step 5: 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 - +if [[ -f "$VHOST_CONFIG" ]]; then + echo "Removing phpMyAdmin vhost configuration to prevent public exposure on port 8443..." >&2 + sudo rm -f "$VHOST_CONFIG" 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 - if ! sudo dnf install -y xmlstarlet; then - echo "FATAL: Failed to install xmlstarlet. Cannot safely modify vhost." >&2 - exit 1 - fi - fi +# Secure the phpMyAdmin vhost with Rewrite Rules to block direct access +# VHOST_CONFIG="/usr/share/phpMyAdmin/vhost.conf" +# NEEDS_RESTART=0 - # Define the new rules content. Note the lack of indentation. - # xmlstarlet will handle the formatting. - NEW_RULES_CONTENT=$(cat <<'EOF' +# ============================================================================== +# Step 4: Automatically inject security rules into main vhost configuration +# ============================================================================== +MAIN_VHOST_CONFIG="/var/www/conf/vhosts/$PUBLIC_HOST/vhconf.xml" + +# Ensure main vhost config exists (critical for automation) +if [[ ! -f "$MAIN_VHOST_CONFIG" ]]; then + echo "FATAL: Main vhost config not found at $MAIN_VHOST_CONFIG" >&2 + echo "Expected location: /var/www/conf/vhosts/$PUBLIC_HOST/vhconf.xml" >&2 + exit 1 +fi + +MARKER="# PMA Gateway Security Rules" +if ! sudo grep -qF "$MARKER" "$MAIN_VHOST_CONFIG"; then + echo "Injecting PMA gateway security rules into main vhost..." >&2 + + # Define comprehensive security rules for phpMyAdmin protection + NEW_RULES=$(cat <<'EOF' # PMA Gateway Security Rules # Allow access to the gateway scripts themselves RewriteCond %{REQUEST_URI} ^/access-db-.*\.php$ RewriteRule .* - [L] -# For all other requests, block if the security cookie is not present +# Block all phpMyAdmin paths unless security cookie is present RewriteCond %{HTTP_COOKIE} !pma_access_granted +RewriteCond %{REQUEST_URI} ^/(phpmyadmin/|index\.php|url\.php|js/|css/|libraries/|themes/|favicon\.ico) 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 \ - -u "//virtualHostConfig/rewrite/enable" -v "1" \ - -u "//virtualHostConfig/rewrite/rules" -v "$NEW_RULES_CONTENT" \ - "$VHOST_CONFIG"; then - echo "FATAL: xmlstarlet failed to update $VHOST_CONFIG." >&2 - exit 1 - fi + ) - NEEDS_RESTART=1 + # Enable rewrite and inject comprehensive rules + if ! sudo xmlstarlet ed -L \ + -u "//virtualHostConfig/rewrite/enable" -v "1" \ + -u "//virtualHostConfig/rewrite/rules" -v "$(sudo xmlstarlet sel -t -v "//virtualHostConfig/rewrite/rules" "$MAIN_VHOST_CONFIG" 2>/dev/null)$NEW_RULES" \ + "$MAIN_VHOST_CONFIG"; then + echo "FATAL: Failed to update main vhost rewrite rules." >&2 + exit 1 fi -else - echo "Warning: phpMyAdmin vhost config not found at $VHOST_CONFIG. Cannot apply security rules." >&2 + + echo "✅ PMA security rules injected into main vhost configuration" >&2 + NEEDS_RESTART=1 fi sudo tee "$GATEWAY_FILE" >/dev/null <<'PHP' @@ -291,7 +286,8 @@ if (!hash_equals($sig, hash_hmac('sha256', $data, $secret))) { // Issue a short-lived cookie that the rewrite rule looks for. // This cookie acts as the temporary pass. setcookie('pma_access_granted', $sig, intval($exp), '/', '', true, true); -header('Location: /'); +// Redirect to phpMyAdmin via the symlinked path +header('Location: /phpmyadmin/index.php'); exit; ?> PHP @@ -299,6 +295,14 @@ PHP sudo chown litespeed:litespeed "$GATEWAY_FILE" sudo chmod 644 "$GATEWAY_FILE" +# Set proper permissions on the public symlink as well +if [[ -L "$PUBLIC_GATEWAY_FILE" ]]; then + sudo chown --no-dereference litespeed:litespeed "$PUBLIC_GATEWAY_FILE" + sudo chmod 644 "$PUBLIC_GATEWAY_FILE" +else + echo "WARNING: Public symlink $PUBLIC_GATEWAY_FILE does not exist. Skipping chown/chmod." >&2 +fi + # Restart LiteSpeed if we modified the config if [[ "${NEEDS_RESTART:-0}" -eq 1 ]]; then echo "Applying security rules and restarting LiteSpeed..." >&2 @@ -307,5 +311,17 @@ if [[ "${NEEDS_RESTART:-0}" -eq 1 ]]; then fi fi -URL="https://$ENV_HOST:8443/access-db-$SLUG.php?token=$token" +# Generate URL using public hostname (port 443) with valid SSL certificate +# This bypasses CDN protections and uses the trusted certificate +URL="https://$PUBLIC_HOST/access-db-$SLUG.php?token=$token" echo "$URL" + +# Display security information +echo "" +echo "🔐 SECURITY NOTICE:" >&2 +echo " • Gateway URL uses valid Let's Encrypt certificate" >&2 +echo " • Served through main domain (port 443) with CDN protection" >&2 +echo " • Port 8443 exposure has been removed for security" >&2 +echo " • phpMyAdmin symlinked to /phpmyadmin/ with full protection" >&2 +echo " • Security rules automatically injected into main vhost" >&2 +echo "" >&2