Engineering note ·
Why our installer refuses curl | sh
Piping a script from the network into a shell skips every chance to verify it. Our installer detects the pipe and aborts — here's the design behind that.
Every developer tool wants a one-line install. The standard one-liner is curl … | sh — and for a system that will eventually hold financial state, we decided we couldn’t offer it. TulaDB’s installers are still one line to run, but if you pipe one straight from the network into a shell, it aborts:
refusing to run from stdin; download install.sh and install.sh.sha256,
verify them, then run sh install.sh
Updated 1 November 2026: the TulaDB server now installs with
tuladb-installer, one signed static binary per platform, served fromdownloads.tuladb.comand listed on Downloads once 0.0.1-preview is released. The shell installers this post describes are the evaluation stacks’ (Docker, Compose, Amazon EKS): they are available to design partners on request ([email protected]) and will be published as theevalbundle on the same downloads site. The examples below now use the downloads site. The design they describe is unchanged.
What’s actually wrong with the pipe
curl | sh executes whatever the network delivered, with no artifact left behind to inspect and no opportunity to verify anything before execution begins. If the endpoint is compromised — or anything in the path to it — you run the attacker’s script, once, tracelessly.
The fix isn’t asking users to be more careful. It’s making the safe path the only path the installer will accept. So the published path is download, verify, then run. For the server, that is one binary and the release’s signed manifest:
B=https://downloads.tuladb.com/core/0.0.1-preview
for f in tuladb-installer-0.0.1-preview-linux-x86_64 SHA256SUMS SHA256SUMS.cosign.bundle trusted_root.json; do
curl -fsSLO "$B/$f"
done
cosign verify-blob --offline --trusted-root trusted_root.json \
--bundle SHA256SUMS.cosign.bundle \
--certificate-identity https://github.com/TulaDB/tuladb-core/.github/workflows/release.yml@refs/tags/v0.0.1-preview \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
SHA256SUMS
sha256sum -c --ignore-missing SHA256SUMS
A few more lines than the pipe, and no decisions in them. But now there’s a file on disk, a manifest next to it signed by the release workflow at the release tag, and nothing has run until both checks pass. The Downloads page prints these commands for every release.
Three layers, each covering the previous one’s gap
The installer verifies itself first. A shell installer hashes itself before any other action and compares against its .sha256 sidecar. Running from stdin would make self-verification impossible — there’s no file to hash — which is exactly why stdin is refused rather than tolerated. tuladb-installer does the equivalent for the release it carries: it checks the embedded payload against the SHA-256 recorded when it was built, and refuses to run on a mismatch.
Every downloaded asset is pinned by an embedded digest. Self-verification only proves the script matches its sidecar — if both came from the same compromised endpoint, that proves little. So each evaluation-stack installer carries the expected SHA-256 of every asset it will download, embedded in the script body itself: the compose file, the engine configuration, the cluster manifest. You verified the installer once; the installer then refuses any asset that doesn’t match what it was built to fetch. tuladb-installer downloads nothing at all: the packages it installs are inside the binary you verified.
Releases require a signature, not just a hash. A hash published next to a file proves only that the two agree. Each release’s SHA256SUMS carries a keyless cosign (Sigstore) signature from the release workflow, checked against an exact signer identity — the workflow file at the release’s tag — and the GitHub Actions OIDC issuer. A missing bundle, a bad signature or an absent hash means: do not run it. Before a release appears on the Downloads page, our CI downloads every file and repeats those checks, against Sigstore’s own trust root as well as the published one, and refuses on any mismatch. Container images, by contrast, are pinned by tag rather than by digest.
There is a break-glass escape hatch in the shell installers — an explicit environment variable that disables verification — because sandboxes and air-gapped setups sometimes vet scripts through other means. It’s loud, it’s deliberate, and it’s spelled TULADB_ALLOW_UNVERIFIED_INSTALLER=1, so nobody sets it by accident.
The same posture, everywhere it matters
This is less an installer feature than a habit applied consistently. The same fail-closed reflex shows up across the stack: the engine refuses to boot on an unknown config key rather than guessing; a privileged anonymous role is refused on an exposed bind; the Kubernetes chart refuses to render without TLS material rather than installing insecurely; SDKs refuse to send ledger traffic in plaintext off-box. The installer is simply the first place a new user meets the pattern — about ninety seconds before the ledger itself does.
If you want to check our homework, every shell installer’s sidecar can be verified by hand before you run anything:
sha256sum -c install.sh.sha256 # Linux
shasum -a 256 -c install.sh.sha256 # macOS
In the docs: Install TulaDB covers the server installer; Downloads the verification commands; Deployment the evaluation stacks; Security the fail-closed posture once the engine is running.
Evaluate it yourself
TulaDB 0.0.1-preview is a signed download. Run it locally, check the behaviour this note describes, then bring your workload to a pilot.