From 9ddf5991996b27c26ae4c8371192dad4b4bac926 Mon Sep 17 00:00:00 2001 From: Asmir Avdicevic Date: Fri, 25 Sep 2026 13:09:06 +0200 Subject: [PATCH 1/4] ci(security): add dependabot config The repo had none, so nothing was ever proposed for update. Actions need this in place before pinning, or the pins freeze permanently. Cooldown lets a compromised publish be noticed before we take it. --- .github/dependabot.yml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..54a7f85 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,31 @@ +# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem +# +# The cooldown lets a compromised publish be noticed before we propose it. It +# applies to version updates only -- security updates ignore it. +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + groups: + github-actions: + patterns: + - "*" # Group all Actions updates into a single pull request + cooldown: + default-days: 7 + schedule: + interval: weekly + day: "thursday" + time: "08:00" + timezone: "America/New_York" + - package-ecosystem: cargo + directory: / + cooldown: + default-days: 7 + schedule: + interval: weekly + day: "thursday" + time: "08:00" + timezone: "America/New_York" + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-patch"] From c8a16586e18792e901ef9d593459e645a89e16c6 Mon Sep 17 00:00:00 2001 From: Asmir Avdicevic Date: Fri, 25 Sep 2026 13:09:20 +0200 Subject: [PATCH 2/4] ci(security): pin actions to commit SHAs Floating tags (@v2, @stable, @master) resolve at run time, so a tag move is a silent code change. Pin every action to a SHA with a version comment; refs that carry no semver tag get an inline toolchain input instead. .pinact.yaml keeps the SHAs and comments in sync. --- .github/workflows/ci.yml | 22 +++++++++++++--------- .github/workflows/release.yml | 10 +++++----- .pinact.yaml | 5 +++++ 3 files changed, 23 insertions(+), 14 deletions(-) create mode 100644 .pinact.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 25a12b2..0f8a2cc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,9 +23,11 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: dtolnay/rust-toolchain@stable - - uses: swatinem/rust-cache@v2 + - uses: actions/checkout@0717577d45739eb3c851188b29f50ed6c0b2194e # v2.8.0 + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # v1 + with: + toolchain: stable + - uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - name: cargo fmt run: cargo fmt --all -- --check - name: cargo clippy @@ -54,12 +56,12 @@ jobs: - "beta" - "nightly" steps: - - uses: actions/checkout@v2 - - uses: dtolnay/rust-toolchain@master + - uses: actions/checkout@0717577d45739eb3c851188b29f50ed6c0b2194e # v2.8.0 + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # v1 with: toolchain: ${{ matrix.channel }} targets: ${{ matrix.target.toolchain }} - - uses: swatinem/rust-cache@v2 + - uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - name: cargo test run: cargo test --locked --workspace --all-features --bins --tests --examples @@ -67,8 +69,10 @@ jobs: check-deps: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: dtolnay/rust-toolchain@stable - - uses: swatinem/rust-cache@v2 + - uses: actions/checkout@0717577d45739eb3c851188b29f50ed6c0b2194e # v2.8.0 + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # v1 + with: + toolchain: stable + - uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - name: cargo check run: cargo check --workspace --all-features --bins diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9c9d4d7..00a81f5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -60,13 +60,13 @@ jobs: echo "RELEASE_VERSION=${{ github.event.inputs.release_version }}" >> $GITHUB_ENV echo "version is: ${{ env.RELEASE_VERSION }}" - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: fetch-depth: 1 - name: Create GitHub release id: release if: github.event.inputs.create_release == 'true' || github.event_name == 'push' - uses: actions/create-release@v1 + uses: actions/create-release@0cb9c9b65d5d1901c1f53e5e66eaf4afd303e70e # v1.1.4 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -109,11 +109,11 @@ jobs: runner: [windows-latest] steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: fetch-depth: 1 - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # v1 with: toolchain: ${{ matrix.rust }} targets: ${{ matrix.cargo_targets }} @@ -144,7 +144,7 @@ jobs: echo "ASSET=$staging.tar.gz" >> $GITHUB_ENV fi - name: Upload release archive - uses: actions/upload-release-asset@v1.0.2 + uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 # v1.0.2 if: github.event.inputs.upload_artifacts == 'true' || github.event_name == 'push' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.pinact.yaml b/.pinact.yaml new file mode 100644 index 0000000..3456fbf --- /dev/null +++ b/.pinact.yaml @@ -0,0 +1,5 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/suzuki-shunsuke/pinact/refs/heads/main/json-schema/pinact.json +version: 3 + +min_age: + value: 7 # days, matches the dependabot cooldown From 83df014ce3ca1a9c958e60d3e3b2e211710af7b3 Mon Sep 17 00:00:00 2001 From: Asmir Avdicevic Date: Fri, 25 Sep 2026 13:10:17 +0200 Subject: [PATCH 3/4] ci(security): read-only permissions, lockfile builds and gh releases Every workflow now starts at contents: read; the two release jobs grant write explicitly. actions/checkout also leaves the job token in .git/config by default, where any build script can read it. --locked on the release build and the one cargo check that lacked it, so a job can never silently resolve a dependency the lockfile does not name. actions/create-release and actions/upload-release-asset are both archived upstream; gh ships on the runner and replaces the pair, which also retires the upload_url handoff between the two jobs. Release version and archive names come from env rather than being interpolated into the shell. --- .github/workflows/ci.yml | 12 +++++- .github/workflows/release.yml | 72 +++++++++++++++++++++++------------ 2 files changed, 58 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f8a2cc..15eab04 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,11 +19,17 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true +# Default to read-only; jobs that need more grant it explicitly. +permissions: + contents: read + jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@0717577d45739eb3c851188b29f50ed6c0b2194e # v2.8.0 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # v1 with: toolchain: stable @@ -57,6 +63,8 @@ jobs: - "nightly" steps: - uses: actions/checkout@0717577d45739eb3c851188b29f50ed6c0b2194e # v2.8.0 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # v1 with: toolchain: ${{ matrix.channel }} @@ -70,9 +78,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@0717577d45739eb3c851188b29f50ed6c0b2194e # v2.8.0 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # v1 with: toolchain: stable - uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - name: cargo check - run: cargo check --workspace --all-features --bins + run: cargo check --locked --workspace --all-features --bins diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 00a81f5..73ee442 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,12 +38,17 @@ env: BIN_NAME: sendme IROH_FORCE_STAGING_RELAYS: "1" +# Default to read-only; jobs that need more grant it explicitly. +permissions: + contents: read + jobs: create-release: + permissions: + contents: write # drafts the GitHub release name: create-release runs-on: ubuntu-latest outputs: - upload_url: ${{ steps.release.outputs.upload_url }} release_version: ${{ env.RELEASE_VERSION }} steps: - name: Get the release version from the tag (push) @@ -51,28 +56,37 @@ jobs: if: env.RELEASE_VERSION == '' && github.event_name == 'push' run: | # See: https://github.community/t5/GitHub-Actions/How-to-get-just-the-tag-name/m-p/32167/highlight/true#M1027 - echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV - echo "version is: ${{ env.RELEASE_VERSION }}" + version="${GITHUB_REF#refs/tags/}" + echo "RELEASE_VERSION=$version" >> "$GITHUB_ENV" + echo "version is: $version" - name: Get the release version from the tag (dispatch) shell: bash if: github.event_name == 'workflow_dispatch' + env: + INPUT_RELEASE_VERSION: ${{ github.event.inputs.release_version }} run: | - echo "RELEASE_VERSION=${{ github.event.inputs.release_version }}" >> $GITHUB_ENV - echo "version is: ${{ env.RELEASE_VERSION }}" + echo "RELEASE_VERSION=$INPUT_RELEASE_VERSION" >> "$GITHUB_ENV" + echo "version is: $INPUT_RELEASE_VERSION" - name: Checkout repository uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: + persist-credentials: false fetch-depth: 1 + # actions/create-release is archived. gh ships on the runner, so the + # release jobs need no third-party action despite holding contents: write. - name: Create GitHub release id: release if: github.event.inputs.create_release == 'true' || github.event_name == 'push' - uses: actions/create-release@0cb9c9b65d5d1901c1f53e5e66eaf4afd303e70e # v1.1.4 env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ env.RELEASE_VERSION }} - release_name: ${{ env.RELEASE_VERSION }} + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + if ! gh release view "$RELEASE_VERSION" >/dev/null 2>&1; then + gh release create "$RELEASE_VERSION" --title "$RELEASE_VERSION" + fi build-release: + permissions: + contents: write # uploads the release archives name: build-release needs: create-release runs-on: ${{ matrix.runner }} @@ -111,6 +125,7 @@ jobs: - name: Checkout repository uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: + persist-credentials: false fetch-depth: 1 - name: Install Rust uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # v1 @@ -122,34 +137,41 @@ jobs: run: sudo apt-get install musl-tools -y - name: Build release binary shell: bash + env: + MATRIX_NAME: ${{ matrix.name }} + CARGO_TARGET: ${{ matrix.cargo_targets }} run: | - if [ "${{ matrix.name }}" = "ubuntu-arm-latest" ]; then + if [ "$MATRIX_NAME" = "ubuntu-arm-latest" ]; then export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=aarch64-linux-gnu-gcc export CC=aarch64-linux-gnu-gcc fi - cargo build --verbose --release --target ${{ matrix.cargo_targets }} + cargo build --locked --verbose --release --target "$CARGO_TARGET" - name: Build archive shell: bash + env: + VERSION: ${{ needs.create-release.outputs.release_version }} + TARGET: ${{ matrix.target }} + CARGO_TARGET: ${{ matrix.cargo_targets }} + RUNNER_OS_NAME: ${{ matrix.os }} run: | - staging="${{ env.BIN_NAME }}-${{ needs.create-release.outputs.release_version }}-${{ matrix.target }}" + staging="$BIN_NAME-$VERSION-$TARGET" mkdir -p "$staging" - if [ "${{ matrix.os }}" = "windows-latest" ]; then - cp "target/${{ matrix.cargo_targets }}/release/${{ env.BIN_NAME }}.exe" "$staging/" + if [ "$RUNNER_OS_NAME" = "windows-latest" ]; then + cp "target/$CARGO_TARGET/release/$BIN_NAME.exe" "$staging/" cd "$staging" 7z a "../$staging.zip" . - echo "ASSET=$staging.zip" >> $GITHUB_ENV + echo "ASSET=$staging.zip" >> "$GITHUB_ENV" else - cp "target/${{ matrix.cargo_targets }}/release/${{ env.BIN_NAME }}" "$staging/" + cp "target/$CARGO_TARGET/release/$BIN_NAME" "$staging/" tar czf "$staging.tar.gz" -C "$staging" . - echo "ASSET=$staging.tar.gz" >> $GITHUB_ENV + echo "ASSET=$staging.tar.gz" >> "$GITHUB_ENV" fi - name: Upload release archive - uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 # v1.0.2 if: github.event.inputs.upload_artifacts == 'true' || github.event_name == 'push' + shell: bash env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.create-release.outputs.upload_url }} - asset_path: ${{ env.ASSET }} - asset_name: ${{ env.ASSET }} - asset_content_type: application/octet-stream + GH_TOKEN: ${{ github.token }} + VERSION: ${{ needs.create-release.outputs.release_version }} + run: | + set -euo pipefail + gh release upload "$VERSION" "$ASSET" --clobber From 2f2194b3a354f643c4f3003588bf7317572aebf1 Mon Sep 17 00:00:00 2001 From: Asmir Avdicevic Date: Fri, 25 Sep 2026 13:10:21 +0200 Subject: [PATCH 4/4] ci(security): lint workflows with zizmor and pinact zizmor audits the workflows themselves; pinact fails if a pinned SHA drifts from its version comment or is younger than the cooldown. No zizmor config: the tree is clean with no accepted exceptions. --- .github/workflows/zizmor.yaml | 54 +++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .github/workflows/zizmor.yaml diff --git a/.github/workflows/zizmor.yaml b/.github/workflows/zizmor.yaml new file mode 100644 index 0000000..72038c6 --- /dev/null +++ b/.github/workflows/zizmor.yaml @@ -0,0 +1,54 @@ +# Static analysis of our own workflows. The tree is clean, so this gates on +# every finding, not just High. +name: Workflow Lint + +on: + pull_request: + paths: ['.github/workflows/**', '.github/dependabot.yml', '.pinact.yaml'] + push: + branches: [main] + paths: ['.github/workflows/**', '.github/dependabot.yml', '.pinact.yaml'] + workflow_dispatch: + +permissions: + contents: read + +jobs: + zizmor: + timeout-minutes: 10 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@0717577d45739eb3c851188b29f50ed6c0b2194e # v2.8.0 + with: + persist-credentials: false + - uses: taiki-e/install-action@3f74d7c16a4242f1c95561e98edc25d36adb4375 # v2.87.12 + with: + tool: zizmor + # whole repo, not just workflows/ -- dependabot.yml is audited too + - run: zizmor --offline . + + pinact: + # Fails if a pinned SHA drifts from its version comment, or is younger + # than the cooldown in .pinact.yaml. + timeout-minutes: 10 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@0717577d45739eb3c851188b29f50ed6c0b2194e # v2.8.0 + with: + persist-credentials: false + # Release tarball + checksum instead of pinact-action, which wants + # contents:write to push commits. This job only reads. + - name: Install pinact + env: + PINACT_VERSION: "4.1.1" + run: | + set -euo pipefail + base="https://github.com/suzuki-shunsuke/pinact/releases/download/v${PINACT_VERSION}" + curl -fsSL -O "$base/pinact_linux_amd64.tar.gz" + curl -fsSL -O "$base/pinact_${PINACT_VERSION}_checksums.txt" + grep ' pinact_linux_amd64.tar.gz$' "pinact_${PINACT_VERSION}_checksums.txt" | sha256sum -c - + tar xzf pinact_linux_amd64.tar.gz pinact + install -m755 pinact /usr/local/bin/pinact + - run: pinact run --check --verify-comment --verify-min-age + env: + GITHUB_TOKEN: ${{ github.token }}