docs(release): second review of #199: the runbook no longer contradicts itself, and says which file the manifests are made from
- The copy of a release to git.kof22.com is a numbered step, with the attestation check before it; the verification step says what is checked there and from where, and no longer tells to merge a pull request on GitHub. - winget: the job stays switched off while it would submit the GitHub URL; the command for an update by hand is given. - A hand run of Update-PackageManifests.ps1 takes the CLI zip from the GitHub release: given the file from git.kof22.com it would compare that file with itself. - The dev edition's installer is named as the section on the dev edition names it. - Threat model: T11 is not 'Mitigated' as a whole while the copy is manual; T2 says when Chocolatey's hash is computed in CI. Refs #183 Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> Signed-off-by: James Maes <james@kof22.com>
This commit is contained in:
1 parent
821425f931
commit
271090571f
2 files changed
+41
-20
No files matched your search
+39
-18
@@ -50,7 +50,7 @@ its own channel:
|
||||
|
||||
- Users who installed from a **stable** release (`gclo-stable-Setup.exe`) get
|
||||
updates only when you tag a new stable release.
|
||||
- Users who installed from a **dev** release (`gclo-dev-Setup.exe`) get every
|
||||
- Users who installed from a **dev** release (`gclo-dev-dev-Setup.exe`) get every
|
||||
prerelease you tag.
|
||||
|
||||
A stable release `v1.2.3` carries:
|
||||
@@ -173,10 +173,17 @@ Invoke-WebRequest https://aka.ms/wingetcreate/latest -OutFile wingetcreate.exe
|
||||
.\wingetcreate.exe new https://git.kof22.com/KofTwentyTwo/gclo/releases/download/v1.2.3/gclo-stable-Setup.exe --token <your GitHub PAT>
|
||||
```
|
||||
|
||||
Once that first PR is merged and the package is live, set the repository variable
|
||||
`WINGET_PACKAGE_ID=KofTwentyTwo.gclo` and the `WINGET_TOKEN` secret. Re-running the
|
||||
`winget` job of the bootstrapped release is not needed (it is already in winget);
|
||||
every later stable release updates the manifest automatically.
|
||||
Once that first PR is merged and the package is live, the `winget` job can update
|
||||
the manifest for every later stable release: it runs when the repository variable
|
||||
`WINGET_PACKAGE_ID=KofTwentyTwo.gclo` and the `WINGET_TOKEN` secret are set.
|
||||
|
||||
**Leave `WINGET_PACKAGE_ID` unset for now.** The job submits the download URL of
|
||||
the GitHub release, and the downloads are on git.kof22.com (ADR 0009). Until the
|
||||
pipeline runs there (#197), update winget by hand for each stable release:
|
||||
|
||||
```powershell
|
||||
.\wingetcreate.exe update KofTwentyTwo.gclo --version 1.2.3 --urls https://git.kof22.com/KofTwentyTwo/gclo/releases/download/v1.2.3/gclo-stable-Setup.exe --submit --token <your GitHub PAT>
|
||||
```
|
||||
|
||||
## Scoop and Chocolatey
|
||||
|
||||
@@ -202,8 +209,13 @@ Both install the same asset, `gclo-cli-win-x64-<version>.zip`, by URL and SHA-25
|
||||
403 (v1.1.0 hit this while 1.0.1 was queued). The `packages` job opens the
|
||||
manifests PR *before* the push so Scoop is never held up; re-run the failed job
|
||||
once the queued version is approved or rejected.
|
||||
- To re-point the manifests by hand (for example after a missed release):
|
||||
- To re-point the manifests by hand (for example after a missed release, and for
|
||||
the commit on Gitea):
|
||||
`./.github/scripts/Update-PackageManifests.ps1 -Version 1.2.3 -CliZipPath <zip>`.
|
||||
`<zip>` is the CLI zip downloaded from the GitHub release, where it was built,
|
||||
after `gh attestation verify` has passed for it. The script compares the file
|
||||
on git.kof22.com with the one it is given: given a zip taken from
|
||||
git.kof22.com, it would compare the server's file with itself.
|
||||
|
||||
## The release on git.kof22.com
|
||||
|
||||
@@ -226,9 +238,9 @@ does on GitHub:
|
||||
downloads the CLI zip from git.kof22.com and refuses to write a manifest when it
|
||||
does not answer or is not, by its SHA-256, the file that was built; so the
|
||||
`packages` job fails until the copy is done and right. Run it again afterwards.
|
||||
- **winget by hand.** The workflow's winget step still submits the download on
|
||||
GitHub; the manual command above names git.kof22.com. Until the pipeline moves,
|
||||
submit a version to winget by hand.
|
||||
- **winget by hand.** The workflow's winget step would submit the download on
|
||||
GitHub, so it stays switched off ("First winget submission"); a version is
|
||||
submitted to winget by hand, with the URL on git.kof22.com.
|
||||
- **No more than nine prereleases newer than the latest stable release.** The
|
||||
updater reads the ten newest releases and picks by channel; a longer run of betas
|
||||
hides the stable release from stable installs. Delete old prereleases.
|
||||
@@ -258,7 +270,7 @@ immutable. Treat a published version as final anyway (see "A broken release").
|
||||
|
||||
3. **Rehearse on a prerelease.** Tag `vX.Y.Z-beta.N` first: it runs the whole
|
||||
pipeline except the stable-only publishers, on the `dev` channel. Install
|
||||
`gclo-dev-Setup.exe` on a clean machine, run a Quick Sync, and let an
|
||||
`gclo-dev-dev-Setup.exe` on a clean machine, run a Quick Sync, and let an
|
||||
installed earlier prerelease self-update to it. Copy each prerelease to
|
||||
git.kof22.com first ("The release on git.kof22.com"): the self-update is read
|
||||
from there, and it is tried from a machine outside the local network. A
|
||||
@@ -305,16 +317,25 @@ immutable. Treat a published version as final anyway (see "A broken release").
|
||||
- **Scoop and Chocolatey**, **winget**, **NuGet** — the publishers described
|
||||
above; each fails loudly when it cannot publish.
|
||||
|
||||
7. **Verify.** On the [releases page](https://git.kof22.com/KofTwentyTwo/gclo/releases)
|
||||
every asset listed above is attached, with the same hashes as in `SHA256SUMS`
|
||||
and the right prerelease flag (see "The release on git.kof22.com"). On GitHub,
|
||||
where the release was built,
|
||||
7. **Copy the release to git.kof22.com.** On GitHub, where the release was built,
|
||||
`gh attestation verify <asset> --repo KofTwentyTwo/gclo --signer-repo KofTwentyTwo/standards`
|
||||
passes. Install the previous version and let it update itself to this one.
|
||||
Merge the packaging PR. Check the winget PR on microsoft/winget-pkgs.
|
||||
Tick the "Ship" section of the checklist issue.
|
||||
passes for each asset. Then create the release on Gitea with the same tag, the
|
||||
same assets and the same prerelease flag ("The release on git.kof22.com").
|
||||
For a stable release: re-run the `packages` job, which could not finish before
|
||||
the download was there, and commit the three packaging files on Gitea through
|
||||
a pull request (`Update-PackageManifests.ps1` with the CLI zip from the GitHub
|
||||
release, see "Scoop and Chocolatey").
|
||||
|
||||
8. **If something failed**, where it failed decides the recovery:
|
||||
8. **Verify.** On the [releases page](https://git.kof22.com/KofTwentyTwo/gclo/releases)
|
||||
every asset listed above is attached, with the same hashes as in `SHA256SUMS`
|
||||
and the right prerelease flag. Install the previous version and let it update
|
||||
itself to this one; then, from a machine outside the local network, the
|
||||
updated app answers "up to date" in **Help > Check for updates** (it reads
|
||||
git.kof22.com from now on). The packaging pull request on Gitea is merged and
|
||||
`scoop update` offers the version. winget is updated by hand ("First winget
|
||||
submission"). Tick the "Ship" section of the checklist issue.
|
||||
|
||||
9. **If something failed**, where it failed decides the recovery:
|
||||
|
||||
- **In `verify`, `gates`, or inside `release` before "Publish release"**:
|
||||
nothing is public. Delete the draft release if one was created
|
||||
|
||||
@@ -86,7 +86,7 @@ flowchart LR
|
||||
| # | Boundary / component | Category | Threat | Likelihood | Impact | Mitigation | Status |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| T1 | Update feed | Tampering | Attacker serves a malicious update | Low | Critical | HTTPS to the project's releases on `git.kof22.com` only (ADR 0009); Velopack verifies each package against the size and hash in the feed. The feed and the packages come from the same server, and the updater does not check an Authenticode signature or an attestation: whoever controls that server, the repository's release permission, or the server's TLS certificate can deliver a package that installs without a warning. The mitigations are therefore the server's own: who may publish a release there, how its certificate and DNS are held. Releases are not immutable on Gitea. | Accepted for 1.1.1; depends on the server's access control |
|
||||
| T2 | Package managers / downloads | Tampering | Modified installer or CLI zip on a mirror | Low | High | `SHA256SUMS`; Scoop and Chocolatey manifests pin the hash; "Verifying a release" instructions; the binaries are Authenticode-signed. Since ADR 0009 the Scoop manifest, the zip it names and `SHA256SUMS` are all served by `git.kof22.com`: whoever controls that server, its TLS certificate or its DNS can change the file and the hash together, which is the position of T1 (release permission alone does not change the manifest on `main`). Chocolatey's hash is computed in CI from the built zip and held at chocolatey.org, independently of that server. | Mitigated against a modified copy elsewhere; for Scoop, accepted against the release server (as T1) |
|
||||
| T2 | Package managers / downloads | Tampering | Modified installer or CLI zip on a mirror | Low | High | `SHA256SUMS`; Scoop and Chocolatey manifests pin the hash; "Verifying a release" instructions; the binaries are Authenticode-signed. Since ADR 0009 the Scoop manifest, the zip it names and `SHA256SUMS` are all served by `git.kof22.com`: whoever controls that server, its TLS certificate or its DNS can change the file and the hash together, which is the position of T1 (release permission alone does not change the manifest on `main`). Chocolatey's hash is computed from the built zip (in CI by the `packages` job; from the GitHub release's file when the package is pushed by hand) and held at chocolatey.org, independently of that server. | Mitigated against a modified copy elsewhere; for Scoop, accepted against the release server (as T1) |
|
||||
| T3 | Git remotes | Tampering / EoP | Repository with Windows-invalid or traversal paths (`..`, reserved names, case collisions) writes outside or corrupts the target | Medium | High | Pre-checkout validation of every incoming path, which also refuses a `.git` segment in any spelling Windows opens as that folder; `RepositoryPathResolver` containment; recovery by rename/skip or clone in WSL; `core.longpaths`. The recovery materializes the tree itself, so every write and delete it makes goes through `WorkingTree`: inside the working directory, never in `.git`, never through a link, and an existing file is replaced rather than written through (#141). Residual: the link check and the write are two steps, so a link swapped in between them by something already running as the user is not caught | Mitigated |
|
||||
| T4 | Git remotes | Tampering | Hostile `.git` config or hooks in a pre-existing target repository | Low | High | gclo runs no hooks (libgit2, no shell); only fetch and fast-forward; `gclo.checkoutpending` marks incomplete trees; a folder is fetched only when its `origin` names the listed repository, and the remote contacted is the listed URL, not origin's spelling of it; a `url.<base>.insteadOf` rewrite in the folder's own configuration is refused, and so is its `core.sshCommand` when the fetch is not HTTPS (`RemoteUrl`, #139). The user's global git configuration still applies, as it does to git itself. The recovery file `.git\gclo-recovery.json` is validated when it is loaded: a listed path that is not in the working tree fails the row instead of being deleted (#141) | Mitigated for the remote, the transport program, and the recovery file; other `.git` content of an existing folder is still trusted |
|
||||
| T5 | PAT handling | Information disclosure | Token leaks to disk, logs, process listings, or UI | Medium | Critical | Memory-only in Quick Sync; Credential Manager otherwise; accounts may reference the default token (`TokenSource`), resolved at use time through one resolver and never copied; logs record lengths; no `--token`; WSL via env only; secret scanning + gitleaks + CodeQL in CI | Mitigated |
|
||||
@@ -95,7 +95,7 @@ flowchart LR
|
||||
| T8 | GitHub API / remotes | Spoofing | A MITM impersonates GitHub | Low | Critical | HTTPS with system trust store (Octokit, libgit2); no HTTP fallback: an existing folder whose origin is `http://` or SSH is fetched through the listed HTTPS URL instead (unless the user's own global configuration rewrites that URL) | Mitigated |
|
||||
| T9 | WSL recovery | Elevation of privilege | Hostile org/repo names injected into the shell script, or used to leave `~/gclo` | Low | High | Script fed on stdin with positional parameters; organization and repository each validated as one plain name inside `WslCloner` (and the organization again before any listing request); the URL must be HTTPS and is passed after `--`; no string interpolation into the script | Mitigated |
|
||||
| T10 | Logs and crash output | Information disclosure | Exception text carries a token or private path | Low | Medium | Crash net logs messages only; log review in incident handling; tokens never in exception messages (tests) | Mitigated |
|
||||
| T11 | Release pipeline | Tampering | Compromised action or cache poisons a release, or a file is changed on its way to the release server | Low | Critical | Until a pipeline on `git.kof22.com` exists, a release's assets are copied there by hand from the GitHub release (docs/RELEASING.md): that step is outside the attested pipeline, and nothing the app checks ties a file on that server to the provenance (`Update-PackageManifests.ps1` does compare the CLI zip served there with the built one's hash before it writes the manifests). For the build itself: Build runs in the standards' shared release workflow (SLSA Build L3 provenance, `--signer-repo KofTwentyTwo/standards`), SHA-pinned actions, no caches, read-only tokens, publishing tokens only in the tag-restricted `release` environment, `protect-release-tags`, required gates re-run on the tag | Mitigated |
|
||||
| T11 | Release pipeline | Tampering | Compromised action or cache poisons a release, or a file is changed on its way to the release server | Low | Critical | Until a pipeline on `git.kof22.com` exists, a release's assets are copied there by hand from the GitHub release (docs/RELEASING.md): that step is outside the attested pipeline, and nothing the app checks ties a file on that server to the provenance (`Update-PackageManifests.ps1` compares the CLI zip served there with the hash of the file it is given, which has to be the built one, before it writes the manifests). For the build itself: Build runs in the standards' shared release workflow (SLSA Build L3 provenance, `--signer-repo KofTwentyTwo/standards`), SHA-pinned actions, no caches, read-only tokens, publishing tokens only in the tag-restricted `release` environment, `protect-release-tags`, required gates re-run on the tag | Mitigated for the build; the copy to the release server is manual and unattested (accepted with T1) |
|
||||
| T12 | Repository | Repudiation | Unattributed change reaches `main` | Low | High | Signed, signed-off commits; squash-only through gated pull requests; no bypass actors | Mitigated |
|
||||
| T14 | PAT handling | Information disclosure | The token is offered to a host other than the one the repository was listed on: an existing folder whose `origin` points elsewhere, or a server named by the repository's own configuration during the WSL clone (for example an LFS endpoint) | Low | Critical | The git credential callback answers only a remote whose URL is HTTPS on the listed URL's host and port, and refuses every other remote with a clear error (a redirect that the listed host itself issues is followed by libgit2; that host already holds the token); a folder whose origin names another repository fails before any request; the WSL credential helper is scoped to the listed host (`credential.https://<host>.helper`) and an existing WSL destination is pulled only when its stored origin is the listed URL (#139) | Mitigated |
|
||||
| T15 | Logs and terminal | Spoofing / Tampering | Text from a repository or a server (a file path may contain a newline, an escape sequence, or a character that reorders or hides text) forges an entry in the activity log or drives the terminal the CLI prints to | Low | Medium | `DisplayText` shows control, separator and format characters (direction marks and overrides, invisible characters, tag characters) and unpaired surrogates as visible `\uXXXX` text: every log message is one line, and an exception's lines are indented under their entry so none can start like one. The CLI writes every line through one place, `Terminal`, which also shows any character the console's code page cannot represent in that form, because a legacy code page turns some printable characters into control bytes; a test fails if any other CLI file writes to the console. The JSON modes rely on the JSON encoder (#143) | Mitigated for the log and the CLI. Not covered: the desktop app's own surfaces (row error text, the path-recovery dialog), where a newline is harmless but a direction override in a path the user is asked to approve is not; and text that merely looks like something else (look-alike letters, an escape written out literally) |
|
||||
|
||||
Reference in new issue
Block a user