mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-28 00:24:19 +00:00
7c2598fae9
* feat(install): add non-interactive install path for cloud/golden-image use Trigger non-interactive mode when XUI_NONINTERACTIVE=1 or stdin is not a TTY (curl | bash, cloud-init). Every prompt is then replaced by an env var or a sane default; interactive prompts stay byte-for-byte identical. Honored env vars: XUI_USERNAME, XUI_PASSWORD, XUI_PANEL_PORT, XUI_WEB_BASE_PATH (unset => random, as before), XUI_SSL_MODE=none|ip|domain (default none), XUI_DOMAIN, XUI_ACME_EMAIL, XUI_DB_TYPE/XUI_DB_DSN, plus additive XUI_ACME_HTTP_PORT, XUI_SSL_IPV6, XUI_SERVER_IP. On success, write /etc/x-ui/install-result.env (mode 600) with the panel creds + access URL + api token, in both interactive and non-interactive modes, so cloud-init/MOTD can surface them. Postgres in non-interactive mode requires XUI_DB_DSN or installs locally; never silently downgrades. * feat(deploy): add first-boot per-instance credential generation Golden images ship with no x-ui.db. x-ui-firstboot.sh runs once (guarded by /etc/x-ui/.firstboot-done), before x-ui.service, and replaces the seeded admin/admin with fresh random username/password on a random high port, regenerates the session secret/panel GUID via 'x-ui setting -reset', mints an API token, and writes the creds to /etc/x-ui/credentials.txt (600) + /etc/motd. Idempotent: skips regeneration if a non-default admin already exists. The oneshot unit is ordered After=network-online/cloud-init and Before=x-ui.service so the panel never serves default credentials. * chore(deploy): force LF for cloud-image deploy assets (.service/.hcl/.yaml) * feat(deploy): add Packer config + provisioning scripts for golden image One build, two sources: amazon-ebs (AWS AMI, Canonical Ubuntu 24.04 base via source_ami_filter) and qemu (qcow2 + raw, NoCloud-seeded for build-time SSH). Provisioner order is fixed: provision.sh -> harden.sh -> cleanup.sh. - provision.sh: downloads the released x-ui tarball (no Go build), installs the panel + firstboot unit, enables but does NOT start services, creates NO DB. - harden.sh: key-only SSH, no root password login, locks default account passwords, enables unattended-upgrades (scanner-compliant). - cleanup.sh: wipes any DB/creds, SSH host keys, authorized_keys, machine-id, cloud-init state, logs and history; fails the build if any secret survives. packer fmt -check clean; packer validate passes for both sources. * feat(deploy): add generic cloud-init user-data for unattended install cloud-init.yaml installs the latest 3x-ui non-interactively (XUI_NONINTERACTIVE=1) on any cloud-init platform, generating unique per-instance credentials and surfacing them via /etc/x-ui/install-result.env, serial console and MOTD. README documents per-provider usage (Hetzner/AWS/DO/Vultr/GCP/Azure/Oracle) and all XUI_* knobs. * ci: add image.yml to build cloud images on release On release: published (or workflow_dispatch with a tag), waits for the x-ui-linux-amd64.tar.gz asset (handles the release-matrix upload race), then: - qemu-image (always): builds the qcow2 with Packer and attaches a compressed .qcow2.xz + sha256 to the GitHub release. Uses KVM when /dev/kvm exists, else TCG. - ami-image (gated): builds the AWS AMI only when AWS creds exist (OIDC role preferred, else access keys), so forks skip cleanly. Prints the AMI ID to the job summary. No secrets or AMI IDs are committed. * test(deploy): add container smoke tests for install + firstboot smoke-noninteractive.sh: runs install.sh piped (no TTY) with XUI_NONINTERACTIVE=1 in an Ubuntu container; asserts install-result.env (600) holds random non-default creds, hasDefaultCredential is false, and the panel serves HTTP. smoke-firstboot.sh: installs the released binary with no DB, runs x-ui-firstboot.sh; asserts per-instance creds + credentials.txt (600) + MOTD, no admin/admin, and that a second run is a no-op (sentinel honored). smoke.yml runs both as gated jobs on PRs/pushes touching install.sh or deploy/**. Both pass locally against the v3.3.1 release binary. * docs(deploy): add Packer/marketplace docs and link from README - deploy/README.md: index of the cloud-deploy tooling and the two models - deploy/packer/README.md: how to build locally, variables, first-boot behavior - deploy/marketplace/aws/README.md: seller registration -> AMI scan -> limited-visibility preview -> go-public checklist - deploy/marketplace/hetzner/README.md: cloud-init-first guidance + snapshot caveat (delete x-ui.db first) + hetznercloud/apps reference - README.md: link the unattended-install / cloud-image docs from Quick Start * feat(deploy): build golden images for arm64 as well as amd64 The install path was already multi-arch (install.sh auto-detects arch); this extends the golden image + CI to arm64: - packer: xui_arch (amd64|arm64, validated) now derives the base AMI filter and the Ubuntu cloud image; the qemu source switches to qemu-system-aarch64 + virt machine + AAVMF UEFI firmware for arm64. amd64 path unchanged. - image.yml: arch matrix. AMIs for amd64 (t3.small) + arm64 (t4g.small/Graviton) from one runner; qcow2 for amd64 on a standard runner and arm64 on a native ubuntu-24.04-arm runner. Waits for both release tarballs. - smoke.yml: run install + firstboot smoke tests on amd64 and arm64 runners; smoke-firstboot.sh now resolves the arch tarball via dpkg. - docs updated for both arches. packer fmt/validate pass for amd64 and arm64; actionlint + shellcheck clean. Verified locally: non-interactive install AND firstboot run on the real arm64 release binary under emulation (ELF aarch64, no admin/admin). * chore(deploy): default AWS region to eu-central-1 (Frankfurt) Replace the us-east-1 fallback in image.yml (4 sites) and the Packer 'region' default + doc examples. Still overridable via the AWS_REGION repo variable / the -var 'region=...' flag. * feat(deploy): add Amazon Lightsail support (launch script + snapshot builder) Lightsail can't launch from an EC2 AMI and its blueprint list isn't self-publishable, so add the two self-service paths instead: - launch-script.sh: paste into Lightsail 'Add launch script' (or --user-data) to install 3x-ui non-interactively with unique per-instance credentials. - snapshot-userdata.sh + build-snapshot.sh: AWS CLI pipeline that provisions a build instance (panel installed, NO DB, firstboot enabled), runs the shared cleanup.sh, then snapshots it. Instances launched from the snapshot mint their own credentials on first boot. Optional --panel-port pins a known port for the Lightsail firewall. - README documents both paths, the firewall caveat, and the blueprint reality. EC2 AMI / Marketplace path kept untouched alongside. All scripts shellcheck-clean. * fix(deploy): address Copilot PR review findings - install.sh + firstboot: write install-result.env / credentials.txt values with printf %q so the files stay safe to source even if creds are pinned with shell metacharacters (no-op for the alphanumeric random defaults). - firstboot: fail closed if 'x-ui setting -show' can't be parsed to true/false — exit without writing the sentinel so the next boot retries, instead of silently skipping regeneration and risking admin/admin. - firstboot + cloud-init + lightsail launch-script: keep secrets out of the world-readable /etc/motd (show URL + username only; full creds via the mode-600 file / serial console). - lightsail build-snapshot: handle download-default-key-pair returning either a PEM or base64, and assert a valid PEM before using it for SSH. - image.yml: pin hashicorp/setup-packer@v3 (was @main). - deploy/README: document XUI_ACME_HTTP_PORT / XUI_SSL_IPV6 / XUI_SERVER_IP. Both container smoke tests still pass; shellcheck + actionlint clean.
167 lines
6.3 KiB
Bash
167 lines
6.3 KiB
Bash
#!/usr/bin/env bash
|
|
#
|
|
# x-ui-firstboot.sh — generate per-instance 3x-ui panel credentials on first boot.
|
|
#
|
|
# A golden image (AMI / qcow2) MUST ship without an initialized x-ui.db: the
|
|
# panel seeds a hardcoded admin/admin user and generates its session secret +
|
|
# panel GUID on first start, so a baked DB would make every clone share the same
|
|
# credentials and secret. This script runs ONCE, before x-ui.service starts, and
|
|
# replaces the default admin with fresh random credentials on a random high port.
|
|
#
|
|
# Idempotent: a sentinel file guards against re-running. If a non-default admin
|
|
# already exists (operator pre-configured the box), regeneration is skipped.
|
|
#
|
|
# Wired up by deploy/packer/scripts/provision.sh; ordered Before=x-ui.service.
|
|
|
|
set -u
|
|
|
|
SENTINEL="/etc/x-ui/.firstboot-done"
|
|
CRED_FILE="/etc/x-ui/credentials.txt"
|
|
MOTD_FILE="/etc/motd"
|
|
XUI_DIR="${XUI_MAIN_FOLDER:-/usr/local/x-ui}"
|
|
XUI_BIN="${XUI_DIR}/x-ui"
|
|
|
|
log() { echo "[x-ui-firstboot] $*"; }
|
|
|
|
# Already provisioned — nothing to do (idempotent on re-run / re-image).
|
|
if [ -f "$SENTINEL" ]; then
|
|
log "sentinel $SENTINEL present; skipping."
|
|
exit 0
|
|
fi
|
|
|
|
if [ ! -x "$XUI_BIN" ]; then
|
|
log "ERROR: x-ui binary not found at $XUI_BIN"
|
|
exit 1
|
|
fi
|
|
|
|
# Inherit DB configuration (sqlite default; postgres via XUI_DB_TYPE/XUI_DB_DSN)
|
|
# from the same env files the systemd unit loads, so the binary talks to the
|
|
# same database the panel will use.
|
|
for ef in /etc/default/x-ui /etc/conf.d/x-ui /etc/sysconfig/x-ui; do
|
|
if [ -r "$ef" ]; then
|
|
set -a
|
|
# shellcheck disable=SC1090
|
|
. "$ef"
|
|
set +a
|
|
fi
|
|
done
|
|
|
|
install -d -m 755 /etc/x-ui 2> /dev/null || true
|
|
|
|
# Defense-in-depth: make sure the panel is not running while we mutate the DB.
|
|
if command -v systemctl > /dev/null 2>&1; then
|
|
systemctl stop x-ui > /dev/null 2>&1 || true
|
|
fi
|
|
|
|
gen_random_string() {
|
|
local length="$1"
|
|
openssl rand -base64 $((length * 2)) | tr -dc 'a-zA-Z0-9' | head -c "$length"
|
|
}
|
|
|
|
# Best-effort public IPv4 for the displayed access URL (cosmetic only — the
|
|
# panel binds 0.0.0.0). Falls back to the primary local IP, then a placeholder.
|
|
detect_ip() {
|
|
local ip=""
|
|
local url
|
|
for url in https://api4.ipify.org https://ipv4.icanhazip.com https://4.ident.me; do
|
|
ip=$(curl -fsS4 --max-time 3 "$url" 2> /dev/null | tr -d '[:space:]')
|
|
if [[ "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
echo "$ip"
|
|
return 0
|
|
fi
|
|
done
|
|
ip=$(hostname -I 2> /dev/null | awk '{print $1}')
|
|
if [ -n "$ip" ]; then
|
|
echo "$ip"
|
|
return 0
|
|
fi
|
|
echo "<server-ip>"
|
|
}
|
|
|
|
# Detect whether the seeded admin/admin default is still in place.
|
|
default_creds=$("$XUI_BIN" setting -show true 2> /dev/null | grep -Eo 'hasDefaultCredential: .+' | awk '{print $2}')
|
|
|
|
# The parse MUST yield exactly "true" or "false". If the command failed or its
|
|
# output format changed, refuse to proceed: do NOT write the sentinel, so the
|
|
# next boot retries instead of silently leaving admin/admin in place.
|
|
if [ "$default_creds" != "true" ] && [ "$default_creds" != "false" ]; then
|
|
log "ERROR: could not determine credential state (hasDefaultCredential='${default_creds}'); not writing sentinel, will retry next boot."
|
|
exit 1
|
|
fi
|
|
|
|
if [ "$default_creds" = "false" ]; then
|
|
log "non-default admin already configured; skipping credential regeneration."
|
|
{
|
|
echo "3x-ui first-boot: a non-default admin account already exists on this"
|
|
echo "instance, so credentials were left unchanged."
|
|
} > "$MOTD_FILE" 2> /dev/null || true
|
|
: > "$SENTINEL" 2> /dev/null || true
|
|
chmod 600 "$SENTINEL" 2> /dev/null || true
|
|
exit 0
|
|
fi
|
|
|
|
log "generating per-instance credentials..."
|
|
|
|
NEW_USER="${XUI_USERNAME:-$(gen_random_string 10)}"
|
|
NEW_PASS="${XUI_PASSWORD:-$(gen_random_string 16)}"
|
|
NEW_PATH="${XUI_WEB_BASE_PATH:-$(gen_random_string 18)}"
|
|
NEW_PORT="${XUI_PANEL_PORT:-$(shuf -i 1024-62000 -n 1)}"
|
|
|
|
# Clean settings slate: drops any baked port/webBasePath and forces the panel
|
|
# to regenerate its session secret + panel GUID on next start (per-instance).
|
|
"$XUI_BIN" setting -reset > /dev/null 2>&1 || true
|
|
|
|
# Apply fresh random identity. UpdateFirstUser renames the seeded admin row and
|
|
# rehashes the password, so admin/admin no longer exists after this call.
|
|
if ! "$XUI_BIN" setting -username "$NEW_USER" -password "$NEW_PASS" -port "$NEW_PORT" -webBasePath "$NEW_PATH" > /dev/null 2>&1; then
|
|
log "ERROR: failed to apply new panel settings."
|
|
exit 1
|
|
fi
|
|
|
|
API_TOKEN=$("$XUI_BIN" setting -getApiToken true 2> /dev/null | grep -Eo 'apiToken: .+' | awk '{print $2}')
|
|
SERVER_IP=$(detect_ip)
|
|
ACCESS_URL="http://${SERVER_IP}:${NEW_PORT}/${NEW_PATH}"
|
|
|
|
# Persist credentials for the operator (root-only). Values are shell-escaped
|
|
# with %q so the file stays safe to `source` even if a value contains shell
|
|
# metacharacters (the smoke test and operators source this file).
|
|
umask 077
|
|
{
|
|
echo "# 3x-ui per-instance credentials (generated on first boot)"
|
|
printf 'XUI_USERNAME=%q\n' "$NEW_USER"
|
|
printf 'XUI_PASSWORD=%q\n' "$NEW_PASS"
|
|
printf 'XUI_PANEL_PORT=%q\n' "$NEW_PORT"
|
|
printf 'XUI_WEB_BASE_PATH=%q\n' "$NEW_PATH"
|
|
printf 'XUI_ACCESS_URL=%q\n' "$ACCESS_URL"
|
|
printf 'XUI_API_TOKEN=%q\n' "$API_TOKEN"
|
|
} > "$CRED_FILE"
|
|
chmod 600 "$CRED_FILE" 2> /dev/null || true
|
|
|
|
# Friendly login banner shown on SSH / console before the panel is reachable.
|
|
# /etc/motd is world-readable, so it MUST NOT contain the password or API token;
|
|
# those secrets live only in ${CRED_FILE} (mode 600). Show non-secret info only.
|
|
cat > "$MOTD_FILE" 2> /dev/null << EOF
|
|
|
|
========================================================================
|
|
3x-ui panel — per-instance credentials (generated on first boot)
|
|
========================================================================
|
|
Access URL : ${ACCESS_URL}
|
|
Username : ${NEW_USER}
|
|
|
|
The password and API token are NOT shown here (this banner is
|
|
world-readable). Read them as root with:
|
|
sudo cat ${CRED_FILE}
|
|
|
|
Change the password after login. If no public IP is shown above,
|
|
replace <server-ip> with the address you reach this server on.
|
|
========================================================================
|
|
|
|
EOF
|
|
|
|
# Mark complete so we never regenerate on subsequent boots.
|
|
: > "$SENTINEL" 2> /dev/null || true
|
|
chmod 600 "$SENTINEL" 2> /dev/null || true
|
|
|
|
log "done. Panel will start on port ${NEW_PORT} with a unique admin account."
|
|
exit 0
|