The Ding PBX installer ISO
What it is
A bootable, unattended-install ISO that turns a bare machine (or VM) into a working Ding PBX
server, the same way a FreePBX distro ISO does. Boot it, walk away, and when it reboots itself
there is a running Asterisk and a Material Asterisk admin surface reachable from a browser on the
local network — with no default password of any kind.
It is built by build-iso.bat at the repository root (console/scripts/build-iso.ps1), following
the same reproducibility discipline as the WSL Asterisk bundle: a base image pinned by digest,
Asterisk compiled from the exact repository commit, every download verified against a recorded
SHA-256 before it is trusted, and a provenance record written beside the finished artifact.
Architecture
The build has three stages, each a separate Docker stage or image so a failure in one is easy to
isolate:
1. iso-payload.Dockerfile compiles Asterisk from source (the same recipe as
asterisk-wsl-runtime.Dockerfile), builds the Material Asterisk server (`npm ci && npm run
build against console/`), and downloads a portable Linux Node.js runtime verified by SHA-256.
These three pieces — Asterisk, the console server, and Node — are assembled into one payload
directory with an install-target.sh script and systemd units.
2. iso-respin.Dockerfile downloads the official Ubuntu 24.04 LTS Server ISO, verified against
a pinned SHA-256 before anything touches it, extracts it, drops in the payload plus an
autoinstall (Subiquity cloud-init) answer file at /server/, points the bootloader at
autoinstall ds=nocloud;s=/cdrom/server/, and repacks a hybrid BIOS+UEFI-bootable ISO with
xorriso.
3. build-iso.ps1 orchestrates both stages from Windows (Docker's Linux engine does the actual
work, since Windows cannot compile the Linux payload or produce an ISO 9660 image natively),
exports and verifies the result, and writes console/release/iso/ding-pbx-installer.iso.json
with the source commit, base image and Node digests, and the finished ISO's own SHA-256.
What happens on the target machine
Booting the ISO runs Ubuntu's ordinary Subiquity installer with no prompts: it partitions the
disk, installs the base OS, then autoinstall's late-commands step runs install-target.sh
(inside the newly installed system, via curtin in-target), which:
- installs the bundled Node.js runtime to
/usr/local/lib/ding-pbx-node - installs the compiled Asterisk tree and enables
asterisk.service - installs the Material Asterisk server under
/opt/ding-pbx-console(reusing
console/server/deploy/install.sh unmodified) and enables ding-pbx-console.service, bound to
0.0.0.0:8443 so it is reachable from the LAN rather than loopback-only
- installs a first-boot banner unit that writes the machine's current LAN address into
/etc/issue, so whoever is at the console sees exactly where to point a browser
First-boot credential flow
No credential of any kind is written to the ISO. The identity.password field in the
autoinstall answer file is the locked sentinel "!", which refuses interactive password login for
that local Unix account entirely — it exists only so the installer has an account to run under,
never as an administrative credential.
The actual admin account is created by the Material Asterisk server itself, the first time anyone
visits it: console/server/auth.ts's createAdminAccount gates every other request behind the
first-run setup screen until an account exists. Whoever reaches the printed LAN address first
creates the admin account. Because the service binds to the LAN rather than loopback by default so
that an operator can reach it at all, **the operative security boundary during the first-boot
window is the network the machine is plugged into**, not a credential — treat that window (from
first boot until an admin account is created) the way you would treat an unconfigured switch port:
keep the machine off an untrusted network, or firewall port 8443 to the operator's own address,
until setup is done.
Requirements
- A machine or VM with x86-64 hardware, at least 2 vCPU / 2 GiB RAM / 8 GiB disk for a minimal
install (Asterisk itself is light; size storage for call recordings and voicemail separately).
- Secure Boot must be disabled, or a custom key enrolled for this ISO. Code signing is
permanently out of scope for this project (see the repository's no-signing policy) — the ISO is
genuinely unsigned, and a machine enforcing Secure Boot will refuse to boot it. This is stated by
the build script's own output and here, rather than left for someone to discover at a boot
prompt.
- Network reachable by DHCP during install (the base OS and package list install from the
network unless a local mirror is configured; the Asterisk/Node/console payload itself needs no
network, since it is embedded on the ISO).
How to boot and install it
1. Write console/release/iso/ding-pbx-installer.iso to a USB drive (dd, Rufus, or Ventoy) or
attach it as a VM CD-ROM.
2. Boot from it. No prompts appear; the machine partitions its disk and installs unattended.
3. On completion the machine reboots itself into the installed system.
4. At the console login screen, read the printed address (Console admin setup: http://<ip>:8443/)
and open it from a browser on the same network.
5. Create the admin account. From then on the console requires that account's credentials for
every request.
Verifying the download
console/release/iso/ding-pbx-installer.iso.json records the exact source commit, the pinned base
Ubuntu ISO URL and SHA-256, the pinned Node.js runtime version and SHA-256, and the finished ISO's
own SHA-256. Compare that last value against a locally computed digest of the downloaded file
before writing it to a USB drive or booting it in a VM.
Honest security posture
- The ISO itself is unsigned; Secure Boot refuses it.
- No credential is embedded anywhere on the ISO or in its build.
- The admin surface binds to the LAN by default during the first-boot window, before any account
exists — see First-boot credential flow above for the mitigation.
late-commands' package list (packages:in the autoinstall answer file) is installed from
whatever apt sources the target machine reaches at install time; only the Asterisk, Node.js, and
Material Asterisk payload itself is fully offline and reproducible from the ISO's own contents.
- Building the ISO requires Docker with a working Linux engine; it cannot be produced on a bare
Windows host.
Building it in CI
.github/workflows/installer-iso.yml builds this ISO reproducibly on a GitHub-hosted
ubuntu-24.04 runner (a Linux Docker engine is required to compile the Linux payload and produce
an ISO 9660 image, which a Windows host cannot do natively -- Docker ships preinstalled on that
runner image, so no separate setup is needed). It runs on workflow_dispatch, and automatically
whenever a push to main touches console/scripts/iso/**, console/scripts/build-iso.ps1,
build-iso.bat, or the workflow file itself.
It runs the same three stages as local build-iso.bat/build-iso.ps1 (payload build, ISO respin,
boot verification), then keeps the same "not tests, not lint" discipline as every other workflow in
this repository: no test job, no lint job, nothing gates the build. A run either builds, packages,
and publishes evidence, or it fails outright on the build or verification step itself.
The 2 GiB release-asset problem
A GitHub release asset is capped at 2 GiB (2,147,483,648 bytes) per file. The ISO this pipeline
produces is roughly 3.47 GiB (3,720,878,080 bytes measured against a real build), so it cannot be
attached to a release as a single file.
The workflow solves this by splitting the verified ISO into 1900 MiB volumes (split -b 1900MiB,
safely under the cap) and publishing all of them as release assets, alongside:
ding-pbx-installer.iso.sha256-- the reassembled image's own SHA-256, for a one-line check.ding-pbx-installer.iso.json-- full provenance (source commit, base ISO URL/digest, Node
runtime version/digest, console build base image digest, ISO byte count and SHA-256, part
count, and the same Secure Boot / no-signing statement as the local build).
ding-pbx-installer.iso.REASSEMBLE.md-- exact reassembly commands for Linux/macOS and Windows,
plus the SHA-256 of every individual volume and of the reassembled whole.
To reassemble and verify a downloaded release:
```sh
cat ding-pbx-installer.iso.part* > ding-pbx-installer.iso
sha256sum -c ding-pbx-installer.iso.sha256
```
On Windows PowerShell:
```powershell
cmd /c "copy /b ding-pbx-installer.iso.part001+ding-pbx-installer.iso.part002+... ding-pbx-installer.iso"
certutil -hashfile ding-pbx-installer.iso SHA256
```
Compare the resulting digest against the one recorded in ding-pbx-installer.iso.sha256 and in
ding-pbx-installer.iso.json before writing the ISO to a USB drive or booting it. **Do not boot an
ISO whose reassembled digest does not match.**
The workflow also uploads the complete, unsplit ISO as an ordinary GitHub Actions workflow
artifact (a separate, larger size limit than a release asset), for convenience when the run is
still fresh -- but workflow artifacts expire (14 days here) and are not a durable distribution
channel, so the split release assets are the one to link to for anyone downloading later.
Boot verification in CI
The workflow re-checks, on the artifact it actually produced, the exact three properties the local
build and console/tests/iso/iso-build.test.mjs already require of the respin recipe: both El
Torito boot catalog entries present (BIOS and UEFI), and a real master boot record signature
(55aa) at byte 510, plus the ISO 9660 primary volume descriptor signature (CD001) at byte
32769. These are the properties that distinguish a genuinely bootable image from a valid-looking
ISO 9660 file that cannot boot -- see the long comment above the repack in
console/scripts/iso/iso-respin.Dockerfile for the real incident that made these checks necessary.
Unsigned, same as the local build
This ISO is unsigned in CI exactly as it is locally -- code signing is permanently out of scope for
this project. The workflow states this in its own release notes and evidence rather than leaving it
to be discovered at a Secure Boot prompt.
Verification state
Everything in console/tests/iso/*.test.mjs (20 tests) is run without Docker or a real ISO: it
statically checks the autoinstall answer file for structural correctness and the absence of any
embedded credential, checks that build-iso.ps1 verifies its downloads and the finished artifact
rather than trusting a green build log, and checks that no code-signing call exists anywhere in the
pipeline. Every one of those checks was proved meaningful by breaking the real file it guards,
observing the test go red, and restoring it.
A full ISO has been built. 3,720,878,080 bytes, from the packaged Asterisk runtime whose digest
was checked against its own manifest first. Its structure was verified rather than assumed: one El
Torito BIOS entry, one EFI entry, an ISO 9660 primary volume descriptor, and a real master boot
record signature at byte 510. The base Ubuntu image and the Linux Node.js runtime are pinned to
their published SHA-256 values, both verified against the sums their vendors publish, and every
download fails closed rather than accepting a file that does not match.
Building it found a defect that no other check would have. The first image produced was a valid
ISO 9660 with entirely correct contents that no machine would boot. The repack followed Ubuntu's own
published autoinstall recipe, which names boot_hybrid.img for the master boot record; Ubuntu
24.04.4 does not ship that file. The hybrid repack failed, a fallback repack ran, and the build
reported success. Every test in the suite passed on that image.
The fallback is gone — one that quietly drops the single property the artifact exists for is worse
than no fallback. The repack now asks the base image to describe its own boot arrangement through
xorriso -report_el_torito as_mkisofs and uses that answer, reading the master boot record and the
appended EFI partition back out of it by byte interval. That is what the vendor shipped rather than
a reconstruction of it, and it does not go stale when a point release moves a file.
Still not done: the image has not been booted. Its structure says it can; only running it proves
it does. That, and the fact that it is unsigned and Secure Boot will therefore refuse it, are the two
things to know before trusting it.
It was built under WSL rather than a container, because the container engine on the build host would
not start. The CI workflow builds it in a container on a Linux runner, which is the reproducible
path; the local script remains the fallback.