Supply chain and release
A hardening tool that runs as root is a prime supply-chain target. Users who care about security are exactly the ones who won’t curl | bash, so trust is a feature.
-
Toolchain: Rust stable (no exact version is pinned today; pinning is planned for reproducible builds, because a different compiler version produces different bytes), edition 2024.
-
Targets:
x86_64-unknown-linux-muslandaarch64-unknown-linux-musl. musl targets link statically by default, so the binary runs on Ubuntu, Debian, Rocky and Alpine without caring about the host’s glibc. Both targets build on an x86_64ubuntu-24.04runner, linked withrust-lldand the musl CRT bundled with the Rust target (see.cargo/config.toml), so no cross toolchain is needed. Later, maybearmv7andriscv64. -
Command:
cargo build --release --locked --target <target>.--lockedmakes the build fail ifCargo.lockwould change, so CI builds exactly the dependency versions that were reviewed. -
Release profile (in the workspace
Cargo.toml):[profile.release]lto = "fat" # whole-program optimization: smaller, faster binarycodegen-units = 1 # slower build, better optimization, more deterministic outputpanic = "abort" # no unwinding tables; a panic ends the processstrip = true # drop symbols; smaller artifactWhy
panic = "abort"is safe here: the engine never relies onDroprunning during a panic to clean up. A crash mid-transaction is handled the same way as a power cut, by the on-disk journal and the dead-man timer (the architecture doc). -
Reproducible builds (the target, not yet verified bit for bit). The steps:
--lockedplus the pinned toolchain: same inputs, same compiler.SOURCE_DATE_EPOCHset to the tag’s commit timestamp, used for anything that would otherwise embed “now” (tarball mtimes, build metadata).- Planned:
RUSTFLAGS="--remap-path-prefix=$HOME/.cargo=/cargo --remap-path-prefix=$PWD=/build", so absolute paths from the build machine don’t leak into panic messages and debug info. - Deterministic archives:
tar --sort=name --mtime=@$SOURCE_DATE_EPOCH --owner=0 --group=0 --numeric-owner, thengzip -n. - Planned:
make reproduce VERSION=v0.3.0: it checks out the tag, builds both targets with the same toolchain in a container, and compares the SHA256 of each tarball with the publishedchecksums.txt. Anyone can run it.
Release pipeline
Section titled “Release pipeline”Hand-written in .github/workflows/release.yml and triggered by pushing a v* tag.
flowchart LR
tag["push tag v*<br/>(protected)"] --> build
subgraph build["build matrix"]
direction TB
x86["x86_64-unknown-linux-musl"]
arm["aarch64-unknown-linux-musl"]
end
build --> sums["checksums.txt<br/>(SHA256)"]
sums --> attest["attest<br/>(SLSA provenance)"]
attest --> sign["cosign sign-blob<br/>(keyless)"]
sign --> sbom["SBOM<br/>(cargo-cyclonedx, from v0.5)"]
sbom --> draft["draft release<br/>+ install.sh"]
draft --> publish["publish<br/>(immutable)"]
Only the last two steps need contents: write, and they run in the release environment, which only v* tags can use. Only a repository admin can create a v* tag, so pushing the tag is the human approval: the release runs without a further click.
What each release ships
Section titled “What each release ships”| Artifact | Purpose |
|---|---|
vps-ready-<ver>-<target>.tar.gz |
the static binary (plus LICENSE-APACHE, LICENSE-MIT, NOTICE and README), one per target |
checksums.txt |
SHA256 of everything |
checksums.txt.sigstore.json |
cosign keyless signature bundle; signing the checksums covers all files |
| GitHub artifact attestation | GitHub build-provenance attestation (SLSA Build L2 today; L3 needs the build in a reusable workflow, planned) |
vps-ready-<ver>.cdx.json |
from v0.5: SBOM in CycloneDX format, generated by cargo-cyclonedx from Cargo.lock |
.deb / .rpm / .apk |
from v0.5, via nFPM (language-agnostic, it just packages our binary), GPG-signed (deb/rpm) and RSA-signed (apk) |
install.sh |
generated per release with the hashes embedded |
Release hardening:
- Turn on GitHub Immutable Releases (assets and tags can’t change after publishing). The workflow creates a draft, uploads everything, then publishes.
- Pin all actions by SHA.
- Default to
permissions: {}, grantingid-token/attestations/contents: writeonly to the jobs that need them. - Use
releaseandcrates-ioenvironments limited tov*tags, plus tag rulesets:v*tags cannot be updated or deleted, and only repository admins can create them.
Install paths (most → least trusted)
Section titled “Install paths (most → least trusted)”- Package repo: apt
signed-by=/ dnfgpgcheck=1/ apk keys. Updates come through the system package manager. (Host on GitHub Pages or Cloudsmith; this can come later, at v0.5+.) - Manual verification:
Terminal window cosign verify-blob --bundle checksums.txt.sigstore.json \--certificate-identity-regexp '^https://github.com/bhimrazy/vps-ready/\.github/workflows/release\.yml@refs/tags/v' \--certificate-oidc-issuer https://token.actions.githubusercontent.com checksums.txtsha256sum -c --ignore-missing checksums.txt# or: gh attestation verify vps-ready-*.tar.gz -R bhimrazy/vps-ready install.sh, made “less bad”:- POSIX sh, about 150 lines, wrapped in
main(){…}; main "$@"so a truncated download can’t half-run. - Attached to the immutable release, with the artifact hashes embedded.
- Verifies SHA256, and cosign/gh too if they’re installed. A failed check stops the install unless
--skip-verify; a gh that is not logged in is skipped; with no usable tool it says so and relies on the embedded hashes.--prefixmust not be group- or other-writable. - Has
--dry-run. - The README shows download, read, run;
curl | shis not documented.
- POSIX sh, about 150 lines, wrapped in
Rust users / CI
Section titled “Rust users / CI”cargo install vps-ready --locked # compiles from source; add --version <version> for a pre-releasecargo install --lockedbuilds on your machine from the source published to crates.io.--lockeduses theCargo.lockshipped in the crate; without it, cargo resolves the newest semver-compatible dependencies, which nobody has reviewed. Be honest about the trust model: this path is only as trustworthy as crates.io and that lockfile. No cosign signature or SLSA attestation covers it. (Publishing from CI with crates.io trusted publishing, so there is no long-lived token to steal, narrows the gap.) For the full chain of trust, use the release tarball and verify it with the commands above.
Publishing to crates.io
Section titled “Publishing to crates.io”- The
cratesjob inrelease.ymlpublishessshd-config, thenvps-ready(which depends on it), thenvpsready, withcargo publish --workspace --lockedin thecrates-ioenvironment, using crates.io trusted publishing (no stored token). - Trusted publishing cannot create a crate, so each crate’s first version (
0.1.0-alpha.1) was published by hand from a cleanmainwith a short-lived API token, without pushing a tag. Both crates are configured for trusted publishing (ownerbhimrazy, repovps-ready, workflowrelease.yml, environmentcrates-io) and have trusted publishing only turned on, so an API token can no longer publish them. - Pre-releases are tags such as
v0.1.0-alpha.2: they go through the same workflow, become GitHub pre-releases, andcargo installskips them unless asked for with--version. - The version appears three times in the root
Cargo.toml:[workspace.package] versionand the=pins ofvps-readyandsshd-configin[workspace.dependencies]. Bump all together. sshd-configis published from v0.3.0 (it hadpublish = falsebefore, andvps-readydepends on it). Trusted publishing cannot create it, andcargo publish --workspacerefuses to start when any of its versions already exists on crates.io, so publishingsshd-config0.3.0 by hand would make the tag’scratesjob fail. Before pushing thev0.3.0tag, once:- From a clean
main, set the version to0.3.0-alpha.1locally (the[workspace.package]version and thesshd-configpin), runcargo publish -p sshd-config --allow-dirtywith a short-lived API token, then discard the edit. This creates the crate; nothing depends on that pre-release. - Configure trusted publishing for
sshd-configas for the other two (ownerbhimrazy, repovps-ready, workflowrelease.yml, environmentcrates-io), turn on trusted publishing only, and revoke the token. - Push the tag: the
cratesjob then publishessshd-config0.3.0 beforevps-ready.
- From a clean
- If
vps-readyuploads butvpsreadyfails, re-running the job fails on the already-publishedvps-ready. To recover, turn off trusted publishing only forvpsreadyin its crates.io settings, runcargo publish -p vpsready --lockedwith a short-lived token, then turn the setting back on and revoke the token.
No self-update
Section titled “No self-update”A root tool that replaces itself is extra attack surface for a program that usually runs once per box. Provide vps-ready version --check (notify only) and let packages handle upgrades.
Repo hygiene
Section titled “Repo hygiene”- Dependencies: a short allowlist (see the architecture doc). Review every new crate with
cargo tree(what does it pull in?) andcargo tree -d(duplicate versions). CI runs:ci.yml:cargo fmt --check,cargo clippy -D warnings,cargo test,cargo doc, an MSRV check, musl builds, andcargo deny check(advisories from the RustSec database, licenses, banned crates and allowed sources, configured indeny.toml);codeql.yml: CodeQL analysis;scorecard.yml: OpenSSF Scorecard.- Later, optionally:
cargo vetorcargo crevto record who audited which dependency version.
- Dependabot:
cargoandgithub-actionsecosystems, grouped, weekly. Itscooldownoption delays PRs for brand-new releases by a few days, which is when malicious versions usually get caught. Switch to Renovate only if we outgrow that. - OpenSSF Scorecard action plus the Best Practices badge.
- CODEOWNERS: one owner (
@bhimrazy) for everything, with explicit entries forcrates/vps-ready/src/engine/,crates/vps-ready/src/modules/ssh/,crates/vps-ready/src/modules/firewall/,.github/andscripts/. Two reviewers on these paths once there are two maintainers. - Why not cargo-dist (for now): with only two musl targets and one tarball format, a generated workflow of several hundred lines isn’t justified. For a tool whose pitch is “trust the release”, we want every release step short enough to audit by reading it. cargo-dist stays a valid option to revisit if installers or targets multiply.
- License: MIT OR Apache-2.0 (the Rust-standard dual license). Apache-2.0 brings a patent grant, MIT keeps it usable by GPLv2 projects; both are friendly to hosting providers and panels that might bundle it. GPL-licensed competitors (hostveil, hardbox) are less embeddable. Use DCO sign-off, no CLA.
- SECURITY.md: GitHub private vulnerability reporting, supported versions, how to verify releases, and which channels are official (GitHub Releases, crates.io
vps-ready, its aliasvpsreadyand thesshd-configlibrary from v0.3.0; the package repos later).
Feedback
Questions, ideas or problems with vps-ready.