Wise Foundry Steel is the container appliance of Wise Foundry: an immutable Alpine Linux virtual machine that runs Docker containers for Wise Foundry Alloy's services.
  • Go 46.4%
  • Shell 26.6%
  • TypeScript 23.4%
  • CSS 2.2%
  • Makefile 0.9%
  • Other 0.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Michael J. Manley 325c544d27 Version from release/YY.MM branches; keep a backup whose live data changed
- build/version.sh: the same rule as Iron's, with its test.
- steel-config backup --include-data: GNU tar's exit 1 (a file changed while
  read, e.g. PostgreSQL's WAL) no longer throws the backup away; it is kept with
  a warning. Any other tar failure, or exit 1 without data, still fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XvgVC2H8Wa7QukpNzTJia9
2026-10-01 16:43:54 -07:00
build Version from release/YY.MM branches; keep a backup whose live data changed 2026-10-01 16:43:54 -07:00
cmd Every string a person reads comes from a translation catalog 2026-09-29 11:46:26 -07:00
docs Version from release/YY.MM branches; keep a backup whose live data changed 2026-10-01 16:43:54 -07:00
initramfs Wise Foundry Steel 1.0.0 2026-09-14 21:28:36 -07:00
internal Every string a person reads comes from a translation catalog 2026-09-29 11:46:26 -07:00
locales Every string a person reads comes from a translation catalog 2026-09-29 11:46:26 -07:00
rootfs Wise Foundry Steel 1.0.0 2026-09-14 21:28:36 -07:00
test Version from release/YY.MM branches; keep a backup whose live data changed 2026-10-01 16:43:54 -07:00
tools Version from release/YY.MM branches; keep a backup whose live data changed 2026-10-01 16:43:54 -07:00
web Every string a person reads comes from a translation catalog 2026-09-29 11:46:26 -07:00
.gitignore Wise Foundry Steel 1.0.0 2026-09-14 21:28:36 -07:00
CHANGELOG.md Version from release/YY.MM branches; keep a backup whose live data changed 2026-10-01 16:43:54 -07:00
CLAUDE.md Version from release/YY.MM branches; keep a backup whose live data changed 2026-10-01 16:43:54 -07:00
go.mod Every string a person reads comes from a translation catalog 2026-09-29 11:46:26 -07:00
go.sum Wise Foundry Steel 1.0.0 2026-09-14 21:28:36 -07:00
LICENSE.md Add NOTICE.md with third-party notices 2026-09-14 21:53:36 -07:00
Makefile Version from release/YY.MM branches; keep a backup whose live data changed 2026-10-01 16:43:54 -07:00
NOTICE.md Slim the image, version by build date, use Steel's icon 2026-09-24 08:23:46 -07:00
README.md Version from release/YY.MM branches; keep a backup whose live data changed 2026-10-01 16:43:54 -07:00
steel-icon.png Slim the image, version by build date, use Steel's icon 2026-09-24 08:23:46 -07:00
steel-product-logo-white.png Slim the image, version by build date, use Steel's icon 2026-09-24 08:23:46 -07:00
steel-product-logo.png Slim the image, version by build date, use Steel's icon 2026-09-24 08:23:46 -07:00

Wise Foundry Steel

Wise Foundry Steel is the container appliance of Wise Foundry: an immutable Alpine Linux system that runs Docker containers for Wise Foundry Alloy's services. It is a Docker-only sibling of Wise Foundry Iron (no libvirt, VMs or LXC) with a single job:

  • Containers are created through the API only. Alloy (or any client with an API token) creates, recreates and removes containers, pulls or loads images and manages volumes and networks over https://<appliance>:8443/api/v1.
  • The web console (https://<appliance>:8443/; port 8080 redirects) starts and stops containers (it never creates or changes them), installs updates and rolls them back, changes the host name, management network and time settings, restarts or shuts down the appliance, and manages SSH and the emergency shell.
  • The console (VM console, monitor or serial port) configures the management network and time, and offers Troubleshooting Options (emergency shell, SSH, agent restart) and Shut Down/Restart. When no administrator password exists yet, it asks for one first.
  • Backup, restore and diagnostics: encrypted configuration backups (with optional container data) that restore at the next boot, support bundles without secrets, host metrics, logs, audit events and remote syslog — all through the API and the web console.
  • SSH and the emergency shell are disabled by default, as in Iron: enabled by an administrator (web console, API or console) for troubleshooting, SSH accepts only root with authorized keys.
  • Upgrades use Iron's mechanism. A/B root slots, Ed25519-signed update bundles (.steelupd), systemd-boot boot counting with a post-boot health check, automatic fallback and rollback — driven through the API, steel-update, or the installer ISO.

The root filesystem is a read-only dm-verity verified EROFS image booted from a signed unified kernel image; configuration lives on /persist and container data on /var. An update or rollback never touches either.

Design and contracts: docs/architecture.md. OVA and deployment properties: docs/ova.md. API: docs/web-api.md. Console and installer: docs/console-ui.md.

Building

Requirements: podman, make. Builds run rootless in an Alpine 3.24 builder container, which also supplies openssl and sbsigntool for make keys.

make             # = make help: every target and variable; builds nothing
make keys        # development signing keys in ./keys (never commit)
make image       # development (debug) images -> out/steel-<v>-dev.{ova,iso,steelupd}
make release     # release images             -> out/steel-<v>.{ova,iso,steelupd}

There are two build variants: dev, the debug build (the default, whose depots go to PREREL), and release (PRODUCTION).

<v> is YY.MM.PATCH (build/version.sh): YY.MM is this repository's branch when it is release/YY.MM, else (dev, any other branch, a detached HEAD, no git) the year and month of the build (UTC); PATCH is 0 unless given (make release PATCH=1); VERSION=YY.MM.PATCH overrides both. On dev, a later make depot in another month needs VERSION=YY.MM.PATCH to find the build.

File Use
steel-<v>.ova Appliance for Wise Foundry Iron (deployment wizard, Iron OVA API) in Iron's OVA format: OVF 1.1 with the Foundry OVF extension, 16 GiB qcow2 disk, UEFI, slot A populated
steel-<v>.iso Hybrid UEFI installer (CD or USB): installs on a disk, or upgrades a disk that holds Steel
steel-<v>.steelupd Signed update bundle for POST /api/v1/updates/* or steel-update apply
steel-<v>.txt Build ID, kernel, root hash, checksums

Development images add a root shell on tty2 and ttyS1 (used by the QEMU tests) and are labelled "development" everywhere; release images have no login shell at all.

Upgrading

Every Steel release upgrades any earlier installation: the bundle format and disk layout are stable, the new version goes to the unused slot, and the previous version stays installed for rollback. A newer build of the same version also counts as an upgrade; anything else needs allow downgrade.

Through the API (Alloy):

H="Authorization: Bearer $TOKEN"
curl -k -H "$H" -H 'X-Steel-Filename: steel-1.1.0.steelupd' --data-binary @steel-1.1.0.steelupd \
  -X POST https://steel01:8443/api/v1/updates/upload                # -> {"path": ".../staging/steel-1.1.0.steelupd"}
curl -k -H "$H" -d '{"path":"/var/lib/steel/staging/steel-1.1.0.steelupd"}' https://steel01:8443/api/v1/updates/verify
curl -k -H "$H" -d '{"path":"/var/lib/steel/staging/steel-1.1.0.steelupd"}' https://steel01:8443/api/v1/updates/apply  # job
curl -k -H "$H" -d '{"action":"reboot"}' https://steel01:8443/api/v1/host/power

The new deployment boots with three tries. steel-health confirms it once steel-agent and Docker are healthy; otherwise the appliance restarts until systemd-boot falls back to the previous deployment, and GET /api/v1/updates/status reports rolled_back. POST /api/v1/updates/rollback (then a reboot) returns to the previous deployment on request.

From installer media: boot the newer ISO, select the disk marked *, choose Upgrade Wise Foundry Steel, preserve configuration and data and confirm with F11. Unattended: MODE=upgrade and DISK=/dev/… in steel-answers.conf on media labelled STEELANSWER.

From a shell: steel-update fetch|verify|apply [--reboot]|status|rollback.

The appliance does not look for updates on its own: Alloy's update service decides when and what to install and drives the API above.

Installing from the ISO

Boot the ISO on a UEFI machine or VM, pick a disk (at least 8 GiB) and confirm. After the first start, set the administrator password and the management network on the console. For unattended installs put steel-answers.conf on a FAT or ISO volume labelled STEELANSWER:

MODE=install
DISK=/dev/sda
WIPE_CONFIRM=yes
HOSTNAME=steel-edge01
NET_MODE=static
NET_ADDRESS=10.20.0.15/24
NET_GATEWAY=10.20.0.1
NET_DNS="10.20.0.2"
ADMIN_PASSWORD_HASH='$2a$12$...'
API_TOKEN=stl_...
POWEROFF=yes

Testing

make test-go       # Go vet and unit tests (API, updates, tokens, console and installer screens, ...)
make test-web      # web console typecheck, lint and tests
make test-unit     # steel-update, steel-install, steel-health, steel-config against fixtures (+ shellcheck)
make test-images   # out/test: base OVA/ISO/bundle, a newer good bundle/ISO, a fault-injected bundle
make test-qemu     # OVA deploy, backup/restore + support bundle, Compose stack, API update + automatic rollback, ISO install, ISO upgrade under QEMU/KVM

make test-qemu needs /dev/kvm, qemu-system-x86_64, qemu-img, OVMF (edk2-ovmf), socat, python3 and curl on the host, and internet access from the VM (it pulls busybox).

Using the API

TOKEN=stl_...   # the steel.api.token deployment property, or: steel-agent token create alloy
curl -k -H "Authorization: Bearer $TOKEN" https://steel01:8443/api/v1/containers
curl -k -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -X POST https://steel01:8443/api/v1/containers \
  -d '{"name":"dns","image":"coredns/coredns:1.12.1","network":"host","restart_policy":"unless-stopped",
       "volumes":[{"type":"data","path":"/dns","destination":"/etc/coredns"}]}'
# Without a registry: load a `docker save` archive (a job; its result lists the loaded tags)
curl -k -H "Authorization: Bearer $TOKEN" -H 'X-Steel-Filename: alloy-images-0.1.0.tar.gz' \
  -T alloy-images-0.1.0.tar.gz -X POST https://steel01:8443/api/v1/containers/images/load

Deploying stacks

Compose stacks are deployed through the API as YAML (docs/compose.md) and appear as expandable sections in the web console's Docker containers page:

python3 -c 'import json; print(json.dumps({"name": "web", "compose": open("compose.yaml").read(),
  "env": [{"key": "TZ", "value": "UTC"}], "deploy": True}))' |
  curl -k -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
    --data-binary @- https://steel01:8443/api/v1/stacks          # -> 202 {"job": {...}}
curl -k -H "Authorization: Bearer $TOKEN" https://steel01:8443/api/v1/stacks/web

Bind mounts must be data directories /var/lib/steel/data/<name>; privileged services, host devices and the host network (except for administrators) are refused.

Accounts

The built-in admin account (password from the deployment properties, the installer answers, or set on the console) signs in to the web console and the console. Additional accounts are managed from a shell with steel-agent user ...; API tokens with steel-agent token ... or /api/v1/tokens. Accounts are also managed through /api/v1/users (docs/web-api.md "Accounts").

Role Can
Administrator everything: containers, tokens, updates, power, host name, network, time, services, SSH, emergency shell
Operator view, start/stop and create/remove containers
Read-only view the appliance and its containers

Secure Boot and signing keys

Steel uses Wise Foundry Iron's key scheme (docs/release-keys.md): key sets dev (keys/, make keys), test (under out/test/) and release (outside the repository, STEEL_SIGNING_KEYS). The build variants are dev (the debug build, depots to PREREL) and release (PRODUCTION). Each set has Secure Boot PK, KEK and db keys (RSA-2048, with .esl/.auth enrollment files) and an Ed25519 update key; db signs systemd-boot and the UKIs, update signs bundle manifests, and its public key is in every image at /usr/share/steel/keys/update.pub.

make keys                                                   # dev set in keys/
make keys KEYSET=release STEEL_SIGNING_KEYS=/secure/steel-release
make release STEEL_SIGNING_KEYS=/secure/steel-release       # out/steel-<v>.*

build/keyguard.sh refuses a key set that does not match the variant; images never contain private keys, and appliances cannot move between key sets.