From 30796dc2ceed52b75c7df693dc42d4b8b5e389e5 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Fri, 26 Jun 2026 00:35:34 +0200 Subject: [PATCH] chore(deploy): drop the AWS golden-image build stack Remove the release-driven Packer AMI/qcow2 pipeline and everything that existed only to feed it: the image.yml workflow, deploy/packer, deploy/lightsail, deploy/firstboot, the AWS Marketplace checklist, and the first-boot smoke test/job. Keep the cloud-agnostic unattended-install path (cloud-init + install.sh non-interactive) and the Hetzner notes, which never depended on the workflow. Hetzner's snapshot path is dropped too since it relied on firstboot to avoid admin/admin on clones; cloud-init regenerates per-instance credentials on its own. Update deploy/README, the cloud-init and Hetzner docs, the root README plus its six translations, and .gitattributes to match. --- .gitattributes | 5 +- .github/workflows/image.yml | 260 ------------------------ .github/workflows/smoke.yml | 16 +- README.ar_EG.md | 8 +- README.es_ES.md | 8 +- README.fa_IR.md | 8 +- README.md | 8 +- README.ru_RU.md | 8 +- README.tr_TR.md | 8 +- README.zh_CN.md | 8 +- deploy/README.md | 25 +-- deploy/cloud-init/README.md | 13 +- deploy/firstboot/x-ui-firstboot.service | 22 -- deploy/firstboot/x-ui-firstboot.sh | 166 --------------- deploy/lightsail/README.md | 94 --------- deploy/lightsail/build-snapshot.sh | 192 ----------------- deploy/lightsail/launch-script.sh | 51 ----- deploy/lightsail/snapshot-userdata.sh | 59 ------ deploy/marketplace/aws/README.md | 92 --------- deploy/marketplace/hetzner/README.md | 27 +-- deploy/packer/.gitignore | 7 - deploy/packer/README.md | 116 ----------- deploy/packer/scripts/cleanup.sh | 59 ------ deploy/packer/scripts/harden.sh | 39 ---- deploy/packer/scripts/provision.sh | 76 ------- deploy/packer/variables.pkr.hcl | 109 ---------- deploy/packer/x-ui.pkr.hcl | 160 --------------- deploy/test/smoke-firstboot.sh | 86 -------- 28 files changed, 40 insertions(+), 1690 deletions(-) delete mode 100644 .github/workflows/image.yml delete mode 100644 deploy/firstboot/x-ui-firstboot.service delete mode 100644 deploy/firstboot/x-ui-firstboot.sh delete mode 100644 deploy/lightsail/README.md delete mode 100644 deploy/lightsail/build-snapshot.sh delete mode 100644 deploy/lightsail/launch-script.sh delete mode 100644 deploy/lightsail/snapshot-userdata.sh delete mode 100644 deploy/marketplace/aws/README.md delete mode 100644 deploy/packer/.gitignore delete mode 100644 deploy/packer/README.md delete mode 100644 deploy/packer/scripts/cleanup.sh delete mode 100644 deploy/packer/scripts/harden.sh delete mode 100644 deploy/packer/scripts/provision.sh delete mode 100644 deploy/packer/variables.pkr.hcl delete mode 100644 deploy/packer/x-ui.pkr.hcl delete mode 100644 deploy/test/smoke-firstboot.sh diff --git a/.gitattributes b/.gitattributes index 6e3ff0631..699f1100e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,8 +5,5 @@ frontend/src/generated/** text eol=lf frontend/public/openapi.json text eol=lf frontend/src/test/__snapshots__/** text eol=lf -# Cloud-image deploy assets are consumed on Linux — force LF regardless of host. -*.service text eol=lf -deploy/**/*.service text eol=lf -deploy/**/*.hcl text eol=lf +# Cloud-init deploy assets are consumed on Linux — force LF regardless of host. deploy/**/*.yaml text eol=lf \ No newline at end of file diff --git a/.github/workflows/image.yml b/.github/workflows/image.yml deleted file mode 100644 index 0d9f6116c..000000000 --- a/.github/workflows/image.yml +++ /dev/null @@ -1,260 +0,0 @@ -name: Build Cloud Images - -# Build golden cloud images from a published release, for amd64 and arm64: -# * qemu -> qcow2 attached to the GitHub release (always) -# * amazon-ebs -> AWS AMI (only when AWS credentials are configured) -# -# Images contain NO database and NO baked credentials; first boot generates -# unique per-instance credentials (see deploy/firstboot + deploy/packer). - -on: - release: - types: [published] - workflow_dispatch: - inputs: - tag: - description: "Release tag to build images for (e.g. v3.3.1)" - required: true - type: string - -permissions: - contents: write - -concurrency: - group: image-${{ github.event.release.tag_name || inputs.tag }} - cancel-in-progress: false - -jobs: - # Resolve the tag and wait until BOTH arch tarballs are actually published - # (the release matrix uploads assets one by one, so 'published' can fire - # before the tarballs exist). - setup: - runs-on: ubuntu-latest - outputs: - tag: ${{ steps.resolve.outputs.tag }} - steps: - - name: Resolve tag - id: resolve - run: | - if [ "${{ github.event_name }}" = "release" ]; then - TAG="${{ github.event.release.tag_name }}" - else - TAG="${{ inputs.tag }}" - fi - [ -n "$TAG" ] || { echo "::error::no tag resolved"; exit 1; } - echo "tag=$TAG" >> "$GITHUB_OUTPUT" - - - name: Wait for released binary assets (amd64 + arm64) - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAG: ${{ steps.resolve.outputs.tag }} - run: | - want="x-ui-linux-amd64.tar.gz x-ui-linux-arm64.tar.gz" - for i in $(seq 1 30); do - names=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json assets -q '.assets[].name') - missing="" - for w in $want; do - echo "$names" | grep -qx "$w" || missing="$missing $w" - done - if [ -z "$missing" ]; then - echo "All assets present on $TAG" - exit 0 - fi - echo "Waiting for$missing on $TAG ($i/30)..." - sleep 20 - done - echo "::error::missing release assets on $TAG after 10 minutes:$missing" - exit 1 - - # Gate the AWS AMI build so forks without secrets skip it cleanly - # (secrets cannot be referenced directly in job-level `if`). - check-aws: - runs-on: ubuntu-latest - outputs: - enabled: ${{ steps.c.outputs.enabled }} - use_oidc: ${{ steps.c.outputs.use_oidc }} - steps: - - id: c - env: - ROLE: ${{ secrets.AWS_ROLE_ARN }} - KEY: ${{ secrets.AWS_ACCESS_KEY_ID }} - run: | - if [ -n "$ROLE" ]; then - echo "enabled=true" >> "$GITHUB_OUTPUT" - echo "use_oidc=true" >> "$GITHUB_OUTPUT" - elif [ -n "$KEY" ]; then - echo "enabled=true" >> "$GITHUB_OUTPUT" - echo "use_oidc=false" >> "$GITHUB_OUTPUT" - else - echo "enabled=false" >> "$GITHUB_OUTPUT" - echo "use_oidc=false" >> "$GITHUB_OUTPUT" - echo "::notice::No AWS credentials configured; skipping the AMI build." - fi - - qemu-image: - needs: setup - timeout-minutes: 90 - strategy: - fail-fast: false - matrix: - include: - - arch: amd64 - runner: ubuntu-latest - qemu_pkgs: qemu-system-x86 qemu-utils - - arch: arm64 - runner: ubuntu-24.04-arm - qemu_pkgs: qemu-system-arm qemu-efi-aarch64 qemu-utils - runs-on: ${{ matrix.runner }} - steps: - - name: Checkout - uses: actions/checkout@v7 - - - name: Install QEMU - run: | - sudo apt-get update - sudo apt-get install -y --no-install-recommends ${{ matrix.qemu_pkgs }} - - - name: Setup Packer - uses: hashicorp/setup-packer@v3 - with: - version: latest - - - name: Verify released binary asset - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAG: ${{ needs.setup.outputs.tag }} - run: | - mkdir -p _asset - gh release download "$TAG" --repo "$GITHUB_REPOSITORY" \ - --pattern "x-ui-linux-${{ matrix.arch }}.tar.gz" --dir _asset - ls -la _asset - - - name: Select accelerator - id: accel - run: | - if [ -e /dev/kvm ]; then echo "value=kvm" >> "$GITHUB_OUTPUT"; else echo "value=tcg" >> "$GITHUB_OUTPUT"; fi - - - name: Packer init - run: packer init deploy/packer/ - - - name: Build qcow2 image - env: - TAG: ${{ needs.setup.outputs.tag }} - ACCEL: ${{ steps.accel.outputs.value }} - run: | - packer build -only='qemu.x-ui' \ - -var "xui_version=${TAG}" \ - -var "xui_arch=${{ matrix.arch }}" \ - -var "qemu_accelerator=${ACCEL}" \ - deploy/packer/ - - - name: Compress qcow2 - id: pack - env: - TAG: ${{ needs.setup.outputs.tag }} - run: | - cd deploy/packer/output-qemu - src="3x-ui-ubuntu-24.04-${{ matrix.arch }}.qcow2" - out="3x-ui-ubuntu-24.04-${TAG}-${{ matrix.arch }}.qcow2.xz" - xz -T0 -6 -c "$src" > "$out" - sha256sum "$out" > "${out}.sha256" - echo "file=deploy/packer/output-qemu/${out}" >> "$GITHUB_OUTPUT" - echo "sha=deploy/packer/output-qemu/${out}.sha256" >> "$GITHUB_OUTPUT" - ls -la - - - name: Attach qcow2 to release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAG: ${{ needs.setup.outputs.tag }} - run: | - gh release upload "$TAG" --repo "$GITHUB_REPOSITORY" --clobber \ - "${{ steps.pack.outputs.file }}" "${{ steps.pack.outputs.sha }}" - - - name: Summary - env: - TAG: ${{ needs.setup.outputs.tag }} - ACCEL: ${{ steps.accel.outputs.value }} - run: | - { - echo "## QEMU image (${{ matrix.arch }})" - echo "- Tag: \`${TAG}\`" - echo "- Accelerator: \`${ACCEL}\`" - echo "- Attached: \`$(basename "${{ steps.pack.outputs.file }}")\`" - } >> "$GITHUB_STEP_SUMMARY" - - ami-image: - needs: [setup, check-aws] - if: needs.check-aws.outputs.enabled == 'true' - runs-on: ubuntu-latest - timeout-minutes: 60 - permissions: - contents: read - id-token: write - strategy: - fail-fast: false - matrix: - include: - - arch: amd64 - instance_type: t3.small - - arch: arm64 - instance_type: t4g.small - steps: - - name: Checkout - uses: actions/checkout@v7 - - - name: Setup Packer - uses: hashicorp/setup-packer@v3 - with: - version: latest - - - name: Configure AWS credentials (OIDC) - if: needs.check-aws.outputs.use_oidc == 'true' - uses: aws-actions/configure-aws-credentials@v6 - with: - role-to-assume: ${{ secrets.AWS_ROLE_ARN }} - aws-region: ${{ vars.AWS_REGION || 'eu-central-1' }} - - - name: Configure AWS credentials (access keys) - if: needs.check-aws.outputs.use_oidc != 'true' - uses: aws-actions/configure-aws-credentials@v6 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ${{ vars.AWS_REGION || 'eu-central-1' }} - - - name: Verify released binary asset - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAG: ${{ needs.setup.outputs.tag }} - run: | - mkdir -p _asset - gh release download "$TAG" --repo "$GITHUB_REPOSITORY" \ - --pattern "x-ui-linux-${{ matrix.arch }}.tar.gz" --dir _asset - ls -la _asset - - - name: Packer init - run: packer init deploy/packer/ - - - name: Build AMI - env: - TAG: ${{ needs.setup.outputs.tag }} - REGION: ${{ vars.AWS_REGION || 'eu-central-1' }} - run: | - packer build -only='amazon-ebs.x-ui' \ - -var "xui_version=${TAG}" \ - -var "xui_arch=${{ matrix.arch }}" \ - -var "instance_type=${{ matrix.instance_type }}" \ - -var "region=${REGION}" \ - deploy/packer/ - - - name: Publish AMI id to summary - env: - REGION: ${{ vars.AWS_REGION || 'eu-central-1' }} - run: | - AMI_ID=$(jq -r '.builds[] | select(.builder_type=="amazon-ebs") | .artifact_id' packer-manifest.json | tail -1 | cut -d: -f2) - { - echo "## AWS AMI (${{ matrix.arch }})" - echo "- Region: \`${REGION}\`" - echo "- Instance type: \`${{ matrix.instance_type }}\`" - echo "- AMI ID: \`${AMI_ID}\`" - } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 8478cdc21..c2a9e5b7b 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -1,7 +1,7 @@ name: Deploy Smoke Tests -# Container smoke tests for the unattended install path and first-boot -# credential generation. Runs only when the install/deploy assets change. +# Container smoke test for the unattended (cloud-init) install path. +# Runs only when the install/deploy assets change. on: push: @@ -30,15 +30,3 @@ jobs: - uses: actions/checkout@v7 - name: Non-interactive install smoke test run: bash deploy/test/smoke-noninteractive.sh - - first-boot: - strategy: - fail-fast: false - matrix: - runner: [ubuntu-latest, ubuntu-24.04-arm] - runs-on: ${{ matrix.runner }} - timeout-minutes: 15 - steps: - - uses: actions/checkout@v7 - - name: First-boot credential smoke test - run: bash deploy/test/smoke-firstboot.sh diff --git a/README.ar_EG.md b/README.ar_EG.md index df8703ead..374660f06 100644 --- a/README.ar_EG.md +++ b/README.ar_EG.md @@ -89,17 +89,15 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install. للحصول على الوثائق الكاملة، يرجى زيارة [ويكي المشروع](https://github.com/MHSanaei/3x-ui/wiki). -### التثبيت غير التفاعلي وصور السحابة +### التثبيت غير التفاعلي -يعمل المثبِّت أيضًا **بشكل غير تفاعلي** لـ cloud-init والصور الجاهزة (golden images). +يعمل المثبِّت أيضًا **بشكل غير تفاعلي** لـ cloud-init. عيّن `XUI_NONINTERACTIVE=1` (أو مرّره عبر أنبوب دون TTY) وسيتولى التثبيت من البداية إلى النهاية دون أي مطالبات، مُنشئًا بيانات اعتماد عشوائية وكاتبًا إياها في `/etc/x-ui/install-result.env`. راجع [`deploy/`](deploy/) لـ: - [بيانات مستخدم cloud-init](deploy/cloud-init/) — تثبيت غير تفاعلي على أي سحابة (Hetzner/AWS/DO/Vultr/GCP/Azure/Oracle) -- [صورة Packer الجاهزة](deploy/packer/) — بناء صورة AWS EC2 AMI و qcow2 (amd64/arm64) مع بيانات اعتماد لكل نسخة يتم إنشاؤها عند الإقلاع الأول -- [Amazon Lightsail](deploy/lightsail/) — سكربت إطلاق وأداة بناء لقطات قابلة لإعادة الاستخدام -- [قائمة تحقق AWS Marketplace](deploy/marketplace/aws/) +- [ملاحظات Hetzner Cloud](deploy/marketplace/hetzner/) — نشر يعتمد على cloud-init على Hetzner ## المنصات المدعومة diff --git a/README.es_ES.md b/README.es_ES.md index 4a4a10b36..ac8c3bc61 100644 --- a/README.es_ES.md +++ b/README.es_ES.md @@ -89,17 +89,15 @@ Durante la instalación se generan un nombre de usuario, una contraseña y una r Para la documentación completa, visita la [Wiki del proyecto](https://github.com/MHSanaei/3x-ui/wiki). -### Instalación desatendida e imágenes de nube +### Instalación desatendida -El instalador también se ejecuta de forma **no interactiva** para cloud-init e imágenes doradas (golden images). +El instalador también se ejecuta de forma **no interactiva** para cloud-init. Define `XUI_NONINTERACTIVE=1` (o canalízalo sin TTY) y realizará la instalación de principio a fin sin ninguna pregunta, generando credenciales aleatorias y escribiéndolas en `/etc/x-ui/install-result.env`. Consulta [`deploy/`](deploy/) para: - [User-data de cloud-init](deploy/cloud-init/) — instalación desatendida en cualquier nube (Hetzner/AWS/DO/Vultr/GCP/Azure/Oracle) -- [Imagen dorada de Packer](deploy/packer/) — crea una AMI de AWS EC2 + qcow2 (amd64/arm64) con credenciales por instancia generadas en el primer arranque -- [Amazon Lightsail](deploy/lightsail/) — script de lanzamiento + constructor de snapshots reutilizable -- [Lista de verificación de AWS Marketplace](deploy/marketplace/aws/) +- [Notas de Hetzner Cloud](deploy/marketplace/hetzner/) — despliegue basado en cloud-init en Hetzner ## Plataformas Compatibles diff --git a/README.fa_IR.md b/README.fa_IR.md index 215ecc533..07b0acc36 100644 --- a/README.fa_IR.md +++ b/README.fa_IR.md @@ -89,17 +89,15 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install. برای مستندات کامل، لطفاً به [ویکی پروژه](https://github.com/MHSanaei/3x-ui/wiki) مراجعه کنید. -### نصب بدون نظارت و ایمیج‌های ابری +### نصب بدون نظارت -نصب‌کننده به‌صورت **غیرتعاملی** نیز برای cloud-init و ایمیج‌های آماده (golden images) اجرا می‌شود. +نصب‌کننده به‌صورت **غیرتعاملی** نیز برای cloud-init اجرا می‌شود. ‏`XUI_NONINTERACTIVE=1` را تنظیم کنید (یا بدون TTY از طریق pipe اجرا کنید) تا نصب به‌صورت سرتاسری و بدون هیچ پرسشی انجام شود، اطلاعات ورود تصادفی تولید کرده و آن‌ها را در `/etc/x-ui/install-result.env` می‌نویسد. برای موارد زیر به [`deploy/`](deploy/) مراجعه کنید: - [user-data مربوط به Cloud-init](deploy/cloud-init/) — نصب بدون نظارت روی هر ابری (Hetzner/AWS/DO/Vultr/GCP/Azure/Oracle) -- [ایمیج آماده‌ی Packer](deploy/packer/) — ساخت یک AMI برای AWS EC2 به‌همراه qcow2 (amd64/arm64) با اطلاعات ورودِ مخصوص هر اینستنس که در نخستین بوت تولید می‌شود -- [Amazon Lightsail](deploy/lightsail/) — اسکریپت راه‌اندازی به‌همراه سازنده‌ی اسنپ‌شات قابل‌استفاده‌ی مجدد -- [چک‌لیست AWS Marketplace](deploy/marketplace/aws/) +- [یادداشت‌های Hetzner Cloud](deploy/marketplace/hetzner/) — استقرار مبتنی بر cloud-init روی Hetzner ## پلتفرم‌های پشتیبانی‌شده diff --git a/README.md b/README.md index 8f46aeaf2..7113f2dc5 100644 --- a/README.md +++ b/README.md @@ -89,17 +89,15 @@ During installation a random username, password, and access path are generated. For full documentation, please visit the [project Wiki](https://github.com/MHSanaei/3x-ui/wiki). -### Unattended install & cloud images +### Unattended install -The installer also runs **non-interactively** for cloud-init and golden images. +The installer also runs **non-interactively** for cloud-init. Set `XUI_NONINTERACTIVE=1` (or pipe with no TTY) and it installs end-to-end with zero prompts, generating random credentials and writing them to `/etc/x-ui/install-result.env`. See [`deploy/`](deploy/) for: - [Cloud-init user-data](deploy/cloud-init/) — unattended install on any cloud (Hetzner/AWS/DO/Vultr/GCP/Azure/Oracle) -- [Packer golden image](deploy/packer/) — build an AWS EC2 AMI + qcow2 (amd64/arm64) with per-instance credentials generated on first boot -- [Amazon Lightsail](deploy/lightsail/) — launch script + reusable snapshot builder -- [AWS Marketplace checklist](deploy/marketplace/aws/) +- [Hetzner Cloud notes](deploy/marketplace/hetzner/) — cloud-init deployment on Hetzner ## Supported Platforms diff --git a/README.ru_RU.md b/README.ru_RU.md index b04d5ada5..9e8169c7e 100644 --- a/README.ru_RU.md +++ b/README.ru_RU.md @@ -89,17 +89,15 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install. Полную документацию смотрите в [вики проекта](https://github.com/MHSanaei/3x-ui/wiki). -### Автоматическая установка и облачные образы +### Автоматическая установка -Установщик также работает в **неинтерактивном** режиме для cloud-init и готовых образов. +Установщик также работает в **неинтерактивном** режиме для cloud-init. Задайте `XUI_NONINTERACTIVE=1` (или передайте по конвейеру без TTY), и установка пройдёт от начала до конца без единого запроса: будут сгенерированы случайные учётные данные и записаны в `/etc/x-ui/install-result.env`. Смотрите [`deploy/`](deploy/) для: - [Cloud-init user-data](deploy/cloud-init/) — автоматическая установка в любом облаке (Hetzner/AWS/DO/Vultr/GCP/Azure/Oracle) -- [Готовый образ Packer](deploy/packer/) — сборка AWS EC2 AMI + qcow2 (amd64/arm64) с учётными данными для каждого экземпляра, генерируемыми при первой загрузке -- [Amazon Lightsail](deploy/lightsail/) — скрипт запуска + переиспользуемый сборщик снимков -- [Чек-лист для AWS Marketplace](deploy/marketplace/aws/) +- [Заметки по Hetzner Cloud](deploy/marketplace/hetzner/) — развёртывание на Hetzner на базе cloud-init ## Поддерживаемые платформы diff --git a/README.tr_TR.md b/README.tr_TR.md index d2eb53ecb..a9d57f538 100644 --- a/README.tr_TR.md +++ b/README.tr_TR.md @@ -89,17 +89,15 @@ Kurulum sırasında rastgele bir kullanıcı adı, şifre ve erişim yolu oluşt Tam dokümantasyon için lütfen [proje Wiki sayfasını](https://github.com/MHSanaei/3x-ui/wiki) ziyaret edin. -### Etkileşimsiz kurulum ve hazır bulut imajları +### Etkileşimsiz kurulum -Yükleyici, cloud-init ve hazır (golden) imajlar için **etkileşimsiz** olarak da çalışır. +Yükleyici, cloud-init için **etkileşimsiz** olarak da çalışır. `XUI_NONINTERACTIVE=1` ayarlayın (veya TTY olmadan boru hattına aktarın); kurulum baştan sona hiçbir soru sormadan tamamlanır, rastgele kimlik bilgileri oluşturup bunları `/etc/x-ui/install-result.env` dosyasına yazar. Şunlar için [`deploy/`](deploy/) klasörüne bakın: - [Cloud-init user-data](deploy/cloud-init/) — herhangi bir bulutta etkileşimsiz kurulum (Hetzner/AWS/DO/Vultr/GCP/Azure/Oracle) -- [Packer hazır imajı](deploy/packer/) — ilk açılışta her örnek (instance) için kimlik bilgileri oluşturan bir AWS EC2 AMI + qcow2 (amd64/arm64) imajı oluşturun -- [Amazon Lightsail](deploy/lightsail/) — başlatma betiği + yeniden kullanılabilir anlık görüntü (snapshot) oluşturucu -- [AWS Marketplace kontrol listesi](deploy/marketplace/aws/) +- [Hetzner Cloud notları](deploy/marketplace/hetzner/) — Hetzner üzerinde cloud-init tabanlı dağıtım ## Desteklenen Platformlar diff --git a/README.zh_CN.md b/README.zh_CN.md index 4234e812d..29cf8290c 100644 --- a/README.zh_CN.md +++ b/README.zh_CN.md @@ -89,17 +89,15 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install. 完整文档请参阅 [项目Wiki](https://github.com/MHSanaei/3x-ui/wiki)。 -### 无人值守安装与云镜像 +### 无人值守安装 -安装程序也可以**非交互式**运行,适用于 cloud-init 和黄金镜像(golden image)。 +安装程序也可以**非交互式**运行,适用于 cloud-init。 设置 `XUI_NONINTERACTIVE=1`(或在无 TTY 的情况下通过管道传入),它就会全程 零提示地完成端到端安装,生成随机凭据并写入 `/etc/x-ui/install-result.env`。请参阅 [`deploy/`](deploy/): - [Cloud-init user-data](deploy/cloud-init/) — 在任意云平台上无人值守安装(Hetzner/AWS/DO/Vultr/GCP/Azure/Oracle) -- [Packer golden image](deploy/packer/) — 构建 AWS EC2 AMI + qcow2(amd64/arm64),首次启动时生成每个实例独有的凭据 -- [Amazon Lightsail](deploy/lightsail/) — 启动脚本 + 可复用的快照构建器 -- [AWS Marketplace 清单](deploy/marketplace/aws/) +- [Hetzner Cloud 说明](deploy/marketplace/hetzner/) — 在 Hetzner 上基于 cloud-init 的部署 ## 支持的平台 diff --git a/deploy/README.md b/deploy/README.md index 78bbbd181..25827285c 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -1,27 +1,20 @@ -# Cloud deployment & golden images +# Cloud deployment (unattended install) -Tooling to ship the 3x-ui panel as a cloud image or via unattended install, -with **per-instance credentials generated on first boot** (never `admin/admin`, -never a shared session secret). Everything here supports **amd64 and arm64**. +Tooling to ship the 3x-ui panel via unattended install, with **per-instance +credentials generated on first boot** (never `admin/admin`, never a shared +session secret). Works on amd64 and arm64. | Path | What it is | Use when | | --- | --- | --- | | [`cloud-init/`](cloud-init/) | Generic cloud-init user-data (unattended `install.sh`) | Any cloud, no image build | -| [`packer/`](packer/) | Packer build → AWS AMI + qcow2/raw | Reusable / Marketplace images | -| [`lightsail/`](lightsail/) | Launch script + snapshot builder | Amazon Lightsail | -| [`firstboot/`](firstboot/) | First-boot unit + script that mints per-instance creds | Used by the Packer/Lightsail images | -| [`marketplace/aws/`](marketplace/aws/) | AWS Marketplace submission checklist | Publishing an EC2 AMI | | [`marketplace/hetzner/`](marketplace/hetzner/) | Hetzner Cloud notes | Hetzner deployments | -| [`test/`](test/) | Container smoke tests | Verifying the install/firstboot paths | +| [`test/`](test/) | Container smoke test | Verifying the install path | -## Two models +## How it works -- **Non-interactive install (cloud-init):** `install.sh` runs unattended when - `XUI_NONINTERACTIVE=1` or stdin is not a TTY. Each instance installs and - configures itself with random credentials. See [`cloud-init/README.md`](cloud-init/README.md). -- **Golden image (Packer):** the image contains the panel but **no DB and no - secrets**; `firstboot` generates unique credentials on first boot. See - [`packer/README.md`](packer/README.md). +`install.sh` runs unattended when `XUI_NONINTERACTIVE=1` or stdin is not a TTY. +Each instance installs and configures itself with random credentials. See +[`cloud-init/README.md`](cloud-init/README.md). ## Unattended install knobs diff --git a/deploy/cloud-init/README.md b/deploy/cloud-init/README.md index 200f70137..a2d539107 100644 --- a/deploy/cloud-init/README.md +++ b/deploy/cloud-init/README.md @@ -1,12 +1,8 @@ -# 3x-ui via cloud-init (generic, no golden image) +# 3x-ui via cloud-init -This is the **secondary** deployment path: a single [`cloud-init.yaml`](cloud-init.yaml) -user-data file that installs 3x-ui non-interactively on a fresh Ubuntu/Debian -VM and generates **unique random credentials per instance**. Use it when you do -not want to build a golden image — it works on any cloud-init platform. - -> For AWS Marketplace / reusable images, use the Packer build in -> [`../packer/`](../packer/) instead. +A single [`cloud-init.yaml`](cloud-init.yaml) user-data file that installs 3x-ui +non-interactively on a fresh Ubuntu/Debian VM and generates **unique random +credentials per instance**. It works on any cloud-init platform. ## How it works @@ -53,7 +49,6 @@ Edit the `export XUI_*` lines inside the `write_files` block of `hcloud server create --image ubuntu-24.04 --user-data-from-file cloud-init.yaml ...` - **AWS EC2** — *Advanced details → User data*: paste the file. Or `aws ec2 run-instances --user-data file://cloud-init.yaml ...` - (For a reusable Marketplace image use the Packer AMI build instead.) - **DigitalOcean** — *Create Droplet → Advanced options → Add Initialization scripts (user data)*: paste the file. Or `doctl compute droplet create --user-data-file cloud-init.yaml ...` - **Vultr** — *Deploy → Additional Features → Cloud-Init User-Data*: paste the file. diff --git a/deploy/firstboot/x-ui-firstboot.service b/deploy/firstboot/x-ui-firstboot.service deleted file mode 100644 index a0e2bc380..000000000 --- a/deploy/firstboot/x-ui-firstboot.service +++ /dev/null @@ -1,22 +0,0 @@ -[Unit] -Description=3x-ui first-boot per-instance credential generation -Documentation=https://github.com/MHSanaei/3x-ui -# Run after the network and cloud-init are up, but BEFORE the panel starts, so -# the panel never serves the default admin/admin account. -After=network-online.target cloud-init.service -Wants=network-online.target -Before=x-ui.service -# Skip entirely once the sentinel exists (cheap guard; the script re-checks too). -ConditionPathExists=!/etc/x-ui/.firstboot-done - -[Service] -Type=oneshot -RemainAfterExit=yes -# Inherit the same DB configuration the panel uses (sqlite default / postgres). -EnvironmentFile=-/etc/default/x-ui -EnvironmentFile=-/etc/conf.d/x-ui -EnvironmentFile=-/etc/sysconfig/x-ui -ExecStart=/usr/local/x-ui/x-ui-firstboot.sh - -[Install] -WantedBy=multi-user.target diff --git a/deploy/firstboot/x-ui-firstboot.sh b/deploy/firstboot/x-ui-firstboot.sh deleted file mode 100644 index 7bd86192a..000000000 --- a/deploy/firstboot/x-ui-firstboot.sh +++ /dev/null @@ -1,166 +0,0 @@ -#!/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 "" -} - -# 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 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 diff --git a/deploy/lightsail/README.md b/deploy/lightsail/README.md deleted file mode 100644 index 0e4a5d334..000000000 --- a/deploy/lightsail/README.md +++ /dev/null @@ -1,94 +0,0 @@ -# 3x-ui on Amazon Lightsail - -Two self-service ways to run 3x-ui on Lightsail, both producing **unique -per-instance credentials** (never `admin/admin`, never a shared secret). - -> **Reality check.** The Lightsail *blueprint* list (WordPress, LAMP, GitLab…) -> is curated by AWS — you **cannot** self-publish your panel there, and Lightsail -> **cannot** launch from an arbitrary EC2 AMI. What you *can* do yourself is the -> two paths below. (For a public AWS listing you'd use the EC2 **AMI** + -> Marketplace path in [`../marketplace/aws/`](../marketplace/aws/), which is a -> different product from Lightsail.) - ---- - -## Path A — launch script (simplest, self-service) - -Install on a fresh instance at creation time. No image to build. - -1. **Create instance** → platform **Linux/Unix** → blueprint **OS Only → Ubuntu 24.04**. -2. **Add launch script** → paste [`launch-script.sh`](launch-script.sh). -3. Create the instance. -4. After it boots, read the credentials: - ```bash - ssh ubuntu@ 'sudo cat /etc/x-ui/install-result.env' - ``` -5. **Open the panel port** (see the firewall note below) and log in. - -CLI equivalent: - -```bash -aws lightsail create-instances \ - --instance-names my-3xui \ - --availability-zone eu-central-1a \ - --blueprint-id ubuntu_24_04 \ - --bundle-id small_3_0 \ - --user-data file://deploy/lightsail/launch-script.sh \ - --region eu-central-1 -``` - -By default the panel uses a **random** high port (in `install-result.env`). To -pin a known port so you can pre-open it, set `export XUI_PANEL_PORT=54321` inside -`launch-script.sh`. - ---- - -## Path B — reusable snapshot (your own "ready image") - -Build a Lightsail **snapshot** once; launch as many instances from it as you -like, each generating its own credentials on first boot (the golden-image model). - -```bash -deploy/lightsail/build-snapshot.sh --region eu-central-1 --panel-port 54321 -``` - -What it does: launches a temporary Ubuntu instance with -[`snapshot-userdata.sh`](snapshot-userdata.sh) (installs the panel, **no DB**, -enables the first-boot unit), strips all state via the shared -[`cleanup.sh`](../packer/scripts/cleanup.sh), then snapshots and deletes the -build instance. Requires `awscli`, `jq`, `ssh` and Lightsail permissions. - -Launch instances from the snapshot: - -```bash -aws lightsail create-instances-from-snapshot \ - --instance-snapshot-name 3x-ui-ubuntu-24.04- \ - --instance-names my-3xui-1 --bundle-id small_3_0 \ - --availability-zone eu-central-1a --region eu-central-1 -``` - -Each launched instance runs `x-ui-firstboot` and writes its unique credentials to -`/etc/x-ui/credentials.txt` + `/etc/motd`. With `--panel-port` the port is the -same across instances (only the credentials differ), so you can pre-open it. - -> Lightsail snapshots are **private to your AWS account** (and region). To use one -> elsewhere you can export it to EC2 (`aws lightsail export-snapshot`) and share -> the resulting AMI. - ---- - -## Lightsail firewall note (important) - -Lightsail's per-instance firewall only opens **22 / 80 / 443** by default. The -panel runs on a different port, so you must open it: - -- Console: instance → **Networking → IPv4 Firewall → Add rule** (TCP, the panel port). -- CLI: - ```bash - aws lightsail open-instance-public-ports --region eu-central-1 \ - --instance-name my-3xui \ - --port-info fromPort=54321,toPort=54321,protocol=TCP - ``` - -The panel port is in `/etc/x-ui/install-result.env` (Path A) or -`/etc/x-ui/credentials.txt` (Path B), or fixed via `--panel-port` / `XUI_PANEL_PORT`. diff --git a/deploy/lightsail/build-snapshot.sh b/deploy/lightsail/build-snapshot.sh deleted file mode 100644 index db6542575..000000000 --- a/deploy/lightsail/build-snapshot.sh +++ /dev/null @@ -1,192 +0,0 @@ -#!/usr/bin/env bash -# -# build-snapshot.sh — build a reusable Amazon Lightsail snapshot of 3x-ui. -# -# Flow (mirrors the Packer golden-image model, via the Lightsail API): -# 1. create an Ubuntu Lightsail instance with snapshot-userdata.sh -# (installs the panel, NO database, enables the first-boot unit) -# 2. wait for provisioning, then (optionally) pin a known panel port and run -# the shared cleanup.sh (wipes any DB/creds/keys/host-keys/cloud-init state) -# 3. stop the instance and create an instance snapshot -# 4. delete the build instance (unless --keep-instance) -# -# Every instance you later launch from the snapshot generates its OWN unique -# credentials on first boot (see deploy/firstboot/). The snapshot is private to -# your AWS account. -# -# Requirements: awscli v2, jq, ssh. AWS credentials with Lightsail permissions. -# Usage: -# deploy/lightsail/build-snapshot.sh --region eu-central-1 [options] -# Options: -# --region AWS region (default: $AWS_REGION or eu-central-1) -# --blueprint-id Lightsail blueprint (default: ubuntu_24_04) -# --bundle-id Lightsail bundle/size (default: small_3_0) -# --availability-zone AZ (default: a) -# --panel-port

Pin the panel port in the snapshot so you can pre-open -# it in the Lightsail firewall (default: random per instance) -# --snapshot-name Snapshot name (default: 3x-ui-ubuntu-24.04-) -# --keep-instance Do not delete the build instance afterwards -set -euo pipefail - -REGION="${AWS_REGION:-eu-central-1}" -BLUEPRINT="ubuntu_24_04" -BUNDLE="small_3_0" -AZ="" -PANEL_PORT="" -SNAPSHOT_NAME="" -KEEP_INSTANCE=0 - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -STAMP="$(date +%Y%m%d-%H%M%S)" -INSTANCE_NAME="3xui-build-${STAMP}" -KEY_FILE="" - -log() { echo "[build-snapshot] $*"; } -die() { - echo "[build-snapshot] ERROR: $*" >&2 - exit 1 -} - -while [ $# -gt 0 ]; do - case "$1" in - --region) REGION="$2"; shift 2 ;; - --blueprint-id) BLUEPRINT="$2"; shift 2 ;; - --bundle-id) BUNDLE="$2"; shift 2 ;; - --availability-zone) AZ="$2"; shift 2 ;; - --panel-port) PANEL_PORT="$2"; shift 2 ;; - --snapshot-name) SNAPSHOT_NAME="$2"; shift 2 ;; - --keep-instance) KEEP_INSTANCE=1; shift ;; - -h | --help) sed -n '2,40p' "$0"; exit 0 ;; - *) die "unknown option: $1" ;; - esac -done - -[ -n "$AZ" ] || AZ="${REGION}a" -[ -n "$SNAPSHOT_NAME" ] || SNAPSHOT_NAME="3x-ui-ubuntu-24.04-${STAMP}" - -for cmd in aws jq ssh; do - command -v "$cmd" > /dev/null 2>&1 || die "'$cmd' is required" -done - -SSH_OPTS=(-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10 -o LogLevel=ERROR) - -cleanup() { - [ -n "$KEY_FILE" ] && rm -f "$KEY_FILE" - if [ "$KEEP_INSTANCE" -eq 0 ]; then - aws lightsail delete-instance --instance-name "$INSTANCE_NAME" --region "$REGION" > /dev/null 2>&1 || true - fi -} -trap cleanup EXIT - -wait_state() { - local want="$1" tries="${2:-60}" st - for _ in $(seq 1 "$tries"); do - st=$(aws lightsail get-instance-state --instance-name "$INSTANCE_NAME" --region "$REGION" \ - --query 'state.name' --output text 2> /dev/null || echo "") - [ "$st" = "$want" ] && return 0 - sleep 5 - done - return 1 -} - -log "creating build instance ${INSTANCE_NAME} (${BLUEPRINT}/${BUNDLE}) in ${REGION}..." -aws lightsail create-instances \ - --instance-names "$INSTANCE_NAME" \ - --availability-zone "$AZ" \ - --blueprint-id "$BLUEPRINT" \ - --bundle-id "$BUNDLE" \ - --user-data "file://${SCRIPT_DIR}/snapshot-userdata.sh" \ - --region "$REGION" > /dev/null - -log "waiting for instance to run..." -wait_state running 60 || die "instance did not reach 'running'" - -IP=$(aws lightsail get-instance --instance-name "$INSTANCE_NAME" --region "$REGION" \ - --query 'instance.publicIpAddress' --output text) -if [ -z "$IP" ] || [ "$IP" = "None" ]; then die "no public IP"; fi -log "instance IP: ${IP}" - -KEY_FILE="$(mktemp)" -# download-default-key-pair returns the key in 'privateKeyBase64'. Despite the -# name, the CLI historically emits the plaintext PEM (-----BEGIN...); the API -# docs describe it as base64. Handle both: write PEM as-is, else base64-decode. -KEY_RAW="$(aws lightsail download-default-key-pair --region "$REGION" \ - --query 'privateKeyBase64' --output text)" -[ -n "$KEY_RAW" ] && [ "$KEY_RAW" != "None" ] || die "failed to download default key pair" -case "$KEY_RAW" in - *-----BEGIN*) printf '%s\n' "$KEY_RAW" > "$KEY_FILE" ;; - *) printf '%s' "$KEY_RAW" | base64 -d > "$KEY_FILE" 2> /dev/null \ - || die "private key is neither PEM nor valid base64" ;; -esac -grep -q -- "-----BEGIN" "$KEY_FILE" || die "downloaded key is not a valid PEM private key" -chmod 600 "$KEY_FILE" - -log "waiting for provisioning to finish (this installs the panel)..." -ok=0 -for _ in $(seq 1 72); do # ~12 min - if ssh "${SSH_OPTS[@]}" -i "$KEY_FILE" "ubuntu@${IP}" \ - 'test -f /var/lib/3xui-provision-done' 2> /dev/null; then - ok=1 - break - fi - sleep 10 -done -[ "$ok" -eq 1 ] || die "provisioning did not complete in time" -log "provisioning complete." - -if [ -n "$PANEL_PORT" ]; then - log "pinning panel port ${PANEL_PORT} (username/password stay random)..." - ssh "${SSH_OPTS[@]}" -i "$KEY_FILE" "ubuntu@${IP}" \ - "echo 'XUI_PANEL_PORT=${PANEL_PORT}' | sudo tee -a /etc/default/x-ui >/dev/null" -fi - -log "stripping instance state (shared cleanup.sh)..." -ssh "${SSH_OPTS[@]}" -i "$KEY_FILE" "ubuntu@${IP}" \ - 'curl -fsSL https://raw.githubusercontent.com/MHSanaei/3x-ui/main/deploy/packer/scripts/cleanup.sh | sudo bash' - -log "stopping instance..." -aws lightsail stop-instance --instance-name "$INSTANCE_NAME" --region "$REGION" > /dev/null -wait_state stopped 60 || die "instance did not stop" - -log "creating snapshot ${SNAPSHOT_NAME}..." -aws lightsail create-instance-snapshot \ - --instance-name "$INSTANCE_NAME" \ - --instance-snapshot-name "$SNAPSHOT_NAME" \ - --region "$REGION" > /dev/null - -log "waiting for snapshot to become available..." -snap_ok=0 -for _ in $(seq 1 120); do # ~20 min - state=$(aws lightsail get-instance-snapshot --instance-snapshot-name "$SNAPSHOT_NAME" \ - --region "$REGION" --query 'instanceSnapshot.state' --output text 2> /dev/null || echo "") - [ "$state" = "available" ] && { - snap_ok=1 - break - } - sleep 10 -done -[ "$snap_ok" -eq 1 ] || die "snapshot did not become available" - -log "DONE." -echo -echo "================================================================" -echo " Lightsail snapshot ready: ${SNAPSHOT_NAME} (region ${REGION})" -echo "================================================================" -echo " Launch an instance from it:" -echo " aws lightsail create-instances-from-snapshot \\" -echo " --instance-snapshot-name ${SNAPSHOT_NAME} \\" -echo " --instance-names my-3xui-1 --bundle-id ${BUNDLE} \\" -echo " --availability-zone ${AZ} --region ${REGION}" -if [ -n "$PANEL_PORT" ]; then - echo - echo " Then open the panel port (pinned to ${PANEL_PORT}):" - echo " aws lightsail open-instance-public-ports --region ${REGION} \\" - echo " --instance-name my-3xui-1 \\" - echo " --port-info fromPort=${PANEL_PORT},toPort=${PANEL_PORT},protocol=TCP" -else - echo - echo " Each instance picks a RANDOM panel port. After it boots, read it from" - echo " sudo cat /etc/x-ui/credentials.txt" - echo " and open that TCP port in the instance's Lightsail IPv4 firewall." -fi -echo "================================================================" diff --git a/deploy/lightsail/launch-script.sh b/deploy/lightsail/launch-script.sh deleted file mode 100644 index 03e08bde5..000000000 --- a/deploy/lightsail/launch-script.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/bin/bash -# -# Amazon Lightsail launch script for 3x-ui (self-service, per-instance creds). -# -# Use it one of two ways when creating an Ubuntu 24.04 Lightsail instance: -# * Console: "Add launch script" -> paste this file. -# * CLI: aws lightsail create-instances --user-data file://launch-script.sh ... -# -# It installs the latest 3x-ui release non-interactively and generates unique -# random credentials for THIS instance. The full credentials land in -# /etc/x-ui/install-result.env (mode 600); /etc/motd shows only the URL + username. -# -# IMPORTANT (Lightsail firewall): Lightsail only opens 22/80/443 by default. The -# panel listens on a random high port, so after boot read the port from -# /etc/x-ui/install-result.env and open it under the instance's Networking tab -# (IPv4 Firewall), or pin a known port below and pre-open it. -set -e -export DEBIAN_FRONTEND=noninteractive - -# --- Non-interactive install knobs ------------------------------------------ -export XUI_NONINTERACTIVE=1 -export XUI_SSL_MODE="${XUI_SSL_MODE:-none}" -# Pin a known panel port so you can pre-open it in the Lightsail firewall -# (otherwise a random high port is chosen). Username/password stay random: -# export XUI_PANEL_PORT="54321" -# Other optional pins (unset => secure random): -# export XUI_USERNAME="admin2" -# export XUI_PASSWORD="change-me" -# export XUI_WEB_BASE_PATH="panel" -# Domain TLS instead of plain HTTP: -# export XUI_SSL_MODE="domain" XUI_DOMAIN="panel.example.com" XUI_ACME_EMAIL="you@example.com" -# ---------------------------------------------------------------------------- - -curl -fsSL https://raw.githubusercontent.com/MHSanaei/3x-ui/main/install.sh | bash - -# /etc/motd is world-readable, so it gets ONLY non-secret info (URL + username); -# the full credentials stay in the root-only /etc/x-ui/install-result.env -# (mode 600) — read them with `sudo cat` over SSH. -if [ -r /etc/x-ui/install-result.env ]; then - # shellcheck disable=SC1091 - . /etc/x-ui/install-result.env - { - echo - echo "=== 3x-ui panel (generated on first boot) ===" - echo "URL: ${XUI_ACCESS_URL:-unknown}" - echo "Username: ${XUI_USERNAME:-unknown}" - echo "Password + API token: sudo cat /etc/x-ui/install-result.env" - echo "Open the panel port in the Lightsail IPv4 firewall, then log in." - echo "=============================================" - } >> /etc/motd 2>/dev/null || true -fi diff --git a/deploy/lightsail/snapshot-userdata.sh b/deploy/lightsail/snapshot-userdata.sh deleted file mode 100644 index dbc67929a..000000000 --- a/deploy/lightsail/snapshot-userdata.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/bin/bash -# -# Lightsail snapshot provisioning user-data (used by build-snapshot.sh). -# -# Installs the 3x-ui panel into a build instance but creates NO database and -# NO credentials, and enables the first-boot unit. The instance is then snapshot -# so that every instance launched from the snapshot generates its own unique -# credentials on first boot (see deploy/firstboot/). -# -# This is the Lightsail equivalent of deploy/packer/scripts/provision.sh. It is -# NOT for end users — use deploy/lightsail/launch-script.sh for a direct install. -set -e -export DEBIAN_FRONTEND=noninteractive - -REPO=MHSanaei/3x-ui -XUI_DIR=/usr/local/x-ui -RAW="https://raw.githubusercontent.com/${REPO}/main" - -apt-get update -apt-get install -y --no-install-recommends \ - ca-certificates curl tar tzdata socat openssl cron jq - -ARCH=$(dpkg --print-architecture) # amd64 | arm64 -VER=$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" | jq -r .tag_name) -if [ -z "$VER" ] || [ "$VER" = "null" ]; then - echo "failed to resolve 3x-ui version" >&2 - exit 1 -fi - -tmp=$(mktemp -d) -curl -fL4 --retry 3 -o "${tmp}/x.tar.gz" \ - "https://github.com/${REPO}/releases/download/${VER}/x-ui-linux-${ARCH}.tar.gz" - -systemctl stop x-ui > /dev/null 2>&1 || true -rm -rf "$XUI_DIR" -tar -xzf "${tmp}/x.tar.gz" -C /usr/local/ -chmod +x "${XUI_DIR}/x-ui" "${XUI_DIR}/x-ui.sh" -chmod +x "${XUI_DIR}"/bin/* 2> /dev/null || true -cp -f "${XUI_DIR}/x-ui.sh" /usr/bin/x-ui -chmod +x /usr/bin/x-ui -mkdir -p /var/log/x-ui - -# Panel + first-boot systemd units. -install -m 644 "${XUI_DIR}/x-ui.service.debian" /etc/systemd/system/x-ui.service -curl -fL4 -o "${XUI_DIR}/x-ui-firstboot.sh" "${RAW}/deploy/firstboot/x-ui-firstboot.sh" -curl -fL4 -o /etc/systemd/system/x-ui-firstboot.service "${RAW}/deploy/firstboot/x-ui-firstboot.service" -chmod 755 "${XUI_DIR}/x-ui-firstboot.sh" -chmod 644 /etc/systemd/system/x-ui-firstboot.service - -systemctl daemon-reload -systemctl enable x-ui-firstboot.service -systemctl enable x-ui.service - -# No DB, no creds in the image — first boot generates them per-instance. -rm -f /etc/x-ui/x-ui.db /etc/x-ui/x-ui.db-* /etc/x-ui/.firstboot-done 2> /dev/null || true - -# Marker that build-snapshot.sh polls for over SSH. -touch /var/lib/3xui-provision-done -echo "[snapshot-userdata] provisioned 3x-ui ${VER} (${ARCH}); no DB created." diff --git a/deploy/marketplace/aws/README.md b/deploy/marketplace/aws/README.md deleted file mode 100644 index bf8fcf52c..000000000 --- a/deploy/marketplace/aws/README.md +++ /dev/null @@ -1,92 +0,0 @@ -# Publishing 3x-ui to the AWS Marketplace (AMI) - -This is the checklist for turning the Packer-built AMI into an AWS Marketplace -listing. It assumes you have already built an AMI with -[`../../packer/`](../../packer/) (locally or via `.github/workflows/image.yml`). - -> Do **not** commit AMI IDs, AWS account numbers, or credentials. The AMI ID is -> printed to the workflow job summary at build time. - -## 1. Seller registration (one-time) - -1. Sign in to the [AWS Marketplace Management Portal](https://aws.amazon.com/marketplace/management/) - with the AWS account that will own the listing. -2. Complete **seller registration** (legal entity, bank, tax interview). Required - before any product can be submitted. - -## 2. Build a compliant AMI - -Build in the seller account (or share the AMI into it): - -```bash -cd deploy/packer -packer init . -# amd64 -packer build -only='amazon-ebs.x-ui' \ - -var 'xui_version=vX.Y.Z' -var 'xui_arch=amd64' -var 'instance_type=t3.small' -var 'region=eu-central-1' . -# arm64 (Graviton) -packer build -only='amazon-ebs.x-ui' \ - -var 'xui_version=vX.Y.Z' -var 'xui_arch=arm64' -var 'instance_type=t4g.small' -var 'region=eu-central-1' . -``` - -You can list both AMIs (amd64 + arm64) as architectures of a single Marketplace -product, or as separate products. - -The image already satisfies the Marketplace AMI policies enforced by `harden.sh` -+ `cleanup.sh`: - -- ✅ `PasswordAuthentication no`, `PermitRootLogin prohibit-password` -- ✅ no default OS account passwords (all locked) -- ✅ no baked `authorized_keys`, no SSH host keys (regenerated on boot) -- ✅ base OS = current Ubuntu 24.04 LTS, patched at build time -- ✅ no application default credentials — the panel admin is generated on first - boot on a random high port (no `admin/admin`, no shipped `x-ui.db`) - -## 3. Run the self-service AMI scan - -1. In the Management Portal: **Server products → AMIs → Upload/scan an AMI**. -2. Share the AMI with the AWS Marketplace scanning account when prompted - (the portal gives you the exact account id and the `modify-image-attribute` - command, or share it from the EC2 console). -3. Start the scan. It checks SSH config, default credentials, open ports, and - for malware. Fix any finding and re-scan. - -Common scan findings and where they're handled: - -| Finding | Fix (already in the build) | -| --- | --- | -| Password authentication enabled | `harden.sh` sshd drop-in | -| Root login with password | `harden.sh` `PermitRootLogin prohibit-password` | -| Default user password set | `harden.sh` `passwd -l` on all accounts | -| Authorized keys present | `cleanup.sh` removes them | -| Out-of-date packages | base image is the latest LTS; `provision.sh` runs `apt-get update` | - -## 4. Create the product (limited / private first) - -1. **Server products → Create new product → AMI** (or AMI + CloudFormation). -2. Add title, description, categories, pricing (free or paid), regions, the AMI - id, recommended instance types, and the **usage instructions** (tell buyers - to read `/etc/x-ui/credentials.txt` / MOTD after first boot for the generated - admin login, then change the password). -3. Submit as a **Limited** (private) listing first. AWS publishes it with - restricted visibility so only your account / allow-listed accounts see it. - -## 5. Preview & launch test - -1. From the limited listing, **subscribe and launch** a test instance. -2. SSH in, `sudo cat /etc/x-ui/credentials.txt`, open the panel URL, log in, - confirm the panel works and the credentials are unique to that instance. -3. Launch a second instance and confirm its credentials differ (no shared - secrets). - -## 6. Go public - -1. Once the scan passes and the preview looks correct, request **public - visibility** (move from Limited to Public) in the listing. -2. AWS does a final review before the listing goes live. - -## References - -- AWS Marketplace seller guide: -- AMI-based product requirements: -- Self-service AMI scanning: diff --git a/deploy/marketplace/hetzner/README.md b/deploy/marketplace/hetzner/README.md index c136330c2..577662d66 100644 --- a/deploy/marketplace/hetzner/README.md +++ b/deploy/marketplace/hetzner/README.md @@ -1,9 +1,10 @@ # 3x-ui on Hetzner Cloud Hetzner Cloud does **not** have a third-party image marketplace the way AWS does. -There are two practical ways to ship 3x-ui on Hetzner. +Ship 3x-ui via **cloud-init**: each instance installs non-interactively and +generates unique per-instance credentials (no `admin/admin`, no shared secret). -## Option A — cloud-init (recommended, no image build) +## cloud-init (no image build) Use the generic user-data from [`../../cloud-init/`](../../cloud-init/). It installs 3x-ui non-interactively and generates unique per-instance credentials. @@ -27,28 +28,6 @@ After boot, fetch the generated credentials: ssh root@ 'cat /etc/x-ui/install-result.env' ``` -## Option B — snapshot from the qcow2 / a configured server - -Hetzner lets you create a **snapshot** of a running server and launch new -servers from it. Two ways to get there: - -1. **From the Packer qcow2:** Hetzner does not allow direct qcow2 upload via the - normal API, but you can boot a server, write the image to its disk in rescue - mode, then take a snapshot — or simply use Option A, which needs no image. -2. **From a configured server:** spin up a server, install via cloud-init - (Option A), verify, then **delete `/etc/x-ui/x-ui.db` and the first-boot - sentinel** before snapshotting so clones regenerate their own credentials: - - ```bash - systemctl stop x-ui - rm -f /etc/x-ui/x-ui.db /etc/x-ui/.firstboot-done /etc/x-ui/credentials.txt - # re-enable first-boot regeneration if you installed via Packer: - systemctl enable x-ui-firstboot 2>/dev/null || true - ``` - - > ⚠️ If you snapshot a server **with** its `x-ui.db`, every clone shares the - > same admin credentials and session secret. Always remove the DB first. - ## "App"-style listing Hetzner's curated apps live in the community repo diff --git a/deploy/packer/.gitignore b/deploy/packer/.gitignore deleted file mode 100644 index be9a06dde..000000000 --- a/deploy/packer/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -# Packer build artifacts (never commit images or manifests) -output-qemu/ -*.qcow2 -*.raw -packer-manifest.json -packer_cache/ -crash.log diff --git a/deploy/packer/README.md b/deploy/packer/README.md deleted file mode 100644 index c75c68757..000000000 --- a/deploy/packer/README.md +++ /dev/null @@ -1,116 +0,0 @@ -# 3x-ui golden image (Packer) - -Builds a cloud image with the 3x-ui panel pre-installed but **not configured**: -the image ships with **no database and no credentials**, and generates a unique -admin account on first boot. This is the **primary** path for AWS Marketplace -and any reusable image. - -Two sources, one build: - -| Source | Output | For | -| --- | --- | --- | -| `amazon-ebs` | AWS AMI | AWS / Marketplace | -| `qemu` | `qcow2` (+ `raw`) | Hetzner, DigitalOcean, Vultr, GCP, Azure, Oracle, bare metal | - -Both sources build for **`amd64` and `arm64`** (select with `-var xui_arch=...`). - -## Why no baked DB - -3x-ui seeds a hardcoded `admin/admin` user and generates its session secret + -panel GUID the first time it starts. If an image shipped an initialized -`x-ui.db`, **every clone would share the same credentials and secret**. So the -build deliberately: - -- installs the panel binary + systemd unit but **never starts it** and **never - creates a DB** (`scripts/provision.sh`); -- wipes any stray DB/credentials/host-keys at the end (`scripts/cleanup.sh`); -- enables `x-ui-firstboot.service`, which on first boot resets settings, sets a - random username/password on a random high port, regenerates the secret/GUID, - and writes the credentials to `/etc/x-ui/credentials.txt` + `/etc/motd` - (`deploy/firstboot/`). - -## Prerequisites - -- [Packer](https://developer.hashicorp.com/packer) ≥ 1.9 -- For `qemu` amd64: `qemu-system-x86`, `qemu-utils` (and `/dev/kvm` for acceptable speed) -- For `qemu` arm64: `qemu-system-arm`, `qemu-efi-aarch64`, `qemu-utils` — best built on an - arm64 host (native KVM); cross-building from x86 works but uses slow TCG emulation -- For `amazon-ebs`: AWS credentials with EC2 build permissions (arm64 builds on a Graviton - instance such as `t4g.small`) - -```bash -cd deploy/packer -packer init . -packer fmt -check . # formatting -packer validate . # both sources -``` - -## Build - -Build a specific release (recommended) or `latest`: - -```bash -# amd64 qcow2 (no cloud account needed) -packer build -only='qemu.x-ui' -var 'xui_version=v3.3.1' -var 'xui_arch=amd64' . - -# arm64 qcow2 (run on an arm64 host for native KVM) -packer build -only='qemu.x-ui' -var 'xui_version=v3.3.1' -var 'xui_arch=arm64' . - -# amd64 AWS AMI -packer build -only='amazon-ebs.x-ui' \ - -var 'xui_version=v3.3.1' -var 'xui_arch=amd64' -var 'instance_type=t3.small' -var 'region=eu-central-1' . - -# arm64 AWS AMI (Graviton) -packer build -only='amazon-ebs.x-ui' \ - -var 'xui_version=v3.3.1' -var 'xui_arch=arm64' -var 'instance_type=t4g.small' -var 'region=eu-central-1' . -``` - -Outputs (per arch): -- `output-qemu/3x-ui-ubuntu-24.04-.qcow2` and `.raw` -- the AMI id (also recorded in `packer-manifest.json`) - -If `/dev/kvm` is unavailable, add `-var 'qemu_accelerator=tcg'` (much slower). - -## Key variables - -See [`variables.pkr.hcl`](variables.pkr.hcl) for the full list. - -| Variable | Default | Notes | -| --- | --- | --- | -| `xui_version` | `latest` | Release tag to install, e.g. `v3.3.1` | -| `xui_arch` | `amd64` | `amd64` or `arm64` (derives the base AMI / cloud image) | -| `region` | `eu-central-1` | AWS region (amazon-ebs) | -| `instance_type` | `t3.small` | EC2 build instance — must match the arch (`t4g.small` for arm64) | -| `qemu_accelerator` | `kvm` | `kvm` or `tcg` | -| `qemu_cpu` | `host` | arm64 `-cpu` model (`host` with KVM, `max` for TCG) | -| `ubuntu_version` | `24.04` | Base Ubuntu LTS (naming/tags) | - -The CI workflow builds both arches automatically: amd64 qcow2 on a standard runner, -arm64 qcow2 on a native `ubuntu-24.04-arm` runner, and both AMIs from a single runner -(the build instance runs in AWS). - -## First boot - -On the first boot of any instance launched from the image: - -1. `x-ui-firstboot.service` runs **before** `x-ui.service`. -2. It generates a unique admin username/password, a random panel port, a random - base path, and an API token. -3. Credentials are written to `/etc/x-ui/credentials.txt` (root-only) and shown - in `/etc/motd`. Retrieve them with `sudo cat /etc/x-ui/credentials.txt`. -4. The panel then starts on the random port. `admin/admin` never exists. - -## CI - -`.github/workflows/image.yml` runs this build on `release: published` (and via -`workflow_dispatch`), attaching the compressed `qcow2` to the release and -building the AMI when AWS credentials are configured. - -## A note on host firewalls - -`scripts/harden.sh` intentionally does **not** enable a restrictive host -firewall. 3x-ui opens Xray inbound ports on admin-chosen ports at runtime, which -a host firewall would block. Use your cloud provider's security groups/firewall -instead, and open the panel port + your inbound ports there. If you still want a -host firewall, add `ufw` rules in `harden.sh` allowing SSH, the panel port and -your inbound ports. diff --git a/deploy/packer/scripts/cleanup.sh b/deploy/packer/scripts/cleanup.sh deleted file mode 100644 index 9b8add987..000000000 --- a/deploy/packer/scripts/cleanup.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env bash -# -# cleanup.sh — strip all instance-specific state and secrets from the image. -# -# Runs LAST. The output image must contain no panel database, no credentials, -# no SSH host keys, and no baked authorized_keys. Fails the build if any of -# those survive. -set -euo pipefail - -echo "[cleanup] removing panel database, credentials and first-boot sentinel..." -rm -f /etc/x-ui/x-ui.db /etc/x-ui/x-ui.db-* 2> /dev/null || true -rm -f /etc/x-ui/install-result.env /etc/x-ui/credentials.txt 2> /dev/null || true -rm -f /etc/x-ui/.firstboot-done 2> /dev/null || true - -echo "[cleanup] removing SSH host keys (regenerated on first boot)..." -rm -f /etc/ssh/ssh_host_* 2> /dev/null || true - -echo "[cleanup] removing any baked authorized_keys..." -rm -f /root/.ssh/authorized_keys 2> /dev/null || true -find /home -maxdepth 3 -name authorized_keys -type f -delete 2> /dev/null || true - -echo "[cleanup] resetting machine-id..." -truncate -s 0 /etc/machine-id 2> /dev/null || true -rm -f /var/lib/dbus/machine-id 2> /dev/null || true -ln -sf /etc/machine-id /var/lib/dbus/machine-id 2> /dev/null || true - -echo "[cleanup] resetting cloud-init so it re-runs on the real first boot..." -cloud-init clean --logs --seed > /dev/null 2>&1 || rm -rf /var/lib/cloud/* 2> /dev/null || true - -echo "[cleanup] truncating logs, history and package caches..." -find /var/log -type f -exec truncate -s 0 {} + 2> /dev/null || true -rm -rf /var/lib/x-ui /var/log/x-ui/* 2> /dev/null || true -apt-get clean || true -rm -rf /var/lib/apt/lists/* 2> /dev/null || true -rm -f /root/.bash_history 2> /dev/null || true -find /home -maxdepth 3 -name .bash_history -type f -delete 2> /dev/null || true -rm -rf /tmp/firstboot 2> /dev/null || true - -echo "[cleanup] verifying the image is clean..." -fail=0 -for f in /etc/x-ui/x-ui.db /etc/x-ui/credentials.txt /etc/x-ui/install-result.env /etc/x-ui/.firstboot-done; do - if [ -e "$f" ]; then - echo "[cleanup] FATAL: $f is present in the image" >&2 - fail=1 - fi -done -if ls /etc/ssh/ssh_host_* > /dev/null 2>&1; then - echo "[cleanup] FATAL: SSH host keys present in the image" >&2 - fail=1 -fi -if [ -e /root/.ssh/authorized_keys ]; then - echo "[cleanup] FATAL: /root/.ssh/authorized_keys present in the image" >&2 - fail=1 -fi -if [ "$fail" -ne 0 ]; then - exit 1 -fi - -echo "[cleanup] OK — no DB, no credentials, no host keys, no authorized_keys." diff --git a/deploy/packer/scripts/harden.sh b/deploy/packer/scripts/harden.sh deleted file mode 100644 index c85e94c45..000000000 --- a/deploy/packer/scripts/harden.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env bash -# -# harden.sh — baseline OS hardening for AWS Marketplace AMI scanner compliance. -# -# Focus: the controls the scanner actually checks — key-only SSH, no root -# password login, and no default OS account passwords. A restrictive host -# firewall is intentionally NOT enforced by default because 3x-ui opens Xray -# inbound ports on admin-chosen ports at runtime (see README for the rationale -# and how to add ufw rules if you want them). -set -euo pipefail -export DEBIAN_FRONTEND=noninteractive - -echo "[harden] applying SSH hardening..." -install -d -m 755 /etc/ssh/sshd_config.d -cat > /etc/ssh/sshd_config.d/99-3xui-hardening.conf << 'EOF' -# 3x-ui golden image hardening (AWS Marketplace scanner compliance) -PasswordAuthentication no -PermitRootLogin prohibit-password -KbdInteractiveAuthentication no -ChallengeResponseAuthentication no -EOF -chmod 644 /etc/ssh/sshd_config.d/99-3xui-hardening.conf - -echo "[harden] locking passwords on default OS accounts..." -# No account may ship with a usable password. Keys are provisioned per-instance -# by the cloud platform (EC2 metadata / cloud-init) on first boot. -# passwd -l locks the PASSWORD only; key-based login keeps working. -for u in root ubuntu admin; do - if id "$u" > /dev/null 2>&1; then - passwd -l "$u" > /dev/null 2>&1 || true - fi -done - -echo "[harden] enabling automatic security updates..." -apt-get update -apt-get install -y --no-install-recommends unattended-upgrades -systemctl enable unattended-upgrades > /dev/null 2>&1 || true - -echo "[harden] done." diff --git a/deploy/packer/scripts/provision.sh b/deploy/packer/scripts/provision.sh deleted file mode 100644 index 1d83346da..000000000 --- a/deploy/packer/scripts/provision.sh +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env bash -# -# provision.sh — install the 3x-ui panel into a golden image (Packer). -# -# Self-contained: mirrors install.sh's download/extract logic but DELIBERATELY -# does NOT run config_after_install and does NOT create a database. The image -# must ship without /etc/x-ui/x-ui.db so that deploy/firstboot generates unique -# per-instance credentials on first boot. Both x-ui.service and -# x-ui-firstboot.service are enabled but NOT started here. -# -# Inputs (from Packer environment_vars): -# XUI_VERSION release tag (e.g. v3.3.1) or 'latest' -# XUI_ARCH amd64 (default) or arm64 -set -euo pipefail - -XUI_VERSION="${XUI_VERSION:-latest}" -XUI_ARCH="${XUI_ARCH:-amd64}" -XUI_DIR="/usr/local/x-ui" -REPO="MHSanaei/3x-ui" -export DEBIAN_FRONTEND=noninteractive - -echo "[provision] installing base packages..." -apt-get update -apt-get install -y --no-install-recommends \ - ca-certificates curl tar tzdata socat openssl cron jq - -echo "[provision] resolving 3x-ui version..." -if [ "$XUI_VERSION" = "latest" ]; then - XUI_VERSION=$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" | jq -r '.tag_name') -fi -if [ -z "$XUI_VERSION" ] || [ "$XUI_VERSION" = "null" ]; then - echo "[provision] ERROR: could not resolve 3x-ui release tag" >&2 - exit 1 -fi -echo "[provision] installing 3x-ui ${XUI_VERSION} (${XUI_ARCH})" - -tarball="x-ui-linux-${XUI_ARCH}.tar.gz" -url="https://github.com/${REPO}/releases/download/${XUI_VERSION}/${tarball}" -tmp="$(mktemp -d)" -trap 'rm -rf "$tmp"' EXIT - -# Download the RELEASED binary tarball (no Go build inside the image). -curl -fL4 --retry 3 -o "${tmp}/${tarball}" "$url" - -# Extract into /usr/local/ (the tarball contains an x-ui/ directory). -systemctl stop x-ui > /dev/null 2>&1 || true -rm -rf "$XUI_DIR" -tar -xzf "${tmp}/${tarball}" -C /usr/local/ -chmod +x "${XUI_DIR}/x-ui" "${XUI_DIR}/x-ui.sh" -chmod +x "${XUI_DIR}"/bin/* 2> /dev/null || true - -# Install the x-ui management CLI. -if [ -f "${XUI_DIR}/x-ui.sh" ]; then - cp -f "${XUI_DIR}/x-ui.sh" /usr/bin/x-ui -else - curl -fL4 -o /usr/bin/x-ui "https://raw.githubusercontent.com/${REPO}/main/x-ui.sh" -fi -chmod +x /usr/bin/x-ui -mkdir -p /var/log/x-ui - -# Panel systemd unit (Ubuntu base => debian variant). -install -m 644 "${XUI_DIR}/x-ui.service.debian" /etc/systemd/system/x-ui.service - -# First-boot per-instance credential unit + script (uploaded to /tmp/firstboot). -install -m 755 /tmp/firstboot/x-ui-firstboot.sh "${XUI_DIR}/x-ui-firstboot.sh" -install -m 644 /tmp/firstboot/x-ui-firstboot.service /etc/systemd/system/x-ui-firstboot.service - -systemctl daemon-reload -# Enable (start on next boot) but do NOT start now — there is no DB yet. -systemctl enable x-ui-firstboot.service -systemctl enable x-ui.service - -# Belt-and-braces: ensure no DB / sentinel was created during provisioning. -rm -f /etc/x-ui/x-ui.db /etc/x-ui/x-ui.db-* /etc/x-ui/.firstboot-done 2> /dev/null || true - -echo "[provision] done — panel installed, services enabled, NO database initialized." diff --git a/deploy/packer/variables.pkr.hcl b/deploy/packer/variables.pkr.hcl deleted file mode 100644 index 7aa6eb636..000000000 --- a/deploy/packer/variables.pkr.hcl +++ /dev/null @@ -1,109 +0,0 @@ -// Input variables for the 3x-ui golden-image build. -// See README.md for usage. Override with -var / -var-file or env (PKR_VAR_*). - -variable "xui_version" { - type = string - description = "3x-ui release tag to install, e.g. v3.3.1. 'latest' resolves the newest GitHub release at build time." - default = "latest" -} - -variable "xui_arch" { - type = string - description = "CPU architecture to build for: amd64 or arm64." - default = "amd64" - validation { - condition = contains(["amd64", "arm64"], var.xui_arch) - error_message = "The xui_arch value must be 'amd64' or 'arm64'." - } -} - -variable "ubuntu_version" { - type = string - description = "Ubuntu LTS version label, used only for image naming/tags." - default = "24.04" -} - -// --- amazon-ebs (AMI) --------------------------------------------------------- - -variable "region" { - type = string - description = "AWS region the AMI is built in." - default = "eu-central-1" -} - -variable "instance_type" { - type = string - description = "EC2 instance type used to build the AMI. Must match xui_arch (e.g. t3.small for amd64, t4g.small for arm64/Graviton)." - default = "t3.small" -} - -variable "ami_name_prefix" { - type = string - description = "Prefix for the produced AMI name." - default = "3x-ui" -} - -variable "source_ami_filter_name" { - type = string - description = "Override for the Canonical Ubuntu base AMI name filter. Empty ⇒ derived from xui_arch (latest patched 24.04 LTS for that arch)." - default = "" -} - -variable "ssh_username" { - type = string - description = "Default SSH user on the base Ubuntu cloud image." - default = "ubuntu" -} - -// --- qemu (qcow2 / raw) ------------------------------------------------------- - -variable "qemu_iso_url" { - type = string - description = "Override for the Ubuntu cloud image used as the qemu base disk. Empty ⇒ derived from xui_arch (amd64/arm64 cloud image)." - default = "" -} - -variable "qemu_iso_checksum" { - type = string - description = "Checksum for the qemu base disk. 'file:' auto-fetches; 'none' skips verification." - default = "file:https://cloud-images.ubuntu.com/releases/24.04/release/SHA256SUMS" -} - -variable "qemu_accelerator" { - type = string - description = "QEMU accelerator: 'kvm' when /dev/kvm is available, else 'tcg' (slow software emulation)." - default = "kvm" -} - -variable "qemu_headless" { - type = bool - description = "Run QEMU without a display (required on CI runners)." - default = true -} - -variable "qemu_build_password" { - type = string - description = "Temporary password injected via cloud-init for Packer's build-time SSH. Locked/removed before the image is finalized." - default = "packer-build-temp-pw" - sensitive = true -} - -# --- qemu arm64-only knobs (ignored for amd64) ------------------------------- - -variable "qemu_cpu" { - type = string - description = "QEMU -cpu model for arm64 builds: 'host' with KVM on an arm64 host, 'max' for TCG emulation." - default = "host" -} - -variable "qemu_efi_code" { - type = string - description = "Path to the arm64 UEFI code firmware (AAVMF). Only used when xui_arch=arm64." - default = "/usr/share/AAVMF/AAVMF_CODE.fd" -} - -variable "qemu_efi_vars" { - type = string - description = "Path to the arm64 UEFI vars firmware template (AAVMF). Only used when xui_arch=arm64." - default = "/usr/share/AAVMF/AAVMF_VARS.fd" -} diff --git a/deploy/packer/x-ui.pkr.hcl b/deploy/packer/x-ui.pkr.hcl deleted file mode 100644 index 5ea2e4f2c..000000000 --- a/deploy/packer/x-ui.pkr.hcl +++ /dev/null @@ -1,160 +0,0 @@ -// 3x-ui golden image — one build, two sources: -// * amazon-ebs : produces an AWS AMI (Marketplace-scannable) -// * qemu : produces a qcow2 (+ raw) for Hetzner/DO/Vultr/GCP/Azure/Oracle -// -// The image ships WITHOUT an initialized x-ui.db and WITHOUT any baked -// credentials. deploy/firstboot/x-ui-firstboot.{sh,service} generates unique -// per-instance credentials on first boot, before x-ui.service starts. -// -// Provisioner order is fixed: provision.sh -> harden.sh -> cleanup.sh. - -packer { - required_plugins { - amazon = { - version = ">= 1.3.0" - source = "github.com/hashicorp/amazon" - } - qemu = { - version = ">= 1.1.0" - source = "github.com/hashicorp/qemu" - } - } -} - -locals { - build_stamp = formatdate("YYYYMMDD-hhmmss", timestamp()) - image_name = "${var.ami_name_prefix}-ubuntu-${var.ubuntu_version}-${var.xui_arch}" - is_arm = var.xui_arch == "arm64" - - # Base images are derived from xui_arch unless explicitly overridden. - source_ami_name = var.source_ami_filter_name != "" ? var.source_ami_filter_name : "ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-${var.xui_arch}-server-*" - qemu_iso_url = var.qemu_iso_url != "" ? var.qemu_iso_url : "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-${var.xui_arch}.img" -} - -source "amazon-ebs" "x-ui" { - region = var.region - instance_type = var.instance_type - ssh_username = var.ssh_username - - ami_name = "${local.image_name}-${var.xui_version}-${local.build_stamp}" - ami_description = "3x-ui panel on Ubuntu ${var.ubuntu_version}. Per-instance credentials are generated on first boot." - - source_ami_filter { - filters = { - name = local.source_ami_name - root-device-type = "ebs" - virtualization-type = "hvm" - } - owners = ["099720109477"] // Canonical - most_recent = true - } - - launch_block_device_mappings { - device_name = "/dev/sda1" - volume_size = 8 - volume_type = "gp3" - delete_on_termination = true - } - - tags = { - Name = local.image_name - Project = "3x-ui" - XuiVersion = var.xui_version - BuildTool = "packer" - BaseOS = "ubuntu-${var.ubuntu_version}" - } -} - -source "qemu" "x-ui" { - iso_url = local.qemu_iso_url - iso_checksum = var.qemu_iso_checksum - disk_image = true - disk_size = "10G" - format = "qcow2" - - accelerator = var.qemu_accelerator - headless = var.qemu_headless - cpus = 2 - memory = 2048 - net_device = "virtio-net" - disk_interface = "virtio" - - // Arch-specific QEMU machine. amd64 uses Packer defaults (BIOS boot, x86_64); - // arm64 needs the aarch64 binary, the 'virt' machine and UEFI (AAVMF) firmware. - qemu_binary = local.is_arm ? "qemu-system-aarch64" : null - machine_type = local.is_arm ? "virt" : null - efi_boot = local.is_arm - efi_firmware_code = local.is_arm ? var.qemu_efi_code : null - efi_firmware_vars = local.is_arm ? var.qemu_efi_vars : null - qemuargs = local.is_arm ? [["-cpu", var.qemu_cpu]] : [] - - output_directory = "output-qemu" - vm_name = "${local.image_name}.qcow2" - - // Build-time access: a NoCloud seed sets a temporary password for the default - // user so Packer can SSH in. The seed is a separate CD-ROM (not part of the - // output disk); the password is locked by harden.sh and state wiped by cleanup.sh. - cd_label = "cidata" - cd_content = { - "meta-data" = "" - "user-data" = <<-EOT - #cloud-config - password: ${var.qemu_build_password} - chpasswd: { expire: false } - ssh_pwauth: true - EOT - } - - ssh_username = var.ssh_username - ssh_password = var.qemu_build_password - ssh_timeout = "20m" - boot_wait = "45s" - - shutdown_command = "sudo shutdown -P now" -} - -build { - name = "3x-ui" - sources = ["source.amazon-ebs.x-ui", "source.qemu.x-ui"] - - // Upload the first-boot unit + script so provision.sh can install them. - provisioner "shell" { - inline = ["mkdir -p /tmp/firstboot"] - } - provisioner "file" { - source = "${path.root}/../firstboot/x-ui-firstboot.sh" - destination = "/tmp/firstboot/x-ui-firstboot.sh" - } - provisioner "file" { - source = "${path.root}/../firstboot/x-ui-firstboot.service" - destination = "/tmp/firstboot/x-ui-firstboot.service" - } - - provisioner "shell" { - environment_vars = [ - "XUI_VERSION=${var.xui_version}", - "XUI_ARCH=${var.xui_arch}", - "DEBIAN_FRONTEND=noninteractive", - ] - execute_command = "chmod +x {{ .Path }}; sudo -E bash {{ .Path }}" - scripts = [ - "${path.root}/scripts/provision.sh", - "${path.root}/scripts/harden.sh", - "${path.root}/scripts/cleanup.sh", - ] - // give cloud-init time to release apt locks on the very first boot - pause_before = "10s" - } - - // Convert the qcow2 to raw for clouds that need it (qemu source only). - post-processor "shell-local" { - only = ["qemu.x-ui"] - inline = ["qemu-img convert -p -O raw output-qemu/${local.image_name}.qcow2 output-qemu/${local.image_name}.raw"] - } - - // Record the AMI id / artifacts for CI to surface. - post-processor "manifest" { - output = "packer-manifest.json" - strip_path = true - } -} diff --git a/deploy/test/smoke-firstboot.sh b/deploy/test/smoke-firstboot.sh deleted file mode 100644 index 9fb103782..000000000 --- a/deploy/test/smoke-firstboot.sh +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env bash -# -# smoke-firstboot.sh — verify the first-boot per-instance credential script. -# -# Installs the released x-ui binary into a container WITHOUT a database, runs -# x-ui-firstboot.sh, and asserts: -# * fresh random credentials are generated (no admin/admin) -# * /etc/x-ui/credentials.txt (600) and /etc/motd are written -# * the sentinel is created and a second run is a no-op (creds unchanged) -# -# Requires Docker and network access. Usage: bash deploy/test/smoke-firstboot.sh -set -euo pipefail - -REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" -IMAGE="${SMOKE_IMAGE:-ubuntu:24.04}" - -if ! command -v docker > /dev/null 2>&1; then - echo "ERROR: docker is required for this smoke test." >&2 - exit 1 -fi - -echo "== first-boot credential smoke test (image: $IMAGE) ==" - -docker run --rm \ - -v "${REPO_ROOT}/deploy/firstboot/x-ui-firstboot.sh:/root/x-ui-firstboot.sh:ro" \ - -e DEBIAN_FRONTEND=noninteractive \ - "$IMAGE" bash -euo pipefail -c ' - apt-get update -qq - apt-get install -y -qq curl tar openssl ca-certificates jq > /dev/null - - echo "--- installing released x-ui binary (no DB, no systemd) ---" - REPO=MHSanaei/3x-ui - ARCH=$(dpkg --print-architecture) # amd64 | arm64 - echo "container arch: $ARCH" - VER=$(curl --fail --location --silent --show-error \ - --retry 5 --retry-all-errors --retry-delay 3 \ - --connect-timeout 15 --max-time 60 \ - "https://api.github.com/repos/${REPO}/releases/latest" | jq -r .tag_name) - [ -n "$VER" ] && [ "$VER" != "null" ] || { echo "FAIL: cannot resolve version"; exit 1; } - tmp=$(mktemp -d) - # 504s and other transient GitHub/CDN hiccups are retried; a real HTTP - # failure (e.g. missing arch asset) still aborts after the retries. - if ! curl -4 --fail --location --silent --show-error \ - --retry 5 --retry-all-errors --retry-delay 3 \ - --connect-timeout 15 --max-time 300 \ - -o "${tmp}/x.tar.gz" \ - "https://github.com/${REPO}/releases/download/${VER}/x-ui-linux-${ARCH}.tar.gz"; then - echo "FAIL: cannot download x-ui-linux-${ARCH}.tar.gz (${VER})" >&2; exit 1 - fi - test -s "${tmp}/x.tar.gz" || { echo "FAIL: downloaded tarball is empty"; exit 1; } - tar -xzf "${tmp}/x.tar.gz" -C /usr/local/ - chmod +x /usr/local/x-ui/x-ui - install -m 755 /root/x-ui-firstboot.sh /usr/local/x-ui/x-ui-firstboot.sh - - # Guarantee a clean slate (the image must never ship a DB). - rm -f /etc/x-ui/x-ui.db /etc/x-ui/.firstboot-done - - echo "--- run 1: generate per-instance credentials ---" - /usr/local/x-ui/x-ui-firstboot.sh - - test -f /etc/x-ui/.firstboot-done || { echo "FAIL: sentinel not created"; exit 1; } - test -f /etc/x-ui/credentials.txt || { echo "FAIL: credentials.txt missing"; exit 1; } - perms=$(stat -c %a /etc/x-ui/credentials.txt) - [ "$perms" = "600" ] || { echo "FAIL: credentials.txt perms=$perms (want 600)"; exit 1; } - grep -q "3x-ui" /etc/motd || { echo "FAIL: motd not written"; exit 1; } - - # shellcheck disable=SC1090 - . /etc/x-ui/credentials.txt - [ -n "${XUI_USERNAME:-}" ] && [ "$XUI_USERNAME" != "admin" ] \ - || { echo "FAIL: username missing or still admin"; exit 1; } - first_user="$XUI_USERNAME" - - /usr/local/x-ui/x-ui setting -show | grep -q "hasDefaultCredential: false" \ - || { echo "FAIL: hasDefaultCredential is not false"; exit 1; } - - echo "--- run 2: must be a no-op (sentinel honored) ---" - /usr/local/x-ui/x-ui-firstboot.sh - # shellcheck disable=SC1090 - . /etc/x-ui/credentials.txt - [ "$XUI_USERNAME" = "$first_user" ] \ - || { echo "FAIL: credentials changed on re-run"; exit 1; } - - echo "SMOKE_PASS: firstboot user=$first_user (stable across re-run)" - ' - -echo "== first-boot smoke test PASSED =="