- Go 46.4%
- Shell 26.6%
- TypeScript 23.4%
- CSS 2.2%
- Makefile 0.9%
- Other 0.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
- 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 |
||
| build | ||
| cmd | ||
| docs | ||
| initramfs | ||
| internal | ||
| locales | ||
| rootfs | ||
| test | ||
| tools | ||
| web | ||
| .gitignore | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| go.mod | ||
| go.sum | ||
| LICENSE.md | ||
| Makefile | ||
| NOTICE.md | ||
| README.md | ||
| steel-icon.png | ||
| steel-product-logo-white.png | ||
| steel-product-logo.png | ||
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.