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

436 lines
15 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)
# Try JELASTIC_ENV_DOMAIN first, then fallback to hostname detection
if [[ -n "${JELASTIC_ENV_DOMAIN:-}" ]]; then
PUBLIC_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)"
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"
# phpMyAdmin vhost configuration will be handled below
# ==============================================================================
# Step 4: Configure phpMyAdmin vhost with SSL certificate detection
# ==============================================================================
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
# 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
if ! sudo dnf install -y xmlstarlet; then
echo "FATAL: Failed to install xmlstarlet. Cannot safely modify vhost." >&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'
# 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
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 \
-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
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
// 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"
# 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
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 <<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 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