Take the release credentials off the command line, where anyone could read them
ci / build and test (push) Successful in 3m17s
ci / android head (push) Successful in 3m45s
ci / api image (push) Successful in 24s
ci / desktop nightly (push) Successful in 1m19s

Three scripts prompted for a secret with Read-Host -AsSecureString or read -s,
said in a comment that it therefore never lands in shell history, and then wrote
it straight into the arguments of the next process. The comment was true and was
answering the wrong question. A command line is not private: on Windows one call
to Get-CimInstance Win32_Process hands every argument of every process running as
this user to anything that asks, /proc/<pid>/cmdline is world-readable on Linux,
and process-creation auditing and EDR agents keep their own copy either way. The
values were the Android release signing key's passwords and the forge token —
between them the whole capability to ship an update that every installed copy
will fetch and run, which ADR 0011 rule 1 and ADR 0013 go to some length to keep
off a runner and which was then left in a process listing on a desk.

Both replacements are documented features of the tools rather than tricks.

The Android passwords use .NET for Android's env: prefix, which puts the name of
a variable on the command line and lets MSBuild read the value out of the
environment itself. Set immediately before the build and cleared in a finally, so
a failed signing does not leave them in the shell for whatever runs next, and
-nodeReuse:false so no persistent MSBuild worker outlives the call still holding
the inherited copy in its own environment.

That prefix is unsupported when AndroidPackageFormat is aab, and the failure is
silent — the package would be signed with the literal string
"env:DODOSSH_ANDROID_STOREPASS" as its password. So the format is read back and
the build stops if it is ever changed, in the same shape as the versionCode guard
a few lines above: a wrong release refused here rather than discovered later on
somebody's phone.

vpk documents VPK_TOKEN as --token's environment fallback, so the flag is simply
gone from both desktop scripts and the token never becomes an argument at all.
Unset afterwards on the failure path too, which on macOS needs the exit status
captured rather than a bare || stop_with.

release-macos.sh had the same fault and was missed when the other two were first
reported. Its Apple credentials were already right — a notarytool keychain profile
rather than --password — and only the forge token was exposed.

What this does not claim: an environment block is readable by a process holding
rights over this one, the same as its memory is. The comments say so rather than
implying the values are now safe. What is bought is real and narrower — off the
command line, out of every process listing, out of the audit trail, and out of any
log that echoes the invocation.

Both PowerShell files parse, all three shell scripts pass bash -n, the
AndroidPackageFormat probe evaluates and answers empty so the guard passes, and
dotnet build accepts -nodeReuse:false. The signing itself is not exercised here:
it needs the release keystore, which by design is not on this machine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
laurensandClaude Opus 5 committed 2026-09-01 14:31:02 +02:00
1 parent 483ae1fed9
commit 597e5ae016
3 files changed
+114 -20

No files matched your search

+31 -5
View File
@@ -16,9 +16,11 @@
2. With -Upload: asks for the forge token and publishes what phase 1 produced. It does not rebuild,
so the bytes that reach users are the bytes that were installed and checked.
The token is prompted for rather than read from a file or an environment variable, and only in the phase
that needs it — the build does not, and the fewer minutes a credential that can publish an update spends
in a shell's memory the better.
The token is prompted for rather than read from a file or an ambient environment variable, and only in
the phase that needs it — the build does not, and the fewer minutes a credential that can publish an
update spends in a shell's memory the better. It reaches vpk through VPK_TOKEN, set for that one call
and cleared afterwards, because the alternative is --token on a command line every process on this
machine can read. See the comment at the upload itself.
.PARAMETER Upload
Publish the packages already in Releases/ instead of building.
@@ -152,10 +154,24 @@ try {
# --merge because Gitea already has a release entry for the pushed tag, and without it the upload
# fails on a release that exists. --pre mirrors the rule the docker image job already applies to the
# same tag, so a release candidate is a prerelease in both channels or in neither.
#
# ◆ NO --token HERE. IT WAS AN ARGUMENT, AND AN ARGUMENT IS PUBLIC ON THIS MACHINE.
#
# The comment above says the token never lands in shell history, which was true and was not the
# whole story: it went straight into vpk's command line, and any process running as this user can
# read that off a running process with one call —
#
# Get-CimInstance Win32_Process | Select-Object Name, CommandLine
#
# — with process-creation auditing and EDR agents keeping their own copy afterwards. A token that
# can publish an update every installed copy will fetch and run deserves better than that.
#
# vpk documents VPK_TOKEN as --token's environment fallback, so the flag simply goes away and the
# value never becomes an argument at all. An environment block is still readable by a process with
# rights over this one; what it is not is a string sitting in a process listing and an audit log.
$uploadArgs = @(
'upload', 'gitea',
'--repoUrl', $RepoUrl,
'--token', $token,
'--outputDir', $ReleasesDir,
'--channel', $Channel,
'--releaseName', $tag,
@@ -169,7 +185,17 @@ try {
}
Write-Step 'Uploading'
& dotnet vpk @uploadArgs
try {
$env:VPK_TOKEN = $token
& dotnet vpk @uploadArgs
}
finally {
# SetEnvironmentVariable rather than Remove-Item, which throws when the variable is not there —
# and this runs on the failure path, where it may well not be.
[Environment]::SetEnvironmentVariable('VPK_TOKEN', $null)
}
if ($LASTEXITCODE -ne 0) { Stop-With 'vpk upload failed.' }
Write-Step "Published $tag."