Fix SSL cert on phpMyadmin

main
Anthony 2025-10-21 01:51:39 +08:00
parent 54e5825dd3
commit 7b7ec7e2fb
1 changed files with 163 additions and 147 deletions

View File

@ -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,165 +129,116 @@ 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'
<?xml version="1.0" encoding="UTF-8"?>
<virtualHostConfig>
<docRoot>/usr/share/phpMyAdmin/</docRoot>
<enableGzip>1</enableGzip>
<logging>
<log>
<useServer>0</useServer>
<fileName>$SERVER_ROOT/logs/error.log</fileName>
<logLevel>DEBUG</logLevel>
<rollingSize>10M</rollingSize>
</log>
<accessLog>
<useServer>0</useServer>
<fileName>$SERVER_ROOT/logs/access.log</fileName>
<rollingSize>10M</rollingSize>
<keepDays>30</keepDays>
<compressArchive>0</compressArchive>
</accessLog>
</logging>
<index>
<useServer>0</useServer>
<indexFiles>index.php, index.html</indexFiles>
<autoIndex>0</autoIndex>
<autoIndexURI>/_autoindex/default.php</autoIndexURI>
</index>
<customErrorPages>
<errorPage>
<errCode>404</errCode>
<url>/error404.html</url>
</errorPage>
</customErrorPages>
<htAccess>
<allowOverride>31</allowOverride>
<accessFileName>.htaccess</accessFileName>
</htAccess>
<expires>
<enableExpires>1</enableExpires>
</expires>
<security>
<wpProtectAction>0</wpProtectAction>
<hotlinkCtrl>
<enableHotlinkCtrl>0</enableHotlinkCtrl>
<suffixes>gif, jpeg, jpg</suffixes>
<allowDirectAccess>1</allowDirectAccess>
<onlySelf>1</onlySelf>
</hotlinkCtrl>
<accessControl>
<allow>*</allow>
</accessControl>
<wpProtectLimit>10</wpProtectLimit></security>
<cache>
<storage>
<cacheStorePath>/tmp/lscache/vhosts/$VH_NAME</cacheStorePath>
</storage>
</cache>
<rewrite>
<enable>0</enable>
<logLevel>0</logLevel>
<rules>RewriteCond %{HTTP_USER_AGENT} ^NameOfBadRobot
RewriteRule ^/nospider/ - [F]</rules>
</rewrite>
<vhssl>
<keyFile>__KEY_FILE_PLACEHOLDER__</keyFile>
<certFile>__CERT_FILE_PLACEHOLDER__</certFile>
<certChain>1</certChain>
</vhssl>
<frontPage>
<enable>0</enable>
<disableAdmin>0</disableAdmin>
</frontPage>
<awstats>
<updateMode>0</updateMode>
<workingDir>$VH_ROOT/awstats</workingDir>
<awstatsURI>/awstats/</awstatsURI>
<siteDomain>localhost</siteDomain>
<siteAliases>127.0.0.1 localhost</siteAliases>
<updateInterval>86400</updateInterval>
<updateOffset>0</updateOffset>
</awstats>
</virtualHostConfig>
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"
# Secure the phpMyAdmin vhost with Rewrite Rules to block direct access
# VHOST_CONFIG="/usr/share/phpMyAdmin/vhost.conf"
# NEEDS_RESTART=0
# If rules are not already in place, add them.
if ! sudo grep -qF "$MARKER" "$VHOST_CONFIG"; then
# ==============================================================================
# Step 4: Automatically inject security rules into main vhost configuration
# ==============================================================================
MAIN_VHOST_CONFIG="/var/www/conf/vhosts/$PUBLIC_HOST/vhconf.xml"
# 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
# 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
fi
# Define the new rules content. Note the lack of indentation.
# xmlstarlet will handle the formatting.
NEW_RULES_CONTENT=$(cat <<'EOF'
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.
# Enable rewrite and inject comprehensive rules
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
-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
echo "✅ PMA security rules injected into main vhost configuration" >&2
NEEDS_RESTART=1
fi
else
echo "Warning: phpMyAdmin vhost config not found at $VHOST_CONFIG. Cannot apply security rules." >&2
fi
sudo tee "$GATEWAY_FILE" >/dev/null <<'PHP'
<?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