Skip to content

test: cover the managed-key lifecycle and the admin session in smoke (T1.2.10) - #50

Merged
kuyazee merged 1 commit into
mainfrom
task/auth-session-smoke
Aug 12, 2026
Merged

kuyazee merged 1 commit into
mainfrom
task/auth-session-smoke

Conversation

@kuyazee

@kuyazee kuyazee commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

The item

T1.2.10, filed 2026-08-07 from the T1.2.2 review. smoke.sh never called /api/keys,
/api/auth/session or /api/auth/logout, and never completed a successful login: the only login
traffic was the 40-request failure burst. Two claims in SECURITY.md rode on that gap. Line 11 says a
key acting above its scope is a vulnerability, line 14 says a managed key, even full, must not be
able to manage keys. Neither was tested. A destructuring typo on issueSession or sessionPrincipal
leaves undefined, does not crash at boot, and leaves the suite green.

What runs now

Fourteen assertions in two blocks, both placed before the login burst, which spends the per-IP
failure budget (10 per 15 minutes) for the rest of the run.

Managed keys, on the bootstrap bearer, because POST /api/keys is admin-only:

  • the create response carries key and a ah_ prefix and no hash
  • a read key lists (200) and cannot publish (403)
  • a full managed key cannot list keys (401)
  • a key minted with expiresAt: 2020-01-01 never authenticates (401)
  • disable then re-enable: 401, then 200 again
  • a revoked key answers 401

The admin session:

  • login sets artifacts_session, HttpOnly, SameSite=Strict, Path=/, read off the Set-Cookie
    line itself rather than the whole header block
  • the cookie alone, with no Authorization header, reaches /api/artifacts and /api/keys
  • /api/auth/session reports authenticated:true for it
  • an unknown username and a wrong password return byte-identical bodies, the cheap proxy for the
    decoy-hash timing guard
  • logout drops it: the same request is 401 afterwards

Skipping, and the one job that used to skip

The session block needs credentials, so it runs when ARTIFACTS_ADMIN_USERNAME and
ARTIFACTS_ADMIN_PASSWORD are in the environment and prints a skip: line naming them when they are
not. Five of the six CI jobs already set them at job level. docker-build passed them to the
container with -e but not to the job shell, so the block would have skipped in the one job that
boots the shipped image. Two lines of ci.yml fix that, with the same values the container gets.

Tests

  • npm test: 64 passing, unchanged by this branch.
  • bash .github/workflows/smoke.sh http://localhost:3000 test with the two admin variables exported:
    all passing, 176 assertions, 14 of them new.
  • Red proof by mutation, not by inverting an expectation. Commenting out if (key.disabled) continue;
    in lib/auth.js:436 and restarting gave FAIL: disabled key refused: expected 401, got 200. The
    mutation was reverted before the commit, and the final run above is against the reverted tree.

Review

95 changed lines, which the rule puts in the two-subagent band (adversarial and QA). This session runs
under an instruction not to spawn agents unless asked, so both lenses were run by hand instead and the
call is logged in AGENT-DECISIONS.md. Two findings came out of it and both are already fixed in this
diff:

  • Adversarial. The cookie flag checks first grepped the entire header block, so any other header
    carrying path=/ could have answered for Set-Cookie. They read the Set-Cookie line now.
  • Adversarial. Two rate-limited logins also compare equal, so the identical-bodies assertion could
    have passed while proving nothing. It now also requires the body to say invalid credentials.

Other things checked and found fine: the sed extractors have exactly one match each in the create
response ("key":" and "id":" appear once; ci-read-key does not contain "key":"), an empty
extraction fails loudly rather than running with an empty bearer, all three keys are deleted at the
end of the block so the later MCP scope section mints into a clean list, and the two failed logins
leave 8 of the 10-per-window budget, which the burst below still exhausts.

QA. Every line of the item's rebuild list is covered except one, deliberately: /api/auth/password
is not driven. Changing the seeded admin's password mid-suite would leave a real instance with a
password nobody recorded, and would break the next run against the same instance. The two-replica CI
step already exercises that route against a throwaway pair.

Findings outside the item

None fixed, one recorded: a key create with a duplicate name is accepted, so a run that dies between
minting and deleting leaves ci-read-key records behind in auth.json. It costs nothing but clutter
on a dev instance, and CI starts from an empty one. Naming keys per run would trade that clutter for
noise in the diff.

Merges

git merge-tree against current main and the one open branch (#43): clean.

…(T1.2.10)

The suite never called /api/keys, /api/auth/session, /api/auth/logout, and never
completed a successful login. Two claims in SECURITY.md rode on that gap: that a
scope is enforced server-side, and that a managed key, even full, cannot manage
keys. A destructuring typo on issueSession or sessionPrincipal left the suite
green.

Fourteen assertions in two blocks.

Managed keys, on the bootstrap bearer, because POST /api/keys is admin-only: the
create response carries the token once with a prefix and no hash, a read key
lists but cannot publish (403), a full managed key cannot list keys (401), a key
minted with a past expiresAt never authenticates, disable then re-enable swings
both ways, and a revoked key answers 401.

The admin session: login sets artifacts_session HttpOnly SameSite=Strict Path=/,
the cookie alone reaches /api/artifacts and /api/keys with no Authorization
header, /api/auth/session reports it, an unknown username and a wrong password
answer identically, and logout drops it.

The session block sits before the login burst, which spends the per-IP failure
budget for 15 minutes. It skips with a printed line when
ARTIFACTS_ADMIN_USERNAME and ARTIFACTS_ADMIN_PASSWORD are not in the
environment. Five of the six CI jobs already export them; docker-build now does
too, so the one job that boots the shipped image covers the session as well.
@kuyazee
kuyazee merged commit fe7565c into main Aug 12, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant