fix(script): SSL management fixes (#4994, #5010, #5070)

- Issue acme.sh HTTP-01 over IPv4 unless the host has no global IPv4
  address: the hardcoded --listen-v6 started a v6-only standalone
  listener, so validation of a domain whose A record points at this
  host always failed (#4994).
- Add a custom cert/key path option to the "Set Cert paths" menu so
  certificates living outside /root/cert (e.g. certbot under
  /etc/letsencrypt) can be wired to the panel from the CLI (#5010).
- Derive the displayed Access URL from the certificate's actual SAN
  list instead of the cert folder name, list the other covered names,
  and show the panel's custom-path certificate in "Show Existing
  Domains" (#5070). Also silence find when /root/cert doesn't exist.
This commit is contained in:
MHSanaei
2026-06-12 01:22:30 +02:00
parent 1a525b4cb4
commit dbee150b33
3 changed files with 89 additions and 10 deletions

View File

@ -56,6 +56,18 @@ is_domain() {
[[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+(xn--[a-z0-9]{2,}|[A-Za-z]{2,})$ ]] && return 0 || return 1
}
# acme.sh's standalone server binds IPv4 by default; --listen-v6 makes it
# v6-only, which breaks HTTP-01 validation when the domain's A record points
# at this host's IPv4 (#4994). Only force IPv6 when the host has no global
# IPv4 address at all.
acme_listen_flag() {
if ip -4 addr show scope global 2> /dev/null | grep -q "inet "; then
echo ""
else
echo "--listen-v6"
fi
}
# Port helpers
is_port_in_use() {
local port="$1"
@ -292,7 +304,7 @@ setup_ssl_certificate() {
echo -e "${yellow}Note: Port 80 must be open and accessible from the internet${plain}"
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force > /dev/null 2>&1
~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport 80 --force
~/.acme.sh/acme.sh --issue -d ${domain} $(acme_listen_flag) --standalone --httpport 80 --force
if [ $? -ne 0 ]; then
echo -e "${yellow}Failed to issue certificate for ${domain}${plain}"
@ -576,7 +588,7 @@ ssl_cert_issue() {
if [[ ${cert_exists} -eq 0 ]]; then
# issue the certificate
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force
~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport ${WebPort} --force
~/.acme.sh/acme.sh --issue -d ${domain} $(acme_listen_flag) --standalone --httpport ${WebPort} --force
if [ $? -ne 0 ]; then
echo -e "${red}Issuing certificate failed, please check logs.${plain}"
rm -rf ~/.acme.sh/${domain} ~/.acme.sh/${domain}_ecc

View File

@ -81,6 +81,18 @@ is_domain() {
[[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+(xn--[a-z0-9]{2,}|[A-Za-z]{2,})$ ]] && return 0 || return 1
}
# acme.sh's standalone server binds IPv4 by default; --listen-v6 makes it
# v6-only, which breaks HTTP-01 validation when the domain's A record points
# at this host's IPv4 (#4994). Only force IPv6 when the host has no global
# IPv4 address at all.
acme_listen_flag() {
if ip -4 addr show scope global 2> /dev/null | grep -q "inet "; then
echo ""
else
echo "--listen-v6"
fi
}
# Port helpers
is_port_in_use() {
local port="$1"
@ -200,7 +212,7 @@ setup_ssl_certificate() {
echo -e "${yellow}Note: Port 80 must be open and accessible from the internet${plain}"
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force > /dev/null 2>&1
~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport 80 --force
~/.acme.sh/acme.sh --issue -d ${domain} $(acme_listen_flag) --standalone --httpport 80 --force
if [ $? -ne 0 ]; then
echo -e "${yellow}Failed to issue certificate for ${domain}${plain}"
@ -465,7 +477,7 @@ ssl_cert_issue() {
if [[ ${cert_exists} -eq 0 ]]; then
# issue the certificate
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force
~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport ${WebPort} --force
~/.acme.sh/acme.sh --issue -d ${domain} $(acme_listen_flag) --standalone --httpport ${WebPort} --force
if [ $? -ne 0 ]; then
echo -e "${red}Issuing certificate failed, please check logs.${plain}"
rm -rf ~/.acme.sh/${domain}

67
x-ui.sh
View File

@ -50,6 +50,18 @@ is_domain() {
[[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+(xn--[a-z0-9]{2,}|[A-Za-z]{2,})$ ]] && return 0 || return 1
}
# acme.sh's standalone server binds IPv4 by default; --listen-v6 makes it
# v6-only, which breaks HTTP-01 validation when the domain's A record points
# at this host's IPv4 (#4994). Only force IPv6 when the host has no global
# IPv4 address at all.
acme_listen_flag() {
if ip -4 addr show scope global 2> /dev/null | grep -q "inet "; then
echo ""
else
echo "--listen-v6"
fi
}
# check root
[[ $EUID -ne 0 ]] && LOGE "ERROR: You must be root to run this script! \n" && exit 1
@ -361,12 +373,26 @@ check_config() {
if [[ -n "$existing_cert" ]]; then
local domain=$(basename "$(dirname "$existing_cert")")
# The cert folder name is only the certificate's first domain. A
# multidomain (SAN) certificate may be served under any name it covers,
# so read the real names from the certificate itself (#5070).
local cert_sans=""
if [[ -f "$existing_cert" ]] && command -v openssl > /dev/null 2>&1; then
cert_sans=$(openssl x509 -in "$existing_cert" -noout -ext subjectAltName 2> /dev/null \
| grep -Eo 'DNS:[^,[:space:]]+' | cut -d: -f2)
if [[ -n "$cert_sans" ]] && ! echo "$cert_sans" | grep -qx "$domain"; then
domain=$(echo "$cert_sans" | head -n1)
fi
fi
if [[ "$domain" =~ ^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
echo -e "${green}Access URL: https://${domain}:${existing_port}${existing_webBasePath}${plain}"
else
echo -e "${green}Access URL: https://${server_ip}:${existing_port}${existing_webBasePath}${plain}"
fi
if [[ -n "$cert_sans" && $(echo "$cert_sans" | wc -l) -gt 1 ]]; then
echo -e "${yellow}The certificate also covers:${plain} $(echo "$cert_sans" | grep -vx "$domain" | tr '\n' ' ')"
fi
else
echo -e "${red}⚠ WARNING: No SSL certificate configured!${plain}"
echo -e "${yellow}You can get a Let's Encrypt certificate for your IP address (valid ~6 days, auto-renews).${plain}"
@ -1231,7 +1257,7 @@ ssl_cert_issue_main() {
ssl_cert_issue_main
;;
2)
local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \;)
local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \; 2> /dev/null)
if [ -z "$domains" ]; then
echo "No certificates found to revoke."
else
@ -1272,7 +1298,7 @@ ssl_cert_issue_main() {
ssl_cert_issue_main
;;
3)
local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \;)
local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \; 2> /dev/null)
if [ -z "$domains" ]; then
echo "No certificates found to renew."
else
@ -1289,9 +1315,9 @@ ssl_cert_issue_main() {
ssl_cert_issue_main
;;
4)
local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \;)
local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \; 2> /dev/null)
if [ -z "$domains" ]; then
echo "No certificates found."
echo "No certificates found under /root/cert."
else
echo "Existing domains and their paths:"
for domain in $domains; do
@ -1306,10 +1332,39 @@ ssl_cert_issue_main() {
fi
done
fi
# The panel's configured certificate may live outside /root/cert
# (e.g. certbot under /etc/letsencrypt) — show it too (#5070).
local panel_cert=$(${xui_folder}/x-ui setting -getCert true | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]')
if [[ -n "${panel_cert}" && "${panel_cert}" != /root/cert/* ]]; then
echo -e "Panel certificate (custom path): ${panel_cert}"
if [[ -f "${panel_cert}" ]] && command -v openssl > /dev/null 2>&1; then
local panel_sans=$(openssl x509 -in "${panel_cert}" -noout -ext subjectAltName 2> /dev/null \
| grep -Eo 'DNS:[^,[:space:]]+' | cut -d: -f2 | tr '\n' ' ')
[[ -n "${panel_sans}" ]] && echo -e "\tCovers: ${panel_sans}"
fi
fi
ssl_cert_issue_main
;;
5)
local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \;)
echo -e "${green}\t1.${plain} Use a certificate from /root/cert"
echo -e "${green}\t2.${plain} Enter custom certificate file paths (e.g. certbot, /etc/letsencrypt/...)"
read -rp "Choose an option: " pathChoice
if [[ "$pathChoice" == "2" ]]; then
read -rp "Certificate file path (fullchain): " webCertFile
read -rp "Private key file path: " webKeyFile
if [[ -f "${webCertFile}" && -f "${webKeyFile}" ]]; then
${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile"
echo "Panel certificate paths set:"
echo " - Certificate File: $webCertFile"
echo " - Private Key File: $webKeyFile"
restart
else
echo "Certificate or private key file not found."
fi
ssl_cert_issue_main
return
fi
local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \; 2> /dev/null)
if [ -z "$domains" ]; then
echo "No certificates found."
else
@ -1691,7 +1746,7 @@ ssl_cert_issue() {
if [[ ${cert_exists} -eq 0 ]]; then
# issue the certificate
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force
~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport ${WebPort} --force
~/.acme.sh/acme.sh --issue -d ${domain} $(acme_listen_flag) --standalone --httpport ${WebPort} --force
if [ $? -ne 0 ]; then
LOGE "Issuing certificate failed, please check logs."
rm -rf ~/.acme.sh/${domain} ~/.acme.sh/${domain}_ecc