Final fix for phpMyadmin SSL cert
parent
7608e38ba3
commit
e6ae58a239
|
|
@ -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 <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<virtualHostConfig>
|
||||
<docRoot>/usr/share/phpMyAdmin/</docRoot>
|
||||
|
|
@ -206,13 +118,13 @@ if [ ! -s "$VHOST_CONFIG" ]; then
|
|||
<logging>
|
||||
<log>
|
||||
<useServer>0</useServer>
|
||||
<fileName>$SERVER_ROOT/logs/error.log</fileName>
|
||||
<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>
|
||||
<fileName>\$SERVER_ROOT/logs/access.log</fileName>
|
||||
<rollingSize>10M</rollingSize>
|
||||
<keepDays>30</keepDays>
|
||||
<compressArchive>0</compressArchive>
|
||||
|
|
@ -251,7 +163,7 @@ if [ ! -s "$VHOST_CONFIG" ]; then
|
|||
<wpProtectLimit>10</wpProtectLimit></security>
|
||||
<cache>
|
||||
<storage>
|
||||
<cacheStorePath>/tmp/lscache/vhosts/$VH_NAME</cacheStorePath>
|
||||
<cacheStorePath>/tmp/lscache/vhosts/\$VH_NAME</cacheStorePath>
|
||||
</storage>
|
||||
</cache>
|
||||
<rewrite>
|
||||
|
|
@ -261,8 +173,8 @@ if [ ! -s "$VHOST_CONFIG" ]; then
|
|||
RewriteRule ^/nospider/ - [F]</rules>
|
||||
</rewrite>
|
||||
<vhssl>
|
||||
<keyFile>__KEY_FILE_PLACEHOLDER__</keyFile>
|
||||
<certFile>__CERT_FILE_PLACEHOLDER__</certFile>
|
||||
<keyFile>$LE_KEY_FILE</keyFile>
|
||||
<certFile>$LE_CERT_FILE</certFile>
|
||||
<certChain>1</certChain>
|
||||
</vhssl>
|
||||
<frontPage>
|
||||
|
|
@ -271,7 +183,7 @@ RewriteRule ^/nospider/ - [F]</rules>
|
|||
</frontPage>
|
||||
<awstats>
|
||||
<updateMode>0</updateMode>
|
||||
<workingDir>$VH_ROOT/awstats</workingDir>
|
||||
<workingDir>\$VH_ROOT/awstats</workingDir>
|
||||
<awstatsURI>/awstats/</awstatsURI>
|
||||
<siteDomain>localhost</siteDomain>
|
||||
<siteAliases>127.0.0.1 localhost</siteAliases>
|
||||
|
|
@ -280,17 +192,6 @@ RewriteRule ^/nospider/ - [F]</rules>
|
|||
</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
|
||||
|
||||
|
|
@ -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
|
||||
// Secure phpMyAdmin gateway – auto-generated, do NOT edit manually.
|
||||
|
|
@ -384,8 +304,7 @@ 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);
|
||||
// Redirect to phpMyAdmin via the symlinked path
|
||||
header('Location: /phpmyadmin/index.php');
|
||||
header('Location: /');
|
||||
exit;
|
||||
?>
|
||||
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 <<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
|
||||
URL="https://$ENV_HOST:8443/access-db-$SLUG.php?token=$token"
|
||||
echo "$URL"
|
||||
Loading…
Reference in New Issue