Compare commits

...

7 Commits

Author SHA1 Message Date
CyberMind
9965f800af
Merge b62adc5421 into 39b0665678 2026-05-17 09:24:35 +00:00
b62adc5421 fix(round): re-disable agent + storage-gadget enables wiped by force-push
PR #175 commented out four ln -sf lines (two for secubox-eye-gadget.service
storage-only mode, two for secubox-eye-agent.service Pydantic-v2/SIGILL
crash loop). A force-push for the dwc2 fix earlier today inadvertently
overwrote that branch HEAD, dropping the four edits. Re-applying them
identically so the branch's intent — only secubox-otg-gadget.service
active at boot — is restored.

Combined with the dwc2 modprobe fix already on the branch, the rpiz now
boots into:
  - secubox-otg-gadget.service modprobes dwc2, libcomposite, usb_f_*
  - waits for /sys/class/udc to populate (poll, 5s)
  - creates composite ECM+ACM gadget on the UDC
  - MOCHAbin enumerates "Linux Foundation Multifunction Composite Gadget"
2026-05-17 11:24:31 +02:00
8e2262f502 fix(round): modprobe dwc2 in secubox-otg-gadget chain (rpiz UDC was implicit on eye-gadget)
The previous build had two gadget services enabled at boot:
  - secubox-eye-gadget.service: ExecStartPre=modprobe dwc2 + libcomposite
  - secubox-otg-gadget.service: ExecStartPre=modprobe libcomposite + usb_f_*
                                (NO dwc2 — relied on eye-gadget loading it first)

When PR #175 disabled secubox-eye-gadget at boot (to stop the UDC conflict),
nobody loaded dwc2 anymore. systemd-modules-load reads /etc/modules but the
order of dwc2 load vs the otg-gadget service start window was tight — and
on the rpiz the journal showed no successful gadget enumeration after the
PR #175 image deployed.

Defensive fix in two places:

1. secubox-otg-gadget.service: prepend ExecStartPre=/sbin/modprobe dwc2.
   Drop the ConditionPathIsDirectory=/sys/class/udc gate so the service
   actually runs and can load dwc2 if it isn't loaded yet (the path is a
   sysfs dir that exists when its parent /sys/class is mounted — it was
   harmless, but removing it makes the chain self-bootstrapping).

2. secubox-otg-gadget.sh check_prerequisites(): also modprobe dwc2 (in
   case the service is invoked manually by an operator), then poll
   /sys/class/udc for up to 5s waiting for the BCM USB controller to
   bind asynchronously. Diagnostic message updated to point at the two
   places to check (dtoverlay + /etc/modules) if no UDC ever shows up.
2026-05-17 11:22:55 +02:00
CyberMind
39b0665678
feat(giteactl): forge repo mirror add/remove/sync/list verb (closes #176) (#179)
Some checks are pending
License Headers / check (push) Waiting to run
Third routing verb of SecuBox, parallel to:
  haproxyctl vhost add/remove       (routing layer)
  mitmproxyctl route add/remove     (interception layer, #173)
  giteactl repo mirror add/remove   (replication layer, this PR)

Without this verb, setting up a github→gitea pull mirror required a
5-step API dance with three failure modes:
  1. No "convert existing repo to mirror" — must DELETE + migrate
  2. Minimum mirror interval = 10m (returns HTTP 500 below)
  3. Filesystem fetches don't trigger Gitea hooks — DB stays empty
The 5h debug session during today's gitea-actions migration A2 phase
exposed all three.

The new verb wraps the dance into one atomic, idempotent call:
  - probes existing state, idempotent if already mirror (just updates interval)
  - --force converts a non-mirror to mirror (DELETE + migrate) with explicit
    opt-in to data loss
  - normalizes interval to the Gitea 10m floor silently with a warn
  - triggers mirror-sync after migrate so the DB picks up the content
  - generates a short-lived admin token via gitea CLI inside the LXC,
    revokes after use (no token stored at rest)

Also fixes pre-existing alignment gaps:
  - LXC_PATH default: /srv/lxc -> auto-detect across /data/lxc /srv/lxc
    /var/lib/lxc (board reality is /data/lxc, not /srv/lxc)
  - LXC_IP default: 192.168.255.40 -> 10.100.0.40 (board reality)
  - Adds GITEA_APP_INI + ADMIN_USER as configurable defaults

Subcommands wired:
  giteactl repo create OWNER/NAME [--private] [--default-branch BR]
  giteactl repo delete OWNER/NAME --force
  giteactl repo list [OWNER]
  giteactl repo mirror add OWNER/NAME GITHUB_URL [--interval 10m] [--force]
  giteactl repo mirror remove OWNER/NAME
  giteactl repo mirror sync OWNER/NAME
  giteactl repo mirror list

Live-tested on gk2 board: command unblocked the gitea-actions migration
A2 phase that had been stuck for hours. github→gitea pull mirror for
secubox/secubox-deb went from setup-to-content in one command call.

  giteactl repo mirror add secubox/secubox-deb \
    https://github.com/CyberMind-FR/secubox-deb.git --interval 10m --force

  --> mirror live, sync every 10m, master HEAD picked up github's
      5e8a2b02 ("ci: mirror .github/workflows -> .gitea/workflows")
      within 60s of setup.

Bumped to 1.5.0.

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 11:02:40 +02:00
CyberMind
5e8a2b02c1
ci: mirror .github/workflows -> .gitea/workflows (closes #177) (#178)
Phase A3 of the GitHub Actions -> Gitea Actions migration. All 12
workflows copied byte-identical from .github/workflows/ so Gitea Actions
on the self-hosted instance at gitea.gk2.secubox.in picks them up on
every push (once the pull mirror, blocked on #176, is live).

Compatibility:
  - runs-on: ubuntu-latest         matches act_runner ubuntu-latest label
  - uses: actions/checkout@v4 etc  auto-fetched via DEFAULT_ACTIONS_URL=github
                                   (set in /var/lib/gitea/custom/conf/app.ini
                                    during Phase A1)
  - Secrets NOT auto-imported by mirror — re-add per repo in Gitea
    Settings -> Actions -> Secrets (GPG_PRIVATE_KEY, DEPLOY_SSH_KEY, ...)
  - Workflows that shell out to `gh` (GitHub CLI) will fail until
    migrated to `tea` (Gitea CLI) — per-workflow follow-ups.

Source of truth remains .github/workflows/; this is a mirror. Divergence
between the two trees is a bug. README.md in the new dir captures the
contract.

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 10:56:49 +02:00
531fd878a6 fix(sync): drop 10.100.0.10 from DEAD_CONTAINER_IPS — mail LXC lives there
The mail Phase 1 LXC went live at 10.100.0.10 (see #136, PR #141,
35ba4c2c). The 'dead container fix' was still rewriting routes that
target 10.100.0.10 to 10.100.0.1:9080, which would have clobbered
mail.gk2 / webmail.gk2 / rspamd.gk2 / autoconfig.gk2 routes. The
production gk2 board already had this correction in its local copy
(diffed 2026-05-17); bringing the repo back in sync so deploying
the script doesn't regress.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 10:42:07 +02:00
4a73cc245c fix(sync): mitmproxy-routes regex anchored at \s|$ instead of space-only
The previous regex `host_\K[a-z0-9_]+(?= )` required a trailing SPACE
after the hostname. HAProxy `use_backend mitmproxy_inspector if
host_<dotted_domain>` lines end at the hostname with no trailing space,
so the regex matched ZERO domains. Sync silently never auto-populated
mitmproxy routes for any haproxyctl-added vhost.

Caught 2026-05-17 when ckwa.gk2.secubox.in returned 502: the vhost was
added via `haproxyctl vhost add ckwa.gk2.secubox.in mitmproxy_inspector
ssl` (HAProxy ACL created correctly), but sync didn't write the
mitmproxy route entry, so the request landed at mitmproxy's default
(127.0.0.1:9080 placeholder) which doesn't exist inside the mitmproxy
LXC.

Fix: change lookahead to `(?=\s|$)` to also match end-of-line. Verified:

  $ printf '    use_backend ... if host_x_y_z\n' | \
    grep -oP 'host_\K[a-z0-9_]+(?=\s|$)' | sed 's/_/./g'
  x.y.z

Live patch applied on gk2 (mitmproxy LXC) to restore ckwa to 200.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 10:39:44 +02:00
18 changed files with 3011 additions and 56 deletions

View 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)

View 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') }}

View 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.

View 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') }}

View 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/

View 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"

View 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.

View 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
```

View 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') }}

View 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

View 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"

View 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') }}

View 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

View File

@ -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,326 @@ 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
}
# ============================================================================
@ -788,6 +1084,15 @@ 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
Backup:
backup [name] Create backup
restore <file> Restore from backup
@ -823,6 +1128,7 @@ case "${1:-}" in
stop) shift; cmd_stop "$@" ;;
restart) shift; cmd_restart "$@" ;;
user) shift; cmd_user "$@" ;;
repo) shift; cmd_repo "$@" ;;
backup) shift; cmd_backup "$@" ;;
restore) shift; cmd_restore "$@" ;;
migrate) shift; cmd_migrate "$@" ;;

View File

@ -178,16 +178,30 @@ check_prerequisites() {
fi
fi
# Charger les modules nécessaires
# Charger les modules nécessaires. dwc2 must be loaded FIRST — it creates
# the UDC node that the gadget functions bind to. Historically dwc2 was
# loaded implicitly by secubox-eye-gadget.service's ExecStartPre; with
# that service disabled at boot (storage-only mode is opt-in), the
# gadget chain now owns its own dwc2 modprobe explicitly.
modprobe dwc2 2>/dev/null || true
modprobe libcomposite 2>/dev/null || true
modprobe usb_f_ecm 2>/dev/null || true
modprobe usb_f_rndis 2>/dev/null || true
modprobe usb_f_acm 2>/dev/null || true
modprobe usb_f_mass_storage 2>/dev/null || true
# dwc2 binds asynchronously to the BCM USB controller — wait up to 5s
# for the UDC node to appear (typically <500ms on a Pi Zero W).
for _ in 1 2 3 4 5 6 7 8 9 10; do
if [[ -d /sys/class/udc ]] && [[ -n "$(ls /sys/class/udc 2>/dev/null)" ]]; then
break
fi
sleep 0.5
done
# Vérifier la présence d'un UDC (USB Device Controller)
if [[ ! -d /sys/class/udc ]] || [[ -z "$(ls /sys/class/udc 2>/dev/null)" ]]; then
err "Aucun UDC trouvé — ce script doit être exécuté sur un RPi Zero W"
err "Aucun UDC trouvé — vérifier dtoverlay=dwc2 dans /boot/config.txt et module dwc2 dans /etc/modules"
return 1
fi

View File

@ -650,9 +650,14 @@ mkdir -p "$ROOT_MNT/etc/systemd/system/dnsmasq.service.d"
cp "$SCRIPT_DIR/files/etc/systemd/system/dnsmasq.service.d/secubox-eye.conf" \
"$ROOT_MNT/etc/systemd/system/dnsmasq.service.d/"
# Enable new gadget service
ln -sf /etc/systemd/system/secubox-eye-gadget.service \
"$ROOT_MNT/etc/systemd/system/multi-user.target.wants/"
# secubox-eye-gadget is STORAGE-only mode for U-Boot rescue. Enabling it
# at boot alongside secubox-otg-gadget.service (composite ECM+ACM) caused
# UDC contention. Install the unit but DO NOT enable at boot.
# Manual U-Boot rescue mode:
# systemctl disable secubox-otg-gadget.service
# systemctl enable --now secubox-eye-gadget.service
# ln -sf /etc/systemd/system/secubox-eye-gadget.service \
# "$ROOT_MNT/etc/systemd/system/multi-user.target.wants/"
# Copy framebuffer dashboard (Pi Zero W has no NEON, can't run Chromium)
log "Installing framebuffer dashboard..."
@ -684,10 +689,15 @@ if [[ -f "$SCRIPT_DIR/secubox-eye-agent.service" && -f "$SCRIPT_DIR/config.toml.
# Install agent service
cp "$SCRIPT_DIR/secubox-eye-agent.service" "$ROOT_MNT/etc/systemd/system/"
# Enable agent service via symlink (atomic, no chroot needed)
# The agent depends on Pydantic v2 (pydantic_core, Rust) which has no
# ARMv6 wheel — pip ships an ARMv7 wheel that crashes with SIGILL on
# the Pi Zero W BCM2835 (status=4/ILL). v2.2.1 design moved metrics
# rendering to secubox-fallback-display.service (pure-Python Pillow),
# so we install the unit but DO NOT enable at boot. ARMv7+ boards:
# systemctl enable --now secubox-eye-agent.service
mkdir -p "$ROOT_MNT/etc/systemd/system/multi-user.target.wants"
ln -sf /etc/systemd/system/secubox-eye-agent.service \
"$ROOT_MNT/etc/systemd/system/multi-user.target.wants/"
# ln -sf /etc/systemd/system/secubox-eye-agent.service \
# "$ROOT_MNT/etc/systemd/system/multi-user.target.wants/"
# v2.2.0: Install menu system icons for radial menu
if [[ -d "$SCRIPT_DIR/assets/icons" ]]; then
@ -766,7 +776,10 @@ ln -sf /etc/systemd/system/eye-firstboot-hostname.service "$ROOT_MNT/etc/systemd
ln -sf /etc/systemd/system/hyperpixel2r-init.service "$ROOT_MNT/etc/systemd/system/multi-user.target.wants/" 2>/dev/null || true
# Eye Remote services
ln -sf /etc/systemd/system/secubox-eye-gadget.service "$ROOT_MNT/etc/systemd/system/multi-user.target.wants/" 2>/dev/null || true
# secubox-eye-gadget (storage-only, U-Boot rescue) is NOT enabled at boot
# to avoid UDC contention with secubox-otg-gadget.service. See comment at
# line 653 for the manual rescue-mode recipe.
# ln -sf /etc/systemd/system/secubox-eye-gadget.service "$ROOT_MNT/etc/systemd/system/multi-user.target.wants/" 2>/dev/null || true
# v2.2.1: Use fallback-display instead of eye-agent (3D cube + rainbow rings, stable)
ln -sf /etc/systemd/system/secubox-fallback-display.service "$ROOT_MNT/etc/systemd/system/multi-user.target.wants/" 2>/dev/null || true
# NOTE: secubox-eye-agent is broken (import errors) - disabled pending fix

View File

@ -17,15 +17,19 @@ After=systemd-modules-load.service
Before=network-pre.target
Wants=network-pre.target
# Conditions : seulement sur un périphérique avec UDC (RPi Zero W)
ConditionPathIsDirectory=/sys/class/udc
# Conditions : configfs disponible (UDC est crééable dynamiquement via dwc2,
# voir ExecStartPre — pas de pré-condition stricte sur /sys/class/udc).
ConditionPathExists=/sys/kernel/config
[Service]
Type=oneshot
RemainAfterExit=yes
# Chargement des modules nécessaires
# Chargement des modules nécessaires. dwc2 est chargé en premier pour créer
# le UDC (sinon le gadget ne peut s'attacher). secubox-eye-gadget.service
# le chargeait historiquement avant; depuis qu'il est désactivé au boot
# (storage-only mode opt-in), on doit le charger ici.
ExecStartPre=/sbin/modprobe dwc2
ExecStartPre=/sbin/modprobe libcomposite
ExecStartPre=/sbin/modprobe usb_f_ecm
ExecStartPre=/sbin/modprobe usb_f_acm

View File

@ -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
}