From 0bb56cd8d23486b5b0f827ce77eafabc97463353 Mon Sep 17 00:00:00 2001 From: Anthony Date: Wed, 20 Aug 2025 00:04:45 +0800 Subject: [PATCH] Test SNI Certs --- scripts/ssl-manager/ssl_manager.sh | 362 +++++++++++------------------ 1 file changed, 138 insertions(+), 224 deletions(-) diff --git a/scripts/ssl-manager/ssl_manager.sh b/scripts/ssl-manager/ssl_manager.sh index 81f106e..d40fbc6 100644 --- a/scripts/ssl-manager/ssl_manager.sh +++ b/scripts/ssl-manager/ssl_manager.sh @@ -18,6 +18,9 @@ PUBLIC_IP="" DOMAINS=() EMAIL="" VERBOSE=0 +VHOST_NAME="Jelastic" # Default vhost name, can be overridden with --vhost parameter +VHOSTS_DIR="$SERVER_ROOT/conf/vhosts" +DEFAULT_DOCROOT="/var/www/webroot/ROOT" SCRIPT_LOG="${LOG_DIR}/ssl_manager.log" ERROR_LOG="${LOG_DIR}/ssl_manager-error.log" @@ -95,15 +98,23 @@ trap 'on_exit' EXIT check_command() { local cmd="$1" - local apt_pkg="$2" + local pkg="$2" if ! command -v "$cmd" &>/dev/null; then - log "Required command '$cmd' not found. Attempting to install package '$apt_pkg'..." - if sudo yum install -y "$apt_pkg"; then - log_success "Successfully installed '$apt_pkg'" - else - log_error "Failed to install '$apt_pkg'" - SCRIPT_EXIT_STATUS=1; return 1 + log "Required command '$cmd' not found. Attempting to install package '$pkg'..." + if command -v dnf &>/dev/null; then + if sudo dnf install -y "$pkg"; then + log_success "Successfully installed '$pkg' via dnf" + return 0 + fi fi + if command -v yum &>/dev/null; then + if sudo yum install -y "$pkg"; then + log_success "Successfully installed '$pkg' via yum" + return 0 + fi + fi + log_error "Failed to install '$pkg'" + SCRIPT_EXIT_STATUS=1; return 1 fi } @@ -143,240 +154,140 @@ validate_dns() { validate_http_access() { local token token=$(openssl rand -hex 16) - echo "$token" > "/var/www/webroot/ROOT/.well-known/acme-challenge/test-token" + local acme_dir="/var/www/webroot/ROOT/.well-known/acme-challenge" + sudo mkdir -p "$acme_dir" + echo "$token" | sudo tee "$acme_dir/test-token" >/dev/null [[ "$(curl -s "http://$1/.well-known/acme-challenge/test-token")" == "$token" ]] } issue_certificate() { - if [[ -f "$CERT_DIR/$1/fullchain.pem" ]]; then - log_success "Certificate already exists for '$1'. Skipping issuance." - return - fi - log "Issuing SSL certificate for domain '$1' with email '$2'..." - sudo certbot certonly --standalone --preferred-challenges http -d "$1" --non-interactive --agree-tos --email "$2" || { - log_error "Failed to issue certificate for '$1'" - SCRIPT_EXIT_STATUS=1; return 1 - } - log_success "Certificate successfully issued for '$1'" + if [[ -f "$CERT_DIR/$1/fullchain.pem" ]]; then + log_success "Certificate already exists for '$1'. Skipping issuance." + return + fi + log "Issuing SSL certificate for domain '$1' with email '$2'..." + sudo certbot certonly --webroot -w "/var/www/webroot/ROOT" -d "$1" --non-interactive --agree-tos --email "$2" || { + log_error "Failed to issue certificate for '$1'" + SCRIPT_EXIT_STATUS=1; return 1 + } + log_success "Certificate successfully issued for '$1'" +} + +issue_certificate_san() { + local email="$1"; shift + local primary="$1"; shift + local others=("$@") + + local args=(certonly --webroot -w "$DEFAULT_DOCROOT" --non-interactive --agree-tos --email "$email") + for d in "${others[@]}"; do + args+=( -d "$d" ) + done + log "Requesting SAN certificate for: $primary ${others[*]}" + sudo certbot "${args[@]}" || { log_error "SAN issuance failed"; return 1; } + return 0 } update_httpd_config() { - local domain="$1" - local ip="$2" - local listener_name="HTTPS-$domain" - local vhost_name="Default" + local domain="$1" + local ip="$2" + local vhost_name="$domain" # vhost named after the domain + local vhost_dir="$VHOSTS_DIR/$domain" + local vhconf_file="$vhost_dir/vhconf.xml" - log "Checking if listener exists for '$listener_name'..." - local existing - existing=$(xmlstarlet sel -t -v "/httpServerConfig/listenerList/listener[name='$listener_name']/name" "$CONF_FILE" 2>/dev/null || true) + log "Configuring SNI for domain '$domain' on existing HTTPS listener (443)" + sudo cp -a "$CONF_FILE" "$BACKUP_FILE" - if [[ -n "$existing" ]]; then - log "Listener '$listener_name' already exists. Checking for vhost mapping..." - local vhost_map_exists - vhost_map_exists=$(xmlstarlet sel -t -v "/httpServerConfig/listenerList/listener[name='$listener_name']/vhostMapList/vhostMap/domain" "$CONF_FILE" 2>/dev/null || true) - if [[ -z "$vhost_map_exists" ]]; then - log "Adding vhostMapList to existing listener for '$listener_name'..." - sudo cp -a "$CONF_FILE" "$BACKUP_FILE" - - # Create a temporary XML file with the new vhostMapList - local temp_xml=$(mktemp) - cat > "$temp_xml" << EOF - - - - $vhost_name - $domain - - + # 1) Ensure virtualHostList entry exists for this domain + local vh_exists + vh_exists=$(xmlstarlet sel -t -v "/httpServerConfig/virtualHostList/virtualHost[name='$vhost_name']/name" "$CONF_FILE" 2>/dev/null || true) + if [[ -z "$vh_exists" ]]; then + log "Adding virtualHost '$vhost_name' to httpd_config.xml" + # Create vhost directory + sudo mkdir -p "$vhost_dir" + # Write minimal vhconf.xml with SSL at vhost level (SNI) + cat </dev/null + + + $VH_ROOT/ROOT/ + 1 + + /etc/letsencrypt/live/$domain/privkey.pem + /etc/letsencrypt/live/$domain/fullchain.pem + 1 + + EOF + # Add virtualHost entry + sudo xmlstarlet ed -L \ + -s "/httpServerConfig/virtualHostList" -t elem -n "virtualHost" \ + "$CONF_FILE" || { log_error "Failed to create virtualHost node"; return 1; } - # First validate the temporary XML - if ! xmllint --noout "$temp_xml" 2>/dev/null; then - log_error "Invalid temporary XML structure" - rm -f "$temp_xml" - SCRIPT_EXIT_STATUS=1 - return 1 - fi + sudo xmlstarlet ed -L \ + -s "/httpServerConfig/virtualHostList/virtualHost[last()]" -t elem -n "name" -v "$vhost_name" \ + "$CONF_FILE" || { log_error "Failed to set virtualHost name"; return 1; } - # Insert the vhostMapList into the existing listener - sudo xmlstarlet ed -L \ - -i "/httpServerConfig/listenerList/listener[name='$listener_name']" \ - -t elem -n "vhostMapList" \ - -v "$(xmlstarlet sel -t -c "//vhostMapList/*" "$temp_xml")" \ - "$CONF_FILE" || { - rm -f "$temp_xml" - log_error "Failed to insert vhostMapList" - SCRIPT_EXIT_STATUS=1 - return 1 - } - - rm -f "$temp_xml" + sudo xmlstarlet ed -L \ + -s "/httpServerConfig/virtualHostList/virtualHost[last()]" -t elem -n "vhRoot" -v "$SERVER_ROOT/webroot/" \ + "$CONF_FILE" || { log_error "Failed to set vhRoot"; return 1; } - # Validate the modified config - if ! xmllint --noout "$CONF_FILE" 2>/dev/null; then - log_error "Invalid XML structure after modification. Restoring backup..." - sudo cp -a "$BACKUP_FILE" "$CONF_FILE" - SCRIPT_EXIT_STATUS=1 - return 1 - fi - log_success "Successfully added vhostMapList to existing listener" - else - log_success "vhostMap already present. No update needed." - fi - return - fi + sudo xmlstarlet ed -L \ + -s "/httpServerConfig/virtualHostList/virtualHost[last()]" -t elem -n "configFile" -v "$vhost_dir/vhconf.xml" \ + "$CONF_FILE" || { log_error "Failed to set configFile"; return 1; } - log "Updating httpd_config.xml for domain '$domain' with IP '$ip'..." - sudo cp -a "$CONF_FILE" "$BACKUP_FILE" + sudo xmlstarlet ed -L \ + -s "/httpServerConfig/virtualHostList/virtualHost[last()]" -t elem -n "allowSymbolLink" -v "1" \ + "$CONF_FILE" || { log_error "Failed to set allowSymbolLink"; return 1; } - # Create a temporary XML file with the new listener configuration - local temp_xml=$(mktemp) - cat > "$temp_xml" << EOF - - - $listener_name -
*:443
- 1 - /etc/letsencrypt/live/$domain/privkey.pem - /etc/letsencrypt/live/$domain/fullchain.pem - - - $vhost_name - $domain - - -
-EOF + sudo xmlstarlet ed -L \ + -s "/httpServerConfig/virtualHostList/virtualHost[last()]" -t elem -n "enableScript" -v "1" \ + "$CONF_FILE" || { log_error "Failed to set enableScript"; return 1; } - # First validate the temporary XML - if ! xmllint --noout "$temp_xml" 2>/dev/null; then - log_error "Invalid temporary XML structure" - rm -f "$temp_xml" - SCRIPT_EXIT_STATUS=1 - return 1 - fi + sudo xmlstarlet ed -L \ + -s "/httpServerConfig/virtualHostList/virtualHost[last()]" -t elem -n "restrained" -v "1" \ + "$CONF_FILE" || { log_error "Failed to set restrained"; return 1; } + else + log "virtualHost '$vhost_name' already exists" + fi - # Insert the new listener into the configuration using a different approach - sudo xmlstarlet ed -L \ - -s "//listenerList" \ - -t elem -n "listener" \ - "$CONF_FILE" || { - rm -f "$temp_xml" - log_error "Failed to create new listener element" - SCRIPT_EXIT_STATUS=1 - return 1 - } + # 2) Map domain to this vhost in existing HTTPS listener + local https_listener_exists + https_listener_exists=$(xmlstarlet sel -t -v "/httpServerConfig/listenerList/listener[name='HTTPS']/name" "$CONF_FILE" 2>/dev/null || true) + local https6_listener_exists + https6_listener_exists=$(xmlstarlet sel -t -v "/httpServerConfig/listenerList/listener[name='HTTPS-ipv6']/name" "$CONF_FILE" 2>/dev/null || true) + if [[ -z "$https_listener_exists" ]]; then + log_error "HTTPS listener not found in $CONF_FILE. Cannot proceed with SNI mapping." + SCRIPT_EXIT_STATUS=1; return 1 + fi - # Now add each element individually - sudo xmlstarlet ed -L \ - -s "//listenerList/listener[last()]" \ - -t elem -n "name" \ - -v "$listener_name" \ - "$CONF_FILE" || { - rm -f "$temp_xml" - log_error "Failed to add name element" - SCRIPT_EXIT_STATUS=1 - return 1 - } + for ln in HTTPS ${https6_listener_exists:+HTTPS-ipv6}; do + local mapping_exists + mapping_exists=$(xmlstarlet sel -t -v "/httpServerConfig/listenerList/listener[name='$ln']/vhostMapList/vhostMap[domain='$domain']/domain" "$CONF_FILE" 2>/dev/null || true) + if [[ -z "$mapping_exists" ]]; then + log "Adding vhostMap for domain '$domain' to $ln listener" + # Ensure vhostMapList exists + local vhost_maplist_exists + vhost_maplist_exists=$(xmlstarlet sel -t -v "/httpServerConfig/listenerList/listener[name='$ln']/vhostMapList/vhostMap/domain" "$CONF_FILE" 2>/dev/null || true) + if [[ -z "$vhost_maplist_exists" ]]; then + sudo xmlstarlet ed -L -s "/httpServerConfig/listenerList/listener[name='$ln']" -t elem -n "vhostMapList" "$CONF_FILE" || { log_error "Failed to create vhostMapList for $ln"; return 1; } + fi - sudo xmlstarlet ed -L \ - -s "//listenerList/listener[last()]" \ - -t elem -n "address" \ - -v "*:443" \ - "$CONF_FILE" || { - rm -f "$temp_xml" - log_error "Failed to add address element" - SCRIPT_EXIT_STATUS=1 - return 1 - } + # Add vhostMap + sudo xmlstarlet ed -L -s "/httpServerConfig/listenerList/listener[name='$ln']/vhostMapList" -t elem -n "vhostMap" "$CONF_FILE" || { log_error "Failed to create vhostMap for $ln"; return 1; } + sudo xmlstarlet ed -L -s "/httpServerConfig/listenerList/listener[name='$ln']/vhostMapList/vhostMap[last()]" -t elem -n "vhost" -v "$vhost_name" "$CONF_FILE" || { log_error "Failed to set vhost in map for $ln"; return 1; } + sudo xmlstarlet ed -L -s "/httpServerConfig/listenerList/listener[name='$ln']/vhostMapList/vhostMap[last()]" -t elem -n "domain" -v "$domain" "$CONF_FILE" || { log_error "Failed to set domain in map for $ln"; return 1; } + else + log "vhostMap for '$domain' already exists on $ln listener" + fi + done - sudo xmlstarlet ed -L \ - -s "//listenerList/listener[last()]" \ - -t elem -n "secure" \ - -v "1" \ - "$CONF_FILE" || { - rm -f "$temp_xml" - log_error "Failed to add secure element" - SCRIPT_EXIT_STATUS=1 - return 1 - } + # 3) Validate final config + if ! xmllint --noout "$CONF_FILE" 2>/dev/null; then + log_error "Invalid XML structure after SNI configuration. Restoring backup..." + sudo cp -a "$BACKUP_FILE" "$CONF_FILE" + SCRIPT_EXIT_STATUS=1; return 1 + fi - sudo xmlstarlet ed -L \ - -s "//listenerList/listener[last()]" \ - -t elem -n "keyFile" \ - -v "/etc/letsencrypt/live/$domain/privkey.pem" \ - "$CONF_FILE" || { - rm -f "$temp_xml" - log_error "Failed to add keyFile element" - SCRIPT_EXIT_STATUS=1 - return 1 - } - - sudo xmlstarlet ed -L \ - -s "//listenerList/listener[last()]" \ - -t elem -n "certFile" \ - -v "/etc/letsencrypt/live/$domain/fullchain.pem" \ - "$CONF_FILE" || { - rm -f "$temp_xml" - log_error "Failed to add certFile element" - SCRIPT_EXIT_STATUS=1 - return 1 - } - - # Add vhostMapList and its children - sudo xmlstarlet ed -L \ - -s "//listenerList/listener[last()]" \ - -t elem -n "vhostMapList" \ - "$CONF_FILE" || { - rm -f "$temp_xml" - log_error "Failed to add vhostMapList element" - SCRIPT_EXIT_STATUS=1 - return 1 - } - - sudo xmlstarlet ed -L \ - -s "//listenerList/listener[last()]/vhostMapList" \ - -t elem -n "vhostMap" \ - "$CONF_FILE" || { - rm -f "$temp_xml" - log_error "Failed to add vhostMap element" - SCRIPT_EXIT_STATUS=1 - return 1 - } - - sudo xmlstarlet ed -L \ - -s "//listenerList/listener[last()]/vhostMapList/vhostMap" \ - -t elem -n "vhost" \ - -v "$vhost_name" \ - "$CONF_FILE" || { - rm -f "$temp_xml" - log_error "Failed to add vhost element" - SCRIPT_EXIT_STATUS=1 - return 1 - } - - sudo xmlstarlet ed -L \ - -s "//listenerList/listener[last()]/vhostMapList/vhostMap" \ - -t elem -n "domain" \ - -v "$domain" \ - "$CONF_FILE" || { - rm -f "$temp_xml" - log_error "Failed to add domain element" - SCRIPT_EXIT_STATUS=1 - return 1 - } - - rm -f "$temp_xml" - - # Validate the modified config - if ! xmllint --noout "$CONF_FILE" 2>/dev/null; then - log_error "Invalid XML structure after modification. Restoring backup..." - sudo cp -a "$BACKUP_FILE" "$CONF_FILE" - SCRIPT_EXIT_STATUS=1 - return 1 - fi - - log_success "Listener + vhostMap added for '$listener_name'" + log_success "SNI configured for '$domain' on port 443 with vhost '$vhost_name'" } cleanup_xml() { @@ -427,6 +338,7 @@ main() { --domain=*) PRIMARY_DOMAIN="${arg#*=}"; DOMAINS=("$PRIMARY_DOMAIN"); log_verbose "Set primary domain: $PRIMARY_DOMAIN";; --domains=*) IFS=',' read -ra DOMAINS <<< "${arg#*=}"; PRIMARY_DOMAIN="${DOMAINS[0]}"; log_verbose "Set domains: ${DOMAINS[*]}";; --email=*) EMAIL="${arg#*=}"; log_verbose "Set email: $EMAIL";; + --vhost=*) VHOST_NAME="${arg#*=}"; log_verbose "Set vhost name: $VHOST_NAME";; --verbose) VERBOSE=1; log "Verbose mode enabled";; *) log_error "Invalid argument: $arg"; SCRIPT_EXIT_STATUS=1; exit 1;; esac @@ -445,14 +357,16 @@ main() { validate_http_access "$PRIMARY_DOMAIN" || { log_error "HTTP access validation failed"; SCRIPT_EXIT_STATUS=1; exit 1; } log_verbose "Checking dependencies..." - check_command xmllint libxml2-utils + check_command xmllint libxml2 check_command xmlstarlet xmlstarlet check_command certbot certbot + check_command dig bind-utils + check_command curl curl + check_command openssl openssl create_default_backup - issue_certificate "$PRIMARY_DOMAIN" "$EMAIL" - for domain in "${DOMAINS[@]}"; do + issue_certificate "$domain" "$EMAIL" update_httpd_config "$domain" "$PUBLIC_IP" cleanup_xml "$domain" done @@ -464,4 +378,4 @@ main() { } # === Entry Point === -main "$@" \ No newline at end of file +main "$@"