mb-admin/scripts/pma-gateway/create_pma_gateway.sh

346 lines
12 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#!/bin/bash
# ==============================================================================
# Script: create_pma_gateway.sh
# 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
for arg in "$@"; do
case $arg in
--slug=*) SLUG="${arg#*=}" ;;
--validity=*) VALIDITY="${arg#*=}" ;;
*) echo "ERROR: Unknown argument $arg"; exit 1 ;;
esac
done
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)
# 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
# ==============================================================================
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)"
if [[ ! -f "$SECRET_FILE" ]]; then
sudo sh -c "openssl rand -hex 32 > $SECRET_FILE"
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
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
token="$base.$mac"
# 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"
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
# Secure the phpMyAdmin vhost with Rewrite Rules to block direct access
# VHOST_CONFIG="/usr/share/phpMyAdmin/vhost.conf"
# NEEDS_RESTART=0
# ==============================================================================
# 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]
# 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
)
# 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
echo "✅ PMA security rules injected into main vhost configuration" >&2
NEEDS_RESTART=1
fi
sudo tee "$GATEWAY_FILE" >/dev/null <<'PHP'
<?php
// Secure phpMyAdmin gateway auto-generated, do NOT edit manually.
ini_set('session.cookie_httponly', 1);
$param = 'token';
function deny() {
http_response_code(403);
echo 'Access denied';
exit;
}
if (!isset($_GET[$param])) {
deny();
}
$token = $_GET[$param];
if (strpos($token, '.') === false) {
deny();
}
list($base, $sig) = explode('.', $token, 2);
$data = base64_decode($base, true);
if ($data === false) {
deny();
}
if (strpos($data, ':') === false) {
deny();
}
list($slug, $exp) = explode(':', $data, 2);
if (time() > intval($exp)) {
unlink(__FILE__); // Self-destruct if expired
deny();
}
$secret = trim(file_get_contents('/var/lib/jelastic/keys/mbadmin_secret'));
if (!hash_equals($sig, hash_hmac('sha256', $data, $secret))) {
deny();
}
// 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);
// Redirect to phpMyAdmin via the symlinked path
header('Location: /phpmyadmin/index.php');
exit;
?>
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
if ! sudo systemctl restart lsws; then
echo "Warning: LiteSpeed restart failed. Manual restart may be required." >&2
fi
fi
# 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"
# Output JSON response for Cloud Scripting compatibility
# Cloud Scripting expects structured JSON output from custom actions
cat <<EOF
{
"status": "success",
"url": "$URL",
"slug": "$SLUG",
"validity_minutes": $VALIDITY,
"expires_at": $expires,
"message": "phpMyAdmin gateway created successfully",
"security_info": {
"ssl_certificate": "$CERT_FILE_PATH",
"uses_valid_cert": "$([[ "$CERT_FILE_PATH" != '$SERVER_ROOT/ssl/litespeed.crt' ]] && echo 'true' || echo 'false')",
"port_443_only": "true",
"cdn_protected": "true",
"auto_expires": "true"
}
}
EOF
# Display security information to stderr (not part of JSON response)
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