#!/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). # Resolves certs for the environment domain and can issue cert on demand. # Usage: create_pma_gateway.sh --validity=30 [--slug=myalias] [--cert-domain=env.example.com] [--email=admin@example.com] # Outputs: Prints the generated URL. # ============================================================================== set -euo pipefail SLUG="" VALIDITY=30 # minutes CERT_DOMAIN="" CONTACT_EMAIL="" for arg in "$@"; do case $arg in --slug=*) SLUG="${arg#*=}" ;; --validity=*) VALIDITY="${arg#*=}" ;; --cert-domain=*) CERT_DOMAIN="${arg#*=}" ;; --email=*) CONTACT_EMAIL="${arg#*=}" ;; *) echo "ERROR: Unknown argument $arg"; exit 1 ;; esac done if [[ -z "$SLUG" ]]; then SLUG=$(openssl rand -hex 4) # 8-char random fi # Reject unresolved template placeholders if they are passed through literally. if [[ "$CERT_DOMAIN" == *'${'* ]]; then CERT_DOMAIN="" fi if [[ "$CONTACT_EMAIL" == *'${'* ]]; then CONTACT_EMAIL="" fi # Build domain candidates in priority order: # 1) explicit --cert-domain # 2) JELASTIC_ENV_DOMAIN # 3) hostname-derived domain # Additionally, if a legacy *.sites.mightybox.dev candidate is seen, # derive *.sites.mightybox.cloud as a compatibility fallback. # If a regional sites domain is seen, also derive short .mightybox.cloud # as preferred public URL host candidate. HOSTNAME_DOMAIN=$(hostname -f) HOSTNAME_DOMAIN=${HOSTNAME_DOMAIN#node*-} DOMAIN_CANDIDATES=() SEEN_DOMAINS="|" for candidate in "$CERT_DOMAIN" "${JELASTIC_ENV_DOMAIN:-}" "$HOSTNAME_DOMAIN"; do # Normalize host-like input (strip scheme/path/port if present). candidate="${candidate#https://}" candidate="${candidate#http://}" candidate="${candidate%%/*}" if [[ "$candidate" =~ ^(.+):[0-9]+$ ]]; then candidate="${BASH_REMATCH[1]}" fi if [[ -n "$candidate" ]] && [[ "$candidate" != *'${'* ]] && [[ "$SEEN_DOMAINS" != *"|$candidate|"* ]]; then DOMAIN_CANDIDATES+=("$candidate") SEEN_DOMAINS="${SEEN_DOMAINS}${candidate}|" fi if [[ "$candidate" == *.sites.mightybox.dev ]]; then cloud_candidate="${candidate%.sites.mightybox.dev}.sites.mightybox.cloud" if [[ -n "$cloud_candidate" ]] && [[ "$SEEN_DOMAINS" != *"|$cloud_candidate|"* ]]; then DOMAIN_CANDIDATES+=("$cloud_candidate") SEEN_DOMAINS="${SEEN_DOMAINS}${cloud_candidate}|" echo "INFO: Added cloud fallback domain candidate '$cloud_candidate' from legacy '$candidate'." >&2 fi fi if [[ "$candidate" =~ ^([^.]+)\.[^.]+\.sites\.mightybox\.(dev|cloud)$ ]]; then short_cloud_candidate="${BASH_REMATCH[1]}.mightybox.cloud" if [[ -n "$short_cloud_candidate" ]] && [[ "$SEEN_DOMAINS" != *"|$short_cloud_candidate|"* ]]; then DOMAIN_CANDIDATES+=("$short_cloud_candidate") SEEN_DOMAINS="${SEEN_DOMAINS}${short_cloud_candidate}|" echo "INFO: Added short cloud domain candidate '$short_cloud_candidate' from '$candidate'." >&2 fi fi done # Never use legacy *.sites.mightybox.dev domains for PMA gateway certificates. FILTERED_DOMAINS=() for candidate in "${DOMAIN_CANDIDATES[@]}"; do if [[ "$candidate" == *.sites.mightybox.dev ]]; then echo "WARNING: Skipping legacy domain candidate '$candidate' for PMA gateway SSL." >&2 continue fi FILTERED_DOMAINS+=("$candidate") done DOMAIN_CANDIDATES=("${FILTERED_DOMAINS[@]}") if [[ "${#DOMAIN_CANDIDATES[@]}" -eq 0 ]]; then echo "FATAL: No valid non-legacy environment domain candidate found for PMA gateway certificate resolution." >&2 echo " Provide --cert-domain with the current default environment domain." >&2 exit 1 fi # Fallback for email when not explicitly passed. if [[ -z "$CONTACT_EMAIL" ]] && [[ -n "${JELASTIC_USER_EMAIL:-}" ]]; then CONTACT_EMAIL="$JELASTIC_USER_EMAIL" fi ENV_HOST="${DOMAIN_CANDIDATES[0]}" URL_HOST="$ENV_HOST" # Prefer short site-name.mightybox.cloud for user-facing gateway links. for candidate in "${DOMAIN_CANDIDATES[@]}"; do if [[ "$candidate" =~ ^[^.]+\.mightybox\.cloud$ ]]; then URL_HOST="$candidate" break fi done # Otherwise prefer the first host-like candidate containing letters (avoid bare IP if possible). if [[ ! "$URL_HOST" =~ ^[^.]+\.mightybox\.cloud$ ]]; then for candidate in "${DOMAIN_CANDIDATES[@]}"; do if [[ "$candidate" =~ [A-Za-z] ]]; then URL_HOST="$candidate" break fi done fi PMADB_DIR="/usr/share/phpMyAdmin" GATEWAY_FILE="$PMADB_DIR/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" 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') mac=$(php -r "echo hash_hmac('sha256', '$data', '$SECRET');") token="$base.$mac" # Secure the phpMyAdmin vhost with Rewrite Rules to block direct access VHOST_CONFIG="/usr/share/phpMyAdmin/vhost.conf" NEEDS_RESTART=0 # --- Let's Encrypt certificate resolution for domain candidates --- LE_LIVE_DIR="/etc/letsencrypt/live" LE_CERT_DIR="" CERT_DOMAIN_USED="" CERT_SOURCE="Let's Encrypt" # Find an existing certificate for the first matching candidate. for candidate_host in "${DOMAIN_CANDIDATES[@]}"; do if [[ -d "$LE_LIVE_DIR/$candidate_host" ]] && [[ -f "$LE_LIVE_DIR/$candidate_host/privkey.pem" ]] && [[ -f "$LE_LIVE_DIR/$candidate_host/fullchain.pem" ]]; then LE_CERT_DIR="$LE_LIVE_DIR/$candidate_host" CERT_DOMAIN_USED="$candidate_host" echo "INFO: Found exact Let's Encrypt cert directory for '$candidate_host': $LE_CERT_DIR" >&2 break fi for dir in "$LE_LIVE_DIR/$candidate_host"-*/; do if [[ -d "$dir" ]] && [[ -f "$dir/privkey.pem" ]] && [[ -f "$dir/fullchain.pem" ]]; then LE_CERT_DIR="${dir%/}" CERT_DOMAIN_USED="$candidate_host" echo "INFO: Found suffixed Let's Encrypt cert directory for '$candidate_host': $LE_CERT_DIR" >&2 break fi done if [[ -n "$LE_CERT_DIR" ]]; then break fi done if [[ -n "$CERT_DOMAIN_USED" ]]; then ENV_HOST="$CERT_DOMAIN_USED" fi # If no cert exists yet, attempt one-time issuance for highest-priority candidate. if [[ -z "$LE_CERT_DIR" ]]; then ENV_HOST="${DOMAIN_CANDIDATES[0]}" echo "WARNING: No Let's Encrypt certificate found for domain candidates (${DOMAIN_CANDIDATES[*]}). Attempting to issue one for '$ENV_HOST' now..." >&2 CERTBOT_CMD="" if command -v certbot >/dev/null 2>&1; then CERTBOT_CMD="certbot" else # On some images certbot exists but is not in PATH for non-login shells. for certbot_path in /usr/bin/certbot /usr/local/bin/certbot /snap/bin/certbot; do if [[ -x "$certbot_path" ]]; then CERTBOT_CMD="$certbot_path" break fi done fi if [[ -z "$CERTBOT_CMD" ]] && [[ -f "/opt/certbot/certbot-auto" ]]; then sudo chmod a+x /opt/certbot/certbot-auto >/dev/null 2>&1 || true if [[ -x "/opt/certbot/certbot-auto" ]]; then CERTBOT_CMD="/opt/certbot/certbot-auto" fi fi if [[ -z "$CERTBOT_CMD" ]]; then echo "WARNING: certbot is not available. Attempting to bootstrap certbot-auto..." >&2 sudo mkdir -p /opt/certbot >/dev/null 2>&1 || true if command -v curl >/dev/null 2>&1; then sudo curl -fsSL https://dl.eff.org/certbot-auto -o /opt/certbot/certbot-auto >/dev/null 2>&1 || true elif command -v wget >/dev/null 2>&1; then sudo wget -q -O /opt/certbot/certbot-auto https://dl.eff.org/certbot-auto >/dev/null 2>&1 || true fi sudo chmod a+x /opt/certbot/certbot-auto >/dev/null 2>&1 || true if [[ -x "/opt/certbot/certbot-auto" ]]; then CERTBOT_CMD="/opt/certbot/certbot-auto" fi fi if [[ -z "$CERTBOT_CMD" ]]; then echo "WARNING: certbot/certbot-auto is still unavailable. Attempting package manager install..." >&2 if command -v dnf >/dev/null 2>&1; then if ! sudo dnf install -y --setopt=install_weak_deps=False certbot >/dev/null 2>&1; then echo "WARNING: dnf failed to install certbot (possibly resource constrained)." >&2 fi elif command -v yum >/dev/null 2>&1; then if ! sudo yum install -y certbot >/dev/null 2>&1; then echo "WARNING: yum failed to install certbot." >&2 fi elif command -v apt-get >/dev/null 2>&1; then sudo apt-get update -qq >/dev/null 2>&1 || true if ! sudo apt-get install -y certbot >/dev/null 2>&1; then echo "WARNING: apt-get failed to install certbot." >&2 fi fi if command -v certbot >/dev/null 2>&1; then CERTBOT_CMD="certbot" elif [[ -x "/opt/certbot/certbot-auto" ]]; then CERTBOT_CMD="/opt/certbot/certbot-auto" else for certbot_path in /usr/bin/certbot /usr/local/bin/certbot /snap/bin/certbot; do if [[ -x "$certbot_path" ]]; then CERTBOT_CMD="$certbot_path" break fi done fi fi if [[ -z "$CERTBOT_CMD" ]]; then echo "WARNING: certbot is unavailable for on-demand issuance. Will try existing listener certificate files as fallback." >&2 else WEBROOT_PATH="/var/www/webroot/ROOT" ACME_CHALLENGE_DIR="$WEBROOT_PATH/.well-known/acme-challenge" sudo mkdir -p "$ACME_CHALLENGE_DIR" if [[ -n "$CONTACT_EMAIL" ]]; then if ! sudo "$CERTBOT_CMD" certonly --webroot -w "$WEBROOT_PATH" -d "$ENV_HOST" --non-interactive --agree-tos --email "$CONTACT_EMAIL"; then echo "FATAL: Failed to issue Let's Encrypt certificate for '$ENV_HOST' using contact email '$CONTACT_EMAIL'." >&2 exit 1 fi else if ! sudo "$CERTBOT_CMD" certonly --webroot -w "$WEBROOT_PATH" -d "$ENV_HOST" --non-interactive --agree-tos --register-unsafely-without-email; then echo "FATAL: Failed to issue Let's Encrypt certificate for '$ENV_HOST' without contact email." >&2 exit 1 fi fi # Re-check exact and suffixed certificate directories after issuance. if [[ -d "$LE_LIVE_DIR/$ENV_HOST" ]] && [[ -f "$LE_LIVE_DIR/$ENV_HOST/privkey.pem" ]] && [[ -f "$LE_LIVE_DIR/$ENV_HOST/fullchain.pem" ]]; then LE_CERT_DIR="$LE_LIVE_DIR/$ENV_HOST" else for dir in "$LE_LIVE_DIR/$ENV_HOST"-*/; do if [[ -d "$dir" ]] && [[ -f "$dir/privkey.pem" ]] && [[ -f "$dir/fullchain.pem" ]]; then LE_CERT_DIR="${dir%/}" break fi done fi fi 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 FALLBACK_KEY_FILE="" FALLBACK_CERT_FILE="" if [[ -f "/var/www/ssl/litespeed.key" ]] && [[ -f "/var/www/ssl/litespeed.crt" ]]; then FALLBACK_KEY_FILE="/var/www/ssl/litespeed.key" FALLBACK_CERT_FILE="/var/www/ssl/litespeed.crt" elif [[ -f "/usr/local/lsws/conf/server.key" ]] && [[ -f "/usr/local/lsws/conf/server.crt" ]]; then FALLBACK_KEY_FILE="/usr/local/lsws/conf/server.key" FALLBACK_CERT_FILE="/usr/local/lsws/conf/server.crt" fi if [[ -n "$FALLBACK_KEY_FILE" ]] && [[ -n "$FALLBACK_CERT_FILE" ]]; then LE_KEY_FILE="$FALLBACK_KEY_FILE" LE_CERT_FILE="$FALLBACK_CERT_FILE" CERT_SOURCE="Listener fallback" echo "WARNING: No Let's Encrypt certificate available for '$ENV_HOST'. Using existing listener certificate files instead." >&2 else echo "FATAL: No usable certificate files were found for PMA gateway TLS." >&2 echo " Checked Let's Encrypt candidates: ${DOMAIN_CANDIDATES[*]}" >&2 echo " Checked LE exact path: $LE_LIVE_DIR/$ENV_HOST" >&2 echo " Checked LE suffixed paths: $LE_LIVE_DIR/${ENV_HOST}-*" >&2 echo " Checked listener fallback paths: /var/www/ssl/litespeed.{key,crt}, /usr/local/lsws/conf/server.{key,crt}" >&2 exit 1 fi 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 certificate source: $CERT_SOURCE" >&2 echo "INFO: Using 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 with Let's Encrypt certs." >&2 sudo tee "$VHOST_CONFIG" > /dev/null < /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] $LE_KEY_FILE $LE_CERT_FILE 1 0 0 0 \$VH_ROOT/awstats /awstats/ localhost 127.0.0.1 localhost 86400 0 EOF 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 # --- 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 or update SSL." >&2 fi sudo tee "$GATEWAY_FILE" >/dev/null <<'PHP' 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); header('Location: /'); exit; ?> PHP sudo chown litespeed:litespeed "$GATEWAY_FILE" sudo chmod 644 "$GATEWAY_FILE" # Restart LiteSpeed if we modified the config if [[ "${NEEDS_RESTART:-0}" -eq 1 ]]; then 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 if [[ -z "${URL_HOST:-}" ]]; then URL_HOST="$ENV_HOST" fi # Defensive sanitization before composing final URL. URL_HOST="${URL_HOST#https://}" URL_HOST="${URL_HOST#http://}" URL_HOST="${URL_HOST%%/*}" if [[ "$URL_HOST" =~ ^(.+):[0-9]+$ ]]; then URL_HOST="${BASH_REMATCH[1]}" fi echo "INFO: Gateway URL host selected: $URL_HOST" >&2 URL="https://$URL_HOST:8443/access-db-$SLUG.php?token=$token" echo "$URL"