diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..ae6c0a2 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + cooldown: + default-days: 7 # give a hijacked or retagged release time to be noticed + groups: + actions: + patterns: ["*"] diff --git a/.github/scripts/guard-remote-exec.sh b/.github/scripts/guard-remote-exec.sh new file mode 100755 index 0000000..cf08512 --- /dev/null +++ b/.github/scripts/guard-remote-exec.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# Fails if a script or guide pipes a download straight into a shell, or fetches +# this repository's files from the mutable main branch. Operators run these +# commands as root, so every download has to be pinned and checked first. +# +# Run from anywhere: bash .github/scripts/guard-remote-exec.sh + +set -euo pipefail + +cd "$(dirname "$0")/../.." + +paths=(scripts sysop developer community-manager README.md) + +# `curl ... | sh`, `wget ... | sudo -E bash -` and similar. +pipe_to_shell='(curl|wget)[^#]*\|[[:space:]]*(sudo[[:space:]]+(-[A-Za-z]+[[:space:]]+)*)?(ba|z|da)?sh([^[:alnum:]_]|$)' +# `bash <(curl ...)` +process_substitution='(ba|z|da)?sh[[:space:]]+<\([[:space:]]*(curl|wget)' +# Raw fetches from this repo's main branch. +raw_main='raw\.githubusercontent\.com/OpenVTC/vti-setup/(refs/heads/)?main/' + +status=0 +check() { + local description="$1" pattern="$2" + if grep -rnE -- "$pattern" "${paths[@]}"; then + echo "::error::${description}" >&2 + status=1 + fi +} + +check "download piped into a shell; download, verify, then run instead" "$pipe_to_shell" +check "download run through process substitution; download, verify, then run instead" "$process_substitution" +check "fetch from OpenVTC/vti-setup main; use a tagged release asset instead" "$raw_main" + +if [ "$status" -eq 0 ]; then + echo "No remote-exec patterns found." +fi +exit "$status" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..5eb8413 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,27 @@ +name: lint + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + shell: + name: shellcheck and remote-exec guard + runs-on: ubuntu-24.04 + steps: + - name: Check out + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: shellcheck + run: | + shellcheck --version + shellcheck scripts/*.sh .github/scripts/*.sh + + - name: No downloads piped into a shell, no scripts fetched from main + run: bash .github/scripts/guard-remote-exec.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..93f46da --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,95 @@ +name: release + +# Publishes scripts/setup-explore.sh as a GitHub Release asset with a +# SHA256SUMS file and a build provenance attestation, so operators can verify +# what they run as root. See RELEASING.md. + +on: + push: + tags: + - "v*" + +permissions: {} + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +jobs: + attest: + name: Checksum and attest release assets + runs-on: ubuntu-24.04 + permissions: + contents: read + id-token: write # Sigstore signing certificate for the attestation + attestations: write # store the attestation on this repository + outputs: + sha256: ${{ steps.assets.outputs.sha256 }} + steps: + - name: Check out the tagged commit + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + fetch-depth: 0 + + - name: Require the tag to point at a commit on main + run: git merge-base --is-ancestor "$GITHUB_SHA" refs/remotes/origin/main + + - name: Build release assets + id: assets + run: | + mkdir dist + install -m 0644 scripts/setup-explore.sh dist/setup-explore.sh + (cd dist && sha256sum setup-explore.sh > SHA256SUMS) + cat dist/SHA256SUMS + echo "sha256=$(cut -d ' ' -f 1 dist/SHA256SUMS)" >> "$GITHUB_OUTPUT" + + - name: Attest build provenance + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-path: | + dist/setup-explore.sh + dist/SHA256SUMS + + publish: + name: Publish GitHub Release + needs: attest + runs-on: ubuntu-24.04 + environment: release + permissions: + contents: write # create the release and upload its assets + steps: + - name: Check out the tagged commit + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Rebuild assets and match the attested digest + env: + ATTESTED_SHA256: ${{ needs.attest.outputs.sha256 }} + run: | + mkdir dist + install -m 0644 scripts/setup-explore.sh dist/setup-explore.sh + (cd dist && sha256sum setup-explore.sh > SHA256SUMS) + printf '%s setup-explore.sh\n' "$ATTESTED_SHA256" | (cd dist && sha256sum -c -) + + - name: Create the release + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ github.ref_name }} + ATTESTED_SHA256: ${{ needs.attest.outputs.sha256 }} + run: | + { + echo "setup-explore.sh SHA-256: \`${ATTESTED_SHA256}\`" + echo + echo "Check the download against the hash pinned in sysop/explore/01-server-setup.md, then verify provenance:" + echo + echo '```bash' + echo "gh attestation verify setup-explore.sh --repo ${GITHUB_REPOSITORY} --signer-workflow ${GITHUB_REPOSITORY}/.github/workflows/release.yml" + echo '```' + } > notes.md + gh release create "$TAG" dist/setup-explore.sh dist/SHA256SUMS \ + --repo "$GITHUB_REPOSITORY" \ + --verify-tag \ + --title "$TAG" \ + --notes-file notes.md diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..caa9460 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,46 @@ +# Releasing + +`scripts/setup-explore.sh` runs as root, so the explore guide only has operators run a copy from a tagged GitHub Release, checked against a SHA-256 pinned in [`sysop/explore/01-server-setup.md`](sysop/explore/01-server-setup.md). Pushing a `v*` tag runs [`.github/workflows/release.yml`](.github/workflows/release.yml), which attaches `setup-explore.sh` and `SHA256SUMS` to the release and records a build provenance attestation for both. + +## One-time repository setup + +- Create the `release` environment (Settings → Environments) with required reviewers from the maintainers team. The publish job waits for that approval. Without it, GitHub creates the environment unprotected on the first run. +- Add a tag ruleset that restricts creating, updating and deleting `v*` tags to maintainers. + +## Cutting a release + +The pinned hash covers only the script, not the guide, so the guide can be updated before the tag exists. + +1. Pick the next version, for example `v1.1.0`, and hash the script on `main`: + + ```bash + git switch main && git pull --ff-only + sha256sum scripts/setup-explore.sh + ``` + +2. Open a PR that sets `VER` and `SHA256` in Step 3 of `sysop/explore/01-server-setup.md` to that version and hash. The PR must not change `scripts/setup-explore.sh`. Merge it. +3. Tag the merge commit and push the tag straight away, because the guide now points at it: + + ```bash + git pull --ff-only + sha256sum scripts/setup-explore.sh # must still equal the pinned hash + git tag -a v1.1.0 -m "v1.1.0" # or -s if you sign tags + git push origin v1.1.0 + ``` + +4. Approve the `release` environment deployment when the workflow asks for it. +5. Verify the published assets the way an operator will: + + ```bash + VER=v1.1.0 + SHA256= + curl -fsSLO "https://github.com/OpenVTC/vti-setup/releases/download/${VER}/setup-explore.sh" + curl -fsSLO "https://github.com/OpenVTC/vti-setup/releases/download/${VER}/SHA256SUMS" + echo "${SHA256} setup-explore.sh" | sha256sum -c - + sha256sum -c SHA256SUMS + gh attestation verify setup-explore.sh \ + --repo OpenVTC/vti-setup \ + --signer-workflow OpenVTC/vti-setup/.github/workflows/release.yml + ``` + +If any check fails, delete the release and the tag, fix the pin or the script, and release again under a new version. Never move a published tag. diff --git a/scripts/setup-explore.sh b/scripts/setup-explore.sh index 24babeb..4177b8a 100755 --- a/scripts/setup-explore.sh +++ b/scripts/setup-explore.sh @@ -4,8 +4,11 @@ # # Run once, as root, over the default SSH session of a fresh Ubuntu 26.04 host. # Installs everything needed to play with the stack: build toolchain, Rust, -# Node.js, Docker, nginx, certbot, ufw. Wires up four nginx vhosts -# (mediator, vta, vtc, dids) and obtains Let's Encrypt certificates. +# Valkey, nginx, certbot, ufw. Wires up four nginx vhosts (mediator, vta, vtc, +# dids) and obtains Let's Encrypt certificates. +# +# Node.js is not installed here. Only the optional did-hosting-ui source build +# needs it, and sysop/explore/01-server-setup.md installs it at that step. # # Single DID Hosting topology only (integrated daemon). For the standalone # topology, use the deploy stream instead. @@ -13,30 +16,28 @@ # DO NOT use a box set up this way for real keys or production data. This # stream is for learning and experimentation only. # -# Usage: curl -sSL https://raw.githubusercontent.com/OpenVTC/vti-setup/main/scripts/setup-explore.sh | bash -s -- [email] -# Example: ... | bash -s -- example.com -# Example: ... | bash -s -- example.com admin@example.com +# Get this script from a tagged GitHub release and check it before running it +# (sysop/explore/01-server-setup.md, Step 3). Never run a copy taken from the +# main branch. +# +# Usage: sudo bash setup-explore.sh [email] +# Example: sudo bash setup-explore.sh example.com +# Example: sudo bash setup-explore.sh example.com admin@example.com -set -e +set -euo pipefail -# This script is documented as a `curl | bash` one-liner, so stdin is the pipe, -# not a terminal. Any debconf dialog (keyboard layout, needrestart, changed -# config files) would be unanswerable and hang the run — everything that -# installs packages has to take its defaults instead. -# -# The exports below are for third-party installers we pipe into `sudo -E bash`, -# such as the NodeSource setup script, which run apt themselves and inherit our -# environment. They do NOT cover our own apt calls: those go through plain -# `sudo`, whose env_reset strips both variables. apt_get() therefore sets them -# again on the command line, where sudo cannot drop them. -export DEBIAN_FRONTEND=noninteractive -export NEEDRESTART_MODE=a +# Everything below is a definition until the call to main on the last line. If +# a download of this file is cut short, bash hits end-of-file inside a function +# body and runs nothing, rather than running whatever prefix arrived. -apt_get() { - sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get -y \ - -o Dpkg::Options::=--force-confdef \ - -o Dpkg::Options::=--force-confold "$@" -} +# Fallback Rust installer, used only when the apt archive has no rustup +# package. The URL is rustup's immutable per-version archive, and the hashes +# are the rustup-init.sha256 values published for that version (checked +# against the downloaded binaries when the pin was set). Bump all three +# together. +RUSTUP_VERSION=1.29.1 +RUSTUP_INIT_SHA256_X86_64=dda7234360b7f578ca8b0ddcb80145646fa61a67c1720a5abc7051b35c9fcb71 +RUSTUP_INIT_SHA256_AARCH64=15f6e4ce9f583b929c996c91562bad6d4454f3281de858b02cdfdef615fac433 # Colors for output RED='\033[0;31m' @@ -44,123 +45,117 @@ GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' +# No debconf dialog (keyboard layout, needrestart, changed config files) may +# open during the run: everything that installs packages has to take its +# defaults. The variables go on the command line because sudo's env_reset +# strips them from the exported environment. +apt_get() { + sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get -y \ + -o Dpkg::Options::=--force-confdef \ + -o Dpkg::Options::=--force-confold "$@" +} + usage() { - echo "Usage: curl -sSL https://raw.githubusercontent.com/OpenVTC/vti-setup/main/scripts/setup-explore.sh | bash -s -- [email]" - echo "Example: ... | bash -s -- example.com" - echo "Example: ... | bash -s -- example.com admin@example.com" - echo "" - echo "Domain is required. Email is optional (used for Let's Encrypt certificate expiry notifications)." + echo "Usage: sudo bash setup-explore.sh [email]" >&2 + echo "Example: sudo bash setup-explore.sh example.com" >&2 + echo "Example: sudo bash setup-explore.sh example.com admin@example.com" >&2 + echo "" >&2 + echo "Domain is required. Email is optional (used for Let's Encrypt certificate expiry notifications)." >&2 exit 1 } -DOMAIN="${1:-}" -EMAIL="${2:-}" - -if [ -z "$DOMAIN" ]; then - echo -e "${RED}Error: domain is required.${NC}" - usage -fi - -echo -e "${GREEN}=== VTI Stack Explore Setup ===${NC}" -echo -e "${GREEN}Domain: $DOMAIN${NC}" -if [ -n "$EMAIL" ]; then - echo -e "${GREEN}Email: $EMAIL${NC}" -else - echo -e "${YELLOW}Email: (not provided — certbot will register without email)${NC}" -fi -echo "" - -# ----------------------------------------------------------------------------- -echo -e "${GREEN}>>> Step 1/10: Update system <<<${NC}" -# ----------------------------------------------------------------------------- -apt_get update && apt_get upgrade - -# ----------------------------------------------------------------------------- -echo -e "${GREEN}>>> Step 2/10: Install build and runtime dependencies <<<${NC}" -# ----------------------------------------------------------------------------- -apt_get install git curl build-essential pkg-config libssl-dev clang cmake ca-certificates libdbus-1-dev ufw valkey-server - -# Valkey backs the mediator's queue + storage. Debian/Ubuntu packaging -# binds 127.0.0.1 and enables the unit on install — confirm both. -sudo systemctl is-active --quiet valkey-server || sudo systemctl enable --now valkey-server -ss -tlnp 'sport = :6379' 2>/dev/null | grep -q 127.0.0.1 \ - && echo -e "${GREEN}Valkey listening on 127.0.0.1:6379.${NC}" \ - || echo -e "${YELLOW}Valkey not on 127.0.0.1:6379 — check /etc/valkey/valkey.conf bind setting.${NC}" - -# ----------------------------------------------------------------------------- -echo -e "${GREEN}>>> Step 3/10: Configure UFW firewall <<<${NC}" -# ----------------------------------------------------------------------------- -sudo ufw default deny incoming -sudo ufw default allow outgoing -sudo ufw allow 22/tcp -sudo ufw allow 80/tcp -sudo ufw allow 443/tcp -sudo ufw --force enable -sudo ufw status - -# ----------------------------------------------------------------------------- -echo -e "${GREEN}>>> Step 4/10: Install Rust <<<${NC}" -# ----------------------------------------------------------------------------- -if command -v rustc &>/dev/null; then - echo -e "${GREEN}Rust already installed: $(rustc --version)${NC}" -else - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y - source "$HOME/.cargo/env" -fi -rustc --version -cargo --version - -# ----------------------------------------------------------------------------- -echo -e "${GREEN}>>> Step 5/10: Install Node.js (v22) <<<${NC}" -# ----------------------------------------------------------------------------- -if command -v node &>/dev/null && [ "$(node -v | cut -d. -f1 | tr -d 'v')" -ge 22 ] 2>/dev/null; then - echo -e "${GREEN}Node.js already installed: $(node -v)${NC}" -else - curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - - apt_get install nodejs -fi -node -v -npm -v - -# ----------------------------------------------------------------------------- -echo -e "${GREEN}>>> Step 6/10: Install Docker <<<${NC}" -# ----------------------------------------------------------------------------- -if command -v docker &>/dev/null; then - echo -e "${GREEN}Docker already installed: $(docker --version)${NC}" -else - apt_get install ca-certificates curl gnupg - sudo install -m 0755 -d /etc/apt/keyrings - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg - sudo chmod a+r /etc/apt/keyrings/docker.gpg - echo \ - "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ - $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ - sudo tee /etc/apt/sources.list.d/docker.list > /dev/null - apt_get update - apt_get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -fi -sudo systemctl enable --now docker -sudo usermod -aG docker "$USER" -docker --version - -# ----------------------------------------------------------------------------- -echo -e "${GREEN}>>> Step 7/10: Install Nginx and Certbot <<<${NC}" -# ----------------------------------------------------------------------------- -apt_get install nginx -sudo systemctl enable --now nginx -if command -v certbot &>/dev/null; then - echo -e "${GREEN}Certbot already installed.${NC}" -else - sudo snap install --classic certbot - sudo ln -sf /snap/bin/certbot /usr/bin/certbot -fi - -# ----------------------------------------------------------------------------- -echo -e "${GREEN}>>> Step 8/10: Create Nginx configs and enable sites <<<${NC}" -# ----------------------------------------------------------------------------- -echo -e "${YELLOW}Creating Nginx configuration files...${NC}" - -MEDIATOR_CONFIG=$(cat <&2 + exit 1 +} + +# Both values end up in nginx server_name lines and certbot arguments, so +# accept only plain hostnames and addresses, before anything else runs. +validate_args() { + local label='[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?' + local domain_re="^(${label}\\.)+[A-Za-z]{2,63}\$" + local email_re="^[A-Za-z0-9._%+-]+@(${label}\\.)+[A-Za-z]{2,63}\$" + + if [ -z "$DOMAIN" ]; then + echo -e "${RED}Error: domain is required.${NC}" >&2 + usage + fi + # "mediator." is the longest prefix added below; keep every name within 253. + if [ "${#DOMAIN}" -gt 244 ] || ! [[ $DOMAIN =~ $domain_re ]]; then + echo -e "${RED}Error: '$DOMAIN' is not a valid domain name.${NC}" >&2 + usage + fi + if [ -n "$EMAIL" ] && { [ "${#EMAIL}" -gt 254 ] || ! [[ $EMAIL =~ $email_re ]]; }; then + echo -e "${RED}Error: '$EMAIL' is not a valid email address.${NC}" >&2 + usage + fi +} + +apt_has_candidate() { + local candidate + candidate=$(apt-cache policy "$1" 2>/dev/null | awk '/Candidate:/ {print $2}') || true + [ -n "$candidate" ] && [ "$candidate" != "(none)" ] +} + +install_rustup_init() { + local triple expected workdir + case "$(uname -m)" in + x86_64) triple=x86_64-unknown-linux-gnu; expected=$RUSTUP_INIT_SHA256_X86_64 ;; + aarch64 | arm64) triple=aarch64-unknown-linux-gnu; expected=$RUSTUP_INIT_SHA256_AARCH64 ;; + *) die "no pinned rustup-init checksum for architecture $(uname -m)." ;; + esac + + workdir=$(mktemp -d) + curl --proto '=https' --tlsv1.2 -fsSL -o "$workdir/rustup-init" \ + "https://static.rust-lang.org/rustup/archive/${RUSTUP_VERSION}/${triple}/rustup-init" + if ! printf '%s %s\n' "$expected" "$workdir/rustup-init" | sha256sum -c --quiet -; then + rm -rf "$workdir" + die "rustup-init ${RUSTUP_VERSION} for ${triple} does not match its pinned SHA-256; refusing to run it." + fi + chmod 0755 "$workdir/rustup-init" + "$workdir/rustup-init" -y --default-toolchain stable + rm -rf "$workdir" + # shellcheck source=/dev/null + . "$HOME/.cargo/env" +} + +# Ubuntu's rustup package puts cargo and rustc in /usr/bin, but `cargo install` +# still writes to ~/.cargo/bin. Put that on PATH for new login shells, as +# rustup-init does for its own install. +ensure_cargo_bin_on_path() { + # shellcheck disable=SC2016 # expanded by the login shell, not here + local line='export PATH="$HOME/.cargo/bin:$PATH"' + local rc + for rc in "$HOME/.profile" "$HOME/.bashrc"; do + grep -qxF "$line" "$rc" 2>/dev/null || printf '\n%s\n' "$line" >>"$rc" + done + export PATH="$HOME/.cargo/bin:$PATH" +} + +install_rust() { + # Pick up a rustup-init install left by a previous run. + if [ -f "$HOME/.cargo/env" ]; then + # shellcheck source=/dev/null + . "$HOME/.cargo/env" + fi + + if rustc --version >/dev/null 2>&1; then + echo -e "${GREEN}Rust already installed: $(rustc --version)${NC}" + elif apt_has_candidate rustup; then + # Signed Ubuntu archive package; rustup then installs the toolchain. + apt_get install rustup + rustup default stable + else + echo -e "${YELLOW}No rustup package in the apt archive; using pinned rustup-init ${RUSTUP_VERSION}.${NC}" + install_rustup_init + fi + ensure_cargo_bin_on_path + rustc --version + cargo --version +} + +write_nginx_configs() { + sudo tee /etc/nginx/sites-available/mediator.conf >/dev/null </dev/null < /dev/null < /dev/null < /dev/null < /dev/null <>> Step 9/10: Obtain SSL certificates (Certbot) <<<${NC}" -# ----------------------------------------------------------------------------- - -CERTBOT_DOMAINS="-d vtc.${DOMAIN} -d vta.${DOMAIN} -d dids.${DOMAIN} -d mediator.${DOMAIN}" - -if [ -n "$EMAIL" ]; then - if sudo certbot --nginx $CERTBOT_DOMAINS --email "$EMAIL" --agree-tos --non-interactive; then - echo -e "${GREEN}Certbot completed successfully.${NC}" +obtain_certificates() { + local -a domains=(-d "vtc.${DOMAIN}" -d "vta.${DOMAIN}" -d "dids.${DOMAIN}" -d "mediator.${DOMAIN}") + local -a account + if [ -n "$EMAIL" ]; then + account=(--email "$EMAIL") else - echo -e "${YELLOW}Certbot did not complete (e.g. DNS not ready).${NC}" - echo -e "You can run manually later:" - echo " sudo certbot --nginx $CERTBOT_DOMAINS --email $EMAIL --agree-tos" + account=(--register-unsafely-without-email) fi -else - if sudo certbot --nginx $CERTBOT_DOMAINS --register-unsafely-without-email --agree-tos --non-interactive; then + + if sudo certbot --nginx "${domains[@]}" "${account[@]}" --agree-tos --non-interactive; then echo -e "${GREEN}Certbot completed successfully.${NC}" else echo -e "${YELLOW}Certbot did not complete (e.g. DNS not ready).${NC}" echo -e "You can run manually later:" - echo " sudo certbot --nginx $CERTBOT_DOMAINS --register-unsafely-without-email --agree-tos" + echo " sudo certbot --nginx ${domains[*]} ${account[*]} --agree-tos" fi -fi - -# ----------------------------------------------------------------------------- -echo -e "${GREEN}>>> Step 10/10: Verify URLs <<<${NC}" -# ----------------------------------------------------------------------------- -echo "" +} check_url() { local url="$1" local code - code=$(curl -sS -o /dev/null -w "%{http_code}" --connect-timeout 5 "$url" 2>/dev/null || echo "000") + code=$(curl -sS -o /dev/null -w "%{http_code}" --connect-timeout 5 "$url" 2>/dev/null) || code="000" if [ "$code" = "502" ]; then echo -e " ${GREEN}$url → 502 (backend not running yet) — URL set up successfully.${NC}" elif [ "$code" = "200" ]; then @@ -308,20 +234,121 @@ check_url() { fi } -check_url "https://mediator.${DOMAIN}" -check_url "https://vta.${DOMAIN}" -check_url "https://vtc.${DOMAIN}" -check_url "https://dids.${DOMAIN}" - -echo "" -echo -e "${GREEN}Setup complete.${NC}" -echo -e " Sites:" -echo -e " - https://mediator.${DOMAIN} → localhost:7037" -echo -e " - https://vta.${DOMAIN} → localhost:8100" -echo -e " - https://vtc.${DOMAIN} → localhost:8200" -echo -e " - https://dids.${DOMAIN} → localhost:8534" -echo "" -echo -e "${YELLOW}NOTE: Rust/Cargo were installed in this script's subshell.${NC}" -echo -e "${YELLOW}To use 'cargo' in your current shell, run:${NC}" -echo -e " source \$HOME/.cargo/env" -echo -e "${YELLOW}Or start a new login shell (logout and back in).${NC}" +main() { + if [ "$#" -gt 2 ]; then + usage + fi + DOMAIN="${1:-}" + EMAIL="${2:-}" + validate_args + + echo -e "${GREEN}=== VTI Stack Explore Setup ===${NC}" + echo -e "${GREEN}Domain: $DOMAIN${NC}" + if [ -n "$EMAIL" ]; then + echo -e "${GREEN}Email: $EMAIL${NC}" + else + echo -e "${YELLOW}Email: (not provided — certbot will register without email)${NC}" + fi + echo "" + + # --------------------------------------------------------------------------- + echo -e "${GREEN}>>> Step 1/8: Update system <<<${NC}" + # --------------------------------------------------------------------------- + apt_get update + apt_get upgrade + + # --------------------------------------------------------------------------- + echo -e "${GREEN}>>> Step 2/8: Install build and runtime dependencies <<<${NC}" + # --------------------------------------------------------------------------- + apt_get install git curl build-essential pkg-config libssl-dev clang cmake ca-certificates libdbus-1-dev ufw valkey-server + + # Valkey backs the mediator's queue + storage. Debian/Ubuntu packaging + # binds 127.0.0.1 and enables the unit on install — confirm both. + sudo systemctl is-active --quiet valkey-server || sudo systemctl enable --now valkey-server + local listeners + listeners=$(ss -tln 'sport = :6379' 2>/dev/null) || listeners="" + if grep -q '127\.0\.0\.1' <<<"$listeners"; then + echo -e "${GREEN}Valkey listening on 127.0.0.1:6379.${NC}" + else + echo -e "${YELLOW}Valkey not on 127.0.0.1:6379 — check /etc/valkey/valkey.conf bind setting.${NC}" + fi + + # --------------------------------------------------------------------------- + echo -e "${GREEN}>>> Step 3/8: Configure UFW firewall <<<${NC}" + # --------------------------------------------------------------------------- + sudo ufw default deny incoming + sudo ufw default allow outgoing + sudo ufw allow 22/tcp + sudo ufw allow 80/tcp + sudo ufw allow 443/tcp + sudo ufw --force enable + sudo ufw status + + # --------------------------------------------------------------------------- + echo -e "${GREEN}>>> Step 4/8: Install Rust <<<${NC}" + # --------------------------------------------------------------------------- + install_rust + + # --------------------------------------------------------------------------- + echo -e "${GREEN}>>> Step 5/8: Install Nginx and Certbot <<<${NC}" + # --------------------------------------------------------------------------- + apt_get install nginx + sudo systemctl enable --now nginx + if command -v certbot >/dev/null 2>&1; then + echo -e "${GREEN}Certbot already installed.${NC}" + else + sudo snap install --classic certbot + sudo ln -sf /snap/bin/certbot /usr/bin/certbot + fi + + # --------------------------------------------------------------------------- + echo -e "${GREEN}>>> Step 6/8: Create Nginx configs and enable sites <<<${NC}" + # --------------------------------------------------------------------------- + echo -e "${YELLOW}Creating Nginx configuration files...${NC}" + write_nginx_configs + + echo -e "${YELLOW}Enabling sites...${NC}" + local site + for site in mediator vta vtc dids; do + sudo ln -sf "/etc/nginx/sites-available/${site}.conf" /etc/nginx/sites-enabled/ + done + + echo -e "${YELLOW}Testing Nginx configuration...${NC}" + if sudo nginx -t; then + echo -e "${GREEN}Nginx config test passed.${NC}" + else + die "Nginx config test failed. Please check your setup." + fi + + echo -e "${YELLOW}Reloading Nginx...${NC}" + sudo systemctl reload nginx || sudo service nginx reload + + # --------------------------------------------------------------------------- + echo -e "${GREEN}>>> Step 7/8: Obtain SSL certificates (Certbot) <<<${NC}" + # --------------------------------------------------------------------------- + obtain_certificates + + # --------------------------------------------------------------------------- + echo -e "${GREEN}>>> Step 8/8: Verify URLs <<<${NC}" + # --------------------------------------------------------------------------- + echo "" + check_url "https://mediator.${DOMAIN}" + check_url "https://vta.${DOMAIN}" + check_url "https://vtc.${DOMAIN}" + check_url "https://dids.${DOMAIN}" + + echo "" + echo -e "${GREEN}Setup complete.${NC}" + echo -e " Sites:" + echo -e " - https://mediator.${DOMAIN} → localhost:7037" + echo -e " - https://vta.${DOMAIN} → localhost:8100" + echo -e " - https://vtc.${DOMAIN} → localhost:8200" + echo -e " - https://dids.${DOMAIN} → localhost:8534" + echo "" + echo -e "${YELLOW}NOTE: binaries from 'cargo install' go to ~/.cargo/bin, which new login shells now have on PATH.${NC}" + echo -e "${YELLOW}To use them in your current shell, run:${NC}" + echo -e " export PATH=\"\$HOME/.cargo/bin:\$PATH\"" + echo -e "${YELLOW}Or start a new login shell (logout and back in).${NC}" +} + +main "$@" diff --git a/sysop/README.md b/sysop/README.md index 63bc58a..499e1a5 100644 --- a/sysop/README.md +++ b/sysop/README.md @@ -6,7 +6,7 @@ You install and manage the VTI infrastructure: VTA, VTC, DIDComm Mediator, and D | Stream | When to pick it | Shape | | --- | --- | --- | -| [Explore](explore/) | "I want to play with the stack and learn how the pieces fit together." | Single VM, single root SSH session, everything installed (Rust, Node, Docker, build deps). Interactive TUI wizards. `nohup` for processes. **Do not put real keys here.** | +| [Explore](explore/) | "I want to play with the stack and learn how the pieces fit together." | Single VM, single root SSH session, build tools installed (Rust, build deps; Node.js only for the optional UI source build). Interactive TUI wizards. `nohup` for processes. **Do not put real keys here.** | | [Deploy](deploy/) | "I want a hardened production deployment." | Hardened Kubernetes deployment with HashiCorp Vault as the secret store. _(To be documented.)_ | Both streams use the **offline sealed-bundle bootstrap** flow over DIDComm — the same flow you'd use to set up a VTI where the VTA is air-gapped from Mediator and DID Host, even when they happen to share a host. diff --git a/sysop/explore/01-server-setup.md b/sysop/explore/01-server-setup.md index e92978e..19b97a1 100644 --- a/sysop/explore/01-server-setup.md +++ b/sysop/explore/01-server-setup.md @@ -39,63 +39,79 @@ Create the following DNS **A records**, all pointing to the public IP from Step ## Step 3: Run the setup script -SSH into your server as **root** and run the setup script directly: +SSH into your server as **root**. Download the setup script from a tagged release of this repository and check it before you run it. Do not run a copy from the `main` branch: `main` changes without notice and is not tied to a reviewed release. ```bash -curl -sSL https://raw.githubusercontent.com/OpenVTC/vti-setup/main/scripts/setup-explore.sh | bash -s -- +VER=v1.0.0 +SHA256=c59f53c74f423327a233bb3178651bf7958449efd88df05df8c2f8d179db1340 +curl -fsSLO "https://github.com/OpenVTC/vti-setup/releases/download/${VER}/setup-explore.sh" +curl -fsSLO "https://github.com/OpenVTC/vti-setup/releases/download/${VER}/SHA256SUMS" +echo "${SHA256} setup-explore.sh" | sha256sum -c - +sha256sum -c SHA256SUMS +``` + +Both checks must print `setup-explore.sh: OK`. Stop if either fails. The first compares your download with the hash pinned on this page for `VER`, and is the check that matters. The second only confirms that the release's own `SHA256SUMS` agrees; that file comes from the same place as the script, so on its own it cannot catch a tampered release. + +Then verify the build provenance attestation, which shows the file was published by this repository's release workflow. This needs GitHub CLI 2.49 or later, signed in with `gh auth login`. Ubuntu 26.04's `gh` package (2.46) is too old, so if the server has no newer `gh`, run this on your workstation against a copy downloaded there, and check that `sha256sum setup-explore.sh` prints the same hash as above: + +```bash +gh attestation verify setup-explore.sh \ + --repo OpenVTC/vti-setup \ + --signer-workflow OpenVTC/vti-setup/.github/workflows/release.yml +``` + +Run the verified script: + +```bash +sudo bash setup-explore.sh # or with email (used for Let's Encrypt expiry notifications): -curl -sSL https://raw.githubusercontent.com/OpenVTC/vti-setup/main/scripts/setup-explore.sh | bash -s -- +sudo bash setup-explore.sh ``` Example: ```bash -curl -sSL https://raw.githubusercontent.com/OpenVTC/vti-setup/main/scripts/setup-explore.sh | bash -s -- example.com +sudo bash setup-explore.sh example.com ``` +The script checks that `` is a plain hostname and `` a plain address, and stops before changing anything if either is not. The whole script is one function called on its last line, so an incomplete download runs nothing. + The script will: 1. Update system packages 2. Install build and runtime dependencies (Git, OpenSSL, build toolchain, Valkey) 3. Configure UFW firewall (allow ports 22, 80, 443) -4. Install Rust -5. Install Node.js v22 -6. Install Docker -7. Install Nginx and Certbot (via snap) -8. Create Nginx reverse proxy configs (4 services) -9. Obtain SSL certificates via Certbot -10. Verify each HTTPS URL responds +4. Install Rust (Ubuntu's `rustup` package, then the stable toolchain) +5. Install Nginx and Certbot (via snap) +6. Create Nginx reverse proxy configs (4 services) +7. Obtain SSL certificates via Certbot +8. Verify each HTTPS URL responds + +Node.js is not installed here: the one step that needs it, the optional DID Hosting UI build in [Option B](#option-b-build-from-source), installs it. Docker is not used anywhere in this guide and is not installed. > **Expected result:** `502 Bad Gateway` on the HTTPS URLs is normal at this stage — the backend services are not running yet. ### If the script stops on a blue configuration dialog -An older copy of the script (or a host with pre-seeded debconf answers) can stop at a full-screen `Configuring keyboard-configuration` dialog during Step 1, or at a `needrestart` "which services should be restarted" list later on. Because the script is piped into `bash`, stdin is the curl pipe rather than your terminal, so the dialog may not accept keystrokes at all. +Older copies of the script, which this page used to pipe straight into `bash`, can stop at a full-screen `Configuring keyboard-configuration` dialog during Step 1, or at a `needrestart` "which services should be restarted" list later on. With the script piped in, stdin was the download rather than your terminal, so the dialog may not accept keystrokes at all. -To get past it, `Ctrl-C` out and either run the script from a file, so stdin stays attached to your terminal: - -```bash -curl -sSLO https://raw.githubusercontent.com/OpenVTC/vti-setup/main/scripts/setup-explore.sh -bash setup-explore.sh -``` - -Or pre-seed the answers as root before re-running the one-liner: +To get past it, `Ctrl-C` out and run the verified release copy as shown above. It sets the apt frontend on every call and runs from a file, so stdin stays attached to your terminal. On a host with pre-seeded debconf answers, you can also pre-seed the answers as root before re-running: ```bash echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections echo 'keyboard-configuration keyboard-configuration/layoutcode string us' | debconf-set-selections ``` -Re-running the whole script is safe, so it does not matter how far in you got before interrupting. The Rust, Node.js, Docker and Certbot steps each skip themselves if the tool is already present, the UFW rules and `systemctl enable` calls are idempotent, and the Nginx vhosts are rewritten from scratch and re-certified on every run. The one thing to watch is Let's Encrypt's rate limit — five duplicate certificates per week — so avoid re-running it many times in a row once certificates have been issued. +Re-running the whole script is safe, so it does not matter how far in you got before interrupting. The Rust and Certbot steps each skip themselves if the tool is already present, the UFW rules and `systemctl enable` calls are idempotent, and the Nginx vhosts are rewritten from scratch and re-certified on every run. The one thing to watch is Let's Encrypt's rate limit — five duplicate certificates per week — so avoid re-running it many times in a row once certificates have been issued. > **Note:** `export DEBIAN_FRONTEND=noninteractive` in your own shell will not help — the script's apt calls go through `sudo`, whose default `env_reset` strips the variable before apt sees it. Seeding the debconf database persists the setting instead, so it applies regardless of environment. The current script sets the frontend on each apt invocation itself, so a fresh copy should never prompt. ## Step 4: Reload shell environment -Rust and Cargo were installed inside the script's subshell. To use `cargo` in your current session, run: +`cargo install` puts the binaries it builds in `~/.cargo/bin`. The script adds that directory to `PATH` for new login shells. To use it in your current session, run: ```bash -source $HOME/.cargo/env +export PATH="$HOME/.cargo/bin:$PATH" ``` Or simply log out and SSH back in — the environment will be loaded automatically on the next login. @@ -106,6 +122,8 @@ Or simply log out and SSH back in — the environment will be loaded automatical Saves 15–40 minutes of build time depending on your hardware. +> **Not integrity-checked.** `download.firstperson.dev` does not publish checksums or signatures yet, so nothing below verifies these binaries beyond HTTPS. That is acceptable only on a throwaway explore host: do not copy them to a machine that holds real keys. + #### Latest tagged release: VTI-Dogwood ```bash @@ -131,7 +149,9 @@ curl -O https://download.firstperson.dev/did-hosting-daemon/latest/did-hosting-d chmod +x did-hosting-daemon && sudo mv did-hosting-daemon /usr/local/bin/ ``` -#### Last compiled commit from main branches +#### Last compiled commit from main branches (unverified) + +Unverified builds of whatever was last merged, published without checksums or signatures. Use them only on a throwaway host, to try a change that is not in a tagged release yet. ```bash curl -O https://download.firstperson.dev/vta/main/vta @@ -158,7 +178,7 @@ chmod +x did-hosting-daemon && sudo mv did-hosting-daemon /usr/local/bin/ ### Option B: Build from source -The setup script installed Rust, Node.js, and the C/C++ build toolchain, so you can also build the binaries yourself. +The setup script installed Rust and the C/C++ build toolchain, so you can also build the binaries yourself. The DID Hosting UI build also needs Node.js, which that step installs. #### VTA, CNM, and PNM @@ -201,10 +221,15 @@ git checkout VTI-Dogwood # latest tagged release, or just stay on main ``` ```bash +# Node.js and npm are needed only for the UI build. apt verifies Ubuntu's +# packages against the Ubuntu archive signing key. +sudo apt-get install -y nodejs npm cd did-hosting-ui && npm install && npm run build:web && cd .. cargo install --path did-hosting-daemon --no-default-features --features "store-fjall,ui,did-methods" ``` +> **Node.js version:** `did-hosting-ui` declares Node.js `>=24.3.0` in its `package.json`, and Ubuntu 26.04 ships Node.js 22. If the UI build fails on Node.js 22, install the pre-built `did-hosting-daemon` from Option A instead. + ## Resulting URL map | URL | Backend |