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
4 changes: 3 additions & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ jobs:
go-version: '1.26.x'
check-latest: true
- name: Integration tests (root)
run: sudo -E env "PATH=$PATH" go test -race -tags integration ./...
# Root integration packages share the host account databases and must
# not run useradd/userdel concurrently across package processes.
run: sudo -E env "PATH=$PATH" go test -race -p 1 -tags integration ./...

static-cross-build:
name: Static cross-build (${{ matrix.goarch }})
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/mirror-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
workflow_dispatch:
inputs:
tag:
description: Immutable release tag to mirror, for example v2.8.1
description: Immutable release tag to mirror, for example v2.8.2
required: true
type: string

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ jobs:
GOPROXY="$GOPROXY" GOSUMDB="$GOSUMDB" GONOSUMDB= GOPRIVATE= GONOPROXY= GOINSECURE= \
GOCACHE="$root_go_root/gocache" GOMODCACHE="$root_go_root/gomodcache" \
GOPATH="$root_go_root/gopath" GOTMPDIR="$root_go_root/gotmp" \
go test -mod=readonly -count=1 -race -tags integration ./...
go test -mod=readonly -count=1 -race -p 1 -tags integration ./...
test -z "$(gofmt -l .)"
sh -n scripts/install.sh
bash -n scripts/*.sh
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

All notable changes to this project are documented here.

## v2.8.2 - 2026-07-27

- Serialize root integration test packages because they share the host account
databases. The signed `v2.8.1` tag exposed a cross-package `useradd` lock
timeout in the release gate before any binary, draft, or Release was created
and remains only as an audit record; no published v2.8.1 artifacts exist.
- Record that the current v1 ed25519 release key has historical network-host
exposure instead of claiming an air-gapped custody history it does not have.

## v2.8.1 - 2026-07-27

- Keep the release-only root integration workspace below a fresh root-owned
Expand Down
42 changes: 25 additions & 17 deletions docs/releasing.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
# Releasing linux-temp-admin v2

Release binaries are reproducibly rebuilt and signed with ed25519. The release
private key is never present on a networked machine, candidate source is never
executed on the signing machine, and CI output is never signed merely because
its own checksum file matches.
Release binaries are reproducibly rebuilt and signed with ed25519. The target
release boundary keeps the private key off networked machines, never executes
candidate source on the signing machine, and never signs CI output merely
because its own checksum file matches. The current v1 ed25519 key predates that
custody boundary and has historical exposure on a networked maintainer host;
releases using it must retain that residual-risk record until the planned key
rotation completes.

## Maintainer model

Expand Down Expand Up @@ -419,10 +422,10 @@ git -c user.name='XXV.CC' \
-c user.signingkey="${TAG_SIGNING_FPR}!" \
-c gpg.format=openpgp \
-c gpg.program=/usr/bin/gpg \
tag -s v2.8.1 "$RELEASE_COMMIT" -m 'linux-temp-admin v2.8.1'
tag -s v2.8.2 "$RELEASE_COMMIT" -m 'linux-temp-admin v2.8.2'
git -c gpg.format=openpgp -c gpg.program=/usr/bin/gpg \
verify-tag --raw v2.8.1
git push origin v2.8.1
verify-tag --raw v2.8.2
git push origin v2.8.2
```

Before pushing, the `VALIDSIG` record from `verify-tag --raw` must identify the
Expand All @@ -432,7 +435,8 @@ an armored OpenPGP signature before continuing. Use that same fingerprint as
`LTA_EXPECTED_TAG_SIGNER_FINGERPRINT` during preparation and publication.

The `Release` workflow uses exactly Go 1.26.5. Its read-only `gate-build` job
runs vet, uncached race tests, root integration tests, formatting, shell checks,
runs vet, uncached race tests, package-serialized root integration tests,
formatting, shell checks,
the mirror receiver policy tests, a clean-worktree check, and
`govulncheck v1.6.0`, then builds static
amd64/arm64 binaries with fixed tuning, `GOWORK=off`, `-mod=readonly`, and
Expand Down Expand Up @@ -485,7 +489,7 @@ printf '\n' >/dev/tty
|| fail "GH_TOKEN must be one non-empty token without whitespace"
export GH_TOKEN
exec /opt/lta-release-tools/prepare-release.sh \
v2.8.1 /srv/linux-temp-admin /srv/release-transfer/v2.8.1-prepared
v2.8.2 /srv/linux-temp-admin /srv/release-transfer/v2.8.2-prepared
LTA_PREPARE_RELEASE
```

Expand All @@ -505,7 +509,7 @@ the candidate or transfer media:
LTA_SIGN_KEY=/offline/keys/release-v1.key
LTA_TRUSTED_SIGNER=/opt/lta-release-tools/lta-release
LTA_TRUSTED_SIGNER_SHA256='<offline-recorded signer sha256>'
LTA_EXPECTED_TAG=v2.8.1
LTA_EXPECTED_TAG=v2.8.2
LTA_EXPECTED_COMMIT='<independently recorded 40-hex commit>'
LTA_EXPECTED_PREPARED_MANIFEST_SHA256='<independently recorded sha256>'
LTA_EXPECTED_RELEASE_SIGNER_PUBKEY='<independently recorded 64-hex OLD public key>'
Expand All @@ -517,7 +521,7 @@ LTA_EXPECTED_RELEASE_SIGNER_PUBKEY='<independently recorded 64-hex OLD public ke
LTA_EXPECTED_PREPARED_MANIFEST_SHA256="$LTA_EXPECTED_PREPARED_MANIFEST_SHA256" \
LTA_EXPECTED_RELEASE_SIGNER_PUBKEY="$LTA_EXPECTED_RELEASE_SIGNER_PUBKEY" \
/opt/lta-release-tools/offline-sign-release.sh \
/media/in/v2.8.1-prepared /media/out/v2.8.1-signed
/media/in/v2.8.2-prepared /media/out/v2.8.2-signed
```

The script copies the removable input into a size-bounded private local snapshot
Expand Down Expand Up @@ -568,7 +572,7 @@ printf '\n' >/dev/tty
|| fail "GH_TOKEN must be one non-empty token without whitespace"
export GH_TOKEN
exec /opt/lta-release-tools/publish-release.sh \
/srv/release-transfer/v2.8.1-signed /srv/linux-temp-admin
/srv/release-transfer/v2.8.2-signed /srv/linux-temp-admin
LTA_PUBLISH_RELEASE
```

Expand Down Expand Up @@ -633,7 +637,7 @@ noncanonical published stable tag, excludes the failed `TAG`, and verifies the
exact resulting Latest state:

```bash
TAG=v2.8.1 # the failed release; verify this value before running
TAG=v2.8.2 # the failed release; verify this value before running
/usr/bin/sudo /usr/bin/env -i \
HOME=/root PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin LC_ALL=C \
TAG="$TAG" /bin/bash -p <<'LTA_LATEST_RECOVERY'
Expand Down Expand Up @@ -992,7 +996,7 @@ the release audit/signing record and a separate authenticated channel, then run:
```bash
INSTALLER_COMMIT='replace-with-the-audited-40-hex-commit'
INSTALLER_SHA256='replace-with-the-independent-64-hex-script-hash'
LTA_RELEASE_TAG='v2.8.1'
LTA_RELEASE_TAG='v2.8.2'
/usr/bin/sudo /usr/bin/env -i \
HOME=/root PATH=/usr/sbin:/usr/bin:/sbin:/bin LC_ALL=C \
INSTALLER_COMMIT="$INSTALLER_COMMIT" INSTALLER_SHA256="$INSTALLER_SHA256" \
Expand Down Expand Up @@ -1136,9 +1140,13 @@ metadata are already safe; otherwise it is atomically repaired.

## Trust boundaries and residual risk

- The private key is protected from candidate code, CI, GitHub, and networked
preparation/publication. The air-gapped OS, fixed signer binary, trusted
offline script, and physical transfer procedure remain trusted.
- The signing phase protects the private key from candidate code, CI, GitHub,
and networked preparation/publication. The current v1 key was historically
stored on a networked maintainer host, so its past confidentiality cannot be
inferred from the isolated signing ceremony. Complete the planned overlap
rotation before claiming an air-gapped custody history for the active key.
The air-gapped OS, fixed signer binary, trusted offline script, and physical
transfer procedure remain trusted for keys that actually follow that model.
- Reproducible comparison binds CI bytes to the audited tag under the fixed Go
toolchain. The audited source, signed-tag identity, trusted preparation copy,
Go distribution, and preparation workstation remain trusted.
Expand Down
22 changes: 22 additions & 0 deletions internal/selfmanage/release_pipeline_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3338,6 +3338,28 @@ func TestVulnerabilityScannerIsPinnedAndRunsInReleaseGate(t *testing.T) {
}
}

func TestRootIntegrationPackagesRunSerially(t *testing.T) {
workflows := map[string]struct {
path string
want string
}{
"Go": {
path: "../../.github/workflows/go.yml",
want: `go test -race -p 1 -tags integration ./...`,
},
"Release": {
path: "../../.github/workflows/release.yml",
want: `go test -mod=readonly -count=1 -race -p 1 -tags integration ./...`,
},
}
for name, workflow := range workflows {
content := readReleaseFile(t, workflow.path)
if !strings.Contains(content, workflow.want) {
t.Errorf("%s workflow does not serialize root integration packages", name)
}
}
}

func TestReleaseKeyringValidationIsPortableAcrossAwkImplementations(t *testing.T) {
const portableCheck = `length($0) != 64 || $0 !~ /^[0-9A-Fa-f]+$/`
const nonPortableCheck = `$0 !~ /^[0-9A-Fa-f]{64}$/`
Expand Down