Files
ci/bin/kms_test.sh
zeekay a8f840f803 fix(kms_test): the login-refusal cases blank the run's own credential
bin/kms spends a run's ID token, or a HANZO_API_TOKEN, before a stored
client. A GitHub job of this repository grants id-token: write, so IAM
exchanged a real token and the refusal case met "refused the secret NAME:
HTTP 403" instead of the login's 401. Every run of hanzoai/ci on GitHub
has failed there since 2026-10-15 (run 38748428994 and on), so neither
image has published since. The old test fails the same way with
HANZO_API_TOKEN set locally; this one passes with it and without.
2026-10-20 08:27:59 -07:00

98 lines
5.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# bin/kms — what it must do without a live KMS.
#
# The three copies this script replaced all failed the same way in a log: an
# outage, a refusal and an unsealed secret were indistinguishable. So the thing
# under test is which SENTENCE comes out, and how many times it asks.
set -uo pipefail
cd "$(dirname "$0")/.."
fails=0 ran=0
check() { # check <name> <expected-substring> <actual>
ran=$((ran+1))
case "$3" in *"$2"*) ;; *) echo "FAIL $1: wanted /$2/, got: $3"; fails=$((fails+1)) ;; esac
}
out=$(ACTIONS_ID_TOKEN_REQUEST_URL= HANZO_API_TOKEN= KMS_CLIENT_ID= KMS_CLIENT_SECRET= bash bin/kms NAME 2>&1); rc=$?
check "no credential names the grant a GitHub caller gives" "grant the job \`permissions: id-token: write\`" "$out"
check "no credential names both variables" "KMS_CLIENT_ID and KMS_CLIENT_SECRET are unset" "$out"
ran=$((ran+1)); [ "$rc" -eq 1 ] || { echo "FAIL no credential: rc=$rc want 1"; fails=$((fails+1)); }
out=$(bash bin/kms 2>&1); rc=$?
check "no name is a usage error" "usage: kms <NAME>" "$out"
ran=$((ran+1)); [ "$rc" -eq 2 ] || { echo "FAIL usage: rc=$rc want 2"; fails=$((fails+1)); }
# A REFUSAL is taken on the first try. The run's own ID token is blanked: in a
# GitHub job that grants id-token: write, bin/kms spends it before any stored
# client, IAM exchanges it for a real token, and the case met a 403 on the
# secret instead of the 401 on the login it is about. Every run of this
# repository on GitHub failed on that from 2026-10-15. An endpoint that answers 401 immediately
# must not be retried — four waits would add a minute to a build that is already
# going to fail, and the old copies could not tell this from an outage.
start=$SECONDS
out=$(ACTIONS_ID_TOKEN_REQUEST_URL= HANZO_API_TOKEN= KMS_CLIENT_ID=x KMS_CLIENT_SECRET=y KMS_ENDPOINT=https://kms.hanzo.ai bash bin/kms NAME 2>&1); rc=$?
took=$((SECONDS - start))
check "a refusal names the code" "refused the login: HTTP 401" "$out"
ran=$((ran+1)); [ "$rc" -eq 1 ] || { echo "FAIL refusal: rc=$rc want 1"; fails=$((fails+1)); }
ran=$((ran+1)); [ "$took" -lt 20 ] || { echo "FAIL refusal took ${took}s — 401 is KMS answering and must not be retried"; fails=$((fails+1)); }
# AN OUTAGE asks again and says so. A refused port yields 000 four times —
# refused rather than black-holed, so the case costs the retry schedule and not
# four connect timeouts on top of it. The message must not read as a missing
# secret, which is the sentence that sent a day of publishes looking for a token
# that was sealed the whole time.
out=$(ACTIONS_ID_TOKEN_REQUEST_URL= HANZO_API_TOKEN= KMS_CLIENT_ID=x KMS_CLIENT_SECRET=y KMS_ENDPOINT=http://127.0.0.1:1 bash bin/kms NAME 2>&1); rc=$?
check "an outage says it is one" "An outage, not a missing secret" "$out"
check "an outage names the tries" "over four tries" "$out"
ran=$((ran+1)); [ "$rc" -eq 1 ] || { echo "FAIL outage: rc=$rc want 1"; fails=$((fails+1)); }
# A RUN THAT HAS AN ID TOKEN spends it, not a stored client: a refusal names the
# exchange, and no login is attempted.
out=$(ACTIONS_ID_TOKEN_REQUEST_URL='http://127.0.0.1:1/token?x=1' ACTIONS_ID_TOKEN_REQUEST_TOKEN=t HANZO_API_TOKEN= KMS_CLIENT_ID= KMS_CLIENT_SECRET= bash bin/kms NAME 2>&1); rc=$?
check "a run asks GitHub for its ID token" "did not answer the ID token" "$out"
check "a run with no token and no stored client says so" "IAM gave this run no token" "$out"
ran=$((ran+1)); case "$out" in *"the login"*) echo "FAIL a run with no stored client tried a login"; fails=$((fails+1)) ;; esac
ran=$((ran+1)); [ "$rc" -eq 1 ] || { echo "FAIL run: rc=$rc want 1"; fails=$((fails+1)); }
# A run whose ID token IAM will not exchange falls back to a stored client.
out=$(ACTIONS_ID_TOKEN_REQUEST_URL='http://127.0.0.1:1/token?x=1' ACTIONS_ID_TOKEN_REQUEST_TOKEN=t HANZO_API_TOKEN= KMS_CLIENT_ID=x KMS_CLIENT_SECRET=y KMS_ENDPOINT=https://kms.hanzo.ai bash bin/kms NAME 2>&1); rc=$?
check "a stored client carries a run IAM would not exchange" "refused the login: HTTP 401" "$out"
# STDOUT IS THE VALUE AND NOTHING ELSE. A run with an ID token goes through the
# whole exchange against a stand-in curl, and what a caller captures must be the
# sealed value byte for byte: no workflow command, no second line. A mask written
# to stdout became the first line of the captured key, the caller masked only
# that line, and printed the real key.
fake=$(mktemp -d)
cat > "$fake/curl" <<'CURL'
#!/usr/bin/env bash
out= url=
while [ $# -gt 0 ]; do
case "$1" in
-o) out=$2; shift ;;
-m|-w|-H|-d|--data-urlencode) shift ;;
http*) url=$1 ;;
esac
shift
done
case "$url" in
*audience=*) body='{"value":"id.token.value"}' ;;
*/v1/iam/oauth/token) body='{"access_token":"iam-bearer"}' ;;
*/v1/kms/secrets/NAME*) body='{"value":"sk-test-0123456789abcdef"}' ;;
*) body='{}'; printf '%s' "$body" > "$out"; printf 404; exit 0 ;;
esac
printf '%s' "$body" > "$out"
printf 200
CURL
chmod +x "$fake/curl"
errf=$(mktemp)
got=$(PATH="$fake:$PATH" ACTIONS_ID_TOKEN_REQUEST_URL='https://token.invalid/?x=1' ACTIONS_ID_TOKEN_REQUEST_TOKEN=t \
HANZO_API_TOKEN= KMS_CLIENT_ID= KMS_CLIENT_SECRET= KMS_ENDPOINT=https://kms.invalid bash bin/kms NAME prod 2>"$errf"); rc=$?
err=$(cat "$errf"); rm -rf "$fake" "$errf"
ran=$((ran+1)); [ "$rc" -eq 0 ] || { echo "FAIL exchange: rc=$rc, stderr: $err"; fails=$((fails+1)); }
ran=$((ran+1)); [ "$got" = "sk-test-0123456789abcdef" ] || { echo "FAIL captured value is not exactly the secret: $(printf '%q' "$got")"; fails=$((fails+1)); }
ran=$((ran+1)); case "$got" in *::*) echo "FAIL a workflow command reached stdout"; fails=$((fails+1)) ;; esac
check "the ID token is masked, on stderr" "::add-mask::id.token.value" "$err"
echo "kms_test: $((ran-fails)) passed, $fails failed"
[ "$fails" -eq 0 ]