diff --git a/.env.example b/.env.example index 86590ee..3a3410f 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,13 @@ RMM_INSECURE_DEV_MODE=false RMM_OPERATOR_USERNAME=admin RMM_OPERATOR_PASSWORD=replace-with-a-long-random-password +# Production release images. Pin an exact server version after the first +# server-v* release; do not rely on latest for controlled upgrades. +RMM_RELEASE_VERSION=latest +RMM_SERVER_IMAGE=ghcr.io/benya9669/openwrt-rmm-server +RMM_TUNNEL_IMAGE=ghcr.io/benya9669/openwrt-rmm-tunnel +RMM_IMAGE_PULL_POLICY=always + # Optional emergency/API bearer token. Leave empty when browser sessions are enough. RMM_OPERATOR_TOKEN= diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f45567b..2b91c1a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -148,6 +148,10 @@ jobs: - name: Build agent and LuCI packages uses: docker/build-push-action@v6 + env: + OPENWRT_USIGN_SECRET_B64: ${{ secrets.OPENWRT_USIGN_SECRET_B64 }} + OPENWRT_USIGN_PUBLIC_B64: ${{ secrets.OPENWRT_USIGN_PUBLIC_B64 }} + OPENWRT_APK_SECRET_B64: ${{ secrets.OPENWRT_APK_SECRET_B64 }} with: context: . file: deploy/luci-builder/Dockerfile @@ -161,6 +165,12 @@ jobs: RMM_GOARM=${{ matrix.goarm || '7' }} RMM_GOMIPS=${{ matrix.gomips || 'softfloat' }} RMM_GOAMD64=v1 + REQUIRE_NATIVE_SIGNATURE=${{ startsWith(github.ref, 'refs/tags/agent-v') }} + REPOSITORY_INDEX_REVISION=${{ github.run_id }} + secret-envs: | + openwrt_usign_secret_b64=OPENWRT_USIGN_SECRET_B64 + openwrt_usign_public_b64=OPENWRT_USIGN_PUBLIC_B64 + openwrt_apk_secret_b64=OPENWRT_APK_SECRET_B64 cache-from: type=gha,scope=openwrt-${{ matrix.release }}-${{ matrix.label }} cache-to: type=gha,mode=max,scope=openwrt-${{ matrix.release }}-${{ matrix.label }} @@ -172,6 +182,20 @@ jobs: test -f artifacts/SHA256SUMS (cd artifacts && sha256sum --check SHA256SUMS) find artifacts -maxdepth 1 -type f \( -name '*.ipk' -o -name '*.apk' \) -print -quit | grep -q . + if find artifacts -maxdepth 1 -type f -name '*.ipk' -print -quit | grep -q .; then + test -f artifacts/Packages + test -f artifacts/Packages.gz + if [[ "$GITHUB_REF" == refs/tags/agent-v* ]]; then + test -f artifacts/Packages.sig + public_key="$(find artifacts -maxdepth 1 -type f -regextype posix-extended -regex '.*/[0-9a-f]{16}' -print -quit)" + test -n "$public_key" + fi + else + test -f artifacts/packages.adb + if [[ "$GITHUB_REF" == refs/tags/agent-v* ]]; then + test -f artifacts/rmm-openwrt.pem + fi + fi - uses: actions/upload-artifact@v4 with: @@ -187,14 +211,15 @@ jobs: runs-on: ubuntu-24.04 permissions: contents: write + id-token: write + attestations: write steps: + - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 with: pattern: openwrt-* path: release-assets - - name: Create release and upload packages - env: - GH_TOKEN: ${{ github.token }} + - name: Prepare release assets shell: bash run: | set -euo pipefail @@ -202,8 +227,46 @@ jobs: while IFS= read -r file; do artifact="$(basename "$(dirname "$file")")" cp "$file" "publish/${artifact}-$(basename "$file")" - done < <(find release-assets -type f \( -name '*.ipk' -o -name '*.apk' \) -print | sort) + done < <(find release-assets -type f ! -name 'SHA256SUMS' -print | sort) (cd publish && sha256sum ./* > SHA256SUMS) + + - name: Prepare signed package repository + shell: bash + run: | + set -euo pipefail + bash scripts/prepare-package-repository.sh \ + release-assets \ + package-repository \ + "${GITHUB_REF_NAME#agent-v}" + + - uses: actions/upload-artifact@v4 + with: + name: package-repository-site + path: package-repository + if-no-files-found: error + retention-days: 7 + + - name: Attest agent release assets + uses: actions/attest-build-provenance@v2 + with: + subject-path: publish/* + + - uses: sigstore/cosign-installer@v3 + + - name: Sign release checksums with GitHub OIDC + shell: bash + run: | + set -euo pipefail + cosign sign-blob --yes \ + --bundle publish/SHA256SUMS.sigstore.json \ + publish/SHA256SUMS + + - name: Create release and upload packages + env: + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail mapfile -t files < <(find publish -type f -print | sort) if gh release view "${GITHUB_REF_NAME}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then gh release upload "${GITHUB_REF_NAME}" "${files[@]}" \ @@ -215,3 +278,26 @@ jobs: --generate-notes \ --title "OpenWrt RMM Agent ${GITHUB_REF_NAME#agent-v}" fi + + package-repository: + name: Publish signed package repository + if: startsWith(github.ref, 'refs/tags/agent-v') + needs: release + runs-on: ubuntu-24.04 + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/download-artifact@v4 + with: + name: package-repository-site + path: package-repository + - uses: actions/upload-pages-artifact@v3 + with: + path: package-repository + - name: Deploy package repository to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/release-server.yml b/.github/workflows/release-server.yml index 052960b..3ace70d 100644 --- a/.github/workflows/release-server.yml +++ b/.github/workflows/release-server.yml @@ -8,6 +8,8 @@ on: permissions: contents: write packages: write + id-token: write + attestations: write concurrency: group: server-release-${{ github.ref }} @@ -53,6 +55,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build and optionally publish server image + id: server_image uses: docker/build-push-action@v6 with: context: . @@ -69,8 +72,11 @@ jobs: org.opencontainers.image.licenses=AGPL-3.0-only cache-from: type=gha,scope=rmm-server cache-to: type=gha,mode=max,scope=rmm-server + provenance: mode=max + sbom: true - name: Build and optionally publish tunnel image + id: tunnel_image uses: docker/build-push-action@v6 with: context: deploy/tunnel @@ -87,6 +93,37 @@ jobs: org.opencontainers.image.licenses=AGPL-3.0-only cache-from: type=gha,scope=rmm-tunnel cache-to: type=gha,mode=max,scope=rmm-tunnel + provenance: mode=max + sbom: true + + - uses: sigstore/cosign-installer@v3 + if: startsWith(github.ref, 'refs/tags/server-v') + + - name: Sign published images with GitHub OIDC + if: startsWith(github.ref, 'refs/tags/server-v') + shell: bash + run: | + set -euo pipefail + cosign sign --yes \ + "ghcr.io/benya9669/openwrt-rmm-server@${{ steps.server_image.outputs.digest }}" + cosign sign --yes \ + "ghcr.io/benya9669/openwrt-rmm-tunnel@${{ steps.tunnel_image.outputs.digest }}" + + - name: Attest server image provenance + if: startsWith(github.ref, 'refs/tags/server-v') + uses: actions/attest-build-provenance@v2 + with: + subject-name: ghcr.io/benya9669/openwrt-rmm-server + subject-digest: ${{ steps.server_image.outputs.digest }} + push-to-registry: true + + - name: Attest tunnel image provenance + if: startsWith(github.ref, 'refs/tags/server-v') + uses: actions/attest-build-provenance@v2 + with: + subject-name: ghcr.io/benya9669/openwrt-rmm-tunnel + subject-digest: ${{ steps.tunnel_image.outputs.digest }} + push-to-registry: true - name: Create GitHub release if: startsWith(github.ref, 'refs/tags/server-v') diff --git a/.gitignore b/.gitignore index 6fbf670..69172a6 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ rmm.db /bin/ /dist/ /tmp/ +/.tmp/ /.gocache/ /.gopath/ /rmm-server @@ -13,5 +14,6 @@ rmm.db *.exe~ deploy/tunnel/data/ secrets/ +release-keys/ router_tunnel_key router_tunnel_key.pub diff --git a/CHECKLIST.md b/CHECKLIST.md index 203b323..583ee97 100644 --- a/CHECKLIST.md +++ b/CHECKLIST.md @@ -1,6 +1,6 @@ # Актуальный инженерный checklist -Синхронизировано с кодом 2026-07-22. Источник продуктового порядка — `ROADMAP.md`. +Синхронизировано с кодом 2026-07-23. Источник продуктового порядка — `ROADMAP.md`. ## Работает сейчас @@ -22,6 +22,9 @@ ## Обязательное перед следующим production-релизом +- [ ] Сгенерировать и офлайн сохранить ключи `usign`/APK, добавить base64-значения в GitHub Actions Secrets. +- [ ] Включить GitHub Pages из Actions и проверить первый подписанный package feed на реальном роутере. +- [ ] Выпустить первый `server-v*`, проверить Cosign/provenance и закрепить `RMM_RELEASE_VERSION` в production `.env`. - [ ] Настроить SMTP и/или `RMM_TELEGRAM_BOT_TOKEN` в production environment. - [ ] Выполнить тестовую отправку из реального пользовательского профиля. - [ ] Проверить active → repeat → resolved на тестовом роутере. @@ -39,8 +42,10 @@ - [ ] Подписанный webhook channel. - [ ] Quiet hours/timezone и per-device notification overrides. - [ ] Конфигурационные backup artifacts и retention. -- [ ] CI для тестов, Docker, APK/IPK и release artifacts. -- [ ] Signed update manifest и безопасное обновление агента. +- [x] CI для тестов, Docker, multi-version APK/IPK и release artifacts. +- [x] Keyless Cosign, provenance/SBOM контейнеров и подписанные checksum релизов. +- [x] Нативные подписанные `Packages.sig`/`packages.adb` и публикация package feed. +- [ ] Signed update manifest и безопасное обновление агента из кабинета. - [ ] Per-device tunnel credentials. - [ ] Organizations и расширенный RBAC. - [ ] MFA, command signatures и replay protection. diff --git a/RELEASES.md b/RELEASES.md index 4f12729..59716c8 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -64,4 +64,12 @@ git push origin agent-v0.6.2 Pushing a server tag publishes the container image and creates a GitHub Release. Pushing an agent tag builds the full OpenWrt matrix and creates a GitHub Release with packages and -checksums. `workflow_dispatch` can test either workflow without creating a release. +checksums. It also publishes signed package feeds through GitHub Pages. Repository setup, +key generation and router configuration are documented in +[`docs/package-repository.md`](docs/package-repository.md). `workflow_dispatch` can test +either workflow without creating a release. + +Server and tunnel images are published with SBOM/provenance attestations and keyless +Sigstore signatures bound to the release workflow identity. Agent checksums receive the +same GitHub OIDC-backed signature. IPK and APK feeds additionally use their native OpenWrt +repository signatures so `opkg` and `apk` can enforce trust on the router. diff --git a/ROADMAP.md b/ROADMAP.md index 036e7b4..88640dd 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,6 +1,6 @@ # OpenWrt RMM — актуальный roadmap -Актуализировано: 2026-07-22. Текущая стабильная линия агента: `0.6.1`. +Актуализировано: 2026-07-23. Текущая стабильная линия агента: `0.6.1`. ## Цель продукта @@ -47,6 +47,8 @@ OpenWrt через исходящее соединение агента. Пол - [x] NPMplus overlay и wildcard device domain. - [x] Healthcheck, ограничение capabilities/resources и persistent volumes. - [x] SMTP STARTTLS/TLS и Telegram bot configuration через environment. +- [x] Версионированные server/tunnel images в GHCR и production Compose overlay. +- [x] SBOM/provenance, keyless Cosign и подписанный APK/IPK package repository. ## Следующие этапы @@ -76,8 +78,9 @@ OpenWrt через исходящее соединение агента. Пол ### Этап 3 — обновления и release pipeline -- [ ] Подписанный release manifest агента и LuCI-приложения. -- [ ] Автосборка APK/IPK и multi-architecture artifacts в CI. +- [x] Автосборка APK/IPK и multi-architecture artifacts в CI. +- [x] Нативная подпись IPK/APK feed, Cosign checksums и публикация репозитория. +- [ ] Подписанный update manifest агента и LuCI-приложения. - [ ] Обновление одного роутера из кабинета. - [ ] Canary/поэтапный rollout с остановкой при потере связи. - [ ] История и безопасный rollback версии агента. @@ -107,6 +110,6 @@ OpenWrt через исходящее соединение агента. Пол ## Ближайший приоритет -1. Развернуть и проверить notification release на production. -2. Добавить webhook и quiet hours. -3. Начать конфигурационные backup/restore. +1. Создать и сохранить ключи package feed, включить GitHub Pages и проверить первый подписанный agent/server release. +2. Развернуть и проверить notification release на production. +3. Добавить webhook и quiet hours, затем начать конфигурационные backup/restore. diff --git a/compose.release.yaml b/compose.release.yaml new file mode 100644 index 0000000..a98c473 --- /dev/null +++ b/compose.release.yaml @@ -0,0 +1,8 @@ +services: + rmm-server: + image: "${RMM_SERVER_IMAGE:-ghcr.io/benya9669/openwrt-rmm-server}:${RMM_RELEASE_VERSION:-latest}" + pull_policy: "${RMM_IMAGE_PULL_POLICY:-always}" + + tunnel-ssh: + image: "${RMM_TUNNEL_IMAGE:-ghcr.io/benya9669/openwrt-rmm-tunnel}:${RMM_RELEASE_VERSION:-latest}" + pull_policy: "${RMM_IMAGE_PULL_POLICY:-always}" diff --git a/deploy/luci-builder/Dockerfile b/deploy/luci-builder/Dockerfile index 139a6e8..ffca304 100644 --- a/deploy/luci-builder/Dockerfile +++ b/deploy/luci-builder/Dockerfile @@ -69,6 +69,9 @@ RUN case "${OPENWRT_SDK_URL}" in \ FROM sdk AS builder +ARG REQUIRE_NATIVE_SIGNATURE=false +ARG REPOSITORY_INDEX_REVISION=local + USER root RUN apt-get update \ @@ -144,7 +147,11 @@ RUN printf '%s\n' 'CONFIG_PACKAGE_luci-app-rmm-agent=m' >> .config \ RUN make -j1 package/luci-app-rmm-agent/compile V=s -RUN mkdir -p /home/builder/artifacts \ +RUN --mount=type=secret,id=openwrt_usign_secret_b64,uid=1000,required=false \ + --mount=type=secret,id=openwrt_usign_public_b64,uid=1000,required=false \ + --mount=type=secret,id=openwrt_apk_secret_b64,uid=1000,required=false \ + test -n "${REPOSITORY_INDEX_REVISION}" \ + && mkdir -p /home/builder/artifacts \ && find bin -type f \ \( -name 'luci-app-rmm-agent*.apk' \ -o -name 'luci-app-rmm-agent*.ipk' \ @@ -153,7 +160,54 @@ RUN mkdir -p /home/builder/artifacts \ -exec cp '{}' /home/builder/artifacts/ \; \ && test -n "$(find /home/builder/artifacts -maxdepth 1 -type f \( -name '*.apk' -o -name '*.ipk' \) -print -quit)" \ && cd /home/builder/artifacts \ - && find . -maxdepth 1 -type f \( -name '*.apk' -o -name '*.ipk' \) -print \ + && if find . -maxdepth 1 -type f -name '*.ipk' -print -quit | grep -q .; then \ + /home/builder/sdk/scripts/ipkg-make-index.sh . > Packages; \ + gzip -9nc Packages > Packages.gz; \ + if [ -s /run/secrets/openwrt_usign_secret_b64 ] \ + && [ -s /run/secrets/openwrt_usign_public_b64 ]; then \ + base64 -d /run/secrets/openwrt_usign_secret_b64 > /tmp/usign.sec; \ + base64 -d /run/secrets/openwrt_usign_public_b64 > /tmp/usign.pub; \ + chmod 600 /tmp/usign.sec; \ + /home/builder/sdk/staging_dir/host/bin/usign -S -m Packages -s /tmp/usign.sec; \ + /home/builder/sdk/staging_dir/host/bin/usign -V -m Packages -p /tmp/usign.pub; \ + key_id="$(/home/builder/sdk/staging_dir/host/bin/usign -F -p /tmp/usign.pub)"; \ + cp /tmp/usign.pub "${key_id}"; \ + rm -f /tmp/usign.sec /tmp/usign.pub; \ + elif [ "${REQUIRE_NATIVE_SIGNATURE}" = "true" ]; then \ + echo 'IPK release signing keys are required' >&2; \ + exit 1; \ + fi; \ + else \ + if [ -s /run/secrets/openwrt_apk_secret_b64 ]; then \ + base64 -d /run/secrets/openwrt_apk_secret_b64 > /tmp/apk-signing.pem; \ + chmod 600 /tmp/apk-signing.pem; \ + /home/builder/sdk/staging_dir/host/bin/openssl ec \ + -in /tmp/apk-signing.pem -pubout -out rmm-openwrt.pem; \ + /home/builder/sdk/staging_dir/host/bin/apk mkndx \ + --root /home/builder/sdk \ + --keys-dir /home/builder/artifacts \ + --allow-untrusted \ + --sign /tmp/apk-signing.pem \ + --output packages.adb \ + ./*.apk; \ + rm -f /tmp/apk-signing.pem; \ + elif [ "${REQUIRE_NATIVE_SIGNATURE}" = "true" ]; then \ + echo 'APK release signing key is required' >&2; \ + exit 1; \ + else \ + /home/builder/sdk/staging_dir/host/bin/apk mkndx \ + --root /home/builder/sdk \ + --keys-dir /home/builder/artifacts \ + --allow-untrusted \ + --output packages.adb \ + ./*.apk; \ + fi; \ + fi \ + && find . -maxdepth 1 -type f \( \ + -name '*.apk' -o -name '*.ipk' \ + -o -name 'Packages' -o -name 'Packages.gz' -o -name 'Packages.sig' \ + -o -name 'packages.adb' -o -name 'rmm-openwrt.pem' \ + -o -name '????????????????' \) -print \ | LC_ALL=C sort \ | xargs sha256sum > SHA256SUMS diff --git a/docs/docker-compose.md b/docs/docker-compose.md index fb76a2c..f9f3421 100644 --- a/docs/docker-compose.md +++ b/docs/docker-compose.md @@ -58,6 +58,23 @@ by retention maintenance. ## 2. Start The Stack +For a production deployment, pin the server release in `.env` and use the release +overlay. This pulls the server and tunnel images published by the same `server-v*` tag: + +```dotenv +RMM_RELEASE_VERSION=0.8.0 +``` + +```powershell +docker compose -f compose.yaml -f compose.release.yaml pull +docker compose -f compose.yaml -f compose.release.yaml up -d +docker compose -f compose.yaml -f compose.release.yaml ps +``` + +Keep the exact version instead of `latest` so an upgrade is deliberate and reversible. +The base Compose file remains buildable from source for development and emergency +recovery: + ```powershell docker compose up -d --build docker compose ps diff --git a/docs/openwrt.md b/docs/openwrt.md index dd48186..2d05100 100644 --- a/docs/openwrt.md +++ b/docs/openwrt.md @@ -147,7 +147,9 @@ The workflow resolves each SDK filename and SHA256 from the official OpenWrt mir falling back to the OpenWrt archive for end-of-life branches. Releases through `24.10` produce `.ipk`; `25.12` produces `.apk`. Manual runs retain each target as a workflow artifact for 30 days. An `agent-v*` tag also creates a GitHub Release containing all packages and -a combined `SHA256SUMS`. +a combined `SHA256SUMS`, a Sigstore signature bundle and native signed package feeds. +See [package-repository.md](package-repository.md) for key provisioning and the commands +used to connect `opkg` or `apk` to the repository. This matrix represents common CPU families, not every OpenWrt target. Add an explicit matrix row for another target/subtarget and select the matching Go architecture rather diff --git a/docs/package-repository.md b/docs/package-repository.md new file mode 100644 index 0000000..ae25b2a --- /dev/null +++ b/docs/package-repository.md @@ -0,0 +1,110 @@ +# Signed OpenWrt package repository + +Agent release tags publish a repository for every supported OpenWrt release and target. +The default public base URL is: + +```text +https://benya9669.github.io/openwrt-rmm/feeds/stable/openwrt +``` + +Before the first release, open **Repository settings → Pages** and select **GitHub +Actions** as the deployment source. A custom domain such as `packages.daemonlord.ru` +can be attached later in the same Pages settings; keep the GitHub Pages URL available +until DNS and TLS for the custom domain have been verified. + +## Signing keys + +Keep private keys offline and out of the repository. Generate them on a trusted Linux or +WSL host with `usign` and OpenSSL installed: + +```sh +umask 077 +mkdir -p release-keys +usign -G \ + -s release-keys/openwrt-usign.sec \ + -p release-keys/openwrt-usign.pub \ + -c "OpenWrt RMM package repository" +openssl ecparam -name prime256v1 -genkey -noout \ + -out release-keys/openwrt-apk.pem +openssl ec -in release-keys/openwrt-apk.pem -pubout \ + -out release-keys/openwrt-apk.pub.pem +``` + +Back up `openwrt-usign.sec` and `openwrt-apk.pem` in an encrypted offline location. +The files under `release-keys/` are ignored by Git. + +Encode the keys without printing private material into CI logs: + +```sh +base64 -w0 release-keys/openwrt-usign.sec > release-keys/openwrt-usign.sec.b64 +base64 -w0 release-keys/openwrt-usign.pub > release-keys/openwrt-usign.pub.b64 +base64 -w0 release-keys/openwrt-apk.pem > release-keys/openwrt-apk.pem.b64 +``` + +Create these GitHub Actions repository secrets from the corresponding `.b64` files: + +| Secret | Source file | +| --- | --- | +| `OPENWRT_USIGN_SECRET_B64` | `openwrt-usign.sec.b64` | +| `OPENWRT_USIGN_PUBLIC_B64` | `openwrt-usign.pub.b64` | +| `OPENWRT_APK_SECRET_B64` | `openwrt-apk.pem.b64` | + +An `agent-v*` release fails closed when the key required by an OpenWrt generation is +missing. Manual workflow runs may still create unsigned test artifacts. BuildKit secret +mounts expose private keys only to the repository-index build step; private keys are not +copied into images, artifacts or build cache. + +## OpenWrt 24.10 and older: IPK/opkg + +Choose the directory matching the firmware release and target. For example MT7621 on +OpenWrt 24.10: + +```sh +feed='https://benya9669.github.io/openwrt-rmm/feeds/stable/openwrt/24.10.7/ramips-mt7621' +key_base='https://benya9669.github.io/openwrt-rmm/keys/usign' +key_id='<16-character-key-id>' + +wget -O "/etc/opkg/keys/${key_id}" "${key_base}/${key_id}" +printf 'src/gz rmm %s\n' "$feed" > /etc/opkg/customfeeds.conf.d/rmm.conf +opkg update +opkg install rmm-agent-go-production luci-app-rmm-agent +``` + +The workflow creates `Packages`, `Packages.gz` and `Packages.sig`. `opkg` verifies the +signature of the repository metadata and the package hashes contained in that metadata. +The key ID is the filename published under `/keys/usign/`. + +## OpenWrt 25.12 and newer: APK + +For MT7621 on OpenWrt 25.12: + +```sh +base='https://benya9669.github.io/openwrt-rmm' +repo="${base}/feeds/stable/openwrt/25.12.4/ramips-mt7621/packages.adb" + +wget -O /etc/apk/keys/rmm-openwrt.pem "${base}/keys/apk/rmm-openwrt.pem" +printf '%s\n' "$repo" > /etc/apk/repositories.d/rmm.list +apk update +apk add rmm-agent-go-production luci-app-rmm-agent +``` + +APK verifies the signed `packages.adb` index. Installation should not require +`--allow-untrusted`; needing that flag means the repository key or signature chain is +not configured correctly. + +## Release verification + +Release assets also include a keyless Sigstore bundle for the combined checksums: + +```sh +cosign verify-blob \ + --bundle SHA256SUMS.sigstore.json \ + --certificate-identity-regexp \ + '^https://github.com/Benya9669/openwrt-rmm/.github/workflows/build.yml@refs/tags/agent-v.*$' \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + SHA256SUMS +sha256sum --check SHA256SUMS +``` + +This Sigstore verification complements native package-manager trust; it does not replace +the `usign` or APK repository signature. diff --git a/scripts/prepare-package-repository.sh b/scripts/prepare-package-repository.sh new file mode 100644 index 0000000..0768e83 --- /dev/null +++ b/scripts/prepare-package-repository.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [ "$#" -ne 3 ]; then + echo "usage: $0 " >&2 + exit 2 +fi + +source_dir="$1" +output_dir="$2" +agent_version="$3" + +if [ ! -d "$source_dir" ]; then + echo "artifact directory does not exist: $source_dir" >&2 + exit 1 +fi + +case "$agent_version" in + ''|*[!0-9A-Za-z._-]*) + echo "invalid agent version: $agent_version" >&2 + exit 1 + ;; +esac + +if [ -e "$output_dir" ] && [ -n "$(find "$output_dir" -mindepth 1 -maxdepth 1 -print -quit 2>/dev/null)" ]; then + echo "output directory must be empty: $output_dir" >&2 + exit 1 +fi +mkdir -p "$output_dir/feeds/$agent_version/openwrt" "$output_dir/keys/usign" "$output_dir/keys/apk" + +artifact_count=0 +for artifact_dir in "$source_dir"/openwrt-*; do + [ -d "$artifact_dir" ] || continue + artifact_name="$(basename "$artifact_dir")" + remainder="${artifact_name#openwrt-}" + openwrt_release="${remainder%%-*}" + target_label="${remainder#"$openwrt_release"-}" + + if [ -z "$openwrt_release" ] || [ -z "$target_label" ] || [ "$target_label" = "$remainder" ]; then + echo "cannot parse artifact name: $artifact_name" >&2 + exit 1 + fi + + destination="$output_dir/feeds/$agent_version/openwrt/$openwrt_release/$target_label" + mkdir -p "$destination" + cp "$artifact_dir"/* "$destination/" + + while IFS= read -r public_key; do + cp "$public_key" "$output_dir/keys/usign/$(basename "$public_key")" + done < <(find "$artifact_dir" -maxdepth 1 -type f -regextype posix-extended \ + -regex '.*/[0-9a-f]{16}' -print) + + if [ -f "$artifact_dir/rmm-openwrt.pem" ]; then + cp "$artifact_dir/rmm-openwrt.pem" "$output_dir/keys/apk/rmm-openwrt.pem" + fi + artifact_count=$((artifact_count + 1)) +done + +if [ "$artifact_count" -eq 0 ]; then + echo "no openwrt-* artifact directories found in $source_dir" >&2 + exit 1 +fi + +mkdir -p "$output_dir/feeds/stable" +cp -a "$output_dir/feeds/$agent_version/." "$output_dir/feeds/stable/" +touch "$output_dir/.nojekyll" + +cat > "$output_dir/index.html" < + + + + + OpenWrt RMM package repository + + +
+

OpenWrt RMM package repository

+

Текущая стабильная версия агента: ${agent_version}.

+

Инструкции подключения находятся в документации проекта.

+
+ + +EOF