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

384 lines
13 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.
# 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
# Prefer explicit cert domain and fallback to Jelastic environment domain.
if [[ -z "$CERT_DOMAIN" ]]; then
if [[ -n "${JELASTIC_ENV_DOMAIN:-}" ]]; then
CERT_DOMAIN="$JELASTIC_ENV_DOMAIN"
else
CERT_DOMAIN=$(hostname -f)
CERT_DOMAIN=${CERT_DOMAIN#node*-} # strip nodeXXXX-
fi
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="$CERT_DOMAIN"
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 ENV_HOST ---
LE_LIVE_DIR="/etc/letsencrypt/live"
LE_CERT_DIR=""
# Attempt exact match first.
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"
echo "INFO: Found exact Let's Encrypt cert directory: $LE_CERT_DIR" >&2
fi
# Then try certbot-suffixed directories (e.g., domain-0001).
if [[ -z "$LE_CERT_DIR" ]]; then
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%/}"
echo "INFO: Found suffixed Let's Encrypt cert directory: $LE_CERT_DIR" >&2
break
fi
done
fi
# If no cert exists yet, attempt one-time issuance for ENV_HOST.
if [[ -z "$LE_CERT_DIR" ]]; then
echo "WARNING: No Let's Encrypt certificate found for '$ENV_HOST'. Attempting to issue one now..." >&2
CERTBOT_CMD=""
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"
fi
if [[ -z "$CERTBOT_CMD" ]]; then
echo "FATAL: certbot is not available and no existing Let's Encrypt certificate was found for '$ENV_HOST'." >&2
exit 1
fi
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
# 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 for ENV_HOST: $ENV_HOST" >&2
echo " Checked specific path: $LE_LIVE_DIR/$ENV_HOST" >&2
echo " Checked suffixed paths: $LE_LIVE_DIR/${ENV_HOST}-*" >&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 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>
<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>$LE_KEY_FILE</keyFile>
<certFile>$LE_CERT_FILE</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
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'
<?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);
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
URL="https://$ENV_HOST:8443/access-db-$SLUG.php?token=$token"
echo "$URL"