Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/allowed_signers
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ivan.vydrin.99@gmail.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEP+eaAvmygExDDthOJmJRDbzptN1yVzTOi9FQVhpbgQ
16 changes: 16 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,22 @@ jobs:
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

# The provenance attestation proves WHAT built an artifact. It cannot prove WHO authorised
# the release: anyone able to push a tag can start this workflow, and the attestation would
# be perfectly valid. The signing key is the one credential GitHub never holds, so requiring
# a signed tag is what makes a stolen GitHub account insufficient on its own.
- name: Require a signed tag
if: startsWith(github.ref, 'refs/tags/v')
run: |
set -euo pipefail
git fetch --force origin "refs/tags/${GITHUB_REF_NAME}:refs/tags/${GITHUB_REF_NAME}"
git config gpg.ssh.allowedSignersFile .github/allowed_signers
if ! git verify-tag "${GITHUB_REF_NAME}"; then
echo "::error::Tag ${GITHUB_REF_NAME} is not signed by a key listed in .github/allowed_signers. See RELEASING.md."
exit 1
fi

- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: '10.0.x'
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ reconstructed is now a real session. What has been verified and what has not is

### Security

- **Release tags must now be signed.** The workflow verifies the tag's SSH signature against
`.github/allowed_signers` before it builds anything. The provenance attestation proves *what*
built an artifact; it cannot prove *who* authorised the release, because anyone able to push a
tag starts the workflow and the resulting attestation would be valid. The signing key is the one
credential GitHub does not hold, so this is what makes a stolen GitHub account insufficient on
its own. See [RELEASING.md](RELEASING.md#signed-tags), including what it does not protect
against.

- **The service-principal recipe no longer puts the client secret in a command-line argument.**
`curl --user "$ID:$SECRET"` — the form Databricks' own documentation shows — makes the secret
readable from the process table by anything else on the machine while the request runs. On a
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
[![LakeSpeak.Genie](https://img.shields.io/nuget/v/LakeSpeak.Genie?label=LakeSpeak.Genie)](https://www.nuget.org/packages/LakeSpeak.Genie/)
[![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/ivanvyd/LakeSpeak.NET/badge)](https://scorecard.dev/viewer/?uri=github.com/ivanvyd/LakeSpeak.NET)
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/13969/badge)](https://www.bestpractices.dev/projects/13969)

Talk to governed Databricks data from your terminal and .NET applications.

Expand Down
39 changes: 38 additions & 1 deletion RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,36 @@ lakespeak --version
CI already does this on every PR (the `tool-smoke` job), but doing it by hand once before a real
release is cheap.

## Signed tags

The release workflow verifies the tag's signature before it builds anything, against the public
keys in [`.github/allowed_signers`](.github/allowed_signers).

This exists because the provenance attestation answers a different question than people assume. It
proves **what** built an artifact — this workflow, this repository, this commit. It cannot prove
**who** authorised the release: anyone able to push a tag starts the workflow, and the attestation
on the result would be perfectly valid. The signing key is the one credential GitHub does not hold,
so requiring a signed tag is what makes a stolen GitHub account insufficient by itself.

Signing uses SSH, not GPG — the same key already used for commits, so there is no second key to
manage:

```bash
git config gpg.format ssh
git config user.signingkey ~/.ssh/id_ed25519.pub
git config tag.gpgSign true
```

Two consequences worth knowing before relying on this.

**A lost key blocks releases** until a new public key is committed to `.github/allowed_signers`.
That is the trade: the control is only as available as the key. Keep a second maintainer key in
that file if the project ever gains one.

**It is not absolute.** Someone holding the GitHub account could open a pull request removing the
verification step and merge it. Signing makes that a multi-step attack recorded in git history
rather than a single silent tag push, which is the realistic protection available here.

## Cut the release

### Prerequisites, once
Expand Down Expand Up @@ -134,13 +164,20 @@ recreating it under the same name, and publishing as if nothing changed.
2. Confirm `docs/compatibility.md` reflects what has actually been verified for this version.
An entry there with no evidence behind it is worse than a missing one.
3. Merge those to `main`.
4. Tag and push:
4. Tag and push. **The tag must be signed** — the workflow refuses to build an unsigned one:

```bash
git tag -a v1.2.3 -m "v1.2.3"
git push origin v1.2.3
```

`tag.gpgSign` is set locally to `true`, so `git tag -a` signs without `-s`. Check before
pushing if you want to be sure:

```bash
git -c gpg.ssh.allowedSignersFile=.github/allowed_signers verify-tag v1.2.3
```

5. The workflow runs. If the environment gate is configured (see prerequisites), it stops
there for approval — otherwise it publishes straight away.
6. Check the GitHub release: three binaries, checksums, SBOM, generated notes.
Expand Down
11 changes: 11 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ Recorded in [docs/security/threat-model.md](docs/security/threat-model.md).
effect of pushing a tag.
- Live integration tests never run for pull requests from forks, because they need workspace
credentials.
- Release tags must be signed by a key in `.github/allowed_signers`; the workflow refuses to build
an unsigned tag, so a release cannot be triggered with GitHub access alone.

## Verifying a release

Expand All @@ -105,6 +107,15 @@ A passing check tells you the file was built by `.github/workflows/release.yml`
repository, and not rebuilt or replaced by anyone afterwards. A failing one means the file did not
come from here — treat it as hostile rather than as a tooling problem.

The attestation answers *what built this*. **Who authorised it** is a separate question, answered
by the release tag: the workflow verifies the tag's SSH signature against
[`.github/allowed_signers`](.github/allowed_signers) before it builds, so a release cannot be
started by someone holding only the GitHub account. You can check any release tag yourself:

```bash
git -c gpg.ssh.allowedSignersFile=.github/allowed_signers verify-tag v0.1.0
```

To check the binaries against their published digests instead:

```bash
Expand Down
Loading