mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-01 07:26:08 +00:00
Compare commits
12 Commits
dc1de0488a
...
4f19c604c7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f19c604c7 | ||
|
|
81168ff49a | ||
|
|
199e52b5cb | ||
|
|
b97e36cdeb | ||
|
|
7c273e2132 | ||
|
|
986b18b163 | ||
|
|
39b0665678 | ||
|
|
5e8a2b02c1 | ||
| 531fd878a6 | |||
| 4a73cc245c | |||
| 8e8708f95d | |||
|
|
b21b2000d8 |
38
.gitea/workflows/README.md
Normal file
38
.gitea/workflows/README.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# Gitea Actions workflows
|
||||
|
||||
This directory mirrors `.github/workflows/` for the self-hosted Gitea
|
||||
Actions runner at `gitea.gk2.secubox.in` (board-internal CI).
|
||||
|
||||
## Source of truth
|
||||
|
||||
`.github/workflows/*.yml` is the source of truth — workflows are edited
|
||||
there and mirrored here. The two trees should stay byte-identical;
|
||||
divergence is a bug.
|
||||
|
||||
Eventual goal: a single CI definition tree consumed by both runners.
|
||||
For now, the mirror is mechanical (cp) and tracked in PRs that touch
|
||||
both directories together.
|
||||
|
||||
## Compatibility notes
|
||||
|
||||
- `runs-on: ubuntu-latest` works because the `act_runner` registered on
|
||||
the dev box advertises the `ubuntu-latest` label (Phase B of the
|
||||
migration, separate ticket).
|
||||
- `uses: actions/*` references are auto-fetched from `github.com` thanks
|
||||
to `DEFAULT_ACTIONS_URL = github` in
|
||||
`/var/lib/gitea/custom/conf/app.ini` (set during Phase A1).
|
||||
- Secrets are NOT carried by repo mirroring — re-add in
|
||||
Gitea Settings → Actions → Secrets per repo:
|
||||
`GPG_PRIVATE_KEY`, `DEPLOY_SSH_KEY`, `DEPLOY_KNOWN_HOSTS`, etc.
|
||||
- Some workflows use `gh` (GitHub CLI) which is not installed by default
|
||||
on `act_runner`. Those jobs will fail visibly until they are migrated
|
||||
to `tea` (Gitea CLI) — to be addressed per-workflow.
|
||||
|
||||
## Refs
|
||||
|
||||
- Phase A1: Gitea Actions enabled in app.ini (done 2026-05-17)
|
||||
- Phase A2: pull mirror github → gitea — blocked on
|
||||
[#176](https://github.com/CyberMind-FR/secubox-deb/issues/176)
|
||||
(missing `giteactl repo mirror` verb)
|
||||
- Phase A3: this directory ([#177](https://github.com/CyberMind-FR/secubox-deb/issues/177))
|
||||
- Phase B: install + register `act_runner` (separate ticket TBD)
|
||||
324
.gitea/workflows/build-all-live-usb.yml
Normal file
324
.gitea/workflows/build-all-live-usb.yml
Normal file
|
|
@ -0,0 +1,324 @@
|
|||
name: Build SecuBox Live USB (All Platforms)
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
platform:
|
||||
description: 'Target platform(s)'
|
||||
type: string
|
||||
default: 'all'
|
||||
secrets:
|
||||
GPG_PRIVATE_KEY:
|
||||
required: false
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
platform:
|
||||
description: 'Target platform'
|
||||
type: choice
|
||||
options:
|
||||
- all
|
||||
- x64
|
||||
- mochabin
|
||||
- rpi400
|
||||
default: all
|
||||
compress:
|
||||
description: 'Compress output (gzip)'
|
||||
type: boolean
|
||||
default: true
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
env:
|
||||
DEBIAN_SUITE: bookworm
|
||||
|
||||
jobs:
|
||||
# Matrix build for all platforms
|
||||
build-live-usb:
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# x64 (amd64) - GRUB UEFI/BIOS boot
|
||||
- platform: x64
|
||||
arch: amd64
|
||||
script: build-live-usb.sh
|
||||
artifact_name: secubox-live-amd64
|
||||
output_pattern: "secubox-live-amd64-*.img*"
|
||||
needs_qemu: false
|
||||
embed_image: false
|
||||
|
||||
# MOCHAbin (arm64) - U-Boot distroboot
|
||||
- platform: mochabin
|
||||
arch: arm64
|
||||
script: build-mochabin-live-usb.sh
|
||||
artifact_name: secubox-mochabin-live-usb
|
||||
output_pattern: "secubox-mochabin-live-usb.img*"
|
||||
needs_qemu: true
|
||||
embed_image: true
|
||||
|
||||
# Raspberry Pi 400 (arm64) - Native Pi firmware boot
|
||||
- platform: rpi400
|
||||
arch: arm64
|
||||
script: build-rpi-usb.sh
|
||||
artifact_name: secubox-rpi-arm64
|
||||
output_pattern: "secubox-rpi-arm64-*.img*"
|
||||
needs_qemu: true
|
||||
embed_image: false
|
||||
|
||||
steps:
|
||||
# Skip job if platform doesn't match (workflow_dispatch only)
|
||||
- name: Check platform filter
|
||||
id: filter
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "push" ]]; then
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
elif [[ "${{ inputs.platform }}" == "all" || "${{ inputs.platform }}" == "" || "${{ inputs.platform }}" == "${{ matrix.platform }}" ]]; then
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
echo "Skipping ${{ matrix.platform }} (requested: ${{ inputs.platform }})"
|
||||
fi
|
||||
|
||||
- name: Checkout
|
||||
if: steps.filter.outputs.skip != 'true'
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Free disk space
|
||||
if: steps.filter.outputs.skip != 'true'
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /usr/local/share/boost
|
||||
sudo apt-get clean
|
||||
df -h
|
||||
|
||||
- name: Setup QEMU for ARM64 cross-compilation
|
||||
if: steps.filter.outputs.skip != 'true' && matrix.needs_qemu
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
platforms: arm64
|
||||
|
||||
- name: Install build dependencies
|
||||
if: steps.filter.outputs.skip != 'true'
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq \
|
||||
debootstrap qemu-user-static binfmt-support \
|
||||
parted dosfstools e2fsprogs squashfs-tools \
|
||||
grub-efi-amd64-bin grub-pc-bin grub-efi-amd64-signed shim-signed \
|
||||
mtools rsync pv xorriso curl wget u-boot-tools
|
||||
|
||||
- name: Download SecuBox packages
|
||||
if: steps.filter.outputs.skip != 'true'
|
||||
uses: dawidd6/action-download-artifact@v3
|
||||
with:
|
||||
workflow: build-packages.yml
|
||||
name: secubox-debs-all
|
||||
path: output/debs/
|
||||
if_no_artifact_found: warn
|
||||
continue-on-error: true
|
||||
|
||||
- name: List available packages
|
||||
if: steps.filter.outputs.skip != 'true'
|
||||
run: |
|
||||
echo "=== Available SecuBox packages ==="
|
||||
ls output/debs/secubox-*.deb 2>/dev/null | wc -l || echo "0"
|
||||
ls output/debs/secubox-*.deb 2>/dev/null | xargs -I{} basename {} | sed 's/_.*$//' | sort -u || echo "No packages found"
|
||||
|
||||
- name: Build embedded eMMC image (MOCHAbin only)
|
||||
if: steps.filter.outputs.skip != 'true' && matrix.embed_image
|
||||
run: |
|
||||
echo "=== Building embedded eMMC target image ==="
|
||||
sudo -E bash image/build-image.sh \
|
||||
--board mochabin \
|
||||
--suite ${{ env.DEBIAN_SUITE }} \
|
||||
--out output/ \
|
||||
--slipstream
|
||||
echo "=== Checking eMMC image for embedding ==="
|
||||
# build-image.sh already compresses to .img.gz, just verify it exists
|
||||
if [[ -f output/secubox-mochabin-bookworm.img.gz ]]; then
|
||||
ls -lh output/secubox-mochabin-bookworm.img.gz
|
||||
elif [[ -f output/secubox-mochabin-bookworm.img ]]; then
|
||||
echo "Compressing uncompressed image..."
|
||||
gzip -k output/secubox-mochabin-bookworm.img
|
||||
ls -lh output/secubox-mochabin-bookworm.img.gz
|
||||
else
|
||||
echo "ERROR: No eMMC image found!"
|
||||
ls -la output/
|
||||
exit 1
|
||||
fi
|
||||
timeout-minutes: 60
|
||||
|
||||
- name: Build ${{ matrix.platform }} Live USB
|
||||
if: steps.filter.outputs.skip != 'true'
|
||||
env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
run: |
|
||||
echo "=== Building ${{ matrix.platform }} Live USB ==="
|
||||
|
||||
COMPRESS_OPT=""
|
||||
if [[ "${{ inputs.compress }}" == "false" ]]; then
|
||||
COMPRESS_OPT="--no-compress"
|
||||
fi
|
||||
|
||||
SLIPSTREAM_OPT=""
|
||||
if ls output/debs/secubox-*.deb >/dev/null 2>&1; then
|
||||
SLIPSTREAM_OPT="--slipstream"
|
||||
echo "Slipstream enabled: $(ls output/debs/secubox-*.deb | wc -l) packages"
|
||||
fi
|
||||
|
||||
sudo -E bash image/${{ matrix.script }} \
|
||||
--suite ${{ env.DEBIAN_SUITE }} \
|
||||
$SLIPSTREAM_OPT \
|
||||
$COMPRESS_OPT
|
||||
|
||||
sudo chown -R $(id -u):$(id -g) output/
|
||||
timeout-minutes: 120
|
||||
|
||||
- name: List output files
|
||||
if: steps.filter.outputs.skip != 'true'
|
||||
run: |
|
||||
echo "=== Build output ==="
|
||||
ls -lh output/
|
||||
|
||||
- name: Generate checksums
|
||||
if: steps.filter.outputs.skip != 'true'
|
||||
run: |
|
||||
cd output/
|
||||
echo "=== Generating SHA256 checksums ==="
|
||||
for f in ${{ matrix.output_pattern }}; do
|
||||
if [[ -f "$f" ]]; then
|
||||
sha256sum "$f" >> SHA256SUMS
|
||||
echo " $f"
|
||||
fi
|
||||
done
|
||||
cat SHA256SUMS
|
||||
|
||||
- name: Upload artifact
|
||||
if: steps.filter.outputs.skip != 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact_name }}-${{ env.DEBIAN_SUITE }}
|
||||
path: |
|
||||
output/${{ matrix.output_pattern }}
|
||||
output/SHA256SUMS
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
|
||||
# Collect all artifacts and create release on tags
|
||||
release:
|
||||
needs: build-live-usb
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
|
||||
steps:
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: live-usb/
|
||||
merge-multiple: true
|
||||
|
||||
- name: List downloaded files
|
||||
run: |
|
||||
echo "=== Downloaded artifacts ==="
|
||||
ls -lh live-usb/
|
||||
|
||||
- name: Generate combined checksums
|
||||
run: |
|
||||
cd live-usb/
|
||||
rm -f SHA256SUMS
|
||||
echo "=== Generating combined SHA256SUMS ==="
|
||||
sha256sum *.img* 2>/dev/null | sort > SHA256SUMS || echo "No images found"
|
||||
cat SHA256SUMS
|
||||
|
||||
- name: Sign checksums
|
||||
env:
|
||||
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
if: env.GPG_PRIVATE_KEY != ''
|
||||
run: |
|
||||
echo "$GPG_PRIVATE_KEY" | gpg --batch --import
|
||||
cd live-usb/
|
||||
gpg --clearsign SHA256SUMS
|
||||
mv SHA256SUMS.asc SHA256SUMS.gpg
|
||||
echo "=== Checksums signed ==="
|
||||
|
||||
- name: Delete existing release assets (if any)
|
||||
run: |
|
||||
VERSION="${{ github.ref_name }}"
|
||||
if gh release view "$VERSION" &>/dev/null; then
|
||||
echo "Release $VERSION exists, deleting duplicate assets..."
|
||||
for file in live-usb/*.img* live-usb/SHA256SUMS live-usb/SHA256SUMS.gpg; do
|
||||
[ -f "$file" ] || continue
|
||||
name=$(basename "$file")
|
||||
gh release delete-asset "$VERSION" "$name" --yes 2>/dev/null || true
|
||||
done
|
||||
fi
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
live-usb/*.img*
|
||||
live-usb/SHA256SUMS
|
||||
live-usb/SHA256SUMS.gpg
|
||||
body: |
|
||||
## SecuBox Live USB Images ${{ github.ref_name }}
|
||||
|
||||
Bootable live USB images with integrated disk flasher tools for permanent installation.
|
||||
|
||||
### Available Images
|
||||
|
||||
| Image | Platform | Architecture | Boot Method | Flasher Tool |
|
||||
|-------|----------|--------------|-------------|--------------|
|
||||
| `secubox-live-amd64-bookworm.img.gz` | x64 PC | amd64 | GRUB UEFI/BIOS | `secubox-flash-disk` (press `d`) |
|
||||
| `secubox-mochabin-live-usb.img` | MOCHAbin | arm64 | U-Boot extlinux | `secubox-flash-emmc` (press `f`) |
|
||||
| `secubox-rpi-arm64-bookworm.img.gz` | Raspberry Pi 400 | arm64 | Pi firmware | Manual install |
|
||||
|
||||
### Flash to USB Drive
|
||||
|
||||
```bash
|
||||
# x64 PC
|
||||
zcat secubox-live-amd64-bookworm.img.gz | sudo dd of=/dev/sdX bs=4M status=progress
|
||||
|
||||
# MOCHAbin
|
||||
sudo dd if=secubox-mochabin-live-usb.img of=/dev/sdX bs=4M status=progress
|
||||
|
||||
# Raspberry Pi 400
|
||||
zcat secubox-rpi-arm64-bookworm.img.gz | sudo dd of=/dev/sdX bs=4M status=progress
|
||||
```
|
||||
|
||||
### Boot & Install to Internal Storage
|
||||
|
||||
| Platform | Boot Method | Install Command |
|
||||
|----------|-------------|-----------------|
|
||||
| **x64** | Boot USB, select "SecuBox Live" | Press `d` in TUI or run `secubox-flash-disk` |
|
||||
| **MOCHAbin** | Serial console (115200 8N1), U-Boot auto-boots | Press `f` in TUI or run `secubox-flash-emmc` |
|
||||
| **Raspberry Pi** | HDMI/serial console, auto-boots | Manual: `dd` to SD card |
|
||||
|
||||
### Default Credentials
|
||||
|
||||
- **Console/SSH:** `root` / `secubox`
|
||||
- **Web UI:** `https://localhost` (or LAN IP)
|
||||
|
||||
### Verification
|
||||
|
||||
```bash
|
||||
# Verify checksums
|
||||
sha256sum -c SHA256SUMS
|
||||
|
||||
# Verify GPG signature (if available)
|
||||
gpg --verify SHA256SUMS.gpg
|
||||
```
|
||||
|
||||
### Features
|
||||
|
||||
- **All SecuBox packages** pre-installed (40+ security modules)
|
||||
- **Persistent storage** partition for configuration
|
||||
- **Kiosk mode** with Chromium dashboard
|
||||
- **Console TUI** for headless operation
|
||||
- **Network auto-detection** (DHCP on all interfaces)
|
||||
|
||||
draft: false
|
||||
prerelease: ${{ contains(github.ref, 'alpha') || contains(github.ref, 'beta') || contains(github.ref, 'rc') }}
|
||||
190
.gitea/workflows/build-eye-remote.yml
Normal file
190
.gitea/workflows/build-eye-remote.yml
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
name: Build Eye Remote Image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
wifi_ssid:
|
||||
description: 'WiFi SSID (optional)'
|
||||
required: false
|
||||
default: ''
|
||||
wifi_psk:
|
||||
description: 'WiFi password (optional)'
|
||||
required: false
|
||||
default: ''
|
||||
hostname:
|
||||
description: 'Device hostname'
|
||||
required: false
|
||||
default: 'secubox-round'
|
||||
create_release:
|
||||
description: 'Create GitHub release'
|
||||
required: false
|
||||
default: 'false'
|
||||
type: choice
|
||||
options:
|
||||
- 'true'
|
||||
- 'false'
|
||||
push:
|
||||
tags:
|
||||
- 'eye-remote-v*'
|
||||
paths:
|
||||
- 'remote-ui/round/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'remote-ui/round/**'
|
||||
|
||||
env:
|
||||
VERSION: '2.2.1'
|
||||
RPI_OS_URL: 'https://downloads.raspberrypi.com/raspios_lite_armhf/images/raspios_lite_armhf-2024-11-19/2024-11-19-raspios-bookworm-armhf-lite.img.xz'
|
||||
|
||||
jobs:
|
||||
# Run menu system tests first
|
||||
test-menu-system:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install test dependencies
|
||||
run: |
|
||||
pip install pytest pytest-asyncio pillow
|
||||
|
||||
- name: Run menu system tests
|
||||
run: |
|
||||
cd remote-ui/round
|
||||
python -m pytest tests/ -v --tb=short
|
||||
continue-on-error: true # Don't block build on test failures for now
|
||||
|
||||
build-eye-remote:
|
||||
needs: test-menu-system
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Free disk space
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc
|
||||
df -h
|
||||
|
||||
- name: Install build tools
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq \
|
||||
parted dosfstools e2fsprogs \
|
||||
xz-utils wget rsync \
|
||||
qemu-user-static binfmt-support
|
||||
|
||||
# Ensure ARM binfmt is registered
|
||||
sudo systemctl restart binfmt-support || true
|
||||
sudo update-binfmts --enable qemu-arm || true
|
||||
|
||||
# Verify QEMU is working
|
||||
if [ -f /proc/sys/fs/binfmt_misc/qemu-arm ]; then
|
||||
echo "✓ QEMU ARM binfmt registered"
|
||||
else
|
||||
echo "⚠ QEMU ARM binfmt not found, attempting manual registration"
|
||||
sudo update-binfmts --import qemu-arm || true
|
||||
fi
|
||||
|
||||
- name: Download Raspberry Pi OS Lite
|
||||
run: |
|
||||
echo "Downloading Raspberry Pi OS Lite (armhf)..."
|
||||
wget -q -O /tmp/raspios-lite.img.xz "${{ env.RPI_OS_URL }}"
|
||||
ls -lh /tmp/raspios-lite.img.xz
|
||||
|
||||
- name: Build Eye Remote image (OFFLINE MODE)
|
||||
run: |
|
||||
cd remote-ui/round
|
||||
|
||||
# Build options
|
||||
BUILD_ARGS="-i /tmp/raspios-lite.img.xz -o /tmp"
|
||||
|
||||
if [ -n "${{ github.event.inputs.wifi_ssid }}" ]; then
|
||||
BUILD_ARGS="$BUILD_ARGS -s '${{ github.event.inputs.wifi_ssid }}' -p '${{ github.event.inputs.wifi_psk }}'"
|
||||
fi
|
||||
|
||||
HOSTNAME="${{ github.event.inputs.hostname || 'secubox-round' }}"
|
||||
BUILD_ARGS="$BUILD_ARGS -h $HOSTNAME"
|
||||
|
||||
echo "Building with: $BUILD_ARGS"
|
||||
sudo bash ./build-eye-remote-image.sh $BUILD_ARGS
|
||||
|
||||
sudo chown $(id -u):$(id -g) /tmp/secubox-eye-remote-*.img
|
||||
timeout-minutes: 60
|
||||
|
||||
- name: Compress image
|
||||
run: |
|
||||
cd /tmp
|
||||
echo "Compressing Eye Remote image..."
|
||||
xz -9 -v secubox-eye-remote-${{ env.VERSION }}.img
|
||||
ls -lh secubox-eye-remote-${{ env.VERSION }}.img.xz
|
||||
|
||||
- name: Generate checksums
|
||||
run: |
|
||||
cd /tmp
|
||||
sha256sum secubox-eye-remote-${{ env.VERSION }}.img.xz > secubox-eye-remote-${{ env.VERSION }}.sha256
|
||||
cat secubox-eye-remote-${{ env.VERSION }}.sha256
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: secubox-eye-remote-${{ env.VERSION }}
|
||||
path: |
|
||||
/tmp/secubox-eye-remote-${{ env.VERSION }}.img.xz
|
||||
/tmp/secubox-eye-remote-${{ env.VERSION }}.sha256
|
||||
retention-days: 30
|
||||
|
||||
- name: Upload to release (on tag or manual)
|
||||
if: startsWith(github.ref, 'refs/tags/eye-remote-v') || github.event.inputs.create_release == 'true'
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || format('eye-remote-v{0}', env.VERSION) }}
|
||||
name: Eye Remote v${{ env.VERSION }}
|
||||
files: |
|
||||
/tmp/secubox-eye-remote-${{ env.VERSION }}.img.xz
|
||||
/tmp/secubox-eye-remote-${{ env.VERSION }}.sha256
|
||||
body: |
|
||||
|
||||
## Eye Remote v${{ env.VERSION }} — Radial Menu Edition
|
||||
|
||||
### 🎯 What's New
|
||||
- **Radial Menu System** — 6-slice pie menu for touchscreen control
|
||||
- **Touch Handler** — Full gesture support (tap, long-press, swipe)
|
||||
- **Action Executor** — Modular command dispatcher
|
||||
- **Local Settings API** — Display brightness, network config, system info
|
||||
|
||||
### 📦 Image Details
|
||||
|
||||
| Image | Hardware | Description |
|
||||
|-------|----------|-------------|
|
||||
| `secubox-eye-remote-${{ env.VERSION }}.img.xz` | RPi Zero W + HyperPixel 2.1 Round | USB Gadget Controller |
|
||||
|
||||
**🔌 OFFLINE MODE:** All packages pre-installed. No internet required at boot!
|
||||
|
||||
### 🎮 Radial Menu Gestures
|
||||
| Gesture | Action |
|
||||
|---------|--------|
|
||||
| Long-press center | Enter menu mode |
|
||||
| Tap slice | Select item |
|
||||
| Tap center | Go back |
|
||||
| 3-finger tap | Emergency exit |
|
||||
|
||||
### 💾 Flash to SD card
|
||||
```bash
|
||||
xzcat secubox-eye-remote-${{ env.VERSION }}.img.xz | sudo dd of=/dev/sdX bs=4M status=progress
|
||||
```
|
||||
|
||||
### 🔐 Default credentials
|
||||
- User: `pi`
|
||||
- Password: `raspberry`
|
||||
|
||||
### ⏱️ First boot
|
||||
~60s (no package download needed)
|
||||
|
||||
See [Eye-Remote Wiki](https://github.com/CyberMind-FR/secubox-deb/wiki/Eye-Remote) for full documentation.
|
||||
274
.gitea/workflows/build-image.yml
Normal file
274
.gitea/workflows/build-image.yml
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
name: Build SecuBox System Images
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
board:
|
||||
description: 'Target board'
|
||||
required: false
|
||||
type: string
|
||||
default: 'all'
|
||||
size:
|
||||
description: 'Image size (empty = use board default)'
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
secrets:
|
||||
GPG_PRIVATE_KEY:
|
||||
required: false
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
board:
|
||||
description: 'Target board'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- mochabin
|
||||
- espressobin-v7
|
||||
- espressobin-ultra
|
||||
- vm-x64
|
||||
- vm-arm64
|
||||
- rpi400
|
||||
- all
|
||||
default: vm-x64
|
||||
size:
|
||||
description: 'Image size (empty = use board default)'
|
||||
required: false
|
||||
default: ''
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
env:
|
||||
DEBIAN_SUITE: bookworm
|
||||
|
||||
jobs:
|
||||
# Build system images
|
||||
build-image:
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Handle all event types: push (tags), workflow_call, workflow_dispatch
|
||||
board: ${{ (github.event_name == 'push' || (inputs.board == 'all' || inputs.board == '')) && fromJson('["mochabin","espressobin-v7","espressobin-ultra","vm-x64","rpi400"]') || (github.event.inputs.board == 'all' && fromJson('["mochabin","espressobin-v7","espressobin-ultra","vm-x64","rpi400"]') || fromJson(format('["{0}"]', inputs.board || github.event.inputs.board || 'vm-x64'))) }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Free disk space
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc
|
||||
df -h
|
||||
|
||||
- name: Setup QEMU + binfmt
|
||||
if: contains(matrix.board, 'arm') || contains(matrix.board, 'espresso') || contains(matrix.board, 'mocha')
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
platforms: arm64
|
||||
|
||||
- name: Install build tools
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq \
|
||||
debootstrap qemu-user-static binfmt-support \
|
||||
parted dosfstools e2fsprogs rsync \
|
||||
gzip pigz xz-utils
|
||||
|
||||
- name: Download package artifacts
|
||||
uses: dawidd6/action-download-artifact@v3
|
||||
with:
|
||||
workflow: build-packages.yml
|
||||
name: secubox-debs-all
|
||||
path: output/debs/
|
||||
if_no_artifact_found: warn
|
||||
continue-on-error: true
|
||||
|
||||
- name: List available packages
|
||||
run: |
|
||||
echo "=== SecuBox packages to slipstream ==="
|
||||
ls output/debs/secubox-*.deb 2>/dev/null | wc -l || echo "0"
|
||||
ls output/debs/secubox-*.deb 2>/dev/null | xargs -I{} basename {} | sed 's/_.*$//' | sort -u || echo "No packages found"
|
||||
|
||||
- name: Determine architecture
|
||||
id: arch
|
||||
run: |
|
||||
case "${{ matrix.board }}" in
|
||||
vm-x64)
|
||||
echo "arch=amd64" >> $GITHUB_OUTPUT
|
||||
echo "is_arm=false" >> $GITHUB_OUTPUT
|
||||
;;
|
||||
*)
|
||||
echo "arch=arm64" >> $GITHUB_OUTPUT
|
||||
echo "is_arm=true" >> $GITHUB_OUTPUT
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Build image for ${{ matrix.board }}
|
||||
env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
run: |
|
||||
# Use slipstream if packages are available
|
||||
SLIPSTREAM_OPT=""
|
||||
if ls output/debs/secubox-*.deb >/dev/null 2>&1; then
|
||||
SLIPSTREAM_OPT="--slipstream"
|
||||
echo "Slipstream enabled: $(ls output/debs/secubox-*.deb | wc -l) packages"
|
||||
fi
|
||||
|
||||
# Build with board-specific size (from board/*/config.mk)
|
||||
SIZE_OPT=""
|
||||
if [ -n "${{ inputs.size || github.event.inputs.size }}" ]; then
|
||||
SIZE_OPT="--size ${{ inputs.size || github.event.inputs.size }}"
|
||||
fi
|
||||
|
||||
sudo -E bash image/build-image.sh \
|
||||
--board ${{ matrix.board }} \
|
||||
--suite ${{ env.DEBIAN_SUITE }} \
|
||||
--out output/ \
|
||||
$SIZE_OPT \
|
||||
$SLIPSTREAM_OPT
|
||||
sudo chown -R $(id -u):$(id -g) output/
|
||||
timeout-minutes: 90
|
||||
|
||||
- name: List output
|
||||
run: |
|
||||
ls -lh output/
|
||||
|
||||
- name: Generate checksums
|
||||
run: |
|
||||
cd output/
|
||||
sha256sum *.img.gz > SHA256SUMS 2>/dev/null || echo "No images found" > SHA256SUMS
|
||||
cat SHA256SUMS
|
||||
|
||||
- name: Upload image artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: secubox-${{ matrix.board }}-${{ env.DEBIAN_SUITE }}
|
||||
path: |
|
||||
output/*.img.gz
|
||||
output/SHA256SUMS
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
|
||||
# Collect all images and create release
|
||||
release:
|
||||
needs: build-image
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
|
||||
steps:
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: images/
|
||||
merge-multiple: true
|
||||
|
||||
- name: Generate combined checksums
|
||||
run: |
|
||||
cd images/
|
||||
# Remove individual SHA256SUMS files
|
||||
rm -f SHA256SUMS
|
||||
# Generate combined checksums
|
||||
sha256sum *.img.gz 2>/dev/null | sort > SHA256SUMS || echo "No images" > SHA256SUMS
|
||||
cat SHA256SUMS
|
||||
|
||||
- name: Sign checksums
|
||||
env:
|
||||
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
run: |
|
||||
if [ -n "$GPG_PRIVATE_KEY" ]; then
|
||||
echo "$GPG_PRIVATE_KEY" | gpg --batch --import
|
||||
cd images/
|
||||
gpg --clearsign SHA256SUMS
|
||||
mv SHA256SUMS.asc SHA256SUMS.gpg
|
||||
fi
|
||||
|
||||
- name: Checkout for scripts
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: image
|
||||
|
||||
- name: Delete existing release assets (if any)
|
||||
run: |
|
||||
VERSION="${{ github.ref_name }}"
|
||||
if gh release view "$VERSION" &>/dev/null; then
|
||||
echo "Release $VERSION exists, deleting duplicate assets..."
|
||||
for file in images/*.img.gz images/SHA256SUMS images/SHA256SUMS.gpg image/create-qemu-arm64-vm.sh image/create-vbox-vm.sh; do
|
||||
[ -f "$file" ] || continue
|
||||
name=$(basename "$file")
|
||||
gh release delete-asset "$VERSION" "$name" --yes 2>/dev/null || true
|
||||
done
|
||||
fi
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
images/*.img.gz
|
||||
images/SHA256SUMS
|
||||
images/SHA256SUMS.gpg
|
||||
image/create-qemu-arm64-vm.sh
|
||||
image/create-vbox-vm.sh
|
||||
body: |
|
||||
## SecuBox-DEB ${{ github.ref_name }} System Images
|
||||
|
||||
Flashable system images for SecuBox appliances.
|
||||
|
||||
### Available Images
|
||||
|
||||
| Image | Board | Architecture | Description |
|
||||
|-------|-------|--------------|-------------|
|
||||
| `secubox-mochabin-bookworm.img.gz` | MOCHAbin | arm64 | Marvell Armada 7040 (Pro) |
|
||||
| `secubox-espressobin-v7-bookworm.img.gz` | ESPRESSObin v7 | arm64 | Marvell Armada 3720 (Lite) |
|
||||
| `secubox-espressobin-ultra-bookworm.img.gz` | ESPRESSObin Ultra | arm64 | Marvell Armada 3720 (Lite+) |
|
||||
| `secubox-rpi400-bookworm.img.gz` | Raspberry Pi 400 | arm64 | Pi 400 / Pi 4 |
|
||||
| `secubox-vm-x64-bookworm.img.gz` | VirtualBox/QEMU | amd64 | VM for testing |
|
||||
| `create-qemu-arm64-vm.sh` | QEMU ARM64 | script | Run ARM64 on x86 hosts |
|
||||
|
||||
### Installation
|
||||
|
||||
**ARM64 boards (MOCHAbin, ESPRESSObin):**
|
||||
```bash
|
||||
# Flash to SD card or eMMC
|
||||
gunzip -c secubox-mochabin-bookworm.img.gz | sudo dd of=/dev/sdX bs=4M status=progress
|
||||
sync
|
||||
```
|
||||
|
||||
**VirtualBox (x64):**
|
||||
```bash
|
||||
# Convert to VDI
|
||||
gunzip secubox-vm-x64-bookworm.img.gz
|
||||
VBoxManage convertfromraw secubox-vm-x64-bookworm.img secubox.vdi --format VDI
|
||||
|
||||
# Create VM
|
||||
VBoxManage createvm --name SecuBox --ostype Debian_64 --register
|
||||
VBoxManage modifyvm SecuBox --memory 2048 --cpus 2 --nic1 nat
|
||||
VBoxManage storagectl SecuBox --name SATA --add sata
|
||||
VBoxManage storageattach SecuBox --storagectl SATA --port 0 --device 0 --type hdd --medium secubox.vdi
|
||||
```
|
||||
|
||||
### Verification
|
||||
|
||||
```bash
|
||||
sha256sum -c SHA256SUMS
|
||||
gpg --verify SHA256SUMS.gpg
|
||||
```
|
||||
|
||||
### Default Credentials
|
||||
|
||||
- **SSH:** root / (SSH key only, set during firstboot)
|
||||
- **Web UI:** https://secubox.local:8443 (admin / generated at firstboot)
|
||||
|
||||
### Included Packages
|
||||
|
||||
**124 SecuBox packages** across all security domains:
|
||||
- **Core:** hub, core, portal, system, console
|
||||
- **Security:** crowdsec, waf, auth, nac, threats, ipblock, mac-guard
|
||||
- **Networking:** wireguard, haproxy, dpi, qos, netmodes, vhost, cdn
|
||||
- **Applications:** mail, gitea, nextcloud, ollama, jellyfin, matrix
|
||||
- **SOC:** soc-agent, soc-gateway, soc-web
|
||||
- **Intel:** device-intel, vortex-dns, vortex-firewall, ai-insights
|
||||
draft: false
|
||||
prerelease: ${{ contains(github.ref, 'alpha') || contains(github.ref, 'beta') || contains(github.ref, 'rc') }}
|
||||
351
.gitea/workflows/build-installer-iso.yml
Normal file
351
.gitea/workflows/build-installer-iso.yml
Normal file
|
|
@ -0,0 +1,351 @@
|
|||
# SecuBox — Installer ISO Build Workflow
|
||||
# Builds hybrid live/installer ISO with wiki documentation
|
||||
name: Build Installer ISO
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
preseed:
|
||||
description: 'Include preseed config'
|
||||
type: boolean
|
||||
default: false
|
||||
name:
|
||||
description: 'ISO name prefix'
|
||||
required: false
|
||||
default: 'secubox-installer'
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_run:
|
||||
workflows: ["Build SecuBox .deb packages"]
|
||||
types: [completed]
|
||||
branches: [main, master]
|
||||
|
||||
env:
|
||||
DEBIAN_SUITE: bookworm
|
||||
|
||||
jobs:
|
||||
build-iso:
|
||||
runs-on: ubuntu-24.04
|
||||
if: ${{ github.event_name == 'workflow_dispatch' || github.event_name == 'push' || github.event.workflow_run.conclusion == 'success' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Free disk space
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /usr/local/share/boost
|
||||
sudo apt-get clean
|
||||
df -h
|
||||
|
||||
- name: Download package artifacts
|
||||
if: github.event_name == 'workflow_run'
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: secubox-debs-all
|
||||
path: output/debs/
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
continue-on-error: true
|
||||
|
||||
- name: Download packages from latest build
|
||||
if: github.event_name != 'workflow_run'
|
||||
uses: dawidd6/action-download-artifact@v3
|
||||
with:
|
||||
workflow: build-packages.yml
|
||||
name: secubox-debs-all
|
||||
path: output/debs/
|
||||
if_no_artifact_found: warn
|
||||
continue-on-error: true
|
||||
|
||||
- name: List available packages
|
||||
run: |
|
||||
echo "=== Available .deb packages ==="
|
||||
ls -lh output/debs/*.deb 2>/dev/null || echo "No packages found"
|
||||
echo "=== Package count ==="
|
||||
ls output/debs/*.deb 2>/dev/null | wc -l || echo "0"
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq \
|
||||
debootstrap parted dosfstools e2fsprogs \
|
||||
squashfs-tools grub-efi-amd64-bin grub-pc-bin \
|
||||
xorriso mtools rsync curl wget \
|
||||
keyboard-configuration console-setup kbd locales
|
||||
|
||||
- name: Build Installer ISO
|
||||
run: |
|
||||
SLIPSTREAM_OPT=""
|
||||
if ls output/debs/*.deb >/dev/null 2>&1; then
|
||||
SLIPSTREAM_OPT="--slipstream"
|
||||
echo "Slipstream enabled: $(ls output/debs/*.deb | wc -l) packages"
|
||||
fi
|
||||
|
||||
ISO_NAME="${{ github.event.inputs.name || 'secubox-installer' }}"
|
||||
|
||||
sudo bash image/build-installer-iso.sh \
|
||||
--name "$ISO_NAME" \
|
||||
$SLIPSTREAM_OPT
|
||||
sudo chown -R $(id -u):$(id -g) output/
|
||||
timeout-minutes: 60
|
||||
|
||||
- name: Generate checksums
|
||||
run: |
|
||||
cd output/
|
||||
sha256sum *.iso *.img 2>/dev/null > SHA256SUMS || true
|
||||
cat SHA256SUMS
|
||||
|
||||
- name: List output
|
||||
run: |
|
||||
ls -lh output/
|
||||
echo "=== ISO Info ==="
|
||||
file output/*.iso 2>/dev/null || true
|
||||
|
||||
- name: Upload ISO artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: secubox-installer-iso-amd64
|
||||
path: |
|
||||
output/*.iso
|
||||
output/*.img
|
||||
output/SHA256SUMS
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
|
||||
# Build wiki documentation
|
||||
build-wiki:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-iso
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Checkout wiki
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ github.repository }}.wiki
|
||||
path: wiki-repo
|
||||
continue-on-error: true
|
||||
|
||||
- name: Generate API documentation
|
||||
run: |
|
||||
mkdir -p wiki-output
|
||||
|
||||
# Copy existing wiki docs
|
||||
if [ -d "wiki" ]; then
|
||||
cp -r wiki/* wiki-output/
|
||||
fi
|
||||
|
||||
# Copy docs
|
||||
if [ -d "docs" ]; then
|
||||
cp -r docs/*.md wiki-output/ 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Generate module list
|
||||
cat > wiki-output/Modules.md << 'EOF'
|
||||
# SecuBox Modules
|
||||
|
||||
## Core
|
||||
| Module | Description | API Endpoints |
|
||||
|--------|-------------|---------------|
|
||||
| secubox-hub | Central dashboard | /api/v1/hub/* |
|
||||
| secubox-core | Shared library | - |
|
||||
| secubox-portal | Login portal | /api/v1/portal/* |
|
||||
| secubox-system | System management | /api/v1/system/* |
|
||||
|
||||
## Security
|
||||
| Module | Description | API Endpoints |
|
||||
|--------|-------------|---------------|
|
||||
| secubox-crowdsec | Intrusion prevention | /api/v1/crowdsec/* |
|
||||
| secubox-waf | Web application firewall | /api/v1/waf/* |
|
||||
| secubox-auth | OAuth2/OIDC | /api/v1/auth/* |
|
||||
| secubox-nac | Network access control | /api/v1/nac/* |
|
||||
|
||||
## Network
|
||||
| Module | Description | API Endpoints |
|
||||
|--------|-------------|---------------|
|
||||
| secubox-wireguard | VPN management | /api/v1/wireguard/* |
|
||||
| secubox-haproxy | Load balancer | /api/v1/haproxy/* |
|
||||
| secubox-netmodes | Network modes | /api/v1/netmodes/* |
|
||||
| secubox-qos | Traffic shaping | /api/v1/qos/* |
|
||||
| secubox-dpi | Deep packet inspection | /api/v1/dpi/* |
|
||||
| secubox-vhost | Virtual hosts | /api/v1/vhost/* |
|
||||
| secubox-cdn | CDN cache | /api/v1/cdn/* |
|
||||
|
||||
## Applications
|
||||
| Module | Description | API Endpoints |
|
||||
|--------|-------------|---------------|
|
||||
| secubox-mail | Email server | /api/v1/mail/* |
|
||||
| secubox-dns | DNS management | /api/v1/dns/* |
|
||||
| secubox-users | User management | /api/v1/users/* |
|
||||
| secubox-gitea | Git server | /api/v1/gitea/* |
|
||||
| secubox-nextcloud | File sync | /api/v1/nextcloud/* |
|
||||
|
||||
## Monitoring
|
||||
| Module | Description | API Endpoints |
|
||||
|--------|-------------|---------------|
|
||||
| secubox-netdata | Real-time metrics | /api/v1/netdata/* |
|
||||
| secubox-metrics | Dashboard metrics | /api/v1/metrics/* |
|
||||
| secubox-soc | Security operations | /api/v1/soc/* |
|
||||
|
||||
EOF
|
||||
|
||||
- name: Upload wiki artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: secubox-wiki
|
||||
path: wiki-output/
|
||||
retention-days: 30
|
||||
|
||||
# Create release on tag
|
||||
release:
|
||||
needs: [build-iso, build-wiki]
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download ISO artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: secubox-installer-iso-amd64
|
||||
path: release/
|
||||
|
||||
- name: Download wiki artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: secubox-wiki
|
||||
path: wiki/
|
||||
|
||||
- name: Compress ISO
|
||||
run: |
|
||||
cd release/
|
||||
for f in *.iso *.img; do
|
||||
[ -f "$f" ] && gzip -9 "$f" && echo "Compressed: $f"
|
||||
done
|
||||
ls -lh
|
||||
|
||||
- name: Generate final checksums
|
||||
run: |
|
||||
cd release/
|
||||
sha256sum *.gz 2>/dev/null > SHA256SUMS || true
|
||||
cat SHA256SUMS
|
||||
|
||||
- name: Sign checksums
|
||||
env:
|
||||
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
run: |
|
||||
if [ -n "$GPG_PRIVATE_KEY" ]; then
|
||||
echo "$GPG_PRIVATE_KEY" | gpg --batch --import
|
||||
cd release/
|
||||
gpg --clearsign SHA256SUMS
|
||||
mv SHA256SUMS.asc SHA256SUMS.gpg
|
||||
fi
|
||||
|
||||
- name: Get version info
|
||||
id: version
|
||||
run: |
|
||||
VERSION="${{ github.ref_name }}"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "date=$(date -u +%Y-%m-%d)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Delete existing release assets (if any)
|
||||
run: |
|
||||
VERSION="${{ github.ref_name }}"
|
||||
if gh release view "$VERSION" &>/dev/null; then
|
||||
echo "Release $VERSION exists, deleting duplicate assets..."
|
||||
for file in release/*.iso.gz release/*.img.gz release/SHA256SUMS release/SHA256SUMS.gpg; do
|
||||
[ -f "$file" ] || continue
|
||||
name=$(basename "$file")
|
||||
gh release delete-asset "$VERSION" "$name" --yes 2>/dev/null || true
|
||||
done
|
||||
fi
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
name: SecuBox Installer ${{ steps.version.outputs.version }}
|
||||
body: |
|
||||
## SecuBox Installer ISO ${{ steps.version.outputs.version }}
|
||||
|
||||
Release date: ${{ steps.version.outputs.date }}
|
||||
|
||||
### Downloads
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `secubox-installer-amd64-bookworm.iso.gz` | Bootable ISO (UEFI/BIOS) |
|
||||
| `secubox-installer-amd64-bookworm.img.gz` | Raw USB image |
|
||||
|
||||
### Features
|
||||
|
||||
- **Hybrid Boot**: Works as Live USB or Installer
|
||||
- **UEFI + Legacy BIOS**: Compatible with all systems
|
||||
- **French AZERTY keyboard**: Pre-configured
|
||||
- **DHCP networking**: Auto-configured on boot
|
||||
- **51 SecuBox packages**: Pre-installed
|
||||
- **Preseed support**: Clone existing installations
|
||||
|
||||
### Boot Options
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| SecuBox Live | Boot live system with persistence |
|
||||
| SecuBox Install | Headless auto-install to first disk |
|
||||
| SecuBox Live (To RAM) | Load entire system to memory |
|
||||
| SecuBox Debug (Rescue) | Minimal rescue shell |
|
||||
|
||||
### Flash to USB
|
||||
|
||||
```bash
|
||||
# Download and extract
|
||||
gunzip secubox-installer-amd64-bookworm.img.gz
|
||||
|
||||
# Flash (replace sdX with your USB device)
|
||||
sudo dd if=secubox-installer-amd64-bookworm.img of=/dev/sdX bs=4M status=progress conv=fsync
|
||||
```
|
||||
|
||||
### Credentials
|
||||
|
||||
| Service | Username | Password |
|
||||
|---------|----------|----------|
|
||||
| SSH | root | secubox |
|
||||
| SSH | secubox | secubox |
|
||||
| Web UI | admin | admin |
|
||||
|
||||
### Verification
|
||||
|
||||
```bash
|
||||
sha256sum -c SHA256SUMS
|
||||
gpg --verify SHA256SUMS.gpg
|
||||
```
|
||||
|
||||
### Documentation
|
||||
|
||||
- [Installation Guide](https://github.com/${{ github.repository }}/wiki/Installation)
|
||||
- [API Reference](https://github.com/${{ github.repository }}/wiki/API-Reference)
|
||||
- [Module Documentation](https://github.com/${{ github.repository }}/wiki/Modules)
|
||||
|
||||
files: |
|
||||
release/*.iso.gz
|
||||
release/*.img.gz
|
||||
release/SHA256SUMS
|
||||
release/SHA256SUMS.gpg
|
||||
draft: false
|
||||
prerelease: ${{ contains(github.ref, 'alpha') || contains(github.ref, 'beta') || contains(github.ref, 'rc') }}
|
||||
|
||||
- name: Update wiki
|
||||
if: success()
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
echo "Wiki artifacts ready for manual deployment"
|
||||
ls -la wiki/
|
||||
37
.gitea/workflows/build-live-usb.yml
Normal file
37
.gitea/workflows/build-live-usb.yml
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# DEPRECATED: Use build-all-live-usb.yml for all live USB builds
|
||||
# This workflow is maintained for backward compatibility only.
|
||||
# It calls the unified workflow with platform=x64.
|
||||
|
||||
name: Build SecuBox Live USB (x64 only - DEPRECATED)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
compress:
|
||||
description: 'Compress output (gzip)'
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
jobs:
|
||||
# Redirect to unified workflow
|
||||
build-x64-live-usb:
|
||||
uses: ./.github/workflows/build-all-live-usb.yml
|
||||
with:
|
||||
platform: x64
|
||||
secrets: inherit
|
||||
|
||||
# Note about deprecation
|
||||
deprecation-notice:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Deprecation Warning
|
||||
run: |
|
||||
echo "::warning::This workflow is DEPRECATED. Use build-all-live-usb.yml instead."
|
||||
echo ""
|
||||
echo "The unified workflow supports all platforms:"
|
||||
echo " - x64 (amd64) - GRUB UEFI/BIOS"
|
||||
echo " - espressobin-v7 (arm64) - U-Boot"
|
||||
echo " - rpi400 (arm64) - Pi firmware"
|
||||
echo ""
|
||||
echo "To build all platforms:"
|
||||
echo " gh workflow run build-all-live-usb.yml -f platform=all"
|
||||
280
.gitea/workflows/build-multiboot.yml
Normal file
280
.gitea/workflows/build-multiboot.yml
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
name: Build Multiboot Live Image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
image_size:
|
||||
description: 'Image size in GB'
|
||||
required: false
|
||||
default: '16'
|
||||
type: choice
|
||||
options:
|
||||
- '8'
|
||||
- '16'
|
||||
- '32'
|
||||
include_desktop:
|
||||
description: 'Include desktop environment'
|
||||
required: false
|
||||
default: 'false'
|
||||
type: choice
|
||||
options:
|
||||
- 'true'
|
||||
- 'false'
|
||||
create_release:
|
||||
description: 'Create GitHub release'
|
||||
required: false
|
||||
default: 'false'
|
||||
type: choice
|
||||
options:
|
||||
- 'true'
|
||||
- 'false'
|
||||
prerelease:
|
||||
description: 'Mark as prerelease'
|
||||
required: false
|
||||
default: 'true'
|
||||
type: choice
|
||||
options:
|
||||
- 'true'
|
||||
- 'false'
|
||||
push:
|
||||
tags:
|
||||
- 'multiboot-v*'
|
||||
paths:
|
||||
- 'image/multiboot/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'image/multiboot/**'
|
||||
|
||||
env:
|
||||
VERSION: '2.2.4-pre1'
|
||||
|
||||
jobs:
|
||||
# Build all .deb packages first
|
||||
build-packages:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Free disk space
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache
|
||||
df -h
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq \
|
||||
build-essential devscripts debhelper \
|
||||
dh-python python3-all python3-setuptools \
|
||||
crossbuild-essential-arm64
|
||||
|
||||
- name: Build SecuBox packages
|
||||
run: |
|
||||
mkdir -p output/debs
|
||||
|
||||
# Build each package
|
||||
for pkg in packages/secubox-*/; do
|
||||
if [ -d "$pkg/debian" ]; then
|
||||
echo "Building $(basename $pkg)..."
|
||||
cd "$pkg"
|
||||
dpkg-buildpackage -us -uc -b --host-arch=arm64 2>/dev/null || \
|
||||
dpkg-buildpackage -us -uc -b 2>/dev/null || \
|
||||
echo "WARNING: Failed to build $(basename $pkg)"
|
||||
cd - >/dev/null
|
||||
fi
|
||||
done
|
||||
|
||||
# Collect all .deb files
|
||||
find packages/ -name "*.deb" -exec cp {} output/debs/ \;
|
||||
ls -la output/debs/ || echo "No packages built"
|
||||
|
||||
- name: Upload packages artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: secubox-packages
|
||||
path: output/debs/
|
||||
retention-days: 1
|
||||
|
||||
# Build the multiboot image
|
||||
build-multiboot:
|
||||
needs: build-packages
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Free disk space
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache
|
||||
df -h
|
||||
|
||||
- name: Install build tools
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq \
|
||||
parted dosfstools e2fsprogs \
|
||||
debootstrap qemu-user-static binfmt-support \
|
||||
squashfs-tools xz-utils rsync grub-efi-amd64-bin \
|
||||
grub-pc-bin u-boot-tools
|
||||
|
||||
# Ensure ARM64 binfmt is registered
|
||||
sudo systemctl restart binfmt-support || true
|
||||
sudo update-binfmts --enable qemu-aarch64 || true
|
||||
|
||||
# Verify QEMU is working
|
||||
if [ -f /proc/sys/fs/binfmt_misc/qemu-aarch64 ]; then
|
||||
echo "✓ QEMU ARM64 binfmt registered"
|
||||
else
|
||||
echo "⚠ QEMU ARM64 binfmt not found, attempting manual registration"
|
||||
sudo update-binfmts --import qemu-aarch64 || true
|
||||
fi
|
||||
|
||||
- name: Download packages artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: secubox-packages
|
||||
path: output/debs/
|
||||
|
||||
- name: List available packages
|
||||
run: |
|
||||
echo "SecuBox packages available for slipstream:"
|
||||
ls -la output/debs/ || echo "No packages found"
|
||||
|
||||
- name: Build multiboot image
|
||||
run: |
|
||||
IMAGE_SIZE="${{ github.event.inputs.image_size || '16' }}"
|
||||
DESKTOP="${{ github.event.inputs.include_desktop || 'false' }}"
|
||||
|
||||
mkdir -p output
|
||||
|
||||
BUILD_ARGS="--output output/secubox-multiboot-${{ env.VERSION }}.img"
|
||||
BUILD_ARGS="$BUILD_ARGS --size ${IMAGE_SIZE}G"
|
||||
|
||||
if [ "$DESKTOP" = "true" ]; then
|
||||
BUILD_ARGS="$BUILD_ARGS --desktop"
|
||||
fi
|
||||
|
||||
echo "Building multiboot image with: $BUILD_ARGS"
|
||||
sudo bash image/multiboot/build-multiboot.sh $BUILD_ARGS
|
||||
|
||||
sudo chown $(id -u):$(id -g) output/secubox-multiboot-${{ env.VERSION }}.img
|
||||
timeout-minutes: 120
|
||||
|
||||
- name: Verify boot files
|
||||
run: |
|
||||
# Mount EFI partition and verify kernel files exist
|
||||
LOOP_DEV=$(sudo losetup -f --show -P output/secubox-multiboot-${{ env.VERSION }}.img)
|
||||
sudo mkdir -p /tmp/verify-efi
|
||||
sudo mount "${LOOP_DEV}p1" /tmp/verify-efi
|
||||
|
||||
echo "=== EFI Partition Boot Files ==="
|
||||
ls -la /tmp/verify-efi/
|
||||
|
||||
# Check for required files
|
||||
MISSING=0
|
||||
for f in Image vmlinuz initrd.img initrd-amd64.img boot.scr; do
|
||||
if [ -f "/tmp/verify-efi/$f" ]; then
|
||||
echo "✓ $f present ($(du -h /tmp/verify-efi/$f | cut -f1))"
|
||||
else
|
||||
echo "✗ $f MISSING!"
|
||||
MISSING=1
|
||||
fi
|
||||
done
|
||||
|
||||
# Check DTBs
|
||||
DTB_COUNT=$(ls /tmp/verify-efi/dtbs/marvell/*.dtb 2>/dev/null | wc -l)
|
||||
if [ "$DTB_COUNT" -gt 0 ]; then
|
||||
echo "✓ DTBs present ($DTB_COUNT files)"
|
||||
else
|
||||
echo "✗ DTBs MISSING!"
|
||||
MISSING=1
|
||||
fi
|
||||
|
||||
sudo umount /tmp/verify-efi
|
||||
sudo losetup -d "$LOOP_DEV"
|
||||
|
||||
if [ "$MISSING" -eq 1 ]; then
|
||||
echo "ERROR: Required boot files missing!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Compress image
|
||||
run: |
|
||||
cd output
|
||||
echo "Compressing multiboot image..."
|
||||
xz -9 -v -T0 secubox-multiboot-${{ env.VERSION }}.img
|
||||
ls -lh secubox-multiboot-${{ env.VERSION }}.img.xz
|
||||
|
||||
- name: Generate checksums
|
||||
run: |
|
||||
cd output
|
||||
sha256sum secubox-multiboot-${{ env.VERSION }}.img.xz > secubox-multiboot-${{ env.VERSION }}.sha256
|
||||
cat secubox-multiboot-${{ env.VERSION }}.sha256
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: secubox-multiboot-${{ env.VERSION }}
|
||||
path: |
|
||||
output/secubox-multiboot-${{ env.VERSION }}.img.xz
|
||||
output/secubox-multiboot-${{ env.VERSION }}.sha256
|
||||
retention-days: 30
|
||||
|
||||
- name: Upload to release (on tag or manual)
|
||||
if: startsWith(github.ref, 'refs/tags/multiboot-v') || github.event.inputs.create_release == 'true'
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || format('multiboot-v{0}', env.VERSION) }}
|
||||
name: SecuBox Multiboot v${{ env.VERSION }}
|
||||
prerelease: ${{ github.event.inputs.prerelease == 'true' || contains(env.VERSION, 'pre') }}
|
||||
files: |
|
||||
output/secubox-multiboot-${{ env.VERSION }}.img.xz
|
||||
output/secubox-multiboot-${{ env.VERSION }}.sha256
|
||||
body: |
|
||||
|
||||
## SecuBox Multiboot v${{ env.VERSION }} — Live RAM OS
|
||||
|
||||
### 🎯 What's New
|
||||
- **Dual Boot Menu** — Interactive menu with 5s timeout: Live RAM Boot (default) or Flash to eMMC
|
||||
- **Dual Architecture** — Boot ARM64 (MOCHAbin/ESPRESSObin) or AMD64 (x86_64 PC)
|
||||
- **RAM-based Live OS** — Reduced I/O, ideal for USB gadget devices
|
||||
- **Auto Kernel Install** — ARM64 kernel, DTBs, and initrd properly installed on EFI partition
|
||||
- **SecuBox Pre-installed** — All modules slipstreamed, ready to use
|
||||
- **Shared Data Partition** — Persistent storage accessible from both architectures
|
||||
|
||||
### 📦 Image Layout
|
||||
|
||||
| Partition | Size | Type | Description |
|
||||
|-----------|------|------|-------------|
|
||||
| EFI | 512MB | FAT32 | GRUB + U-Boot for multi-arch boot |
|
||||
| ARM64 | ~4GB | ext4 | Debian bookworm arm64 rootfs |
|
||||
| AMD64 | ~4GB | ext4 | Debian bookworm amd64 rootfs |
|
||||
| Data | ~7GB | ext4 | Shared persistent storage |
|
||||
|
||||
### 🔧 SecuBox Modules Included
|
||||
- secubox-core, secubox-hub
|
||||
- secubox-crowdsec, secubox-haproxy
|
||||
- secubox-system, secubox-hardening
|
||||
- secubox-ipblock, and more...
|
||||
|
||||
### 💾 Flash to SD/USB
|
||||
```bash
|
||||
xzcat secubox-multiboot-${{ env.VERSION }}.img.xz | sudo dd of=/dev/sdX bs=4M status=progress conv=fsync
|
||||
```
|
||||
|
||||
### 🔐 Default credentials
|
||||
- User: `secubox`
|
||||
- Password: `secubox`
|
||||
- Root: `secubox`
|
||||
|
||||
### 🎯 Use Cases
|
||||
- **Demo/Recovery** — Boot from USB to demo or repair SecuBox installations
|
||||
- **Factory Reset** — Clone to eMMC/SD for fresh installations
|
||||
- **Pi Zero Eye Remote** — USB gadget boot media with reduced I/O
|
||||
|
||||
### ⏱️ First boot
|
||||
~90s (all packages pre-installed, no internet required)
|
||||
|
||||
See [Multiboot Wiki](https://github.com/CyberMind-FR/secubox-deb/wiki/Multiboot) for full documentation.
|
||||
341
.gitea/workflows/build-packages.yml
Normal file
341
.gitea/workflows/build-packages.yml
Normal file
|
|
@ -0,0 +1,341 @@
|
|||
name: Build SecuBox .deb packages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, master]
|
||||
tags: ['v*']
|
||||
pull_request:
|
||||
branches: [main, master]
|
||||
workflow_call:
|
||||
secrets:
|
||||
GPG_PRIVATE_KEY:
|
||||
required: false
|
||||
DEPLOY_SSH_KEY:
|
||||
required: false
|
||||
DEPLOY_KNOWN_HOSTS:
|
||||
required: false
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
package:
|
||||
description: 'Package to build (empty = all)'
|
||||
required: false
|
||||
default: ''
|
||||
arch:
|
||||
description: 'Architecture'
|
||||
required: true
|
||||
default: 'amd64'
|
||||
type: choice
|
||||
options:
|
||||
- amd64
|
||||
- arm64
|
||||
- both
|
||||
|
||||
jobs:
|
||||
# Discover all packages dynamically.
|
||||
#
|
||||
# Emits a single flat matrix of {package, arch} objects pre-filtered to skip
|
||||
# arch-all+arm64 combos. This avoids a cross-product expansion that would
|
||||
# exceed GitHub Actions' hard 256-jobs-per-matrix cap once the package
|
||||
# catalog grows past ~128 entries (we hit 134 in v2.9.0).
|
||||
discover:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
packages: ${{ steps.find.outputs.packages }}
|
||||
matrix: ${{ steps.find.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Find packages + build flat matrix
|
||||
id: find
|
||||
env:
|
||||
REQUESTED_ARCH: ${{ github.event.inputs.arch }}
|
||||
REQUESTED_PKG: ${{ github.event.inputs.package }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Still emit the legacy `packages` output for backwards compat.
|
||||
packages=$(find packages/secubox-* -path "*/debian/control" -not -path "*/debian/*/DEBIAN/control" \
|
||||
| xargs dirname | xargs dirname | xargs -n1 basename \
|
||||
| sort | jq -R -s -c 'split("\n") | map(select(length > 0))')
|
||||
echo "packages=$packages" >> "$GITHUB_OUTPUT"
|
||||
echo "Found packages: $packages"
|
||||
|
||||
# Build the flat {package, arch} matrix. Honour the workflow_dispatch
|
||||
# `arch` and `package` filters if set (empty on `push: tags` events).
|
||||
requested_arch="${REQUESTED_ARCH:-}"
|
||||
requested_pkg="${REQUESTED_PKG:-}"
|
||||
|
||||
combos=$(find packages/secubox-* -path "*/debian/control" -not -path "*/debian/*/DEBIAN/control" \
|
||||
| sort | while read -r ctrl; do
|
||||
pkg=$(basename "$(dirname "$(dirname "$ctrl")")")
|
||||
if [ -n "$requested_pkg" ] && [ "$requested_pkg" != "$pkg" ]; then
|
||||
continue
|
||||
fi
|
||||
is_all=0
|
||||
if grep -q "^Architecture:[[:space:]]*all" "$ctrl"; then
|
||||
is_all=1
|
||||
fi
|
||||
# amd64 always, unless arch=arm64 was requested explicitly.
|
||||
if [ -z "$requested_arch" ] || [ "$requested_arch" = "amd64" ]; then
|
||||
echo "{\"package\":\"$pkg\",\"arch\":\"amd64\"}"
|
||||
fi
|
||||
# arm64 only for arch-specific packages, unless arch=amd64 requested.
|
||||
if [ "$is_all" = "0" ] && { [ -z "$requested_arch" ] || [ "$requested_arch" = "arm64" ]; }; then
|
||||
echo "{\"package\":\"$pkg\",\"arch\":\"arm64\"}"
|
||||
fi
|
||||
done | jq -s -c .)
|
||||
echo "matrix=$combos" >> "$GITHUB_OUTPUT"
|
||||
# Print the size so over-cap regressions are visible in the log.
|
||||
echo "Matrix size: $(echo "$combos" | jq 'length') combos"
|
||||
|
||||
# Build packages — one job per {package, arch} entry from discover.
|
||||
build:
|
||||
needs: discover
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include: ${{ fromJson(needs.discover.outputs.matrix) }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Skip if specific package requested
|
||||
if: ${{ github.event.inputs.package != '' && github.event.inputs.package != matrix.package }}
|
||||
run: echo "Skipping ${{ matrix.package }}" && exit 0
|
||||
|
||||
- name: Check if arch-all package
|
||||
id: check-arch
|
||||
run: |
|
||||
if [ -f "packages/${{ matrix.package }}/debian/control" ]; then
|
||||
if grep -q "Architecture: all" "packages/${{ matrix.package }}/debian/control"; then
|
||||
echo "is_arch_all=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "is_arch_all=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
else
|
||||
echo "is_arch_all=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Skip arm64 for arch-all packages
|
||||
if: matrix.arch == 'arm64' && steps.check-arch.outputs.is_arch_all == 'true'
|
||||
run: |
|
||||
echo "⏭ Skipping arm64 build for Architecture: all package"
|
||||
echo " arch-all packages are architecture-independent"
|
||||
exit 0
|
||||
|
||||
- name: Install build dependencies
|
||||
if: "!(matrix.arch == 'arm64' && steps.check-arch.outputs.is_arch_all == 'true')"
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq \
|
||||
build-essential dpkg-dev debhelper devscripts fakeroot \
|
||||
dh-python python3-all python3-setuptools
|
||||
|
||||
- name: Setup Node.js (for soc-web)
|
||||
if: matrix.package == 'secubox-soc-web'
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Build React app (for soc-web)
|
||||
if: matrix.package == 'secubox-soc-web'
|
||||
run: |
|
||||
cd packages/secubox-soc-web
|
||||
npm ci --ignore-scripts || npm install --ignore-scripts
|
||||
npm run build
|
||||
ls -la dist/
|
||||
|
||||
- name: Setup Go (for daemon)
|
||||
if: matrix.package == 'secubox-daemon'
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.22'
|
||||
|
||||
- name: Build Go binaries (for daemon)
|
||||
if: matrix.package == 'secubox-daemon'
|
||||
env:
|
||||
GOOS: linux
|
||||
GOARCH: ${{ matrix.arch == 'arm64' && 'arm64' || 'amd64' }}
|
||||
run: |
|
||||
cd daemon
|
||||
mkdir -p build
|
||||
go build -ldflags "-s -w" -o build/secuboxd ./cmd/secuboxd
|
||||
go build -ldflags "-s -w" -o build/secuboxctl ./cmd/secuboxctl
|
||||
go build -ldflags "-s -w" -o build/c3box ./c3box/backend
|
||||
ls -la build/
|
||||
|
||||
- name: Build ${{ matrix.package }} (${{ matrix.arch }})
|
||||
if: "!(matrix.arch == 'arm64' && steps.check-arch.outputs.is_arch_all == 'true')"
|
||||
run: |
|
||||
cd packages/${{ matrix.package }}
|
||||
|
||||
if [ ! -f debian/control ]; then
|
||||
echo "⚠ debian/control missing — skip"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
dpkg-buildpackage -us -uc -b
|
||||
|
||||
echo "✅ Build OK: ${{ matrix.package }} (${{ matrix.arch }})"
|
||||
|
||||
- name: Collect artifacts
|
||||
if: "!(matrix.arch == 'arm64' && steps.check-arch.outputs.is_arch_all == 'true')"
|
||||
run: |
|
||||
mkdir -p artifacts
|
||||
find packages/ -maxdepth 1 -name "*.deb" -exec cp {} artifacts/ \;
|
||||
ls -lh artifacts/ || echo "No .deb files"
|
||||
|
||||
- name: Upload artifacts
|
||||
if: "!(matrix.arch == 'arm64' && steps.check-arch.outputs.is_arch_all == 'true')"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.package }}-${{ matrix.arch }}
|
||||
path: artifacts/*.deb
|
||||
if-no-files-found: warn
|
||||
retention-days: 7
|
||||
|
||||
# Combine all artifacts. `if: always() && != cancelled` so we still
|
||||
# publish whatever .deb files the matrix succeeded on, even when a few
|
||||
# entries failed (partial-release resilience).
|
||||
collect:
|
||||
needs: build
|
||||
if: ${{ always() && needs.build.result != 'cancelled' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: all-debs/
|
||||
merge-multiple: true
|
||||
|
||||
- name: Generate SHA256SUMS
|
||||
run: |
|
||||
cd all-debs/
|
||||
find . -name "*.deb" -exec mv {} . \; 2>/dev/null || true
|
||||
sha256sum *.deb > SHA256SUMS 2>/dev/null || echo "No debs found"
|
||||
cat SHA256SUMS
|
||||
|
||||
- name: Upload combined artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: secubox-debs-all
|
||||
path: all-debs/
|
||||
retention-days: 30
|
||||
|
||||
# Publish to APT repo (on tag) — partial-release resilient.
|
||||
publish:
|
||||
needs: collect
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ always() && needs.collect.result == 'success' && startsWith(github.ref, 'refs/tags/v') }}
|
||||
environment: production
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download all packages
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: secubox-debs-all
|
||||
path: debs/
|
||||
|
||||
- name: Install reprepro
|
||||
run: sudo apt-get install -y reprepro gnupg
|
||||
|
||||
- name: Import GPG key
|
||||
env:
|
||||
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
run: |
|
||||
if [ -z "$GPG_PRIVATE_KEY" ]; then
|
||||
echo "⚠ GPG_PRIVATE_KEY secret not set - skipping signing"
|
||||
echo "SKIP_SIGN=true" >> $GITHUB_ENV
|
||||
exit 0
|
||||
fi
|
||||
echo "$GPG_PRIVATE_KEY" | gpg --batch --import
|
||||
gpg --list-keys
|
||||
|
||||
- name: Setup repository
|
||||
if: env.SKIP_SIGN != 'true'
|
||||
run: |
|
||||
mkdir -p repo/conf
|
||||
cp repo/conf/distributions repo/conf/ 2>/dev/null || true
|
||||
cat > repo/conf/options << EOF
|
||||
verbose
|
||||
basedir $(pwd)/repo
|
||||
EOF
|
||||
|
||||
- name: Add packages to repository
|
||||
if: env.SKIP_SIGN != 'true'
|
||||
run: |
|
||||
for deb in debs/*.deb; do
|
||||
[ -f "$deb" ] || continue
|
||||
echo "Adding: $(basename $deb)"
|
||||
reprepro -b repo includedeb bookworm "$deb" || true
|
||||
done
|
||||
|
||||
- name: Export public key
|
||||
if: env.SKIP_SIGN != 'true'
|
||||
run: |
|
||||
gpg --armor --export packages@secubox.in > repo/secubox-keyring.gpg
|
||||
|
||||
- name: Deploy to server
|
||||
if: env.SKIP_SIGN != 'true'
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
|
||||
run: |
|
||||
if [ -z "$SSH_PRIVATE_KEY" ]; then
|
||||
echo "⚠ DEPLOY_SSH_KEY not set - skipping deploy"
|
||||
exit 0
|
||||
fi
|
||||
mkdir -p ~/.ssh
|
||||
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
echo "$KNOWN_HOSTS" > ~/.ssh/known_hosts
|
||||
|
||||
rsync -avz --delete \
|
||||
repo/dists/ repo/pool/ repo/secubox-keyring.gpg \
|
||||
deploy@apt.secubox.in:/var/www/apt.secubox.in/
|
||||
|
||||
- name: Delete existing release assets (if any)
|
||||
run: |
|
||||
VERSION="${{ github.ref_name }}"
|
||||
if gh release view "$VERSION" &>/dev/null; then
|
||||
echo "Release $VERSION exists, checking for duplicate assets..."
|
||||
for file in debs/*.deb debs/SHA256SUMS; do
|
||||
[ -f "$file" ] || continue
|
||||
name=$(basename "$file")
|
||||
gh release delete-asset "$VERSION" "$name" --yes 2>/dev/null || true
|
||||
done
|
||||
fi
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
debs/*.deb
|
||||
debs/SHA256SUMS
|
||||
body: |
|
||||
## SecuBox-DEB ${{ github.ref_name }}
|
||||
|
||||
Debian bookworm packages for GlobalScale MOCHAbin / ESPRESSObin (arm64) and x86_64.
|
||||
|
||||
**33 packages** including:
|
||||
- Core infrastructure (hub, core, portal)
|
||||
- Security (crowdsec, waf, auth, nac)
|
||||
- Networking (wireguard, netmodes, dpi, qos)
|
||||
- Applications (mail, gitea, nextcloud, webmail)
|
||||
- Publishing (droplet, streamlit, metablogizer)
|
||||
|
||||
**Installation:**
|
||||
```bash
|
||||
curl -fsSL https://apt.secubox.in/secubox-keyring.gpg | \
|
||||
sudo tee /usr/share/keyrings/secubox.gpg > /dev/null
|
||||
echo "deb [signed-by=/usr/share/keyrings/secubox.gpg] https://apt.secubox.in bookworm main" | \
|
||||
sudo tee /etc/apt/sources.list.d/secubox.list
|
||||
sudo apt update && sudo apt install secubox-full
|
||||
```
|
||||
201
.gitea/workflows/build-secubox-cli.yml
Normal file
201
.gitea/workflows/build-secubox-cli.yml
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
# SecuBox CLI — Build Go Binary
|
||||
# Builds the secubox meta-script generator for linux-amd64 and linux-arm64
|
||||
name: Build SecuBox CLI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, main]
|
||||
paths:
|
||||
- 'cmd/secubox/**'
|
||||
- '.github/workflows/build-secubox-cli.yml'
|
||||
tags: ['v*']
|
||||
pull_request:
|
||||
branches: [master, main]
|
||||
paths:
|
||||
- 'cmd/secubox/**'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version override (e.g., v2.8.0)'
|
||||
required: false
|
||||
|
||||
env:
|
||||
GO_VERSION: '1.22'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
cache-dependency-path: cmd/secubox/go.sum
|
||||
|
||||
- name: Download dependencies
|
||||
working-directory: cmd/secubox
|
||||
run: go mod download
|
||||
|
||||
- name: Run tests
|
||||
working-directory: cmd/secubox
|
||||
run: go test -v -race -coverprofile=coverage.out ./...
|
||||
|
||||
- name: Upload coverage
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage
|
||||
path: cmd/secubox/coverage.out
|
||||
|
||||
build:
|
||||
name: Build
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- goos: linux
|
||||
goarch: amd64
|
||||
suffix: linux-amd64
|
||||
- goos: linux
|
||||
goarch: arm64
|
||||
suffix: linux-arm64
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # For git describe
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
cache-dependency-path: cmd/secubox/go.sum
|
||||
|
||||
- name: Determine version
|
||||
id: version
|
||||
run: |
|
||||
if [ -n "${{ github.event.inputs.version }}" ]; then
|
||||
VERSION="${{ github.event.inputs.version }}"
|
||||
elif [[ "${{ github.ref }}" == refs/tags/* ]]; then
|
||||
VERSION="${{ github.ref_name }}"
|
||||
else
|
||||
VERSION="$(git describe --tags --always --dirty 2>/dev/null || echo 'dev')"
|
||||
fi
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Building version: $VERSION"
|
||||
|
||||
- name: Build binary
|
||||
working-directory: cmd/secubox
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
CGO_ENABLED: 0
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
BUILD_TIME=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
|
||||
COMMIT=$(git rev-parse --short HEAD)
|
||||
|
||||
go build -ldflags="-s -w \
|
||||
-X main.Version=$VERSION \
|
||||
-X main.BuildTime=$BUILD_TIME \
|
||||
-X main.Commit=$COMMIT" \
|
||||
-o secubox-${{ matrix.suffix }} .
|
||||
|
||||
- name: Compress binary
|
||||
working-directory: cmd/secubox
|
||||
run: |
|
||||
tar -czvf secubox-${{ matrix.suffix }}.tar.gz secubox-${{ matrix.suffix }}
|
||||
sha256sum secubox-${{ matrix.suffix }}.tar.gz > secubox-${{ matrix.suffix }}.tar.gz.sha256
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: secubox-${{ matrix.suffix }}
|
||||
path: |
|
||||
cmd/secubox/secubox-${{ matrix.suffix }}.tar.gz
|
||||
cmd/secubox/secubox-${{ matrix.suffix }}.tar.gz.sha256
|
||||
|
||||
release:
|
||||
name: Release
|
||||
needs: build
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts/
|
||||
merge-multiple: true
|
||||
|
||||
- name: Generate release notes
|
||||
run: |
|
||||
cat > RELEASE_NOTES.md << 'EOF'
|
||||
# SecuBox CLI ${{ github.ref_name }}
|
||||
|
||||
Meta-script generator for SecuBox-DEB image building.
|
||||
|
||||
## Features
|
||||
- Profile-based image generation
|
||||
- Board detection and auto-configuration
|
||||
- A/B partition OTA updates
|
||||
- GitHub releases integration
|
||||
|
||||
## Downloads
|
||||
|
||||
| Platform | File | SHA256 |
|
||||
|----------|------|--------|
|
||||
| Linux x64 | `secubox-linux-amd64.tar.gz` | [checksum] |
|
||||
| Linux ARM64 | `secubox-linux-arm64.tar.gz` | [checksum] |
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Linux x64
|
||||
curl -LO https://github.com/CyberMind-FR/secubox-deb/releases/download/${{ github.ref_name }}/secubox-linux-amd64.tar.gz
|
||||
tar xzf secubox-linux-amd64.tar.gz
|
||||
sudo mv secubox-linux-amd64 /usr/local/bin/secubox
|
||||
|
||||
# Linux ARM64
|
||||
curl -LO https://github.com/CyberMind-FR/secubox-deb/releases/download/${{ github.ref_name }}/secubox-linux-arm64.tar.gz
|
||||
tar xzf secubox-linux-arm64.tar.gz
|
||||
sudo mv secubox-linux-arm64 /usr/local/bin/secubox
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Interactive wizard
|
||||
secubox gen
|
||||
|
||||
# Generate for specific board
|
||||
secubox gen --board mochabin --tier pro
|
||||
|
||||
# Build image
|
||||
secubox build --board mochabin --output secubox.img
|
||||
|
||||
# Check hardware
|
||||
secubox info
|
||||
|
||||
# OTA update
|
||||
secubox ota update --url https://releases.secubox.in/v2.8.0/secubox-mochabin.img.gz
|
||||
```
|
||||
EOF
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
name: SecuBox CLI ${{ github.ref_name }}
|
||||
body_path: RELEASE_NOTES.md
|
||||
files: |
|
||||
artifacts/*.tar.gz
|
||||
artifacts/*.sha256
|
||||
draft: false
|
||||
prerelease: ${{ contains(github.ref, 'alpha') || contains(github.ref, 'beta') || contains(github.ref, 'rc') }}
|
||||
22
.gitea/workflows/license-check.yml
Normal file
22
.gitea/workflows/license-check.yml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||
# Source-Disclosed License — All rights reserved except as expressly granted.
|
||||
# See LICENCE-CMSD-1.0.md for terms.
|
||||
|
||||
name: License Headers
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Verify CMSD-1.0 headers
|
||||
run: python3 scripts/license-headers.py --check
|
||||
158
.gitea/workflows/publish-packages.yml
Normal file
158
.gitea/workflows/publish-packages.yml
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
# SecuBox — Publish packages to apt.secubox.in
|
||||
# This workflow is triggered after build-packages.yml completes on a tag
|
||||
name: Publish Packages
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
distribution:
|
||||
description: 'Target distribution'
|
||||
required: false
|
||||
type: string
|
||||
default: 'bookworm'
|
||||
secrets:
|
||||
GPG_PRIVATE_KEY:
|
||||
required: true
|
||||
DEPLOY_SSH_KEY:
|
||||
required: true
|
||||
DEPLOY_KNOWN_HOSTS:
|
||||
required: true
|
||||
workflow_run:
|
||||
workflows: ["Build SecuBox .deb packages"]
|
||||
types: [completed]
|
||||
branches: [main, master]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
distribution:
|
||||
description: 'Target distribution'
|
||||
required: true
|
||||
default: 'bookworm'
|
||||
type: choice
|
||||
options:
|
||||
- bookworm
|
||||
- trixie
|
||||
run_id:
|
||||
description: 'Build workflow run ID to publish (leave empty for latest)'
|
||||
required: false
|
||||
|
||||
env:
|
||||
REPO_HOST: apt.secubox.in
|
||||
REPO_USER: deploy
|
||||
REPO_PATH: /var/www/apt.secubox.in
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
# Only run if triggered workflow succeeded and was a tag
|
||||
if: |
|
||||
(github.event.workflow_run.conclusion == 'success' &&
|
||||
startsWith(github.event.workflow_run.head_branch, 'v')) ||
|
||||
github.event_name == 'workflow_dispatch'
|
||||
environment: production
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download artifacts from build workflow
|
||||
uses: dawidd6/action-download-artifact@v3
|
||||
with:
|
||||
workflow: build-packages.yml
|
||||
run_id: ${{ github.event.inputs.run_id || github.event.workflow_run.id }}
|
||||
name: secubox-debs-all
|
||||
path: debs/
|
||||
|
||||
- name: List downloaded packages
|
||||
run: |
|
||||
echo "Downloaded packages:"
|
||||
ls -lh debs/*.deb 2>/dev/null | head -40 || echo "No .deb files found"
|
||||
echo "Total: $(ls -1 debs/*.deb 2>/dev/null | wc -l) packages"
|
||||
|
||||
- name: Install reprepro
|
||||
run: sudo apt-get update && sudo apt-get install -y reprepro gnupg
|
||||
|
||||
- name: Import GPG key
|
||||
env:
|
||||
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
run: |
|
||||
echo "$GPG_PRIVATE_KEY" | gpg --batch --import
|
||||
gpg --list-keys
|
||||
|
||||
- name: Setup repository structure
|
||||
run: |
|
||||
mkdir -p repo/conf
|
||||
cp repo/conf/distributions repo/conf/
|
||||
cat > repo/conf/options << EOF
|
||||
verbose
|
||||
basedir $(pwd)/repo
|
||||
EOF
|
||||
|
||||
- name: Add packages to repository
|
||||
env:
|
||||
DIST: ${{ github.event.inputs.distribution || 'bookworm' }}
|
||||
run: |
|
||||
echo "Adding packages to $DIST..."
|
||||
for deb in debs/*.deb; do
|
||||
[ -f "$deb" ] || continue
|
||||
echo " Adding: $(basename $deb)"
|
||||
reprepro -b repo includedeb "$DIST" "$deb" || echo " Warning: Failed to add $(basename $deb)"
|
||||
done
|
||||
|
||||
- name: Export public GPG key
|
||||
run: |
|
||||
gpg --armor --export packages@secubox.in > repo/secubox-keyring.gpg
|
||||
gpg --export packages@secubox.in > repo/secubox-keyring.gpg.bin
|
||||
cp repo/secubox-keyring.gpg repo/dists/
|
||||
|
||||
- name: Generate install script
|
||||
run: |
|
||||
cat > repo/install.sh << 'INSTALL'
|
||||
#!/bin/bash
|
||||
# SecuBox APT Repository Installation Script
|
||||
set -e
|
||||
|
||||
echo "Installing SecuBox APT repository..."
|
||||
|
||||
# Download and install GPG key
|
||||
curl -fsSL https://apt.secubox.in/secubox-keyring.gpg | \
|
||||
sudo tee /usr/share/keyrings/secubox.gpg > /dev/null
|
||||
|
||||
# Add repository
|
||||
echo "deb [signed-by=/usr/share/keyrings/secubox.gpg] https://apt.secubox.in bookworm main" | \
|
||||
sudo tee /etc/apt/sources.list.d/secubox.list
|
||||
|
||||
# Update package lists
|
||||
sudo apt-get update
|
||||
|
||||
echo ""
|
||||
echo "SecuBox repository installed successfully!"
|
||||
echo ""
|
||||
echo "Install packages with:"
|
||||
echo " sudo apt install secubox-full # All modules"
|
||||
echo " sudo apt install secubox-lite # Essential modules only"
|
||||
echo ""
|
||||
INSTALL
|
||||
chmod +x repo/install.sh
|
||||
|
||||
- name: Deploy to server
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
echo "$KNOWN_HOSTS" > ~/.ssh/known_hosts
|
||||
|
||||
echo "Deploying to ${REPO_HOST}..."
|
||||
rsync -avz --delete \
|
||||
repo/dists/ repo/pool/ repo/secubox-keyring.gpg repo/secubox-keyring.gpg.bin repo/install.sh \
|
||||
${REPO_USER}@${REPO_HOST}:${REPO_PATH}/
|
||||
|
||||
- name: Verify deployment
|
||||
run: |
|
||||
echo "Verifying deployment..."
|
||||
curl -sf "https://${REPO_HOST}/dists/bookworm/Release" | head -10
|
||||
echo ""
|
||||
echo "Repository published successfully!"
|
||||
echo "Install with: curl -fsSL https://${REPO_HOST}/install.sh | sudo bash"
|
||||
226
.gitea/workflows/release.yml
Normal file
226
.gitea/workflows/release.yml
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
# SecuBox — Complete Release Workflow
|
||||
# Orchestrates package builds, image builds, and publishing
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ['v*']
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version tag (e.g., v1.0.0)'
|
||||
required: true
|
||||
build_images:
|
||||
description: 'Build system images'
|
||||
type: boolean
|
||||
default: true
|
||||
publish:
|
||||
description: 'Publish to APT repo'
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
jobs:
|
||||
# Build all packages
|
||||
build-packages:
|
||||
uses: ./.github/workflows/build-packages.yml
|
||||
secrets: inherit
|
||||
|
||||
# Build system images (optional).
|
||||
# `if: always() && != cancelled` lets us still ship images even if a few
|
||||
# matrix entries in build-packages failed (partial-release resilience).
|
||||
build-images:
|
||||
if: ${{ always() && needs.build-packages.result != 'cancelled' && github.event.inputs.build_images != 'false' }}
|
||||
needs: build-packages
|
||||
uses: ./.github/workflows/build-image.yml
|
||||
with:
|
||||
board: all
|
||||
size: '8G'
|
||||
secrets: inherit
|
||||
|
||||
# Build Live USB images (all platforms) — partial-release resilient.
|
||||
build-live-usb:
|
||||
if: ${{ always() && needs.build-packages.result != 'cancelled' }}
|
||||
needs: build-packages
|
||||
uses: ./.github/workflows/build-all-live-usb.yml
|
||||
with:
|
||||
platform: all
|
||||
secrets: inherit
|
||||
|
||||
# Publish to APT repository — partial-release resilient.
|
||||
publish:
|
||||
if: ${{ always() && needs.build-packages.result != 'cancelled' && github.event.inputs.publish != 'false' }}
|
||||
needs: build-packages
|
||||
uses: ./.github/workflows/publish-packages.yml
|
||||
secrets: inherit
|
||||
|
||||
# Create unified release. Ships whatever artifacts the upstream jobs
|
||||
# managed to produce — handles partial successes gracefully (any
|
||||
# `failure` here means "best-effort", `cancelled` we hard-skip).
|
||||
create-release:
|
||||
needs: [build-packages, build-images, build-live-usb]
|
||||
if: ${{ always() && needs.build-packages.result != 'cancelled' }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download package artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: secubox-debs-all
|
||||
path: release/packages/
|
||||
|
||||
- name: Download image artifacts
|
||||
if: needs.build-images.result == 'success'
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: secubox-*-bookworm
|
||||
path: release/images/
|
||||
merge-multiple: true
|
||||
|
||||
- name: Download Live USB artifacts
|
||||
if: needs.build-live-usb.result == 'success'
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: secubox-*-bookworm
|
||||
path: release/live-usb/
|
||||
merge-multiple: true
|
||||
|
||||
- name: Generate release notes
|
||||
run: |
|
||||
VERSION="${{ github.ref_name || github.event.inputs.version }}"
|
||||
|
||||
cat > release/RELEASE_NOTES.md << EOF
|
||||
# SecuBox-DEB ${VERSION}
|
||||
|
||||
## ⚡ CyberMind Security Platform
|
||||
|
||||
### What's Included
|
||||
|
||||
**124 Packages** across all security domains:
|
||||
- **Core:** hub, core, portal, system, console
|
||||
- **Security:** crowdsec, waf, auth, nac, threats, ipblock, mac-guard
|
||||
- **Networking:** wireguard, haproxy, dpi, qos, netmodes, vhost, cdn
|
||||
- **Applications:** mail, gitea, nextcloud, ollama, jellyfin, matrix
|
||||
- **SOC:** soc-agent, soc-gateway, soc-web (hierarchical security operations)
|
||||
- **Intel:** device-intel, vortex-dns, vortex-firewall, ai-insights
|
||||
|
||||
### System Images (Disk Install)
|
||||
- MOCHAbin (Marvell Armada 7040) - arm64
|
||||
- ESPRESSObin v7 (Marvell Armada 3720) - arm64
|
||||
- ESPRESSObin Ultra (Marvell Armada 3720) - arm64
|
||||
- Raspberry Pi 400 - arm64
|
||||
- VM x64 (VirtualBox/QEMU) - amd64
|
||||
- QEMU ARM64 emulation script (x86 hosts)
|
||||
|
||||
### Live USB Images (Bootable + Flasher)
|
||||
- x64 Live USB (GRUB UEFI/BIOS) - amd64
|
||||
- EspressoBin V7 Live USB (U-Boot) - arm64
|
||||
- Raspberry Pi 400 Live USB - arm64
|
||||
|
||||
### Boot Options
|
||||
- ⚡ **SecuBox Live** - Normal boot with persistence
|
||||
- 🖼️ **Kiosk GUI** - Fullscreen browser (default)
|
||||
- 📟 **Console TUI** - Text-based dashboard
|
||||
- 🌉 **Bridge Mode** - Inline transparent bridge
|
||||
- 💾 **Install to Disk** - Permanent installation
|
||||
- 🚀 **To RAM** - Load entire system to memory
|
||||
|
||||
## Quick Install
|
||||
|
||||
### From APT Repository
|
||||
\`\`\`bash
|
||||
curl -fsSL https://apt.secubox.in/install.sh | sudo bash
|
||||
sudo apt install secubox-full
|
||||
\`\`\`
|
||||
|
||||
### From Live USB Image
|
||||
\`\`\`bash
|
||||
# Flash to USB
|
||||
gunzip -c secubox-live-amd64-bookworm.img.gz | sudo dd of=/dev/sdX bs=4M status=progress
|
||||
|
||||
# VirtualBox VM
|
||||
bash image/create-vbox-vm.sh --name SecuBox --memory 4096
|
||||
\`\`\`
|
||||
|
||||
### Default Credentials
|
||||
- **Web UI:** https://<IP>:9443 (admin / secubox)
|
||||
- **SSH:** root / secubox
|
||||
|
||||
## Checksums
|
||||
|
||||
All files are signed with GPG key \`packages@secubox.in\`.
|
||||
|
||||
\`\`\`bash
|
||||
gpg --verify SHA256SUMS.gpg
|
||||
sha256sum -c SHA256SUMS
|
||||
\`\`\`
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Wiki Home](https://github.com/CyberMind-FR/secubox-deb/wiki)
|
||||
- [VirtualBox Quick Start](https://github.com/CyberMind-FR/secubox-deb/wiki/VirtualBox)
|
||||
- [API Reference](https://github.com/CyberMind-FR/secubox-deb/wiki/API-Reference)
|
||||
|
||||
EOF
|
||||
|
||||
- name: Generate combined checksums
|
||||
run: |
|
||||
cd release/
|
||||
find . -name "*.deb" -o -name "*.img.gz" -o -name "*.img.xz" -o -name "*.img" | \
|
||||
xargs sha256sum 2>/dev/null | sort > SHA256SUMS || true
|
||||
|
||||
- name: Sign checksums
|
||||
env:
|
||||
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
run: |
|
||||
if [ -n "$GPG_PRIVATE_KEY" ]; then
|
||||
echo "$GPG_PRIVATE_KEY" | gpg --batch --import
|
||||
cd release/
|
||||
gpg --clearsign SHA256SUMS
|
||||
mv SHA256SUMS.asc SHA256SUMS.gpg
|
||||
fi
|
||||
|
||||
- name: Copy VM scripts to release
|
||||
run: |
|
||||
cp image/create-qemu-arm64-vm.sh release/
|
||||
cp image/create-vbox-vm.sh release/
|
||||
|
||||
- name: Delete existing release assets (for retry)
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
VERSION="${{ github.ref_name || github.event.inputs.version }}"
|
||||
# Check if release exists and delete conflicting assets
|
||||
if gh release view "$VERSION" &>/dev/null; then
|
||||
echo "Release exists, cleaning up for retry..."
|
||||
# Get existing asset names
|
||||
existing=$(gh release view "$VERSION" --json assets --jq '.assets[].name')
|
||||
for asset in release/packages/*.deb release/images/*.img.gz release/images/*.img.xz release/live-usb/*.img*; do
|
||||
[ -f "$asset" ] || continue
|
||||
name=$(basename "$asset")
|
||||
if echo "$existing" | grep -qF "$name"; then
|
||||
echo "Deleting existing asset: $name"
|
||||
gh release delete-asset "$VERSION" "$name" --yes 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
name: SecuBox-DEB ${{ github.ref_name || github.event.inputs.version }}
|
||||
body_path: release/RELEASE_NOTES.md
|
||||
files: |
|
||||
release/packages/*.deb
|
||||
release/images/*.img.gz
|
||||
release/images/*.img.xz
|
||||
release/live-usb/*.img*
|
||||
release/create-qemu-arm64-vm.sh
|
||||
release/create-vbox-vm.sh
|
||||
release/SHA256SUMS
|
||||
release/SHA256SUMS.gpg
|
||||
draft: false
|
||||
fail_on_unmatched_files: false
|
||||
prerelease: ${{ contains(github.ref, 'alpha') || contains(github.ref, 'beta') || contains(github.ref, 'rc') }}
|
||||
167
.gitea/workflows/sync-all.yml
Normal file
167
.gitea/workflows/sync-all.yml
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
name: Sync All — Build, Release, Metrics
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, main]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
force_rebuild:
|
||||
description: 'Force rebuild all packages'
|
||||
required: false
|
||||
default: 'false'
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
SECUBOX_VERSION: "1.9.0"
|
||||
|
||||
jobs:
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 1. Discover changes and compute metrics
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
discover:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
packages_changed: ${{ steps.changes.outputs.packages }}
|
||||
total_packages: ${{ steps.metrics.outputs.total }}
|
||||
total_endpoints: ${{ steps.metrics.outputs.endpoints }}
|
||||
migration_percent: ${{ steps.metrics.outputs.migration }}
|
||||
commit_count: ${{ steps.metrics.outputs.commits }}
|
||||
issue_count: ${{ steps.metrics.outputs.issues }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Detect changed packages
|
||||
id: changes
|
||||
run: |
|
||||
# Find packages changed in this push
|
||||
if [ "${{ github.event_name }}" = "push" ]; then
|
||||
CHANGED=$(git diff --name-only ${{ github.event.before }} ${{ github.sha }} \
|
||||
| grep '^packages/secubox-' \
|
||||
| cut -d'/' -f2 \
|
||||
| sort -u \
|
||||
| jq -R -s -c 'split("\n") | map(select(length > 0))')
|
||||
else
|
||||
CHANGED='[]'
|
||||
fi
|
||||
echo "packages=$CHANGED" >> $GITHUB_OUTPUT
|
||||
echo "Changed packages: $CHANGED"
|
||||
|
||||
- name: Compute project metrics
|
||||
id: metrics
|
||||
run: |
|
||||
# Count packages
|
||||
TOTAL=$(find packages/secubox-* -maxdepth 0 -type d 2>/dev/null | wc -l)
|
||||
echo "total=$TOTAL" >> $GITHUB_OUTPUT
|
||||
|
||||
# Count API endpoints (rough estimate from main.py files)
|
||||
ENDPOINTS=$(grep -r '@app\.\|@router\.' packages/*/api/*.py 2>/dev/null | wc -l || echo "2000")
|
||||
echo "endpoints=$ENDPOINTS" >> $GITHUB_OUTPUT
|
||||
|
||||
# Migration percent (packages with api/main.py)
|
||||
WITH_API=$(find packages/secubox-*/api/main.py 2>/dev/null | wc -l)
|
||||
PERCENT=$((WITH_API * 100 / TOTAL))
|
||||
echo "migration=$PERCENT" >> $GITHUB_OUTPUT
|
||||
|
||||
# Commit count
|
||||
COMMITS=$(git rev-list --count HEAD)
|
||||
echo "commits=$COMMITS" >> $GITHUB_OUTPUT
|
||||
|
||||
# Open issues (via gh)
|
||||
ISSUES=$(gh issue list --state open --limit 1000 --json number | jq length 2>/dev/null || echo "0")
|
||||
echo "issues=$ISSUES" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 2. Build changed packages (or all if forced)
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
build-packages:
|
||||
needs: discover
|
||||
if: needs.discover.outputs.packages_changed != '[]' || github.event.inputs.force_rebuild == 'true'
|
||||
uses: ./.github/workflows/build-packages.yml
|
||||
secrets: inherit
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 3. Update WIP and metrics badges
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
update-metrics:
|
||||
needs: [discover, build-packages]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Update metrics in README
|
||||
run: |
|
||||
# Update package count badge
|
||||
sed -i "s/Debian_Packages-[0-9]*-/Debian_Packages-${{ needs.discover.outputs.total_packages }}-/" README.md
|
||||
|
||||
# Update migration percent
|
||||
sed -i "s/Migration-[0-9]*%25-/Migration-${{ needs.discover.outputs.migration_percent }}%25-/" README.md
|
||||
|
||||
# Update endpoints count
|
||||
sed -i "s/API_Endpoints-[0-9]*+-/API_Endpoints-${{ needs.discover.outputs.total_endpoints }}+-/" README.md
|
||||
|
||||
- name: Update WIP with session info
|
||||
run: |
|
||||
DATE=$(date '+%Y-%m-%d')
|
||||
SESSION=$(grep -oP 'Session \K[0-9]+' .claude/WIP.md | head -1 || echo "0")
|
||||
NEW_SESSION=$((SESSION))
|
||||
|
||||
# Add CI sync entry if not already present today
|
||||
if ! grep -q "CI Sync $DATE" .claude/WIP.md; then
|
||||
echo "" >> .claude/WIP.md
|
||||
echo "## CI Sync $DATE" >> .claude/WIP.md
|
||||
echo "- Packages: ${{ needs.discover.outputs.total_packages }}" >> .claude/WIP.md
|
||||
echo "- Endpoints: ${{ needs.discover.outputs.total_endpoints }}" >> .claude/WIP.md
|
||||
echo "- Migration: ${{ needs.discover.outputs.migration_percent }}%" >> .claude/WIP.md
|
||||
echo "- Commits: ${{ needs.discover.outputs.commit_count }}" >> .claude/WIP.md
|
||||
fi
|
||||
|
||||
- name: Commit metrics update
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add README.md .claude/WIP.md || true
|
||||
git diff --staged --quiet || git commit -m "ci: Update metrics [skip ci]"
|
||||
git push || true
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 4. Create release on tags
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
release:
|
||||
needs: [discover, build-packages]
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: ./.github/workflows/release.yml
|
||||
secrets: inherit
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 5. Summary
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
summary:
|
||||
needs: [discover, build-packages, update-metrics]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Generate summary
|
||||
run: |
|
||||
echo "## SecuBox Sync Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Metric | Value |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Packages | ${{ needs.discover.outputs.total_packages }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| API Endpoints | ${{ needs.discover.outputs.total_endpoints }}+ |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Migration | ${{ needs.discover.outputs.migration_percent }}% |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Commits | ${{ needs.discover.outputs.commit_count }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Open Issues | ${{ needs.discover.outputs.issue_count }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Changed Packages" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```json' >> $GITHUB_STEP_SUMMARY
|
||||
echo '${{ needs.discover.outputs.packages_changed }}' >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
9
.github/workflows/build-packages.yml
vendored
9
.github/workflows/build-packages.yml
vendored
|
|
@ -196,9 +196,12 @@ jobs:
|
|||
if-no-files-found: warn
|
||||
retention-days: 7
|
||||
|
||||
# Combine all artifacts
|
||||
# Combine all artifacts. `if: always() && != cancelled` so we still
|
||||
# publish whatever .deb files the matrix succeeded on, even when a few
|
||||
# entries failed (partial-release resilience).
|
||||
collect:
|
||||
needs: build
|
||||
if: ${{ always() && needs.build.result != 'cancelled' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download all artifacts
|
||||
|
|
@ -221,11 +224,11 @@ jobs:
|
|||
path: all-debs/
|
||||
retention-days: 30
|
||||
|
||||
# Publish to APT repo (on tag)
|
||||
# Publish to APT repo (on tag) — partial-release resilient.
|
||||
publish:
|
||||
needs: collect
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
if: ${{ always() && needs.collect.result == 'success' && startsWith(github.ref, 'refs/tags/v') }}
|
||||
environment: production
|
||||
|
||||
steps:
|
||||
|
|
|
|||
19
.github/workflows/release.yml
vendored
19
.github/workflows/release.yml
vendored
|
|
@ -25,9 +25,11 @@ jobs:
|
|||
uses: ./.github/workflows/build-packages.yml
|
||||
secrets: inherit
|
||||
|
||||
# Build system images (optional)
|
||||
# Build system images (optional).
|
||||
# `if: always() && != cancelled` lets us still ship images even if a few
|
||||
# matrix entries in build-packages failed (partial-release resilience).
|
||||
build-images:
|
||||
if: ${{ github.event.inputs.build_images != 'false' }}
|
||||
if: ${{ always() && needs.build-packages.result != 'cancelled' && github.event.inputs.build_images != 'false' }}
|
||||
needs: build-packages
|
||||
uses: ./.github/workflows/build-image.yml
|
||||
with:
|
||||
|
|
@ -35,25 +37,28 @@ jobs:
|
|||
size: '8G'
|
||||
secrets: inherit
|
||||
|
||||
# Build Live USB images (all platforms)
|
||||
# Build Live USB images (all platforms) — partial-release resilient.
|
||||
build-live-usb:
|
||||
if: ${{ always() && needs.build-packages.result != 'cancelled' }}
|
||||
needs: build-packages
|
||||
uses: ./.github/workflows/build-all-live-usb.yml
|
||||
with:
|
||||
platform: all
|
||||
secrets: inherit
|
||||
|
||||
# Publish to APT repository
|
||||
# Publish to APT repository — partial-release resilient.
|
||||
publish:
|
||||
if: ${{ github.event.inputs.publish != 'false' }}
|
||||
if: ${{ always() && needs.build-packages.result != 'cancelled' && github.event.inputs.publish != 'false' }}
|
||||
needs: build-packages
|
||||
uses: ./.github/workflows/publish-packages.yml
|
||||
secrets: inherit
|
||||
|
||||
# Create unified release
|
||||
# Create unified release. Ships whatever artifacts the upstream jobs
|
||||
# managed to produce — handles partial successes gracefully (any
|
||||
# `failure` here means "best-effort", `cancelled` we hard-skip).
|
||||
create-release:
|
||||
needs: [build-packages, build-images, build-live-usb]
|
||||
if: always() && needs.build-packages.result == 'success'
|
||||
if: ${{ always() && needs.build-packages.result != 'cancelled' }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
13
|
||||
|
|
@ -1,3 +1,14 @@
|
|||
secubox-droplet (1.0.2-1~bookworm1) bookworm; urgency=medium
|
||||
|
||||
* Forge dropletctl (issue #181) — third routing verb on the publishing
|
||||
layer, parallel to giteactl repo mirror (#176) and mitmproxyctl route
|
||||
(#173). Subcommands: lifecycle (start/stop/restart/status/logs),
|
||||
Three-fold JSON (components/access), and file noun verbs (upload,
|
||||
remove, rename, list, info) wrapping the /api/v1/droplet/* endpoints
|
||||
over the Unix socket.
|
||||
|
||||
-- Gerald KERMA <devel@cybermind.fr> Sun, 17 May 2026 11:20:54 +0200
|
||||
|
||||
secubox-droplet (1.0.1-1~bookworm1) bookworm; urgency=medium
|
||||
|
||||
* Add dynamic menu system with menu.d JSON definitions
|
||||
|
|
|
|||
|
|
@ -12,3 +12,6 @@ override_dh_auto_install:
|
|||
# Modular nginx config
|
||||
install -d debian/secubox-droplet/etc/nginx/secubox.d
|
||||
[ -f nginx/droplet.conf ] && cp nginx/droplet.conf debian/secubox-droplet/etc/nginx/secubox.d/ || true
|
||||
# dropletctl (issue #181)
|
||||
install -d debian/secubox-droplet/usr/sbin
|
||||
install -m 755 sbin/dropletctl debian/secubox-droplet/usr/sbin/
|
||||
|
|
|
|||
225
packages/secubox-droplet/sbin/dropletctl
Normal file
225
packages/secubox-droplet/sbin/dropletctl
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
#!/bin/bash
|
||||
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||
# Source-Disclosed License — All rights reserved except as expressly granted.
|
||||
# See LICENCE-CMSD-1.0.md for terms.
|
||||
#
|
||||
# dropletctl — SecuBox Droplet File Publisher control (issue #181)
|
||||
#
|
||||
# Third routing verb on the publishing layer, parallel to:
|
||||
# haproxyctl vhost add/remove (routing)
|
||||
# mitmproxyctl route add/remove (interception, #173)
|
||||
# giteactl repo mirror add (replication, #176)
|
||||
# dropletctl file upload/list (publishing, this)
|
||||
#
|
||||
# The Droplet API exposes /upload, /list, /remove, /rename over a Unix
|
||||
# socket; this ctl wraps those endpoints under a coherent <noun> <verb>
|
||||
# grammar so operators don't have to curl by hand.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="0.1.0"
|
||||
SOCKET="${DROPLET_SOCKET:-/run/secubox/droplet.sock}"
|
||||
API_BASE="http://localhost/api/v1/droplet"
|
||||
SERVICE="secubox-droplet.service"
|
||||
|
||||
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
|
||||
log() { printf "${GREEN}[DROPLET]${NC} %s\n" "$*"; }
|
||||
warn() { printf "${YELLOW}[WARN]${NC} %s\n" "$*"; }
|
||||
error() { printf "${RED}[ERROR]${NC} %s\n" "$*" >&2; }
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
require_socket() {
|
||||
if [ ! -S "$SOCKET" ]; then
|
||||
error "API socket $SOCKET not present — start $SERVICE first"
|
||||
exit 2
|
||||
fi
|
||||
}
|
||||
|
||||
api() {
|
||||
local method="$1" path="$2"
|
||||
shift 2
|
||||
require_socket
|
||||
curl --unix-socket "$SOCKET" -sS -X "$method" \
|
||||
-w "\nHTTP_CODE:%{http_code}\n" \
|
||||
"${API_BASE}${path}" "$@"
|
||||
}
|
||||
|
||||
api_code() {
|
||||
echo "$1" | grep '^HTTP_CODE:' | cut -d: -f2
|
||||
}
|
||||
|
||||
api_body() {
|
||||
echo "$1" | sed '/^HTTP_CODE:/d'
|
||||
}
|
||||
|
||||
# ── Lifecycle (top-level, mirrors mitmproxyctl/giteactl) ──────────────────
|
||||
|
||||
cmd_install() { log "install via dpkg; this ctl assumes package already installed"; return 0; }
|
||||
cmd_start() { systemctl start "$SERVICE" && log "started"; }
|
||||
cmd_stop() { systemctl stop "$SERVICE" && log "stopped"; }
|
||||
cmd_restart() { systemctl restart "$SERVICE" && log "restarted";}
|
||||
cmd_status() {
|
||||
systemctl is-active "$SERVICE" >/dev/null 2>&1 \
|
||||
&& echo "active" || echo "inactive"
|
||||
}
|
||||
cmd_logs() {
|
||||
journalctl -u "$SERVICE" -n "${1:-50}" --no-pager
|
||||
}
|
||||
|
||||
# ── Three-fold (giteactl convention: components / status / access JSON) ───
|
||||
|
||||
cmd_components() {
|
||||
cat <<EOF
|
||||
{
|
||||
"service": "$SERVICE",
|
||||
"socket": "$SOCKET",
|
||||
"api_base": "$API_BASE",
|
||||
"ctl_version": "$VERSION"
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
cmd_access() {
|
||||
cat <<EOF
|
||||
{
|
||||
"socket": "$SOCKET",
|
||||
"api_paths": {
|
||||
"upload": "POST /api/v1/droplet/upload",
|
||||
"list": "GET /api/v1/droplet/list",
|
||||
"remove": "POST /api/v1/droplet/remove",
|
||||
"rename": "POST /api/v1/droplet/rename",
|
||||
"info": "GET /api/v1/droplet/droplet/{name}",
|
||||
"stats": "GET /api/v1/droplet/stats",
|
||||
"storage":"GET /api/v1/droplet/storage"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
# ── file noun verbs (the missing grammar — issue #181) ────────────────────
|
||||
|
||||
cmd_file() {
|
||||
local action="${1:-}"; shift || true
|
||||
case "$action" in
|
||||
upload) cmd_file_upload "$@" ;;
|
||||
remove|rm|del) cmd_file_remove "$@" ;;
|
||||
rename) cmd_file_rename "$@" ;;
|
||||
list|ls) cmd_file_list "$@" ;;
|
||||
info) cmd_file_info "$@" ;;
|
||||
*)
|
||||
cat <<EOF
|
||||
File commands:
|
||||
file upload <path> [--public] [--ttl <e.g. 7d>]
|
||||
file remove <name>
|
||||
file rename <old> <new>
|
||||
file list [--limit N]
|
||||
file info <name>
|
||||
EOF
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
cmd_file_upload() {
|
||||
local path="$1"; shift || { error "file upload <path> required"; return 1; }
|
||||
[ -f "$path" ] || { error "file not found: $path"; return 1; }
|
||||
local public=false ttl=""
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--public) public=true ;;
|
||||
--ttl) ttl="$2"; shift ;;
|
||||
*) error "unknown flag: $1"; return 1 ;;
|
||||
esac; shift
|
||||
done
|
||||
log "uploading $path (public=$public ttl=${ttl:-default})"
|
||||
local args=("-F" "file=@${path}")
|
||||
$public && args+=("-F" "public=true")
|
||||
[ -n "$ttl" ] && args+=("-F" "ttl=$ttl")
|
||||
local out
|
||||
out=$(api POST "/upload" "${args[@]}")
|
||||
local code; code=$(api_code "$out")
|
||||
case "$code" in
|
||||
200|201) api_body "$out" ;;
|
||||
*) error "upload failed (HTTP $code): $(api_body "$out" | head -3)"; return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
cmd_file_remove() {
|
||||
local name="$1"
|
||||
[ -z "$name" ] && { error "file remove <name> required"; return 1; }
|
||||
log "removing $name"
|
||||
local out
|
||||
out=$(api POST "/remove" -H "Content-Type: application/json" \
|
||||
-d "$(printf '{"name":"%s"}' "$name")")
|
||||
local code; code=$(api_code "$out")
|
||||
[ "$code" = "200" ] || { error "remove failed (HTTP $code): $(api_body "$out")"; return 1; }
|
||||
log "removed $name"
|
||||
}
|
||||
|
||||
cmd_file_rename() {
|
||||
local old="$1" new="$2"
|
||||
[ -z "$old" ] || [ -z "$new" ] && { error "file rename <old> <new> required"; return 1; }
|
||||
log "renaming $old -> $new"
|
||||
local out
|
||||
out=$(api POST "/rename" -H "Content-Type: application/json" \
|
||||
-d "$(printf '{"old":"%s","new":"%s"}' "$old" "$new")")
|
||||
local code; code=$(api_code "$out")
|
||||
[ "$code" = "200" ] || { error "rename failed (HTTP $code): $(api_body "$out")"; return 1; }
|
||||
log "renamed"
|
||||
}
|
||||
|
||||
cmd_file_list() {
|
||||
local limit=""
|
||||
[ "${1:-}" = "--limit" ] && { limit="?limit=$2"; }
|
||||
local out
|
||||
out=$(api GET "/list${limit}")
|
||||
api_body "$out"
|
||||
}
|
||||
|
||||
cmd_file_info() {
|
||||
local name="$1"
|
||||
[ -z "$name" ] && { error "file info <name> required"; return 1; }
|
||||
local out
|
||||
out=$(api GET "/droplet/${name}")
|
||||
api_body "$out"
|
||||
}
|
||||
|
||||
# ── Main dispatch ─────────────────────────────────────────────────────────
|
||||
|
||||
show_help() {
|
||||
cat <<EOF
|
||||
SecuBox Droplet Controller v$VERSION (issue #181)
|
||||
File publisher CLI — parallel to giteactl, mitmproxyctl
|
||||
|
||||
Usage: dropletctl <command> [options]
|
||||
|
||||
Lifecycle:
|
||||
install / start / stop / restart / status / logs
|
||||
|
||||
Three-fold (JSON):
|
||||
components / access
|
||||
|
||||
Files (issue #181):
|
||||
file upload <path> [--public] [--ttl 7d]
|
||||
file remove <name>
|
||||
file rename <old> <new>
|
||||
file list [--limit N]
|
||||
file info <name>
|
||||
|
||||
Examples:
|
||||
dropletctl file upload /tmp/report.pdf --public --ttl 7d
|
||||
dropletctl file list
|
||||
dropletctl access | jq .api_paths
|
||||
EOF
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
install|start|stop|restart|status) cmd="$1"; shift; cmd_$cmd "$@" ;;
|
||||
logs) shift; cmd_logs "$@" ;;
|
||||
components) cmd_components ;;
|
||||
access) cmd_access ;;
|
||||
file) shift; cmd_file "$@" ;;
|
||||
help|--help|-h|'') show_help ;;
|
||||
*) error "unknown command: $1"; show_help; exit 1 ;;
|
||||
esac
|
||||
|
|
@ -1 +0,0 @@
|
|||
13
|
||||
|
|
@ -1,3 +1,21 @@
|
|||
secubox-gitea (1.4.1-1~bookworm1) bookworm; urgency=medium
|
||||
|
||||
* giteactl: forge `runner` noun verbs (issue #190) — the CI execution
|
||||
layer of the SecuBox grammar, parallel to `repo mirror` (#176) and
|
||||
`user`. Subcommands:
|
||||
runner token gen
|
||||
runner add NAME --labels L1,L2 [--arch arm64|amd64] [--memory 1G]
|
||||
runner remove NAME [--keep-data]
|
||||
runner list
|
||||
runner logs NAME [--lines N]
|
||||
runner restart NAME
|
||||
Each runner lives in its own LXC `act-runner-<name>` (operator
|
||||
decree: LXC only, no docker, no host-mode). `runner add` bootstraps
|
||||
the LXC, downloads gitea-runner v1.0.3 from gitea.com, registers
|
||||
against the local Gitea, installs a systemd unit, starts it.
|
||||
|
||||
-- Gerald KERMA <devel@cybermind.fr> Sun, 17 May 2026 11:51:30 +0200
|
||||
|
||||
secubox-gitea (1.4.0-1~bookworm1) bookworm; urgency=medium
|
||||
|
||||
* Fix startup script: add PATH and HOME environment variables
|
||||
|
|
|
|||
|
|
@ -3,13 +3,16 @@
|
|||
# Git server in Alpine LXC for Debian
|
||||
# Three-fold architecture: Components, Status, Access
|
||||
|
||||
VERSION="1.4.0"
|
||||
VERSION="1.5.0"
|
||||
CONFIG_FILE="/etc/secubox/gitea.toml"
|
||||
LXC_PATH="/srv/lxc"
|
||||
LXC_PATH="" # auto-detected below if blank, see resolve_lxc_path
|
||||
DATA_PATH="/srv/gitea"
|
||||
CONTAINER="gitea"
|
||||
GITEA_VERSION="1.22.6"
|
||||
|
||||
# Gitea API hard floors
|
||||
MIRROR_MIN_INTERVAL="10m0s"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
|
|
@ -32,7 +35,22 @@ HTTP_PORT=$(config_get "http_port" "3000")
|
|||
SSH_PORT=$(config_get "ssh_port" "2222")
|
||||
DOMAIN=$(config_get "domain" "git.local")
|
||||
APP_NAME=$(config_get "app_name" "SecuBox Git")
|
||||
LXC_IP=$(config_get "lxc_ip" "192.168.255.40")
|
||||
LXC_IP=$(config_get "lxc_ip" "10.100.0.40")
|
||||
GITEA_APP_INI=$(config_get "app_ini" "/var/lib/gitea/custom/conf/app.ini")
|
||||
ADMIN_USER=$(config_get "admin_user" "gandalf")
|
||||
|
||||
# Resolve LXC_PATH: explicit config wins, then fall back to the first
|
||||
# directory containing a `<CONTAINER>/rootfs/` from the board conventions.
|
||||
resolve_lxc_path() {
|
||||
local cfg
|
||||
cfg=$(config_get "lxc_path" "")
|
||||
if [ -n "$cfg" ]; then echo "$cfg"; return; fi
|
||||
for p in /data/lxc /srv/lxc /var/lib/lxc; do
|
||||
[ -d "$p/$CONTAINER/rootfs" ] && { echo "$p"; return; }
|
||||
done
|
||||
echo "/data/lxc" # match modern board reality; install will create it
|
||||
}
|
||||
LXC_PATH=${LXC_PATH:-$(resolve_lxc_path)}
|
||||
|
||||
require_root() {
|
||||
[ "$(id -u)" -eq 0 ] || { error "Root required"; exit 1; }
|
||||
|
|
@ -332,48 +350,597 @@ user_list() {
|
|||
# Mirror Management
|
||||
# ============================================================================
|
||||
|
||||
mirror_add() {
|
||||
local repo_url="$1"
|
||||
local local_name="$2"
|
||||
local owner="${3:-admin}"
|
||||
# ============================================================================
|
||||
# Repo / Mirror — the third routing verb (issue #176)
|
||||
# Forges `repo mirror add/remove/list/sync` parallel to mitmproxyctl route
|
||||
# (issue #173) and haproxyctl vhost. Wraps the 4 quirks of the Gitea
|
||||
# pull-mirror API into one atomic operation:
|
||||
# 1. No "convert existing repo to mirror" -> delete + migrate
|
||||
# 2. Minimum interval = 10m (enforced server-side, 500 below)
|
||||
# 3. Filesystem fetches don't trigger Gitea hooks -> DB stays "empty"
|
||||
# 4. Token generation needs to run inside the LXC as the gitea user
|
||||
# ============================================================================
|
||||
|
||||
if [ -z "$repo_url" ] || [ -z "$local_name" ]; then
|
||||
echo "Usage: giteactl mirror add <repo_url> <local_name> [owner]"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! lxc_running; then
|
||||
error "Container not running"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log "Creating mirror: $local_name from $repo_url"
|
||||
# Use Gitea API for mirror creation
|
||||
local api_token
|
||||
api_token=$(get_admin_token)
|
||||
|
||||
if [ -n "$api_token" ]; then
|
||||
lxc_attach "curl -s -X POST \
|
||||
-H 'Authorization: token $api_token' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
\"clone_addr\": \"$repo_url\",
|
||||
\"repo_name\": \"$local_name\",
|
||||
\"mirror\": true,
|
||||
\"private\": false
|
||||
}' \
|
||||
http://localhost:${HTTP_PORT}/api/v1/repos/migrate"
|
||||
else
|
||||
warn "No admin token available. Create mirror manually via web interface."
|
||||
fi
|
||||
# Generate a short-lived admin token via gitea CLI inside the LXC and echo
|
||||
# it. Token name embeds the PID so concurrent calls don't collide.
|
||||
gitea_token_gen() {
|
||||
require_root
|
||||
if ! lxc_running; then error "Container not running"; return 1; fi
|
||||
local tname="giteactl-$$-$(date +%s)"
|
||||
local out
|
||||
out=$(lxc-attach -n "$CONTAINER" -P "$LXC_PATH" -- \
|
||||
su -s /bin/bash gitea -c \
|
||||
"gitea --config $GITEA_APP_INI admin user generate-access-token \
|
||||
--username $ADMIN_USER --token-name $tname --scopes all 2>&1" \
|
||||
| tail -1)
|
||||
# output line ends with the actual token after the last ':'
|
||||
local token
|
||||
token=$(echo "$out" | awk -F: '{print $NF}' | tr -d ' ')
|
||||
[ -z "$token" ] && { error "token gen failed: $out"; return 1; }
|
||||
echo "$token" "$tname"
|
||||
}
|
||||
|
||||
get_admin_token() {
|
||||
# Get admin token from database
|
||||
local db_file="$DATA_PATH/git/gitea.db"
|
||||
if [ -f "$db_file" ]; then
|
||||
sqlite3 "$db_file" "SELECT token_hash FROM access_token WHERE name='secubox-api' LIMIT 1" 2>/dev/null
|
||||
gitea_token_revoke() {
|
||||
local tname="$1"
|
||||
[ -z "$tname" ] && return 0
|
||||
lxc-attach -n "$CONTAINER" -P "$LXC_PATH" -- \
|
||||
su -s /bin/bash gitea -c \
|
||||
"gitea --config $GITEA_APP_INI admin user delete-access-token \
|
||||
--username $ADMIN_USER --token-name $tname 2>&1" \
|
||||
>/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# Run a curl against the in-LXC Gitea API. Caller passes method + path
|
||||
# (without /api/v1 prefix) + body (or "" if none). Echo HTTP code on stderr,
|
||||
# body on stdout.
|
||||
gitea_api() {
|
||||
local token="$1" method="$2" path="$3" body="$4"
|
||||
local url="http://localhost:${HTTP_PORT}/api/v1${path}"
|
||||
local args=("-s" "-X" "$method" "-H" "Authorization: token $token" "-H" "Content-Type: application/json")
|
||||
[ -n "$body" ] && args+=("-d" "$body")
|
||||
lxc-attach -n "$CONTAINER" -P "$LXC_PATH" -- \
|
||||
curl "${args[@]}" -w "\nHTTP_CODE:%{http_code}\n" "$url"
|
||||
}
|
||||
|
||||
# Normalize an interval string. Gitea requires ≥10m; coerce silently to the
|
||||
# floor so operators can ask for "5m" and get the closest legal value.
|
||||
_normalize_interval() {
|
||||
local i="$1"
|
||||
[ -z "$i" ] && { echo "$MIRROR_MIN_INTERVAL"; return; }
|
||||
# Already in the canonical "Nm0s" form
|
||||
case "$i" in
|
||||
*h*|*d*) echo "$i"; return ;;
|
||||
*m*)
|
||||
local n
|
||||
n=$(echo "$i" | sed 's/[^0-9].*//')
|
||||
if [ -n "$n" ] && [ "$n" -lt 10 ]; then
|
||||
warn "interval ${i} below Gitea floor ${MIRROR_MIN_INTERVAL}; coerced"
|
||||
echo "$MIRROR_MIN_INTERVAL"
|
||||
else
|
||||
# ensure "NmXs" canonical
|
||||
case "$i" in *s) echo "$i" ;; *) echo "${i}0s" ;; esac
|
||||
fi
|
||||
;;
|
||||
*) echo "$i" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Parse "owner/name" or "name" (default owner = ADMIN_USER)
|
||||
_parse_repo_ref() {
|
||||
local ref="$1"
|
||||
case "$ref" in
|
||||
*/*) echo "${ref%/*}" "${ref#*/}" ;;
|
||||
*) echo "$ADMIN_USER" "$ref" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
cmd_repo() {
|
||||
local action="${1:-}"; shift || true
|
||||
case "$action" in
|
||||
create) cmd_repo_create "$@" ;;
|
||||
delete|del) cmd_repo_delete "$@" ;;
|
||||
mirror) cmd_repo_mirror "$@" ;;
|
||||
list|ls) cmd_repo_list "$@" ;;
|
||||
*)
|
||||
cat <<EOF
|
||||
Repo commands:
|
||||
repo create OWNER/NAME [--private] [--default-branch BR]
|
||||
repo delete OWNER/NAME [--force]
|
||||
repo list [OWNER]
|
||||
repo mirror add OWNER/NAME GITHUB_URL [--interval 10m] [--force]
|
||||
(--force allows recreating an
|
||||
existing non-mirror repo)
|
||||
repo mirror remove OWNER/NAME (turns off mirror, repo stays)
|
||||
repo mirror sync OWNER/NAME (trigger an immediate pull)
|
||||
repo mirror list (all mirror repos, JSON)
|
||||
EOF
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
cmd_repo_create() {
|
||||
local ref="$1"; shift || true
|
||||
[ -z "$ref" ] && { error "repo create: OWNER/NAME required"; return 1; }
|
||||
local owner name; read -r owner name <<<"$(_parse_repo_ref "$ref")"
|
||||
local private=false default_branch="master"
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--private) private=true ;;
|
||||
--default-branch) default_branch="$2"; shift ;;
|
||||
*) error "unknown flag: $1"; return 1 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
local tok tname; read -r tok tname <<<"$(gitea_token_gen)" || return 1
|
||||
local body
|
||||
body=$(printf '{"name":"%s","description":"Created by giteactl","private":%s,"default_branch":"%s"}' \
|
||||
"$name" "$private" "$default_branch")
|
||||
local out
|
||||
out=$(gitea_api "$tok" POST "/admin/users/${owner}/repos" "$body")
|
||||
gitea_token_revoke "$tname"
|
||||
local code
|
||||
code=$(echo "$out" | grep HTTP_CODE | cut -d: -f2)
|
||||
case "$code" in
|
||||
201) log "created ${owner}/${name}"; return 0 ;;
|
||||
409) warn "${owner}/${name} already exists"; return 0 ;;
|
||||
*) error "create failed (HTTP $code): $(echo "$out" | head -1)"; return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
cmd_repo_delete() {
|
||||
local ref="$1"; shift || true
|
||||
[ -z "$ref" ] && { error "repo delete: OWNER/NAME required"; return 1; }
|
||||
local force=false
|
||||
[ "${1:-}" = "--force" ] && force=true
|
||||
local owner name; read -r owner name <<<"$(_parse_repo_ref "$ref")"
|
||||
if ! $force; then
|
||||
warn "repo delete is destructive — pass --force to proceed (will delete ${owner}/${name})"
|
||||
return 1
|
||||
fi
|
||||
local tok tname; read -r tok tname <<<"$(gitea_token_gen)" || return 1
|
||||
local out
|
||||
out=$(gitea_api "$tok" DELETE "/repos/${owner}/${name}" "")
|
||||
gitea_token_revoke "$tname"
|
||||
local code
|
||||
code=$(echo "$out" | grep HTTP_CODE | cut -d: -f2)
|
||||
case "$code" in
|
||||
204) log "deleted ${owner}/${name}"; return 0 ;;
|
||||
404) warn "${owner}/${name} did not exist"; return 0 ;;
|
||||
*) error "delete failed (HTTP $code): $(echo "$out" | head -1)"; return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
cmd_repo_list() {
|
||||
local owner="${1:-}"
|
||||
local tok tname; read -r tok tname <<<"$(gitea_token_gen)" || return 1
|
||||
local path
|
||||
if [ -n "$owner" ]; then path="/users/${owner}/repos?limit=100"
|
||||
else path="/repos/search?limit=100"; fi
|
||||
local out
|
||||
out=$(gitea_api "$tok" GET "$path" "")
|
||||
gitea_token_revoke "$tname"
|
||||
echo "$out" | sed '/^HTTP_CODE/d'
|
||||
}
|
||||
|
||||
cmd_repo_mirror() {
|
||||
local action="${1:-}"; shift || true
|
||||
case "$action" in
|
||||
add) cmd_repo_mirror_add "$@" ;;
|
||||
remove|rm) cmd_repo_mirror_remove "$@" ;;
|
||||
sync) cmd_repo_mirror_sync "$@" ;;
|
||||
list|ls) cmd_repo_mirror_list "$@" ;;
|
||||
*)
|
||||
cat <<EOF
|
||||
Mirror commands:
|
||||
repo mirror add OWNER/NAME GITHUB_URL [--interval 10m] [--force]
|
||||
repo mirror remove OWNER/NAME
|
||||
repo mirror sync OWNER/NAME
|
||||
repo mirror list
|
||||
EOF
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
cmd_repo_mirror_add() {
|
||||
local ref="$1" url="$2"; shift 2 || { error "repo mirror add: OWNER/NAME URL required"; return 1; }
|
||||
local interval="$MIRROR_MIN_INTERVAL" force=false
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--interval) interval="$2"; shift ;;
|
||||
--force) force=true ;;
|
||||
*) error "unknown flag: $1"; return 1 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
interval=$(_normalize_interval "$interval")
|
||||
local owner name; read -r owner name <<<"$(_parse_repo_ref "$ref")"
|
||||
local tok tname; read -r tok tname <<<"$(gitea_token_gen)" || return 1
|
||||
|
||||
# 1. Probe existing state
|
||||
local probe
|
||||
probe=$(gitea_api "$tok" GET "/repos/${owner}/${name}" "")
|
||||
local probe_code
|
||||
probe_code=$(echo "$probe" | grep HTTP_CODE | cut -d: -f2)
|
||||
local is_mirror=false
|
||||
if [ "$probe_code" = "200" ]; then
|
||||
echo "$probe" | grep -q '"mirror":true' && is_mirror=true
|
||||
if $is_mirror; then
|
||||
log "${owner}/${name} is already a mirror; updating interval to ${interval}"
|
||||
local body
|
||||
body=$(printf '{"mirror_interval":"%s"}' "$interval")
|
||||
gitea_api "$tok" PATCH "/repos/${owner}/${name}" "$body" >/dev/null
|
||||
cmd_repo_mirror_sync_inner "$tok" "$owner" "$name"
|
||||
gitea_token_revoke "$tname"; return 0
|
||||
else
|
||||
if ! $force; then
|
||||
error "${owner}/${name} exists and is NOT a mirror; pass --force to delete+recreate"
|
||||
gitea_token_revoke "$tname"; return 1
|
||||
fi
|
||||
log "deleting existing non-mirror ${owner}/${name} (force)"
|
||||
gitea_api "$tok" DELETE "/repos/${owner}/${name}" "" >/dev/null
|
||||
sleep 2
|
||||
fi
|
||||
fi
|
||||
|
||||
# 2. Migrate as mirror
|
||||
log "creating pull mirror ${owner}/${name} <- ${url} (interval ${interval})"
|
||||
local body
|
||||
body=$(printf '{"clone_addr":"%s","repo_name":"%s","repo_owner":"%s","mirror":true,"mirror_interval":"%s","service":"github","private":false,"description":"Mirror of %s"}' \
|
||||
"$url" "$name" "$owner" "$interval" "$url")
|
||||
local out
|
||||
out=$(gitea_api "$tok" POST "/repos/migrate" "$body")
|
||||
local code
|
||||
code=$(echo "$out" | grep HTTP_CODE | cut -d: -f2)
|
||||
case "$code" in
|
||||
201) log "mirror created" ;;
|
||||
500)
|
||||
# Gitea returns 500 for "interval below minimum" — surface message
|
||||
local msg
|
||||
msg=$(echo "$out" | grep -o '"message":"[^"]*"' | head -1)
|
||||
error "migrate failed (500): $msg"
|
||||
gitea_token_revoke "$tname"; return 1
|
||||
;;
|
||||
*) error "migrate failed (HTTP $code): $(echo "$out" | head -3)"
|
||||
gitea_token_revoke "$tname"; return 1 ;;
|
||||
esac
|
||||
|
||||
# 3. Wait for initial clone to settle (avoid race with DB rescan)
|
||||
sleep 5
|
||||
cmd_repo_mirror_sync_inner "$tok" "$owner" "$name"
|
||||
gitea_token_revoke "$tname"
|
||||
log "OK — ${owner}/${name} mirrors ${url} every ${interval}"
|
||||
return 0
|
||||
}
|
||||
|
||||
cmd_repo_mirror_remove() {
|
||||
local ref="$1"
|
||||
[ -z "$ref" ] && { error "repo mirror remove: OWNER/NAME required"; return 1; }
|
||||
local owner name; read -r owner name <<<"$(_parse_repo_ref "$ref")"
|
||||
local tok tname; read -r tok tname <<<"$(gitea_token_gen)" || return 1
|
||||
# Setting mirror_interval="" disables auto-sync; repo stays as non-mirror.
|
||||
# Per Gitea docs there is no true "demote-to-non-mirror" — the flag stays.
|
||||
# Cleanest: just set interval to 0 which disables sync.
|
||||
local out
|
||||
out=$(gitea_api "$tok" PATCH "/repos/${owner}/${name}" '{"mirror_interval":"0s"}')
|
||||
gitea_token_revoke "$tname"
|
||||
local code
|
||||
code=$(echo "$out" | grep HTTP_CODE | cut -d: -f2)
|
||||
case "$code" in
|
||||
200) log "${owner}/${name} mirror sync disabled (interval=0); repo content preserved" ;;
|
||||
*) error "remove failed (HTTP $code): $(echo "$out" | head -1)"; return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
cmd_repo_mirror_sync_inner() {
|
||||
local tok="$1" owner="$2" name="$3"
|
||||
log "triggering mirror-sync on ${owner}/${name}"
|
||||
local out
|
||||
out=$(gitea_api "$tok" POST "/repos/${owner}/${name}/mirror-sync" "")
|
||||
local code
|
||||
code=$(echo "$out" | grep HTTP_CODE | cut -d: -f2)
|
||||
case "$code" in
|
||||
200) log "sync triggered" ;;
|
||||
400) warn "sync rejected (HTTP 400) — repo may still be in initial clone state" ;;
|
||||
*) warn "sync returned HTTP $code" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
cmd_repo_mirror_sync() {
|
||||
local ref="$1"
|
||||
[ -z "$ref" ] && { error "repo mirror sync: OWNER/NAME required"; return 1; }
|
||||
local owner name; read -r owner name <<<"$(_parse_repo_ref "$ref")"
|
||||
local tok tname; read -r tok tname <<<"$(gitea_token_gen)" || return 1
|
||||
cmd_repo_mirror_sync_inner "$tok" "$owner" "$name"
|
||||
gitea_token_revoke "$tname"
|
||||
}
|
||||
|
||||
cmd_repo_mirror_list() {
|
||||
local tok tname; read -r tok tname <<<"$(gitea_token_gen)" || return 1
|
||||
local out
|
||||
out=$(gitea_api "$tok" GET "/repos/search?mirror=true&limit=100" "")
|
||||
gitea_token_revoke "$tname"
|
||||
echo "$out" | sed '/^HTTP_CODE/d' | python3 -c "
|
||||
import sys, json
|
||||
try:
|
||||
d = json.load(sys.stdin)
|
||||
for r in d.get('data', []):
|
||||
print(f\" {r['full_name']:40s} <- {r.get('original_url','?')} (every {r.get('mirror_interval','?')})\")
|
||||
if not d.get('data'):
|
||||
print('(no mirror repos)')
|
||||
except Exception as e:
|
||||
print(f'parse error: {e}', file=sys.stderr)
|
||||
" 2>&1
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Runner — the CI execution verb (issue #190)
|
||||
#
|
||||
# Parallel to `repo mirror` (#176) and `user` (existing). Bootstraps a
|
||||
# dedicated LXC for each gitea-runner instance — operator decree: LXC only,
|
||||
# no docker on host, no insecure host-mode.
|
||||
#
|
||||
# Subcommands:
|
||||
# runner token gen Generate a one-shot registration token
|
||||
# runner add NAME --labels L1,L2,... [--arch arm64|amd64] [--memory 1G]
|
||||
# runner remove NAME [--keep-data]
|
||||
# runner list
|
||||
# runner logs NAME [--lines N]
|
||||
# runner restart NAME
|
||||
# ============================================================================
|
||||
|
||||
RUNNER_VERSION="${RUNNER_VERSION:-1.0.3}"
|
||||
RUNNER_DL_BASE="https://gitea.com/gitea/runner/releases/download/v${RUNNER_VERSION}"
|
||||
RUNNER_LXC_PREFIX="act-runner-"
|
||||
RUNNER_GITEA_URL="${RUNNER_GITEA_URL:-http://${LXC_IP}:${HTTP_PORT}}"
|
||||
RUNNER_BRIDGE="${RUNNER_BRIDGE:-br-lxc}"
|
||||
RUNNER_SUBNET="${RUNNER_SUBNET:-10.100.0.0/24}"
|
||||
RUNNER_GATEWAY="${RUNNER_GATEWAY:-10.100.0.1}"
|
||||
RUNNER_IP_POOL_START="${RUNNER_IP_POOL_START:-50}" # 10.100.0.50..200
|
||||
RUNNER_IP_POOL_END="${RUNNER_IP_POOL_END:-200}"
|
||||
RUNNER_DNS="${RUNNER_DNS:-1.1.1.1 8.8.8.8}"
|
||||
|
||||
# Find the next free IP in the runner pool (10.100.0.50..200), avoiding
|
||||
# IPs already declared in existing LXC configs.
|
||||
_runner_next_ip() {
|
||||
local octet ip used
|
||||
used=$(grep -h "lxc.net.0.ipv4.address" "$LXC_PATH"/*/config 2>/dev/null \
|
||||
| sed 's|.*= *\([0-9.]*\)/.*|\1|' \
|
||||
| grep "^10\.100\.0\." | sed 's|.*\.||' | sort -un)
|
||||
for octet in $(seq "$RUNNER_IP_POOL_START" "$RUNNER_IP_POOL_END"); do
|
||||
echo "$used" | grep -qx "$octet" || { echo "10.100.0.$octet"; return 0; }
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
_runner_lxc_name() { echo "${RUNNER_LXC_PREFIX}${1}"; }
|
||||
_runner_lxc_exists() { [ -d "$LXC_PATH/$(_runner_lxc_name "$1")/rootfs" ]; }
|
||||
_runner_lxc_running() {
|
||||
lxc-info -n "$(_runner_lxc_name "$1")" -P "$LXC_PATH" 2>/dev/null | grep -q "State:.*RUNNING"
|
||||
}
|
||||
|
||||
# Generate a runner registration token via gitea CLI inside the Gitea LXC.
|
||||
# Output is the raw token on the last line.
|
||||
cmd_runner_token_gen() {
|
||||
require_root
|
||||
if ! lxc_running; then error "gitea LXC not running"; return 1; fi
|
||||
local out
|
||||
out=$(lxc-attach -n "$CONTAINER" -P "$LXC_PATH" -- \
|
||||
su -s /bin/bash gitea -c \
|
||||
"gitea --config $GITEA_APP_INI actions generate-runner-token 2>&1" \
|
||||
| tail -1)
|
||||
[ -z "$out" ] && { error "token gen failed"; return 1; }
|
||||
echo "$out"
|
||||
}
|
||||
|
||||
cmd_runner_add() {
|
||||
local name="$1"; shift || { error "runner add NAME required"; return 1; }
|
||||
local labels="" arch="" memory="1G" ip=""
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--labels) labels="$2"; shift ;;
|
||||
--arch) arch="$2"; shift ;;
|
||||
--memory) memory="$2"; shift ;;
|
||||
--ip) ip="$2"; shift ;;
|
||||
*) error "unknown flag: $1"; return 1 ;;
|
||||
esac; shift
|
||||
done
|
||||
[ -z "$labels" ] && { error "--labels L1,L2,... required"; return 1; }
|
||||
[ -z "$arch" ] && arch=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')
|
||||
[ -z "$ip" ] && ip=$(_runner_next_ip)
|
||||
[ -z "$ip" ] && { error "no free IP in pool 10.100.0.${RUNNER_IP_POOL_START}..${RUNNER_IP_POOL_END}"; return 1; }
|
||||
|
||||
require_root
|
||||
local lxc_name; lxc_name=$(_runner_lxc_name "$name")
|
||||
if _runner_lxc_exists "$name"; then
|
||||
error "LXC $lxc_name already exists (use 'runner remove $name' first)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log "creating LXC $lxc_name (arch $arch, memory $memory, ip $ip)"
|
||||
lxc-create -n "$lxc_name" -P "$LXC_PATH" -t download -- \
|
||||
-d debian -r bookworm -a "$arch"
|
||||
# Static IP on br-lxc + memory limit (no DHCP on this bridge)
|
||||
cat >> "$LXC_PATH/$lxc_name/config" <<EOF
|
||||
|
||||
# act-runner LXC — set via giteactl runner add (#190)
|
||||
lxc.net.0.ipv4.address = ${ip}/24
|
||||
lxc.net.0.ipv4.gateway = ${RUNNER_GATEWAY}
|
||||
lxc.cgroup2.memory.max = $((${memory%[GgMm]} * (1024**$( [ "${memory: -1}" = "G" ] && echo 3 || echo 2 ))))
|
||||
EOF
|
||||
|
||||
log "starting $lxc_name for bootstrap"
|
||||
lxc-start -n "$lxc_name" -P "$LXC_PATH"
|
||||
sleep 4 # let networking settle
|
||||
|
||||
# Fix DNS — Debian template's resolv.conf symlinks systemd-resolved
|
||||
# stub which is empty until configured; write static nameservers.
|
||||
lxc-attach -n "$lxc_name" -P "$LXC_PATH" -- bash -c "
|
||||
for ns in $RUNNER_DNS; do echo \"nameserver \$ns\"; done > /etc/resolv.conf
|
||||
"
|
||||
|
||||
log "installing gitea-runner v$RUNNER_VERSION inside"
|
||||
local dl_url="${RUNNER_DL_BASE}/gitea-runner-${RUNNER_VERSION}-linux-${arch}"
|
||||
lxc-attach -n "$lxc_name" -P "$LXC_PATH" -- bash -c "
|
||||
set -e
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq curl ca-certificates git
|
||||
curl -sL -o /usr/local/bin/act_runner '$dl_url'
|
||||
chmod 755 /usr/local/bin/act_runner
|
||||
useradd -r -s /usr/sbin/nologin -m -d /var/lib/act_runner act_runner 2>/dev/null || true
|
||||
mkdir -p /etc/act_runner /var/lib/act_runner
|
||||
chown -R act_runner:act_runner /var/lib/act_runner
|
||||
"
|
||||
|
||||
log "generating runner registration token"
|
||||
local token
|
||||
token=$(cmd_runner_token_gen | tail -1)
|
||||
[ -z "$token" ] && { error "could not get registration token"; return 1; }
|
||||
|
||||
log "registering runner $name (labels: $labels) -> $RUNNER_GITEA_URL"
|
||||
lxc-attach -n "$lxc_name" -P "$LXC_PATH" -- bash -c "
|
||||
cd /var/lib/act_runner
|
||||
su -s /bin/bash act_runner -c \"
|
||||
cd /var/lib/act_runner
|
||||
act_runner register \\
|
||||
--no-interactive \\
|
||||
--instance '$RUNNER_GITEA_URL' \\
|
||||
--token '$token' \\
|
||||
--name '$name' \\
|
||||
--labels '$labels'
|
||||
\"
|
||||
"
|
||||
|
||||
log "installing systemd unit + starting"
|
||||
lxc-attach -n "$lxc_name" -P "$LXC_PATH" -- bash -c "
|
||||
cat > /etc/systemd/system/act_runner.service <<UNIT
|
||||
[Unit]
|
||||
Description=Gitea act_runner
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=act_runner
|
||||
Group=act_runner
|
||||
WorkingDirectory=/var/lib/act_runner
|
||||
ExecStart=/usr/local/bin/act_runner daemon
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
UNIT
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now act_runner.service
|
||||
sleep 2
|
||||
systemctl is-active act_runner.service
|
||||
"
|
||||
log "runner $name installed in $lxc_name"
|
||||
}
|
||||
|
||||
cmd_runner_remove() {
|
||||
local name="$1"; shift || { error "runner remove NAME required"; return 1; }
|
||||
local keep_data=false
|
||||
[ "${1:-}" = "--keep-data" ] && keep_data=true
|
||||
require_root
|
||||
local lxc_name; lxc_name=$(_runner_lxc_name "$name")
|
||||
if ! _runner_lxc_exists "$name"; then
|
||||
warn "LXC $lxc_name does not exist; nothing to remove"
|
||||
return 0
|
||||
fi
|
||||
if _runner_lxc_running "$name"; then
|
||||
log "stopping $lxc_name"
|
||||
lxc-stop -n "$lxc_name" -P "$LXC_PATH"
|
||||
fi
|
||||
if $keep_data; then
|
||||
log "keeping LXC rootfs (--keep-data); just stopped"
|
||||
else
|
||||
log "destroying $lxc_name"
|
||||
lxc-destroy -n "$lxc_name" -P "$LXC_PATH"
|
||||
fi
|
||||
log "(note: runner remains registered in Gitea — visit Settings -> Actions -> Runners to delete the dead entry, or wait for it to expire)"
|
||||
}
|
||||
|
||||
cmd_runner_list() {
|
||||
require_root
|
||||
echo "Local LXC runners:"
|
||||
local any=0
|
||||
for d in "$LXC_PATH"/${RUNNER_LXC_PREFIX}*; do
|
||||
[ -d "$d/rootfs" ] || continue
|
||||
any=1
|
||||
local n; n=$(basename "$d" | sed "s|^${RUNNER_LXC_PREFIX}||")
|
||||
local state="STOPPED"
|
||||
_runner_lxc_running "$n" && state="RUNNING"
|
||||
printf " %-25s %s\n" "$n" "$state"
|
||||
done
|
||||
[ $any = 0 ] && echo " (none)"
|
||||
echo
|
||||
echo "Registered runners (gitea API):"
|
||||
local tok tname; read -r tok tname <<<"$(gitea_token_gen)" || return 1
|
||||
local out
|
||||
out=$(gitea_api "$tok" GET "/admin/runners" "")
|
||||
gitea_token_revoke "$tname"
|
||||
api_body() { echo "$1" | sed '/^HTTP_CODE:/d'; }
|
||||
echo "$out" | sed '/^HTTP_CODE:/d' | python3 -c "
|
||||
import sys, json
|
||||
try:
|
||||
d = json.load(sys.stdin)
|
||||
for r in d.get('runners', d if isinstance(d, list) else []):
|
||||
print(f\" {r.get('name','?'):25s} status={r.get('status','?')} labels={','.join(r.get('labels',[]))}\")
|
||||
if not (d.get('runners') if isinstance(d, dict) else d):
|
||||
print(' (none registered)')
|
||||
except Exception as e:
|
||||
print(f' parse error: {e}')
|
||||
" 2>&1
|
||||
}
|
||||
|
||||
cmd_runner_logs() {
|
||||
local name="$1"; [ -z "$name" ] && { error "runner logs NAME required"; return 1; }
|
||||
local lines="${2:-100}"
|
||||
[ "$1" = "--lines" ] && { lines="$2"; name="$3"; }
|
||||
require_root
|
||||
_runner_lxc_running "$name" || { error "LXC $(_runner_lxc_name "$name") not running"; return 1; }
|
||||
lxc-attach -n "$(_runner_lxc_name "$name")" -P "$LXC_PATH" -- \
|
||||
journalctl -u act_runner -n "$lines" --no-pager
|
||||
}
|
||||
|
||||
cmd_runner_restart() {
|
||||
local name="$1"; [ -z "$name" ] && { error "runner restart NAME required"; return 1; }
|
||||
require_root
|
||||
_runner_lxc_running "$name" || { error "LXC not running, start it first"; return 1; }
|
||||
lxc-attach -n "$(_runner_lxc_name "$name")" -P "$LXC_PATH" -- \
|
||||
systemctl restart act_runner
|
||||
log "act_runner restarted in $(_runner_lxc_name "$name")"
|
||||
}
|
||||
|
||||
cmd_runner() {
|
||||
local act="${1:-}"; shift || true
|
||||
case "$act" in
|
||||
token)
|
||||
local sub="${1:-}"
|
||||
case "$sub" in
|
||||
gen|generate) cmd_runner_token_gen ;;
|
||||
*) echo "Usage: giteactl runner token gen" ;;
|
||||
esac
|
||||
;;
|
||||
add) cmd_runner_add "$@" ;;
|
||||
remove|rm|delete) cmd_runner_remove "$@" ;;
|
||||
list|ls) cmd_runner_list ;;
|
||||
logs) cmd_runner_logs "$@" ;;
|
||||
restart) cmd_runner_restart "$@" ;;
|
||||
*)
|
||||
cat <<EOF
|
||||
Runner commands (issue #190 — CI execution layer):
|
||||
runner token gen Generate one-shot registration token
|
||||
runner add NAME --labels L1,L2 [--arch arm64|amd64] [--memory 1G]
|
||||
Bootstrap LXC, install gitea-runner, register, start
|
||||
runner remove NAME [--keep-data] Stop + destroy LXC (Gitea entry stays)
|
||||
runner list Local LXCs + registered runners (JSON)
|
||||
runner logs NAME [--lines N] Tail in-LXC act_runner journal
|
||||
runner restart NAME systemctl restart act_runner in the LXC
|
||||
EOF
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
|
|
@ -788,6 +1355,23 @@ Users:
|
|||
user passwd <n> <p> Change password
|
||||
user list List users
|
||||
|
||||
Repos & Mirrors (issue #176, parallel to mitmproxyctl route):
|
||||
repo create OWNER/NAME [--private] [--default-branch BR]
|
||||
repo delete OWNER/NAME --force
|
||||
repo list [OWNER]
|
||||
repo mirror add OWNER/NAME GITHUB_URL [--interval 10m] [--force]
|
||||
repo mirror remove OWNER/NAME
|
||||
repo mirror sync OWNER/NAME
|
||||
repo mirror list
|
||||
|
||||
Runners (issue #190 — LXC-only act_runner, CI execution layer):
|
||||
runner token gen
|
||||
runner add NAME --labels L1,L2 [--arch arm64|amd64] [--memory 1G]
|
||||
runner remove NAME [--keep-data]
|
||||
runner list
|
||||
runner logs NAME [--lines N]
|
||||
runner restart NAME
|
||||
|
||||
Backup:
|
||||
backup [name] Create backup
|
||||
restore <file> Restore from backup
|
||||
|
|
@ -823,6 +1407,8 @@ case "${1:-}" in
|
|||
stop) shift; cmd_stop "$@" ;;
|
||||
restart) shift; cmd_restart "$@" ;;
|
||||
user) shift; cmd_user "$@" ;;
|
||||
repo) shift; cmd_repo "$@" ;;
|
||||
runner) shift; cmd_runner "$@" ;;
|
||||
backup) shift; cmd_backup "$@" ;;
|
||||
restore) shift; cmd_restore "$@" ;;
|
||||
migrate) shift; cmd_migrate "$@" ;;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,13 @@
|
|||
%:
|
||||
dh $@
|
||||
|
||||
# /usr/local/bin admin-only tools (issue #44): dh_usrlocal only knows how to
|
||||
# rehome directories (Policy 9.1.2), not individual files — it errors out
|
||||
# with "<file> is not a directory" on our regen-safe + render-nginx-webui
|
||||
# scripts. The deliberate placement under /usr/local/bin is the intended
|
||||
# behaviour; we just need debhelper to leave the path alone.
|
||||
override_dh_usrlocal:
|
||||
|
||||
override_dh_auto_install:
|
||||
install -d debian/secubox-haproxy/usr/lib/secubox/haproxy/
|
||||
cp -r api debian/secubox-haproxy/usr/lib/secubox/haproxy/
|
||||
|
|
|
|||
|
|
@ -1,3 +1,18 @@
|
|||
secubox-metablogizer (1.1.1-1~bookworm1) bookworm; urgency=medium
|
||||
|
||||
* metablogizerctl: forge tor noun verbs (issue #184) — the Emancipate
|
||||
verb of the Punk Exposure Engine at the publishing layer.
|
||||
Subcommands:
|
||||
tor expose <site> Publish site via Tor hidden service
|
||||
tor revoke <site> Stop publishing via Tor
|
||||
tor list List Tor-exposed sites with onion addresses
|
||||
tor status <site> Show stanza + onion + tor service state
|
||||
When secubox-exposure is installed, delegates to it for consistency
|
||||
with other exposure channels (DNS+SSL, Mesh). Otherwise falls back
|
||||
to direct /etc/tor/secubox-metablogizer.d/ stanza management.
|
||||
|
||||
-- Gerald KERMA <devel@cybermind.fr> Sun, 17 May 2026 11:23:12 +0200
|
||||
|
||||
secubox-metablogizer (1.1.0-1~bookworm1) bookworm; urgency=medium
|
||||
|
||||
* Add metablogizerctl three-fold commands: components, access (JSON output)
|
||||
|
|
|
|||
|
|
@ -194,6 +194,155 @@ site_unpublish() {
|
|||
log "Site unpublished: $name"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Tor — Emancipate verb (Punk Exposure Engine, issue #184)
|
||||
#
|
||||
# Per CLAUDE.md the Punk Exposure Engine has three verbs (Peek/Poke/Emancipate)
|
||||
# and Tor is one of the three exposure channels. `metablogizerctl tor expose`
|
||||
# is the Emancipate verb at the publishing layer for static sites.
|
||||
#
|
||||
# Implementation: write a per-site Tor HiddenService stanza, reload tor,
|
||||
# read the generated .onion hostname back. If secubox-exposure is installed,
|
||||
# delegate to it for consistency with other channels; otherwise fall back
|
||||
# to direct torrc manipulation.
|
||||
# ============================================================================
|
||||
|
||||
TOR_DROPIN_DIR="${TOR_DROPIN_DIR:-/etc/tor/secubox-metablogizer.d}"
|
||||
TOR_DATA_DIR="${TOR_DATA_DIR:-/var/lib/tor/secubox-metablogizer}"
|
||||
|
||||
_tor_drop_path() { echo "$TOR_DROPIN_DIR/${1}.conf"; }
|
||||
_tor_data_path() { echo "$TOR_DATA_DIR/${1}"; }
|
||||
|
||||
_have_exposure() { command -v secubox-exposure >/dev/null 2>&1; }
|
||||
_have_tor() { command -v tor >/dev/null 2>&1; }
|
||||
|
||||
tor_expose() {
|
||||
local name="$1"
|
||||
[ -z "$name" ] && { error "Usage: metablogizerctl tor expose <site>"; return 1; }
|
||||
|
||||
local site_dir="$SITES_ROOT/$name"
|
||||
[ -d "$site_dir" ] || { error "site not found: $name (run: metablogizerctl site create $name first)"; return 1; }
|
||||
|
||||
# Prefer secubox-exposure when available so all 3 exposure channels share
|
||||
# the same orchestration (Tor / DNS+SSL / Mesh) and Peek shows it.
|
||||
if _have_exposure; then
|
||||
log "delegating to secubox-exposure emancipate (tor channel)"
|
||||
# The static site is served via nginx on port 80 (per site_publish);
|
||||
# secubox-exposure handles the HiddenService wiring and revocation.
|
||||
secubox-exposure emancipate "metablogizer-${name}" 80 --tor
|
||||
return $?
|
||||
fi
|
||||
|
||||
# Fallback: write a tor drop-in directly.
|
||||
_have_tor || { error "tor not installed and secubox-exposure unavailable"; return 1; }
|
||||
mkdir -p "$TOR_DROPIN_DIR"
|
||||
mkdir -p "$TOR_DATA_DIR"
|
||||
local data; data=$(_tor_data_path "$name")
|
||||
local drop; drop=$(_tor_drop_path "$name")
|
||||
log "writing Tor HiddenService stanza for $name"
|
||||
cat > "$drop" <<EOF
|
||||
# metablogizer site: $name (#184 — Emancipate via Tor)
|
||||
HiddenServiceDir $data
|
||||
HiddenServicePort 80 127.0.0.1:80
|
||||
EOF
|
||||
chown -R debian-tor:debian-tor "$TOR_DATA_DIR" 2>/dev/null || true
|
||||
chmod 700 "$data" 2>/dev/null || true
|
||||
log "reloading tor"
|
||||
systemctl reload tor 2>/dev/null || systemctl restart tor
|
||||
# Wait briefly for tor to publish the hostname file
|
||||
local i=0
|
||||
while [ $i -lt 10 ] && [ ! -f "$data/hostname" ]; do sleep 1; i=$((i+1)); done
|
||||
if [ -f "$data/hostname" ]; then
|
||||
local onion; onion=$(cat "$data/hostname")
|
||||
log "site emancipated via Tor: $onion"
|
||||
# Persist the address back into site.json for future Peek calls
|
||||
if [ -f "$site_dir/site.json" ] && command -v python3 >/dev/null 2>&1; then
|
||||
python3 -c "
|
||||
import json, sys
|
||||
p='$site_dir/site.json'
|
||||
d=json.load(open(p))
|
||||
d.setdefault('exposure',{})['tor']='$onion'
|
||||
json.dump(d, open(p,'w'), indent=2)
|
||||
" 2>/dev/null || true
|
||||
fi
|
||||
else
|
||||
warn "tor reload OK but hostname not yet written; check 'metablogizerctl tor status $name'"
|
||||
fi
|
||||
}
|
||||
|
||||
tor_revoke() {
|
||||
local name="$1"
|
||||
[ -z "$name" ] && { error "Usage: metablogizerctl tor revoke <site>"; return 1; }
|
||||
if _have_exposure; then
|
||||
log "delegating to secubox-exposure revoke"
|
||||
secubox-exposure revoke "metablogizer-${name}" --tor
|
||||
return $?
|
||||
fi
|
||||
local drop; drop=$(_tor_drop_path "$name")
|
||||
[ -f "$drop" ] || { warn "no Tor stanza for $name"; return 0; }
|
||||
rm -f "$drop"
|
||||
systemctl reload tor 2>/dev/null || systemctl restart tor
|
||||
log "tor stanza removed for $name (data dir kept under $TOR_DATA_DIR/$name — delete manually if desired)"
|
||||
}
|
||||
|
||||
tor_list() {
|
||||
if _have_exposure; then
|
||||
log "(delegate) secubox-exposure list --tor"
|
||||
secubox-exposure list --tor 2>/dev/null && return
|
||||
fi
|
||||
if [ ! -d "$TOR_DROPIN_DIR" ]; then
|
||||
echo "(no Tor-exposed sites)"
|
||||
return
|
||||
fi
|
||||
local any=0
|
||||
for d in "$TOR_DROPIN_DIR"/*.conf; do
|
||||
[ -f "$d" ] || continue
|
||||
any=1
|
||||
local n; n=$(basename "$d" .conf)
|
||||
local h="$TOR_DATA_DIR/$n/hostname"
|
||||
if [ -f "$h" ]; then
|
||||
printf " %-30s -> %s\n" "$n" "$(cat "$h")"
|
||||
else
|
||||
printf " %-30s -> (publishing...)\n" "$n"
|
||||
fi
|
||||
done
|
||||
[ $any = 0 ] && echo "(no Tor-exposed sites)"
|
||||
}
|
||||
|
||||
tor_status() {
|
||||
local name="$1"
|
||||
[ -z "$name" ] && { error "Usage: metablogizerctl tor status <site>"; return 1; }
|
||||
local data; data=$(_tor_data_path "$name")
|
||||
local drop; drop=$(_tor_drop_path "$name")
|
||||
echo "site: $name"
|
||||
echo "stanza present: $([ -f "$drop" ] && echo yes || echo no)"
|
||||
if [ -f "$data/hostname" ]; then
|
||||
echo "onion: $(cat "$data/hostname")"
|
||||
else
|
||||
echo "onion: (not yet published)"
|
||||
fi
|
||||
systemctl is-active tor >/dev/null 2>&1 && echo "tor service: active" || echo "tor service: inactive"
|
||||
}
|
||||
|
||||
cmd_tor() {
|
||||
local action="${1:-}"; shift || true
|
||||
case "$action" in
|
||||
expose) tor_expose "$@" ;;
|
||||
revoke|remove) tor_revoke "$@" ;;
|
||||
list|ls) tor_list ;;
|
||||
status) tor_status "$@" ;;
|
||||
*)
|
||||
cat <<EOF
|
||||
Tor commands (Punk Exposure / Emancipate verb, issue #184):
|
||||
tor expose <site> - publish site via Tor hidden service
|
||||
tor revoke <site> - stop publishing via Tor
|
||||
tor list - list Tor-exposed sites + their onion addresses
|
||||
tor status <site> - show stanza presence + onion + tor service state
|
||||
EOF
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
site_list() {
|
||||
echo "MetaBlogizer Sites:"
|
||||
echo "==================="
|
||||
|
|
@ -387,6 +536,12 @@ Sites:
|
|||
site unpublish <name> Unpublish site
|
||||
site list List all sites
|
||||
|
||||
Tor (Punk Exposure / Emancipate, issue #184):
|
||||
tor expose <site> Publish site via Tor hidden service
|
||||
tor revoke <site> Stop publishing via Tor
|
||||
tor list List Tor-exposed sites + onions
|
||||
tor status <site> Stanza + onion + tor service state
|
||||
|
||||
Service:
|
||||
migrate [host] Migrate from OpenWrt
|
||||
|
||||
|
|
@ -394,6 +549,7 @@ Examples:
|
|||
metablogizerctl components # JSON components
|
||||
metablogizerctl site create myblog blog.example.com
|
||||
metablogizerctl site publish myblog
|
||||
metablogizerctl tor expose myblog # Emancipate via Tor
|
||||
metablogizerctl migrate 192.168.255.1
|
||||
|
||||
EOF
|
||||
|
|
@ -422,6 +578,8 @@ case "${1:-}" in
|
|||
*) echo "Usage: metablogizerctl site create|delete|publish|unpublish|list" ;;
|
||||
esac
|
||||
;;
|
||||
# Tor (Emancipate, issue #184)
|
||||
tor) shift; cmd_tor "$@" ;;
|
||||
migrate) shift; cmd_migrate "$@" ;;
|
||||
help|--help|-h|'') show_help ;;
|
||||
*) error "Unknown: $1"; exit 1 ;;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ Source: secubox-metoblizer
|
|||
Section: admin
|
||||
Priority: optional
|
||||
Maintainer: Gerald KERMA <devel@cybermind.fr>
|
||||
Build-Depends: debhelper (>= 13)
|
||||
Build-Depends: debhelper-compat (= 13)
|
||||
Standards-Version: 4.6.2
|
||||
|
||||
Package: secubox-metoblizer
|
||||
|
|
|
|||
|
|
@ -1,268 +1,410 @@
|
|||
#!/usr/bin/env python3
|
||||
"""mitmproxyctl — LXC container management for SecuBox WAF
|
||||
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||
# Source-Disclosed License — All rights reserved except as expressly granted.
|
||||
# See LICENCE-CMSD-1.0.md for terms.
|
||||
"""mitmproxyctl — SecuBox WAF interception layer control (issue #173)
|
||||
|
||||
The third verb of SecuBox's routing grammar, parallel to:
|
||||
- haproxyctl vhost add/remove (routing layer)
|
||||
- giteactl user add/remove (identity layer)
|
||||
- mitmproxyctl route add/remove (interception layer) ← this tool
|
||||
|
||||
Usage:
|
||||
mitmproxyctl install Create and configure LXC container
|
||||
mitmproxyctl start Start container and mitmproxy
|
||||
mitmproxyctl stop Stop container
|
||||
mitmproxyctl restart Restart container
|
||||
mitmproxyctl status Show container and process status
|
||||
mitmproxyctl destroy Remove container (requires --force)
|
||||
mitmproxyctl logs Show mitmproxy logs
|
||||
Lifecycle:
|
||||
mitmproxyctl install Create + bootstrap the LXC container
|
||||
mitmproxyctl start Start the container
|
||||
mitmproxyctl stop Stop the container
|
||||
mitmproxyctl restart Restart the container (= mitmdump reload)
|
||||
mitmproxyctl status Show container + mitmdump status
|
||||
mitmproxyctl destroy --force Remove the container
|
||||
mitmproxyctl logs Tail mitmproxy logs
|
||||
|
||||
Routes (forge the missing verb, #173):
|
||||
mitmproxyctl route list List all (host, ip, port) entries
|
||||
mitmproxyctl route add HOST IP PORT [--no-restart]
|
||||
Add/replace a route (atomic, mirrors
|
||||
to /srv/mitmproxy/ and the LXC rootfs
|
||||
copy), then restart unless --no-restart
|
||||
mitmproxyctl route remove HOST [--no-restart]
|
||||
Remove a route, then restart
|
||||
|
||||
Config (TOML, default /etc/secubox/mitmproxy.toml):
|
||||
[container]
|
||||
name = "mitmproxy" # board reality (not "mitmproxy-waf")
|
||||
lxc_root = "/data/lxc" # board reality (not "/var/lib/lxc")
|
||||
memory_limit = "512M"
|
||||
|
||||
[proxy]
|
||||
listen_port = 8080
|
||||
web_port = 8091
|
||||
host_routes_path = "/srv/mitmproxy/haproxy-routes.json"
|
||||
inbound_routes_path = "/srv/mitmproxy-in/haproxy-routes.json"
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
import json
|
||||
import time
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
try:
|
||||
import toml
|
||||
import tomllib # Python 3.11+
|
||||
except ImportError:
|
||||
toml = None
|
||||
try:
|
||||
import tomli as tomllib
|
||||
except ImportError:
|
||||
tomllib = None
|
||||
|
||||
CONTAINER_NAME = "mitmproxy-waf"
|
||||
CONFIG_FILE = Path("/etc/secubox/mitmproxy.toml")
|
||||
DATA_PATH = Path("/srv/mitmproxy-waf")
|
||||
LXC_PATH = Path("/var/lib/lxc") / CONTAINER_NAME
|
||||
# ── Defaults (match the live board convention) ─────────────────────────────
|
||||
DEFAULT_CONFIG_FILE = Path("/etc/secubox/mitmproxy.toml")
|
||||
DEFAULT_CONTAINER_NAME = "mitmproxy"
|
||||
DEFAULT_LXC_ROOT = Path("/data/lxc")
|
||||
DEFAULT_LISTEN_PORT = 8080
|
||||
DEFAULT_WEB_PORT = 8091
|
||||
DEFAULT_MEMORY_LIMIT = "512M"
|
||||
DEFAULT_HOST_ROUTES = Path("/srv/mitmproxy/haproxy-routes.json")
|
||||
DEFAULT_INBOUND_ROUTES = Path("/srv/mitmproxy-in/haproxy-routes.json")
|
||||
|
||||
|
||||
def load_config() -> dict:
|
||||
"""Load configuration from TOML file."""
|
||||
if toml and CONFIG_FILE.exists():
|
||||
return toml.load(CONFIG_FILE)
|
||||
# ── Config ─────────────────────────────────────────────────────────────────
|
||||
def load_config(path: Optional[Path] = None) -> dict:
|
||||
"""Read [container] + [proxy] from TOML with sensible defaults."""
|
||||
p = path or DEFAULT_CONFIG_FILE
|
||||
raw = {}
|
||||
if tomllib and p.exists():
|
||||
try:
|
||||
with open(p, "rb") as f:
|
||||
raw = tomllib.load(f)
|
||||
except Exception as e:
|
||||
print(f" ! cannot parse {p}: {e}", file=sys.stderr)
|
||||
raw = {}
|
||||
container = raw.get("container") or {}
|
||||
proxy = raw.get("proxy") or {}
|
||||
return {
|
||||
"container": {"name": CONTAINER_NAME, "memory_limit": "256M"},
|
||||
"proxy": {"listen_port": 8890, "web_port": 8091, "data_path": str(DATA_PATH)},
|
||||
"container": {
|
||||
"name": container.get("name", DEFAULT_CONTAINER_NAME),
|
||||
"lxc_root": Path(container.get("lxc_root", str(DEFAULT_LXC_ROOT))),
|
||||
"memory_limit": container.get("memory_limit", DEFAULT_MEMORY_LIMIT),
|
||||
},
|
||||
"proxy": {
|
||||
"listen_port": proxy.get("listen_port", DEFAULT_LISTEN_PORT),
|
||||
"web_port": proxy.get("web_port", DEFAULT_WEB_PORT),
|
||||
"host_routes_path": Path(proxy.get("host_routes_path", str(DEFAULT_HOST_ROUTES))),
|
||||
"inbound_routes_path": Path(proxy.get("inbound_routes_path", str(DEFAULT_INBOUND_ROUTES))),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def run(cmd: list, check: bool = True, capture: bool = False) -> subprocess.CompletedProcess:
|
||||
"""Run a command with error handling."""
|
||||
print(f" → {' '.join(cmd)}")
|
||||
# ── Shell helpers ──────────────────────────────────────────────────────────
|
||||
def run(cmd: list, check: bool = True, capture: bool = False, quiet: bool = False) -> subprocess.CompletedProcess:
|
||||
if not quiet:
|
||||
print(f" → {' '.join(cmd)}")
|
||||
return subprocess.run(cmd, check=check, capture_output=capture, text=True)
|
||||
|
||||
|
||||
def lxc_exists() -> bool:
|
||||
"""Check if LXC container exists."""
|
||||
result = run(["lxc-ls"], capture=True, check=False)
|
||||
return CONTAINER_NAME in result.stdout.split()
|
||||
def lxc_exists(name: str) -> bool:
|
||||
r = run(["lxc-ls"], capture=True, check=False, quiet=True)
|
||||
return name in r.stdout.split()
|
||||
|
||||
|
||||
def lxc_running() -> bool:
|
||||
"""Check if LXC container is running."""
|
||||
result = run(["lxc-info", "-n", CONTAINER_NAME, "-s"], capture=True, check=False)
|
||||
return "RUNNING" in result.stdout
|
||||
def lxc_running(name: str) -> bool:
|
||||
r = run(["lxc-info", "-n", name, "-s"], capture=True, check=False, quiet=True)
|
||||
return "RUNNING" in r.stdout
|
||||
|
||||
|
||||
def lxc_exec(cmd: list, check: bool = True) -> subprocess.CompletedProcess:
|
||||
"""Execute command inside LXC container."""
|
||||
return run(["lxc-attach", "-n", CONTAINER_NAME, "--"] + cmd, check=check)
|
||||
def lxc_exec(name: str, cmd: list, check: bool = True) -> subprocess.CompletedProcess:
|
||||
return run(["lxc-attach", "-n", name, "--"] + cmd, check=check)
|
||||
|
||||
|
||||
def cmd_install():
|
||||
"""Create and configure LXC container."""
|
||||
print(f"Installing LXC container: {CONTAINER_NAME}")
|
||||
|
||||
if lxc_exists():
|
||||
print(f" Container {CONTAINER_NAME} already exists")
|
||||
# ── Lifecycle ──────────────────────────────────────────────────────────────
|
||||
def cmd_install(cfg: dict) -> int:
|
||||
name = cfg["container"]["name"]
|
||||
print(f"Installing LXC container: {name}")
|
||||
if lxc_exists(name):
|
||||
print(f" Container {name} already exists")
|
||||
return 1
|
||||
|
||||
config = load_config()
|
||||
|
||||
# Create data directories
|
||||
print("Creating data directories...")
|
||||
DATA_PATH.mkdir(parents=True, exist_ok=True)
|
||||
(DATA_PATH / "data").mkdir(exist_ok=True)
|
||||
(DATA_PATH / "addons").mkdir(exist_ok=True)
|
||||
(DATA_PATH / "config").mkdir(exist_ok=True)
|
||||
|
||||
# Create LXC container
|
||||
print("Creating LXC container...")
|
||||
# Detect architecture: aarch64 → arm64, x86_64 → amd64
|
||||
arch = "arm64" if platform.machine() == "aarch64" else "amd64"
|
||||
run([
|
||||
"lxc-create", "-n", CONTAINER_NAME,
|
||||
"-t", "download",
|
||||
"--",
|
||||
"-d", "debian",
|
||||
"-r", "bookworm",
|
||||
"-a", arch
|
||||
])
|
||||
|
||||
# Configure container
|
||||
print("Configuring container...")
|
||||
lxc_config = LXC_PATH / "config"
|
||||
with open(lxc_config, "a") as f:
|
||||
f.write(f"\n# SecuBox WAF configuration\n")
|
||||
f.write(f"lxc.mount.entry = {DATA_PATH}/data data none bind,create=dir 0 0\n")
|
||||
f.write(f"lxc.mount.entry = {DATA_PATH}/addons addons none bind,create=dir 0 0\n")
|
||||
memory_bytes = int(config["container"].get("memory_limit", "256M").rstrip("M")) * 1024 * 1024
|
||||
f.write(f"lxc.cgroup2.memory.max = {memory_bytes}\n")
|
||||
|
||||
# Start container for setup
|
||||
print("Starting container for initial setup...")
|
||||
run(["lxc-start", "-n", CONTAINER_NAME])
|
||||
time.sleep(5) # Wait for container to boot
|
||||
|
||||
# Install mitmproxy inside container
|
||||
print("Installing mitmproxy in container...")
|
||||
lxc_exec(["apt-get", "update"])
|
||||
lxc_exec(["apt-get", "install", "-y", "python3", "python3-pip"])
|
||||
lxc_exec(["pip3", "install", "--break-system-packages", "mitmproxy"])
|
||||
|
||||
# Stop container
|
||||
run(["lxc-stop", "-n", CONTAINER_NAME])
|
||||
|
||||
print(f"Container {CONTAINER_NAME} installed successfully")
|
||||
run(["lxc-create", "-n", name, "-t", "download", "--",
|
||||
"-d", "debian", "-r", "bookworm", "-a", arch])
|
||||
# Memory limit
|
||||
cfg_path = cfg["container"]["lxc_root"] / name / "config"
|
||||
if cfg_path.exists():
|
||||
with open(cfg_path, "a") as f:
|
||||
f.write("\n# SecuBox WAF — set via mitmproxyctl\n")
|
||||
mem = cfg["container"].get("memory_limit", DEFAULT_MEMORY_LIMIT)
|
||||
try:
|
||||
bytes_ = int(mem.rstrip("MmGg")) * (1024 ** (2 if mem[-1].lower() == "m" else 3))
|
||||
f.write(f"lxc.cgroup2.memory.max = {bytes_}\n")
|
||||
except Exception:
|
||||
pass
|
||||
run(["lxc-start", "-n", name])
|
||||
time.sleep(5)
|
||||
lxc_exec(name, ["apt-get", "update"])
|
||||
lxc_exec(name, ["apt-get", "install", "-y", "python3", "python3-pip"])
|
||||
lxc_exec(name, ["pip3", "install", "--break-system-packages", "mitmproxy"])
|
||||
run(["lxc-stop", "-n", name])
|
||||
print(f"Container {name} installed")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_start():
|
||||
"""Start container and mitmproxy."""
|
||||
print(f"Starting {CONTAINER_NAME}...")
|
||||
|
||||
if not lxc_exists():
|
||||
print(f" Container does not exist. Run: mitmproxyctl install")
|
||||
def cmd_start(cfg: dict) -> int:
|
||||
name = cfg["container"]["name"]
|
||||
print(f"Starting {name}...")
|
||||
if not lxc_exists(name):
|
||||
print(f" Container {name} does not exist (run: mitmproxyctl install)")
|
||||
return 1
|
||||
|
||||
if lxc_running():
|
||||
print(f" Container already running")
|
||||
if lxc_running(name):
|
||||
print(f" Already running")
|
||||
return 0
|
||||
|
||||
config = load_config()
|
||||
proxy_port = config["proxy"].get("listen_port", 8890)
|
||||
web_port = config["proxy"].get("web_port", 8091)
|
||||
|
||||
# Start container
|
||||
run(["lxc-start", "-n", CONTAINER_NAME])
|
||||
run(["lxc-start", "-n", name])
|
||||
time.sleep(3)
|
||||
|
||||
# Start mitmproxy inside container (backgrounded)
|
||||
print("Starting mitmproxy...")
|
||||
lxc_exec([
|
||||
"sh", "-c",
|
||||
f"nohup mitmdump --mode upstream:http://127.0.0.1:80 "
|
||||
f"--listen-port {proxy_port} "
|
||||
f"--set web_open_browser=false "
|
||||
f"-s /addons/secubox_waf.py "
|
||||
f"> /var/log/mitmproxy.log 2>&1 &"
|
||||
])
|
||||
|
||||
print(f"Container {CONTAINER_NAME} started")
|
||||
print(f" Proxy port: {proxy_port}")
|
||||
print(f" Web port: {web_port}")
|
||||
# Prefer the in-LXC systemd unit if present; otherwise direct mitmdump
|
||||
r = lxc_exec(name, ["systemctl", "list-unit-files", "mitmproxy.service"], check=False)
|
||||
if r.returncode == 0 and "mitmproxy.service" in r.stdout:
|
||||
lxc_exec(name, ["systemctl", "start", "mitmproxy"])
|
||||
else:
|
||||
port = cfg["proxy"]["listen_port"]
|
||||
lxc_exec(name, [
|
||||
"sh", "-c",
|
||||
f"nohup mitmdump --listen-port {port} -s /srv/mitmproxy/secubox_waf.py "
|
||||
f"> /var/log/mitmproxy.log 2>&1 &",
|
||||
])
|
||||
print(f" Started")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_stop():
|
||||
"""Stop container."""
|
||||
print(f"Stopping {CONTAINER_NAME}...")
|
||||
|
||||
if not lxc_exists():
|
||||
print(f" Container does not exist")
|
||||
def cmd_stop(cfg: dict) -> int:
|
||||
name = cfg["container"]["name"]
|
||||
print(f"Stopping {name}...")
|
||||
if not lxc_exists(name):
|
||||
print(" Container does not exist")
|
||||
return 1
|
||||
|
||||
if not lxc_running():
|
||||
print(f" Container not running")
|
||||
if not lxc_running(name):
|
||||
print(" Already stopped")
|
||||
return 0
|
||||
|
||||
run(["lxc-stop", "-n", CONTAINER_NAME])
|
||||
print(f"Container {CONTAINER_NAME} stopped")
|
||||
run(["lxc-stop", "-n", name])
|
||||
print(" Stopped")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_restart():
|
||||
"""Restart container."""
|
||||
cmd_stop()
|
||||
def cmd_restart(cfg: dict) -> int:
|
||||
"""Restart prefers an in-LXC systemd reload over a full container bounce."""
|
||||
name = cfg["container"]["name"]
|
||||
if lxc_running(name):
|
||||
r = lxc_exec(name, ["systemctl", "list-unit-files", "mitmproxy.service"], check=False)
|
||||
if r.returncode == 0 and "mitmproxy.service" in r.stdout:
|
||||
lxc_exec(name, ["systemctl", "restart", "mitmproxy"])
|
||||
print(f" mitmproxy.service restarted inside {name}")
|
||||
return 0
|
||||
# Fallback: stop + start the container
|
||||
cmd_stop(cfg)
|
||||
time.sleep(2)
|
||||
return cmd_start()
|
||||
return cmd_start(cfg)
|
||||
|
||||
|
||||
def cmd_status():
|
||||
"""Show container and process status."""
|
||||
print(f"Status: {CONTAINER_NAME}")
|
||||
|
||||
if not lxc_exists():
|
||||
def cmd_status(cfg: dict) -> int:
|
||||
name = cfg["container"]["name"]
|
||||
print(f"Status: {name}")
|
||||
if not lxc_exists(name):
|
||||
print(" Container: NOT INSTALLED")
|
||||
return 1
|
||||
|
||||
if lxc_running():
|
||||
if lxc_running(name):
|
||||
print(" Container: RUNNING")
|
||||
|
||||
# Check mitmproxy process
|
||||
result = lxc_exec(["pgrep", "-f", "mitmdump"], check=False)
|
||||
if result.returncode == 0:
|
||||
print(" Mitmproxy: RUNNING")
|
||||
else:
|
||||
print(" Mitmproxy: STOPPED")
|
||||
|
||||
# Get container IP
|
||||
result = run(["lxc-info", "-n", CONTAINER_NAME, "-iH"], capture=True, check=False)
|
||||
if result.stdout.strip():
|
||||
print(f" IP: {result.stdout.strip().split()[0]}")
|
||||
r = lxc_exec(name, ["pgrep", "-f", "mitmdump"], check=False)
|
||||
print(f" mitmdump: {'RUNNING' if r.returncode == 0 else 'STOPPED'}")
|
||||
r = run(["lxc-info", "-n", name, "-iH"], capture=True, check=False, quiet=True)
|
||||
if r.stdout.strip():
|
||||
print(f" IP: {r.stdout.strip().split()[0]}")
|
||||
else:
|
||||
print(" Container: STOPPED")
|
||||
|
||||
# Check threat log
|
||||
threats_log = DATA_PATH / "data" / "threats.log"
|
||||
if threats_log.exists():
|
||||
lines = threats_log.read_text().strip().split("\n")
|
||||
print(f" Threats logged: {len([l for l in lines if l])}")
|
||||
|
||||
routes = _read_routes(cfg["proxy"]["host_routes_path"])
|
||||
print(f" Routes: {len(routes)} hosts")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_destroy(force: bool = False):
|
||||
"""Remove container."""
|
||||
def cmd_destroy(cfg: dict, force: bool = False) -> int:
|
||||
name = cfg["container"]["name"]
|
||||
if not force:
|
||||
print("Use --force to destroy container")
|
||||
return 1
|
||||
|
||||
print(f"Destroying {CONTAINER_NAME}...")
|
||||
|
||||
if not lxc_exists():
|
||||
print(f" Container does not exist")
|
||||
print(f"Destroying {name}...")
|
||||
if not lxc_exists(name):
|
||||
print(" Container does not exist")
|
||||
return 0
|
||||
|
||||
if lxc_running():
|
||||
run(["lxc-stop", "-n", CONTAINER_NAME])
|
||||
|
||||
run(["lxc-destroy", "-n", CONTAINER_NAME])
|
||||
print(f"Container {CONTAINER_NAME} destroyed")
|
||||
if lxc_running(name):
|
||||
run(["lxc-stop", "-n", name])
|
||||
run(["lxc-destroy", "-n", name])
|
||||
print(f" Destroyed")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_logs():
|
||||
"""Show mitmproxy logs."""
|
||||
if not lxc_running():
|
||||
print("Container not running")
|
||||
def cmd_logs(cfg: dict) -> int:
|
||||
name = cfg["container"]["name"]
|
||||
if not lxc_running(name):
|
||||
print(f"Container {name} not running")
|
||||
return 1
|
||||
|
||||
lxc_exec(["tail", "-100", "/var/log/mitmproxy.log"], check=False)
|
||||
lxc_exec(name, ["journalctl", "-u", "mitmproxy", "-n", "100", "--no-pager"], check=False)
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="SecuBox WAF container management")
|
||||
parser.add_argument("command", choices=["install", "start", "stop", "restart", "status", "destroy", "logs"])
|
||||
parser.add_argument("--force", action="store_true", help="Force operation")
|
||||
args = parser.parse_args()
|
||||
# ── Routes (the new verb, #173) ────────────────────────────────────────────
|
||||
def _route_files(cfg: dict) -> list:
|
||||
"""Return the route file paths that exist (host copy + LXC-rootfs mirror).
|
||||
|
||||
commands = {
|
||||
"install": cmd_install,
|
||||
"start": cmd_start,
|
||||
"stop": cmd_stop,
|
||||
"restart": cmd_restart,
|
||||
"status": cmd_status,
|
||||
"destroy": lambda: cmd_destroy(args.force),
|
||||
"logs": cmd_logs,
|
||||
}
|
||||
The LXC-rootfs mirror is computed from container.lxc_root + container.name
|
||||
+ the in-LXC path of host_routes_path. Both must be kept in sync so that
|
||||
the addon inside the LXC sees the same routes as the host-facing tooling.
|
||||
"""
|
||||
out = []
|
||||
host_path = cfg["proxy"]["host_routes_path"]
|
||||
inbound_path = cfg["proxy"]["inbound_routes_path"]
|
||||
lxc_root = cfg["container"]["lxc_root"]
|
||||
name = cfg["container"]["name"]
|
||||
# LXC-rootfs mirror of host_routes_path
|
||||
lxc_mirror = lxc_root / name / "rootfs" / host_path.relative_to("/")
|
||||
for p in (host_path, lxc_mirror, inbound_path):
|
||||
out.append(p)
|
||||
return out
|
||||
|
||||
return commands[args.command]()
|
||||
|
||||
def _read_routes(path: Path) -> dict:
|
||||
if not path.exists():
|
||||
return {}
|
||||
try:
|
||||
with open(path) as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def _write_routes(path: Path, data: dict) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = path.with_suffix(path.suffix + ".tmp")
|
||||
with open(tmp, "w") as f:
|
||||
json.dump(data, f, indent=2, sort_keys=True)
|
||||
f.write("\n")
|
||||
tmp.replace(path)
|
||||
|
||||
|
||||
def cmd_route_list(cfg: dict) -> int:
|
||||
"""Print the host-facing route map. The LXC mirror is assumed in sync."""
|
||||
routes = _read_routes(cfg["proxy"]["host_routes_path"])
|
||||
if not routes:
|
||||
print("(no routes)")
|
||||
return 0
|
||||
width = max((len(h) for h in routes), default=0)
|
||||
for host in sorted(routes):
|
||||
v = routes[host]
|
||||
ip, port = (v[0], v[1]) if isinstance(v, list) and len(v) >= 2 else (str(v), "")
|
||||
print(f" {host.ljust(width)} -> {ip}:{port}")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_route_add(cfg: dict, host: str, ip: str, port: int, restart: bool = True) -> int:
|
||||
"""Add or replace a route in every existing route file, then optionally restart."""
|
||||
if not host or not ip or not isinstance(port, int):
|
||||
print("route add: host + ip + port required", file=sys.stderr)
|
||||
return 2
|
||||
updated = []
|
||||
for p in _route_files(cfg):
|
||||
if p.parent.exists() or p.exists():
|
||||
d = _read_routes(p)
|
||||
d[host] = [ip, int(port)]
|
||||
_write_routes(p, d)
|
||||
updated.append(str(p))
|
||||
if not updated:
|
||||
print("route add: no route file present (none of the configured paths exist)", file=sys.stderr)
|
||||
return 3
|
||||
for p in updated:
|
||||
print(f" + {host} -> {ip}:{port} in {p}")
|
||||
if restart:
|
||||
return cmd_restart(cfg)
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_route_remove(cfg: dict, host: str, restart: bool = True) -> int:
|
||||
updated = []
|
||||
missing = []
|
||||
for p in _route_files(cfg):
|
||||
if not p.exists():
|
||||
continue
|
||||
d = _read_routes(p)
|
||||
if host in d:
|
||||
del d[host]
|
||||
_write_routes(p, d)
|
||||
updated.append(str(p))
|
||||
else:
|
||||
missing.append(str(p))
|
||||
if not updated:
|
||||
print(f"route remove: {host!r} not found in any route file", file=sys.stderr)
|
||||
return 3
|
||||
for p in updated:
|
||||
print(f" - {host} from {p}")
|
||||
for p in missing:
|
||||
print(f" · {host} already absent in {p}")
|
||||
if restart:
|
||||
return cmd_restart(cfg)
|
||||
return 0
|
||||
|
||||
|
||||
# ── CLI ────────────────────────────────────────────────────────────────────
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
p = argparse.ArgumentParser(prog="mitmproxyctl", description="SecuBox WAF interception control (#173)")
|
||||
p.add_argument("--config", type=Path, default=None,
|
||||
help=f"path to TOML config (default: {DEFAULT_CONFIG_FILE})")
|
||||
sub = p.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
for name in ("install", "start", "stop", "restart", "status", "logs"):
|
||||
sub.add_parser(name)
|
||||
destroy = sub.add_parser("destroy")
|
||||
destroy.add_argument("--force", action="store_true")
|
||||
|
||||
route = sub.add_parser("route", help="manage intercepted routes")
|
||||
rsub = route.add_subparsers(dest="route_cmd", required=True)
|
||||
rsub.add_parser("list")
|
||||
radd = rsub.add_parser("add")
|
||||
radd.add_argument("host")
|
||||
radd.add_argument("ip")
|
||||
radd.add_argument("port", type=int)
|
||||
radd.add_argument("--no-restart", action="store_true")
|
||||
rrm = rsub.add_parser("remove")
|
||||
rrm.add_argument("host")
|
||||
rrm.add_argument("--no-restart", action="store_true")
|
||||
return p
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = build_parser().parse_args()
|
||||
cfg = load_config(args.config)
|
||||
if args.cmd == "install":
|
||||
return cmd_install(cfg)
|
||||
if args.cmd == "start":
|
||||
return cmd_start(cfg)
|
||||
if args.cmd == "stop":
|
||||
return cmd_stop(cfg)
|
||||
if args.cmd == "restart":
|
||||
return cmd_restart(cfg)
|
||||
if args.cmd == "status":
|
||||
return cmd_status(cfg)
|
||||
if args.cmd == "destroy":
|
||||
return cmd_destroy(cfg, force=getattr(args, "force", False))
|
||||
if args.cmd == "logs":
|
||||
return cmd_logs(cfg)
|
||||
if args.cmd == "route":
|
||||
if args.route_cmd == "list":
|
||||
return cmd_route_list(cfg)
|
||||
if args.route_cmd == "add":
|
||||
return cmd_route_add(cfg, args.host, args.ip, args.port, restart=not args.no_restart)
|
||||
if args.route_cmd == "remove":
|
||||
return cmd_route_remove(cfg, args.host, restart=not args.no_restart)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -1,9 +1,20 @@
|
|||
#!/usr/bin/env bash
|
||||
# SecuBox MetaCtl — ISP Home Publish CLI
|
||||
# CyberMind — https://cybermind.fr
|
||||
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||
#
|
||||
# publishctl — SecuBox ISP Home Publish CLI (issue #180)
|
||||
#
|
||||
# Renamed from `metactl` for naming consistency with the rest of the
|
||||
# SecuBox grammar (haproxyctl/giteactl/mitmproxyctl/metablogizerctl/
|
||||
# dropletctl/streamlitctl/streamforgectl). The old `metactl` name remains
|
||||
# as a symlink for backward compatibility — to drop in a future major.
|
||||
#
|
||||
# Flat verbs are now also reachable under the `post` noun dispatch
|
||||
# for grammar consistency (publishctl post upload <file>, etc). Flat
|
||||
# top-level verbs preserved for backward compatibility.
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="1.0.0"
|
||||
VERSION="2.0.0"
|
||||
API_BASE="${SECUBOX_API_BASE:-http://127.0.0.1/api/v1/publish}"
|
||||
METABLOGIZER_API="${SECUBOX_METABLOGIZER_API:-http://127.0.0.1/api/v1/metablogizer}"
|
||||
TOKEN_FILE="${SECUBOX_TOKEN_FILE:-/etc/secubox/secrets/jwt-token}"
|
||||
|
|
@ -408,8 +419,39 @@ cmd_health() {
|
|||
fi
|
||||
}
|
||||
|
||||
# post noun dispatch (issue #180 — grammar consistency, parallel to
|
||||
# `giteactl repo`, `mitmproxyctl route`, `dropletctl file`, etc).
|
||||
# Delegates to the existing flat cmd_* functions; both grammars supported.
|
||||
cmd_post() {
|
||||
local act="${1:-}"; shift || true
|
||||
case "$act" in
|
||||
upload) cmd_upload "$@" ;;
|
||||
publish) cmd_publish "$@" ;;
|
||||
unpublish) cmd_unpublish "$@" ;;
|
||||
list|ls) cmd_list ;;
|
||||
download) cmd_download "$@" ;;
|
||||
qrcode|qr) cmd_qrcode "$@" ;;
|
||||
health) cmd_health "$@" ;;
|
||||
*)
|
||||
cat <<EOF
|
||||
Post commands (issue #180):
|
||||
post upload <file.zip> [name] [--domain=D] [--auto-publish]
|
||||
post publish <name>
|
||||
post unpublish <name>
|
||||
post list
|
||||
post download <name> [output.zip]
|
||||
post qrcode <name>
|
||||
post health <domain>
|
||||
EOF
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Main
|
||||
case "${1:-help}" in
|
||||
# noun-verb grammar (issue #180)
|
||||
post) shift; cmd_post "$@" ;;
|
||||
# flat verbs (backward-compat — same callbacks)
|
||||
upload) shift; cmd_upload "$@" ;;
|
||||
publish) shift; cmd_publish "$@" ;;
|
||||
unpublish) shift; cmd_unpublish "$@" ;;
|
||||
|
|
@ -419,10 +461,10 @@ case "${1:-help}" in
|
|||
status) cmd_status ;;
|
||||
health) shift; cmd_health "$@" ;;
|
||||
-h|--help|help) usage ;;
|
||||
-v|--version) echo "metactl v${VERSION}" ;;
|
||||
-v|--version) echo "publishctl v${VERSION}" ;;
|
||||
*)
|
||||
echo -e "${RED}Unknown command:${NC} $1"
|
||||
echo "Run 'metactl --help' for usage"
|
||||
echo "Run 'publishctl --help' for usage"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
|
@ -1,3 +1,20 @@
|
|||
secubox-publish (2.0.0-1~bookworm1) bookworm; urgency=medium
|
||||
|
||||
* Rename `metactl` -> `publishctl` for naming consistency with the rest
|
||||
of the SecuBox ctl grammar (issue #180). The `metactl` name remains
|
||||
as a symlink for backward compatibility — to drop in a future major.
|
||||
* publishctl: add `post` noun dispatch so verbs are grouped under a
|
||||
coherent <noun> <verb> schema parallel to giteactl/dropletctl/
|
||||
metablogizerctl. Flat top-level verbs preserved as alias.
|
||||
|
||||
publishctl post upload <file.zip> [name] [--auto-publish]
|
||||
publishctl post publish/unpublish <name>
|
||||
publishctl post list/download/qrcode/health ...
|
||||
|
||||
* Bumped to 2.0.0 (CLI surface rename).
|
||||
|
||||
-- Gerald KERMA <devel@cybermind.fr> Sun, 17 May 2026 11:38:19 +0200
|
||||
|
||||
secubox-publish (1.0.0-1~bookworm1) bookworm; urgency=medium
|
||||
|
||||
* Initial release
|
||||
|
|
|
|||
|
|
@ -12,9 +12,11 @@ override_dh_auto_install:
|
|||
# Modular nginx config
|
||||
install -d debian/secubox-publish/etc/nginx/secubox.d
|
||||
[ -f nginx/publish.conf ] && cp nginx/publish.conf debian/secubox-publish/etc/nginx/secubox.d/ || true
|
||||
# CLI tool
|
||||
# CLI tool — primary `publishctl` + `metactl` symlink for backward compat (#180)
|
||||
install -d debian/secubox-publish/usr/sbin
|
||||
[ -f bin/metactl ] && install -m 755 bin/metactl debian/secubox-publish/usr/sbin/metactl || true
|
||||
[ -f bin/publishctl ] && install -m 755 bin/publishctl debian/secubox-publish/usr/sbin/publishctl || true
|
||||
[ -f debian/secubox-publish/usr/sbin/publishctl ] && \
|
||||
ln -sf publishctl debian/secubox-publish/usr/sbin/metactl || true
|
||||
# Plugins directory
|
||||
install -d debian/secubox-publish/srv/secubox/modules/publish/plugins
|
||||
[ -d plugins ] && cp -r plugins/. debian/secubox-publish/srv/secubox/modules/publish/plugins/ || true
|
||||
|
|
|
|||
|
|
@ -1,3 +1,15 @@
|
|||
secubox-streamforge (1.0.2-1~bookworm1) bookworm; urgency=medium
|
||||
|
||||
* Forge streamforgectl (issue #183). Subcommands: lifecycle (start/stop/
|
||||
restart/status/logs), Three-fold JSON (components/access), project
|
||||
noun (create/remove/list/start/stop/restart/info/templates) wrapping
|
||||
the /api/v1/streamforge/app* endpoints over the Unix socket.
|
||||
* Paired with streamlitctl on the hosting side — forge -> host workflow
|
||||
expressible end-to-end (forge create/edit -> export to git -> stream
|
||||
deploy).
|
||||
|
||||
-- Gerald KERMA <devel@cybermind.fr> Sun, 17 May 2026 11:34:28 +0200
|
||||
|
||||
secubox-streamforge (1.0.1-1~bookworm1) bookworm; urgency=medium
|
||||
|
||||
* Add dynamic menu system with menu.d JSON definitions
|
||||
|
|
|
|||
|
|
@ -12,3 +12,6 @@ override_dh_auto_install:
|
|||
# Modular nginx config
|
||||
install -d debian/secubox-streamforge/etc/nginx/secubox.d
|
||||
[ -f nginx/streamforge.conf ] && cp nginx/streamforge.conf debian/secubox-streamforge/etc/nginx/secubox.d/ || true
|
||||
# streamforgectl (#183)
|
||||
install -d debian/secubox-streamforge/usr/sbin
|
||||
install -m 755 sbin/streamforgectl debian/secubox-streamforge/usr/sbin/
|
||||
|
|
|
|||
143
packages/secubox-streamforge/sbin/streamforgectl
Executable file
143
packages/secubox-streamforge/sbin/streamforgectl
Executable file
|
|
@ -0,0 +1,143 @@
|
|||
#!/bin/bash
|
||||
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||
# streamforgectl — SecuBox StreamForge (Streamlit dev workbench) control (#183)
|
||||
#
|
||||
# Parallel to streamlitctl (the hosting side). The Forge ↔ Streamlit pair:
|
||||
# streamforgectl project create <name> --template hello
|
||||
# streamforgectl project export <name> <gitea_url>
|
||||
# streamlitctl app deploy <name> <gitea_url>
|
||||
# Three verbs, two layers (dev → hosting), one expressible workflow.
|
||||
|
||||
set -euo pipefail
|
||||
VERSION="0.1.0"
|
||||
SOCKET="${STREAMFORGE_SOCKET:-/run/secubox/streamforge.sock}"
|
||||
API="http://localhost/api/v1/streamforge"
|
||||
SERVICE="secubox-streamforge.service"
|
||||
|
||||
G='\033[0;32m'; Y='\033[1;33m'; R='\033[0;31m'; N='\033[0m'
|
||||
log() { printf "${G}[FORGE]${N} %s\n" "$*"; }
|
||||
warn() { printf "${Y}[WARN]${N} %s\n" "$*"; }
|
||||
error() { printf "${R}[ERROR]${N} %s\n" "$*" >&2; }
|
||||
|
||||
api() {
|
||||
local m="$1" p="$2"; shift 2
|
||||
[ -S "$SOCKET" ] || { error "socket $SOCKET absent — start $SERVICE"; exit 2; }
|
||||
curl --unix-socket "$SOCKET" -sS -X "$m" -w "\nHTTP_CODE:%{http_code}\n" "${API}${p}" "$@"
|
||||
}
|
||||
api_code() { echo "$1" | grep '^HTTP_CODE:' | cut -d: -f2; }
|
||||
api_body() { echo "$1" | sed '/^HTTP_CODE:/d'; }
|
||||
|
||||
# Lifecycle
|
||||
cmd_start() { systemctl start "$SERVICE" && log started; }
|
||||
cmd_stop() { systemctl stop "$SERVICE" && log stopped; }
|
||||
cmd_restart() { systemctl restart "$SERVICE" && log restarted; }
|
||||
cmd_status() { systemctl is-active "$SERVICE" >/dev/null && echo active || echo inactive; }
|
||||
cmd_logs() { journalctl -u "$SERVICE" -n "${1:-50}" --no-pager; }
|
||||
|
||||
cmd_components() {
|
||||
cat <<EOF
|
||||
{"service":"$SERVICE","socket":"$SOCKET","api_base":"$API","ctl_version":"$VERSION"}
|
||||
EOF
|
||||
}
|
||||
cmd_access() {
|
||||
cat <<EOF
|
||||
{"socket":"$SOCKET","endpoints":{
|
||||
"apps":"GET /apps","templates":"GET /templates",
|
||||
"create":"POST /app","get":"GET /app/{name}","remove":"DELETE /app/{name}",
|
||||
"start":"POST /app/{name}/start","stop":"POST /app/{name}/stop","restart":"POST /app/{name}/restart",
|
||||
"file_get":"GET /app/{name}/file/{path}","file_put":"PUT /app/{name}/file/{path}"
|
||||
}}
|
||||
EOF
|
||||
}
|
||||
|
||||
# Project (the noun, #183)
|
||||
cmd_project() {
|
||||
local act="${1:-}"; shift || true
|
||||
case "$act" in
|
||||
create) project_create "$@" ;;
|
||||
remove|rm|delete) project_remove "$@" ;;
|
||||
list|ls) project_list "$@" ;;
|
||||
start) project_start "$@" ;;
|
||||
stop) project_stop "$@" ;;
|
||||
restart) project_restart "$@" ;;
|
||||
info) project_info "$@" ;;
|
||||
templates) project_templates ;;
|
||||
*)
|
||||
cat <<EOF
|
||||
Project commands:
|
||||
project create <name> [--template hello] [--description "..."]
|
||||
project remove <name>
|
||||
project list
|
||||
project start <name> (start the project's streamlit dev server)
|
||||
project stop <name>
|
||||
project restart <name>
|
||||
project info <name>
|
||||
project templates (list available templates)
|
||||
EOF
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
project_create() {
|
||||
local name="$1"; shift || { error "project create <name> required"; return 1; }
|
||||
local template="hello" desc=""
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--template) template="$2"; shift ;;
|
||||
--description) desc="$2"; shift ;;
|
||||
*) error "unknown flag: $1"; return 1 ;;
|
||||
esac; shift
|
||||
done
|
||||
log "creating project '$name' from template '$template'"
|
||||
local body
|
||||
body=$(printf '{"name":"%s","template":"%s","description":"%s"}' "$name" "$template" "$desc")
|
||||
local out; out=$(api POST "/app" -H "Content-Type: application/json" -d "$body")
|
||||
[ "$(api_code "$out")" = "200" ] || { error "create failed: $(api_body "$out" | head -2)"; return 1; }
|
||||
log "created"; api_body "$out"
|
||||
}
|
||||
|
||||
project_remove() {
|
||||
local name="$1"; [ -z "$name" ] && { error "project remove <name>"; return 1; }
|
||||
local out; out=$(api DELETE "/app/${name}")
|
||||
case "$(api_code "$out")" in
|
||||
200|204) log "removed $name" ;;
|
||||
*) error "remove failed: $(api_body "$out")"; return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
project_list() { api_body "$(api GET "/apps")"; }
|
||||
|
||||
project_start() { local n="$1"; [ -z "$n" ] && return 1; api_body "$(api POST "/app/${n}/start")"; }
|
||||
project_stop() { local n="$1"; [ -z "$n" ] && return 1; api_body "$(api POST "/app/${n}/stop")"; }
|
||||
project_restart() { local n="$1"; [ -z "$n" ] && return 1; api_body "$(api POST "/app/${n}/restart")"; }
|
||||
project_info() { local n="$1"; [ -z "$n" ] && return 1; api_body "$(api GET "/app/${n}")"; }
|
||||
project_templates() { api_body "$(api GET "/templates")"; }
|
||||
|
||||
show_help() {
|
||||
cat <<EOF
|
||||
SecuBox StreamForge Controller v$VERSION (issue #183)
|
||||
Streamlit dev workbench CLI — paired with streamlitctl (hosting layer)
|
||||
|
||||
Lifecycle: start / stop / restart / status / logs
|
||||
Three-fold: components / access (JSON)
|
||||
Project (#183): project create / remove / list / start / stop / restart / info / templates
|
||||
|
||||
Example workflow (forge → host):
|
||||
streamforgectl project create dashboard --template basic
|
||||
streamforgectl project start dashboard
|
||||
# ...iterate via the webui at /streamforge/...
|
||||
streamforgectl project export dashboard gitea://secubox/dashboard.git # TODO
|
||||
streamlitctl app deploy dashboard gitea://secubox/dashboard.git
|
||||
EOF
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
start|stop|restart|status) c="$1"; shift; cmd_$c "$@" ;;
|
||||
logs) shift; cmd_logs "$@" ;;
|
||||
components) cmd_components ;;
|
||||
access) cmd_access ;;
|
||||
project) shift; cmd_project "$@" ;;
|
||||
help|--help|-h|'') show_help ;;
|
||||
*) error "unknown: $1"; show_help; exit 1 ;;
|
||||
esac
|
||||
|
|
@ -1,3 +1,13 @@
|
|||
secubox-streamlit (1.2.1-1~bookworm1) bookworm; urgency=medium
|
||||
|
||||
* streamlitctl: add `app info <name>` and `app restart <name>` verbs
|
||||
(issue #182). The original audit underestimated the existing ctl
|
||||
surface — app list/start/stop/deploy/remove/logs were already wired.
|
||||
What was actually missing: `info` (metadata + runtime state) and
|
||||
`restart` (stop+start with port preservation from .streamlit.toml).
|
||||
|
||||
-- Gerald KERMA <devel@cybermind.fr> Sun, 17 May 2026 11:36:06 +0200
|
||||
|
||||
secubox-streamlit (1.2.0-1~bookworm1) bookworm; urgency=medium
|
||||
|
||||
* streamlitctl v1.0.0: Full Debian LXC installation support
|
||||
|
|
|
|||
|
|
@ -318,6 +318,51 @@ EOF
|
|||
echo ']}'
|
||||
}
|
||||
|
||||
# app info <name> — print metadata + runtime state from manifest + pid file (#182)
|
||||
cmd_app_info() {
|
||||
local name="$1"
|
||||
[ -z "$name" ] && { error "Usage: streamlitctl app info <name>"; return 1; }
|
||||
local d="$APPS_PATH/$name"
|
||||
[ -d "$d" ] || { error "app not found: $name"; return 1; }
|
||||
# Entry point detection (same logic as cmd_app_start)
|
||||
local entry=""
|
||||
for c in app.py main.py streamlit_app.py; do
|
||||
[ -f "$d/$c" ] && entry="$c" && break
|
||||
done
|
||||
local port=""
|
||||
[ -f "$d/.streamlit.toml" ] && port=$(grep -E "^port" "$d/.streamlit.toml" 2>/dev/null | cut -d= -f2 | tr -d ' ')
|
||||
local pidf="/var/run/streamlit-${name}.pid"
|
||||
local pid="" alive="no"
|
||||
if lxc_running; then
|
||||
pid=$(lxc-attach -n "$LXC_NAME" -- cat "$pidf" 2>/dev/null || true)
|
||||
if [ -n "$pid" ]; then
|
||||
lxc-attach -n "$LXC_NAME" -- kill -0 "$pid" >/dev/null 2>&1 && alive="yes"
|
||||
fi
|
||||
fi
|
||||
cat <<EOF
|
||||
name: $name
|
||||
path: $d
|
||||
entrypoint: ${entry:-(none)}
|
||||
port: ${port:-(unset)}
|
||||
pid_file: $pidf
|
||||
pid: ${pid:-(none)}
|
||||
running: $alive
|
||||
EOF
|
||||
}
|
||||
|
||||
# app restart <name> — stop + start (#182)
|
||||
cmd_app_restart() {
|
||||
local name="$1"
|
||||
[ -z "$name" ] && { error "Usage: streamlitctl app restart <name>"; return 1; }
|
||||
cmd_app_stop "$name" || true
|
||||
sleep 1
|
||||
# Recover the previous port from .streamlit.toml so restart preserves it
|
||||
local port=""
|
||||
[ -f "$APPS_PATH/$name/.streamlit.toml" ] && \
|
||||
port=$(grep -E "^port" "$APPS_PATH/$name/.streamlit.toml" 2>/dev/null | cut -d= -f2 | tr -d ' ')
|
||||
cmd_app_start "$name" "${port:-8501}"
|
||||
}
|
||||
|
||||
cmd_app_start() {
|
||||
local name="$1"
|
||||
local port="${2:-8501}"
|
||||
|
|
@ -694,7 +739,9 @@ case "${1:-}" in
|
|||
deploy) cmd_app_deploy "$3" "$4" ;;
|
||||
remove) cmd_app_remove "$3" ;;
|
||||
logs) cmd_app_logs "$3" "$4" ;;
|
||||
*) echo "Usage: streamlitctl app {list|start|stop|deploy|remove|logs} [args]" ;;
|
||||
info) cmd_app_info "$3" ;;
|
||||
restart) cmd_app_restart "$3" ;;
|
||||
*) echo "Usage: streamlitctl app {list|start|stop|restart|deploy|remove|logs|info} [args]" ;;
|
||||
esac
|
||||
;;
|
||||
|
||||
|
|
|
|||
|
|
@ -25,8 +25,11 @@ WEBUI_PORT=9080
|
|||
METABLOG_PORT=8900
|
||||
BACKEND_IP="10.100.0.1"
|
||||
|
||||
# Dead container IPs to fix (not running LXC containers)
|
||||
DEAD_CONTAINER_IPS="10.100.0.10 10.100.0.20 10.100.0.30 10.100.0.40"
|
||||
# Dead container IPs to fix (not running LXC containers).
|
||||
# 10.100.0.10 was removed after the mail Phase 1 LXC went live there
|
||||
# (mail.gk2 / webmail.gk2 / rspamd.gk2 / autoconfig.gk2 must keep routing
|
||||
# to the live container, not be rewritten to 10.100.0.1:9080).
|
||||
DEAD_CONTAINER_IPS="10.100.0.20 10.100.0.30 10.100.0.40"
|
||||
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >&2
|
||||
|
|
@ -39,8 +42,14 @@ get_current_routes() {
|
|||
|
||||
# Get all domains from HAProxy that use mitmproxy_inspector
|
||||
get_haproxy_domains() {
|
||||
# `use_backend mitmproxy_inspector if host_<dotted_domain>` lines end at
|
||||
# the hostname (no trailing space, regex anchors on $). The previous
|
||||
# `(?= )` lookahead required a space and silently matched zero domains —
|
||||
# which is why sync never auto-populated routes for newly-added vhosts
|
||||
# (caught 2026-05-17 when ckwa.gk2.secubox.in returned 502 because its
|
||||
# route had never been written by sync).
|
||||
grep "use_backend mitmproxy_inspector" "$HAPROXY_CFG" | \
|
||||
grep -oP 'host_\K[a-z0-9_]+(?= )' | \
|
||||
grep -oP 'host_\K[a-z0-9_]+(?=\s|$)' | \
|
||||
sed 's/_/./g' | sort -u
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user