2025-08-26 16:26:33 +00:00
|
|
|
#!/bin/bash
|
2025-10-20 18:47:40 +00:00
|
|
|
|
2025-08-26 16:26:33 +00:00
|
|
|
# ==============================================================================
|
|
|
|
|
# Script: create_pma_gateway.sh
|
|
|
|
|
# Purpose: Create a time-limited gateway URL for phpMyAdmin on Virtuozzo LLSMP.
|
2025-10-20 18:47:40 +00:00
|
|
|
# Uses Let's Encrypt cert in the dedicated phpMyAdmin vhost (port 8443).
|
2026-02-26 12:30:22 +00:00
|
|
|
# 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]
|
2025-08-26 16:26:33 +00:00
|
|
|
# Outputs: Prints the generated URL.
|
|
|
|
|
# ==============================================================================
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
|
|
|
SLUG=""
|
|
|
|
|
VALIDITY=30 # minutes
|
2026-02-26 12:30:22 +00:00
|
|
|
CERT_DOMAIN=""
|
|
|
|
|
CONTACT_EMAIL=""
|
2025-08-26 16:26:33 +00:00
|
|
|
|
|
|
|
|
for arg in "$@"; do
|
|
|
|
|
case $arg in
|
|
|
|
|
--slug=*) SLUG="${arg#*=}" ;;
|
|
|
|
|
--validity=*) VALIDITY="${arg#*=}" ;;
|
2026-02-26 12:30:22 +00:00
|
|
|
--cert-domain=*) CERT_DOMAIN="${arg#*=}" ;;
|
|
|
|
|
--email=*) CONTACT_EMAIL="${arg#*=}" ;;
|
2025-10-20 17:51:39 +00:00
|
|
|
*) echo "ERROR: Unknown argument $arg"; exit 1 ;;
|
2025-08-26 16:26:33 +00:00
|
|
|
esac
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
if [[ -z "$SLUG" ]]; then
|
|
|
|
|
SLUG=$(openssl rand -hex 4) # 8-char random
|
|
|
|
|
fi
|
|
|
|
|
|
2026-02-26 12:30:22 +00:00
|
|
|
# Reject unresolved template placeholders if they are passed through literally.
|
|
|
|
|
if [[ "$CERT_DOMAIN" == *'${'* ]]; then
|
|
|
|
|
CERT_DOMAIN=""
|
|
|
|
|
fi
|
|
|
|
|
if [[ "$CONTACT_EMAIL" == *'${'* ]]; then
|
|
|
|
|
CONTACT_EMAIL=""
|
|
|
|
|
fi
|
|
|
|
|
|
2026-02-26 12:47:00 +00:00
|
|
|
# Build domain candidates in priority order:
|
|
|
|
|
# 1) explicit --cert-domain
|
|
|
|
|
# 2) JELASTIC_ENV_DOMAIN
|
|
|
|
|
# 3) hostname-derived domain
|
2026-02-26 13:01:34 +00:00
|
|
|
# Additionally, if a legacy *.sites.mightybox.dev candidate is seen,
|
|
|
|
|
# derive *.sites.mightybox.cloud as a compatibility fallback.
|
2026-02-26 14:48:14 +00:00
|
|
|
# If a regional sites domain is seen, also derive short <site>.mightybox.cloud
|
|
|
|
|
# as preferred public URL host candidate.
|
2026-02-26 12:47:00 +00:00
|
|
|
HOSTNAME_DOMAIN=$(hostname -f)
|
|
|
|
|
HOSTNAME_DOMAIN=${HOSTNAME_DOMAIN#node*-}
|
|
|
|
|
|
|
|
|
|
DOMAIN_CANDIDATES=()
|
|
|
|
|
SEEN_DOMAINS="|"
|
|
|
|
|
for candidate in "$CERT_DOMAIN" "${JELASTIC_ENV_DOMAIN:-}" "$HOSTNAME_DOMAIN"; do
|
2026-02-26 14:51:04 +00:00
|
|
|
# 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
|
|
|
|
|
|
2026-02-26 13:01:34 +00:00
|
|
|
if [[ -n "$candidate" ]] && [[ "$candidate" != *'${'* ]] && [[ "$SEEN_DOMAINS" != *"|$candidate|"* ]]; then
|
2026-02-26 12:47:00 +00:00
|
|
|
DOMAIN_CANDIDATES+=("$candidate")
|
|
|
|
|
SEEN_DOMAINS="${SEEN_DOMAINS}${candidate}|"
|
2026-02-26 12:30:22 +00:00
|
|
|
fi
|
2026-02-26 13:01:34 +00:00
|
|
|
|
|
|
|
|
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
|
2026-02-26 14:48:14 +00:00
|
|
|
|
|
|
|
|
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
|
2026-02-26 12:47:00 +00:00
|
|
|
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
|
2026-02-26 12:30:22 +00:00
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Fallback for email when not explicitly passed.
|
|
|
|
|
if [[ -z "$CONTACT_EMAIL" ]] && [[ -n "${JELASTIC_USER_EMAIL:-}" ]]; then
|
|
|
|
|
CONTACT_EMAIL="$JELASTIC_USER_EMAIL"
|
2025-10-20 15:36:39 +00:00
|
|
|
fi
|
|
|
|
|
|
2026-02-26 12:47:00 +00:00
|
|
|
ENV_HOST="${DOMAIN_CANDIDATES[0]}"
|
2026-02-26 14:48:14 +00:00
|
|
|
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
|
2026-02-26 12:30:22 +00:00
|
|
|
|
2026-02-26 14:51:04 +00:00
|
|
|
# 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
|
|
|
|
|
|
2025-08-26 16:26:33 +00:00
|
|
|
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
|
2025-08-26 17:27:43 +00:00
|
|
|
sudo chown litespeed:litespeed "$SECRET_FILE"
|
|
|
|
|
sudo chmod 644 "$SECRET_FILE"
|
2025-10-20 18:47:40 +00:00
|
|
|
SECRET=$(sudo cat "$SECRET_FILE" | xargs)
|
2025-08-26 16:26:33 +00:00
|
|
|
|
|
|
|
|
now=$(date +%s)
|
|
|
|
|
expires=$((now + VALIDITY*60))
|
|
|
|
|
# token = base64("$SLUG:$expires") . '.' . HMAC_SHA256(secret, data)
|
|
|
|
|
data="$SLUG:$expires"
|
2025-08-26 17:27:43 +00:00
|
|
|
base=$(printf "%s" "$data" | base64 | tr -d '\n')
|
2025-10-20 18:47:40 +00:00
|
|
|
mac=$(php -r "echo hash_hmac('sha256', '$data', '$SECRET');")
|
|
|
|
|
token="$base.$mac"
|
2025-10-20 17:51:39 +00:00
|
|
|
|
2025-10-20 18:47:40 +00:00
|
|
|
# Secure the phpMyAdmin vhost with Rewrite Rules to block direct access
|
|
|
|
|
VHOST_CONFIG="/usr/share/phpMyAdmin/vhost.conf"
|
|
|
|
|
NEEDS_RESTART=0
|
|
|
|
|
|
2026-02-26 12:47:00 +00:00
|
|
|
# --- Let's Encrypt certificate resolution for domain candidates ---
|
2025-10-20 18:47:40 +00:00
|
|
|
LE_LIVE_DIR="/etc/letsencrypt/live"
|
|
|
|
|
LE_CERT_DIR=""
|
2026-02-26 12:47:00 +00:00
|
|
|
CERT_DOMAIN_USED=""
|
2026-02-26 14:06:02 +00:00
|
|
|
CERT_SOURCE="Let's Encrypt"
|
2026-02-26 12:47:00 +00:00
|
|
|
|
|
|
|
|
# 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
|
2025-10-20 18:47:40 +00:00
|
|
|
|
2026-02-26 12:47:00 +00:00
|
|
|
for dir in "$LE_LIVE_DIR/$candidate_host"-*/; do
|
2026-02-26 12:30:22 +00:00
|
|
|
if [[ -d "$dir" ]] && [[ -f "$dir/privkey.pem" ]] && [[ -f "$dir/fullchain.pem" ]]; then
|
|
|
|
|
LE_CERT_DIR="${dir%/}"
|
2026-02-26 12:47:00 +00:00
|
|
|
CERT_DOMAIN_USED="$candidate_host"
|
|
|
|
|
echo "INFO: Found suffixed Let's Encrypt cert directory for '$candidate_host': $LE_CERT_DIR" >&2
|
2026-02-26 12:30:22 +00:00
|
|
|
break
|
2025-10-20 18:47:40 +00:00
|
|
|
fi
|
|
|
|
|
done
|
2026-02-26 12:47:00 +00:00
|
|
|
|
|
|
|
|
if [[ -n "$LE_CERT_DIR" ]]; then
|
|
|
|
|
break
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
if [[ -n "$CERT_DOMAIN_USED" ]]; then
|
|
|
|
|
ENV_HOST="$CERT_DOMAIN_USED"
|
2025-10-20 17:51:39 +00:00
|
|
|
fi
|
|
|
|
|
|
2026-02-26 12:47:00 +00:00
|
|
|
# If no cert exists yet, attempt one-time issuance for highest-priority candidate.
|
2026-02-26 12:30:22 +00:00
|
|
|
if [[ -z "$LE_CERT_DIR" ]]; then
|
2026-02-26 12:47:00 +00:00
|
|
|
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
|
2026-02-26 12:30:22 +00:00
|
|
|
|
|
|
|
|
CERTBOT_CMD=""
|
|
|
|
|
if command -v certbot >/dev/null 2>&1; then
|
|
|
|
|
CERTBOT_CMD="certbot"
|
2026-02-26 13:52:22 +00:00
|
|
|
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
|
2026-02-26 12:30:22 +00:00
|
|
|
fi
|
|
|
|
|
|
2026-02-26 12:47:00 +00:00
|
|
|
if [[ -z "$CERTBOT_CMD" ]]; then
|
2026-02-26 13:52:22 +00:00
|
|
|
echo "WARNING: certbot/certbot-auto is still unavailable. Attempting package manager install..." >&2
|
2026-02-26 12:47:00 +00:00
|
|
|
if command -v dnf >/dev/null 2>&1; then
|
2026-02-26 13:52:22 +00:00
|
|
|
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
|
2026-02-26 12:47:00 +00:00
|
|
|
elif command -v yum >/dev/null 2>&1; then
|
2026-02-26 13:52:22 +00:00
|
|
|
if ! sudo yum install -y certbot >/dev/null 2>&1; then
|
|
|
|
|
echo "WARNING: yum failed to install certbot." >&2
|
|
|
|
|
fi
|
2026-02-26 12:47:00 +00:00
|
|
|
elif command -v apt-get >/dev/null 2>&1; then
|
|
|
|
|
sudo apt-get update -qq >/dev/null 2>&1 || true
|
2026-02-26 13:52:22 +00:00
|
|
|
if ! sudo apt-get install -y certbot >/dev/null 2>&1; then
|
|
|
|
|
echo "WARNING: apt-get failed to install certbot." >&2
|
|
|
|
|
fi
|
2026-02-26 12:47:00 +00:00
|
|
|
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"
|
2026-02-26 13:52:22 +00:00
|
|
|
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
|
2026-02-26 12:47:00 +00:00
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
2026-02-26 12:30:22 +00:00
|
|
|
if [[ -z "$CERTBOT_CMD" ]]; then
|
2026-02-26 14:06:02 +00:00
|
|
|
echo "WARNING: certbot is unavailable for on-demand issuance. Will try existing listener certificate files as fallback." >&2
|
2026-02-26 12:30:22 +00:00
|
|
|
else
|
2026-02-26 14:06:02 +00:00
|
|
|
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
|
2026-02-26 12:30:22 +00:00
|
|
|
fi
|
|
|
|
|
|
2026-02-26 14:06:02 +00:00
|
|
|
# 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
|
2026-02-26 12:30:22 +00:00
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
2025-10-20 18:47:40 +00:00
|
|
|
# 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
|
2026-02-26 14:06:02 +00:00
|
|
|
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
|
2025-10-20 18:47:40 +00:00
|
|
|
fi
|
2025-08-26 16:26:33 +00:00
|
|
|
|
2025-10-20 18:47:40 +00:00
|
|
|
# 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
|
2026-02-26 14:06:02 +00:00
|
|
|
echo "INFO: Using certificate source: $CERT_SOURCE" >&2
|
|
|
|
|
echo "INFO: Using certificate paths:" >&2
|
2025-10-20 18:47:40 +00:00
|
|
|
echo " Key: $LE_KEY_FILE" >&2
|
|
|
|
|
echo " Cert: $LE_CERT_FILE" >&2
|
2025-08-26 17:50:45 +00:00
|
|
|
|
2025-10-20 18:03:05 +00:00
|
|
|
|
2025-10-20 18:47:40 +00:00
|
|
|
# If vhost config is missing or empty, recreate it from a known-good default WITH Let's Encrypt Certs.
|
2025-10-20 18:03:05 +00:00
|
|
|
if [ ! -s "$VHOST_CONFIG" ]; then
|
2025-10-20 18:47:40 +00:00
|
|
|
echo "Warning: $VHOST_CONFIG is empty or missing. Recreating from default with Let's Encrypt certs." >&2
|
|
|
|
|
sudo tee "$VHOST_CONFIG" > /dev/null <<EOF
|
2025-10-20 18:03:05 +00:00
|
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
|
|
|
<virtualHostConfig>
|
|
|
|
|
<docRoot>/usr/share/phpMyAdmin/</docRoot>
|
|
|
|
|
<enableGzip>1</enableGzip>
|
|
|
|
|
<logging>
|
|
|
|
|
<log>
|
|
|
|
|
<useServer>0</useServer>
|
2025-10-20 18:47:40 +00:00
|
|
|
<fileName>\$SERVER_ROOT/logs/error.log</fileName>
|
2025-10-20 18:03:05 +00:00
|
|
|
<logLevel>DEBUG</logLevel>
|
|
|
|
|
<rollingSize>10M</rollingSize>
|
|
|
|
|
</log>
|
|
|
|
|
<accessLog>
|
|
|
|
|
<useServer>0</useServer>
|
2025-10-20 18:47:40 +00:00
|
|
|
<fileName>\$SERVER_ROOT/logs/access.log</fileName>
|
2025-10-20 18:03:05 +00:00
|
|
|
<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>
|
2025-10-20 18:47:40 +00:00
|
|
|
<cacheStorePath>/tmp/lscache/vhosts/\$VH_NAME</cacheStorePath>
|
2025-10-20 18:03:05 +00:00
|
|
|
</storage>
|
|
|
|
|
</cache>
|
|
|
|
|
<rewrite>
|
|
|
|
|
<enable>0</enable>
|
|
|
|
|
<logLevel>0</logLevel>
|
|
|
|
|
<rules>RewriteCond %{HTTP_USER_AGENT} ^NameOfBadRobot
|
|
|
|
|
RewriteRule ^/nospider/ - [F]</rules>
|
|
|
|
|
</rewrite>
|
|
|
|
|
<vhssl>
|
2025-10-20 18:47:40 +00:00
|
|
|
<keyFile>$LE_KEY_FILE</keyFile>
|
|
|
|
|
<certFile>$LE_CERT_FILE</certFile>
|
2025-10-20 18:03:05 +00:00
|
|
|
<certChain>1</certChain>
|
|
|
|
|
</vhssl>
|
|
|
|
|
<frontPage>
|
|
|
|
|
<enable>0</enable>
|
|
|
|
|
<disableAdmin>0</disableAdmin>
|
|
|
|
|
</frontPage>
|
|
|
|
|
<awstats>
|
|
|
|
|
<updateMode>0</updateMode>
|
2025-10-20 18:47:40 +00:00
|
|
|
<workingDir>\$VH_ROOT/awstats</workingDir>
|
2025-10-20 18:03:05 +00:00
|
|
|
<awstatsURI>/awstats/</awstatsURI>
|
|
|
|
|
<siteDomain>localhost</siteDomain>
|
|
|
|
|
<siteAliases>127.0.0.1 localhost</siteAliases>
|
|
|
|
|
<updateInterval>86400</updateInterval>
|
|
|
|
|
<updateOffset>0</updateOffset>
|
|
|
|
|
</awstats>
|
|
|
|
|
</virtualHostConfig>
|
|
|
|
|
EOF
|
|
|
|
|
NEEDS_RESTART=1
|
2025-10-20 17:51:39 +00:00
|
|
|
fi
|
|
|
|
|
|
2025-10-20 18:03:05 +00:00
|
|
|
if [ -f "$VHOST_CONFIG" ]; then
|
|
|
|
|
MARKER="# PMA Gateway Security Rules"
|
2025-10-20 18:47:40 +00:00
|
|
|
|
2025-10-20 18:03:05 +00:00
|
|
|
# If rules are not already in place, add them.
|
|
|
|
|
if ! sudo grep -qF "$MARKER" "$VHOST_CONFIG"; then
|
2025-10-20 18:47:40 +00:00
|
|
|
|
2025-10-20 18:03:05 +00:00
|
|
|
# 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'
|
2025-08-26 18:07:06 +00:00
|
|
|
# PMA Gateway Security Rules
|
|
|
|
|
# Allow access to the gateway scripts themselves
|
|
|
|
|
RewriteCond %{REQUEST_URI} ^/access-db-.*\.php$
|
|
|
|
|
RewriteRule .* - [L]
|
2025-10-20 18:03:05 +00:00
|
|
|
# For all other requests, block if the security cookie is not present
|
2025-08-26 18:07:06 +00:00
|
|
|
RewriteCond %{HTTP_COOKIE} !pma_access_granted
|
|
|
|
|
RewriteRule .* - [F,L]
|
2025-08-26 17:46:27 +00:00
|
|
|
EOF
|
2025-10-20 18:03:05 +00:00
|
|
|
)
|
2025-10-20 18:47:40 +00:00
|
|
|
|
2025-10-20 18:03:05 +00:00
|
|
|
# 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
|
2025-08-26 17:46:27 +00:00
|
|
|
fi
|
2025-10-20 18:47:40 +00:00
|
|
|
|
|
|
|
|
# --- 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
|
|
|
|
|
|
2025-10-20 18:03:05 +00:00
|
|
|
else
|
2025-10-20 18:47:40 +00:00
|
|
|
echo "Warning: phpMyAdmin vhost config not found at $VHOST_CONFIG. Cannot apply security rules or update SSL." >&2
|
2025-08-26 17:46:27 +00:00
|
|
|
fi
|
|
|
|
|
|
2025-10-20 18:47:40 +00:00
|
|
|
|
2025-08-26 17:27:43 +00:00
|
|
|
sudo tee "$GATEWAY_FILE" >/dev/null <<'PHP'
|
2025-08-26 16:26:33 +00:00
|
|
|
<?php
|
2026-02-26 13:52:22 +00:00
|
|
|
// Secure phpMyAdmin gateway - auto-generated, do NOT edit manually.
|
2025-08-26 17:27:43 +00:00
|
|
|
|
2025-08-26 16:26:33 +00:00
|
|
|
ini_set('session.cookie_httponly', 1);
|
2025-08-26 17:27:43 +00:00
|
|
|
$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();
|
|
|
|
|
}
|
2025-08-26 16:26:33 +00:00
|
|
|
|
2025-08-26 17:27:43 +00:00
|
|
|
list($base, $sig) = explode('.', $token, 2);
|
2025-08-26 16:26:33 +00:00
|
|
|
$data = base64_decode($base, true);
|
2025-08-26 17:27:43 +00:00
|
|
|
if ($data === false) {
|
|
|
|
|
deny();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (strpos($data, ':') === false) {
|
|
|
|
|
deny();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
list($slug, $exp) = explode(':', $data, 2);
|
|
|
|
|
if (time() > intval($exp)) {
|
2025-08-26 17:46:27 +00:00
|
|
|
unlink(__FILE__); // Self-destruct if expired
|
2025-08-26 17:27:43 +00:00
|
|
|
deny();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$secret = trim(file_get_contents('/var/lib/jelastic/keys/mbadmin_secret'));
|
|
|
|
|
if (!hash_equals($sig, hash_hmac('sha256', $data, $secret))) {
|
|
|
|
|
deny();
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-26 17:46:27 +00:00
|
|
|
// 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);
|
2025-10-20 18:47:40 +00:00
|
|
|
header('Location: /');
|
2025-08-26 16:26:33 +00:00
|
|
|
exit;
|
|
|
|
|
?>
|
|
|
|
|
PHP
|
|
|
|
|
|
2025-08-26 17:27:43 +00:00
|
|
|
sudo chown litespeed:litespeed "$GATEWAY_FILE"
|
|
|
|
|
sudo chmod 644 "$GATEWAY_FILE"
|
2025-08-26 16:26:33 +00:00
|
|
|
|
2025-08-26 17:46:27 +00:00
|
|
|
# Restart LiteSpeed if we modified the config
|
|
|
|
|
if [[ "${NEEDS_RESTART:-0}" -eq 1 ]]; then
|
2025-10-20 18:47:40 +00:00
|
|
|
echo "Applying security rules and SSL certificate changes, restarting LiteSpeed..." >&2
|
2025-08-26 17:46:27 +00:00
|
|
|
if ! sudo systemctl restart lsws; then
|
|
|
|
|
echo "Warning: LiteSpeed restart failed. Manual restart may be required." >&2
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
2026-02-26 14:48:14 +00:00
|
|
|
if [[ -z "${URL_HOST:-}" ]]; then
|
|
|
|
|
URL_HOST="$ENV_HOST"
|
|
|
|
|
fi
|
|
|
|
|
|
2026-02-26 14:51:04 +00:00
|
|
|
# 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
|
|
|
|
|
|
2026-02-26 15:12:28 +00:00
|
|
|
# Best-effort fallback: if preferred host is not reachable on 8443,
|
|
|
|
|
# try other known domain candidates before printing final gateway URL.
|
|
|
|
|
if command -v timeout >/dev/null 2>&1; then
|
|
|
|
|
URL_HOST_CANDIDATES=()
|
|
|
|
|
URL_HOST_SEEN="|"
|
|
|
|
|
for candidate in "$URL_HOST" "$ENV_HOST" "${DOMAIN_CANDIDATES[@]}"; do
|
|
|
|
|
candidate="${candidate#https://}"
|
|
|
|
|
candidate="${candidate#http://}"
|
|
|
|
|
candidate="${candidate%%/*}"
|
|
|
|
|
if [[ "$candidate" =~ ^(.+):[0-9]+$ ]]; then
|
|
|
|
|
candidate="${BASH_REMATCH[1]}"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if [[ -z "$candidate" ]] || [[ "$URL_HOST_SEEN" == *"|$candidate|"* ]]; then
|
|
|
|
|
continue
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Restrict to simple host/domain tokens for safe /dev/tcp probing.
|
|
|
|
|
if [[ ! "$candidate" =~ ^[A-Za-z0-9._-]+$ ]]; then
|
|
|
|
|
continue
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
URL_HOST_CANDIDATES+=("$candidate")
|
|
|
|
|
URL_HOST_SEEN="${URL_HOST_SEEN}${candidate}|"
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
SELECTED_URL_HOST=""
|
|
|
|
|
for candidate in "${URL_HOST_CANDIDATES[@]}"; do
|
|
|
|
|
if timeout 4 bash -c "exec 3<>/dev/tcp/$candidate/8443" >/dev/null 2>&1; then
|
|
|
|
|
SELECTED_URL_HOST="$candidate"
|
|
|
|
|
break
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
if [[ -n "$SELECTED_URL_HOST" ]] && [[ "$SELECTED_URL_HOST" != "$URL_HOST" ]]; then
|
|
|
|
|
echo "WARNING: Preferred URL host '$URL_HOST' is not reachable on 8443. Using '$SELECTED_URL_HOST' instead." >&2
|
|
|
|
|
URL_HOST="$SELECTED_URL_HOST"
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
2026-02-26 14:48:14 +00:00
|
|
|
echo "INFO: Gateway URL host selected: $URL_HOST" >&2
|
|
|
|
|
URL="https://$URL_HOST:8443/access-db-$SLUG.php?token=$token"
|
2025-10-20 18:47:40 +00:00
|
|
|
echo "$URL"
|