x-ui-firstboot.sh 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. #!/usr/bin/env bash
  2. #
  3. # x-ui-firstboot.sh — generate per-instance 3x-ui panel credentials on first boot.
  4. #
  5. # A golden image (AMI / qcow2) MUST ship without an initialized x-ui.db: the
  6. # panel seeds a hardcoded admin/admin user and generates its session secret +
  7. # panel GUID on first start, so a baked DB would make every clone share the same
  8. # credentials and secret. This script runs ONCE, before x-ui.service starts, and
  9. # replaces the default admin with fresh random credentials on a random high port.
  10. #
  11. # Idempotent: a sentinel file guards against re-running. If a non-default admin
  12. # already exists (operator pre-configured the box), regeneration is skipped.
  13. #
  14. # Wired up by deploy/packer/scripts/provision.sh; ordered Before=x-ui.service.
  15. set -u
  16. SENTINEL="/etc/x-ui/.firstboot-done"
  17. CRED_FILE="/etc/x-ui/credentials.txt"
  18. MOTD_FILE="/etc/motd"
  19. XUI_DIR="${XUI_MAIN_FOLDER:-/usr/local/x-ui}"
  20. XUI_BIN="${XUI_DIR}/x-ui"
  21. log() { echo "[x-ui-firstboot] $*"; }
  22. # Already provisioned — nothing to do (idempotent on re-run / re-image).
  23. if [ -f "$SENTINEL" ]; then
  24. log "sentinel $SENTINEL present; skipping."
  25. exit 0
  26. fi
  27. if [ ! -x "$XUI_BIN" ]; then
  28. log "ERROR: x-ui binary not found at $XUI_BIN"
  29. exit 1
  30. fi
  31. # Inherit DB configuration (sqlite default; postgres via XUI_DB_TYPE/XUI_DB_DSN)
  32. # from the same env files the systemd unit loads, so the binary talks to the
  33. # same database the panel will use.
  34. for ef in /etc/default/x-ui /etc/conf.d/x-ui /etc/sysconfig/x-ui; do
  35. if [ -r "$ef" ]; then
  36. set -a
  37. # shellcheck disable=SC1090
  38. . "$ef"
  39. set +a
  40. fi
  41. done
  42. install -d -m 755 /etc/x-ui 2> /dev/null || true
  43. # Defense-in-depth: make sure the panel is not running while we mutate the DB.
  44. if command -v systemctl > /dev/null 2>&1; then
  45. systemctl stop x-ui > /dev/null 2>&1 || true
  46. fi
  47. gen_random_string() {
  48. local length="$1"
  49. openssl rand -base64 $((length * 2)) | tr -dc 'a-zA-Z0-9' | head -c "$length"
  50. }
  51. # Best-effort public IPv4 for the displayed access URL (cosmetic only — the
  52. # panel binds 0.0.0.0). Falls back to the primary local IP, then a placeholder.
  53. detect_ip() {
  54. local ip=""
  55. local url
  56. for url in https://api4.ipify.org https://ipv4.icanhazip.com https://4.ident.me; do
  57. ip=$(curl -fsS4 --max-time 3 "$url" 2> /dev/null | tr -d '[:space:]')
  58. if [[ "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
  59. echo "$ip"
  60. return 0
  61. fi
  62. done
  63. ip=$(hostname -I 2> /dev/null | awk '{print $1}')
  64. if [ -n "$ip" ]; then
  65. echo "$ip"
  66. return 0
  67. fi
  68. echo "<server-ip>"
  69. }
  70. # Detect whether the seeded admin/admin default is still in place.
  71. default_creds=$("$XUI_BIN" setting -show true 2> /dev/null | grep -Eo 'hasDefaultCredential: .+' | awk '{print $2}')
  72. # The parse MUST yield exactly "true" or "false". If the command failed or its
  73. # output format changed, refuse to proceed: do NOT write the sentinel, so the
  74. # next boot retries instead of silently leaving admin/admin in place.
  75. if [ "$default_creds" != "true" ] && [ "$default_creds" != "false" ]; then
  76. log "ERROR: could not determine credential state (hasDefaultCredential='${default_creds}'); not writing sentinel, will retry next boot."
  77. exit 1
  78. fi
  79. if [ "$default_creds" = "false" ]; then
  80. log "non-default admin already configured; skipping credential regeneration."
  81. {
  82. echo "3x-ui first-boot: a non-default admin account already exists on this"
  83. echo "instance, so credentials were left unchanged."
  84. } > "$MOTD_FILE" 2> /dev/null || true
  85. : > "$SENTINEL" 2> /dev/null || true
  86. chmod 600 "$SENTINEL" 2> /dev/null || true
  87. exit 0
  88. fi
  89. log "generating per-instance credentials..."
  90. NEW_USER="${XUI_USERNAME:-$(gen_random_string 10)}"
  91. NEW_PASS="${XUI_PASSWORD:-$(gen_random_string 16)}"
  92. NEW_PATH="${XUI_WEB_BASE_PATH:-$(gen_random_string 18)}"
  93. NEW_PORT="${XUI_PANEL_PORT:-$(shuf -i 1024-62000 -n 1)}"
  94. # Clean settings slate: drops any baked port/webBasePath and forces the panel
  95. # to regenerate its session secret + panel GUID on next start (per-instance).
  96. "$XUI_BIN" setting -reset > /dev/null 2>&1 || true
  97. # Apply fresh random identity. UpdateFirstUser renames the seeded admin row and
  98. # rehashes the password, so admin/admin no longer exists after this call.
  99. if ! "$XUI_BIN" setting -username "$NEW_USER" -password "$NEW_PASS" -port "$NEW_PORT" -webBasePath "$NEW_PATH" > /dev/null 2>&1; then
  100. log "ERROR: failed to apply new panel settings."
  101. exit 1
  102. fi
  103. API_TOKEN=$("$XUI_BIN" setting -getApiToken true 2> /dev/null | grep -Eo 'apiToken: .+' | awk '{print $2}')
  104. SERVER_IP=$(detect_ip)
  105. ACCESS_URL="http://${SERVER_IP}:${NEW_PORT}/${NEW_PATH}"
  106. # Persist credentials for the operator (root-only). Values are shell-escaped
  107. # with %q so the file stays safe to `source` even if a value contains shell
  108. # metacharacters (the smoke test and operators source this file).
  109. umask 077
  110. {
  111. echo "# 3x-ui per-instance credentials (generated on first boot)"
  112. printf 'XUI_USERNAME=%q\n' "$NEW_USER"
  113. printf 'XUI_PASSWORD=%q\n' "$NEW_PASS"
  114. printf 'XUI_PANEL_PORT=%q\n' "$NEW_PORT"
  115. printf 'XUI_WEB_BASE_PATH=%q\n' "$NEW_PATH"
  116. printf 'XUI_ACCESS_URL=%q\n' "$ACCESS_URL"
  117. printf 'XUI_API_TOKEN=%q\n' "$API_TOKEN"
  118. } > "$CRED_FILE"
  119. chmod 600 "$CRED_FILE" 2> /dev/null || true
  120. # Friendly login banner shown on SSH / console before the panel is reachable.
  121. # /etc/motd is world-readable, so it MUST NOT contain the password or API token;
  122. # those secrets live only in ${CRED_FILE} (mode 600). Show non-secret info only.
  123. cat > "$MOTD_FILE" 2> /dev/null << EOF
  124. ========================================================================
  125. 3x-ui panel — per-instance credentials (generated on first boot)
  126. ========================================================================
  127. Access URL : ${ACCESS_URL}
  128. Username : ${NEW_USER}
  129. The password and API token are NOT shown here (this banner is
  130. world-readable). Read them as root with:
  131. sudo cat ${CRED_FILE}
  132. Change the password after login. If no public IP is shown above,
  133. replace <server-ip> with the address you reach this server on.
  134. ========================================================================
  135. EOF
  136. # Mark complete so we never regenerate on subsequent boots.
  137. : > "$SENTINEL" 2> /dev/null || true
  138. chmod 600 "$SENTINEL" 2> /dev/null || true
  139. log "done. Panel will start on port ${NEW_PORT} with a unique admin account."
  140. exit 0