fix(harbor): refuse non-loopback capture CLI without an admin key - #1194
Conversation
Stop mint-and-print of admin keys (credential exposure). Default --host to 127.0.0.1; require --admin-key or $OPENENV_CAPTURE_ADMIN_KEY for non-loopback binds. Never print or log the key. Co-authored-by: benjamin.burtenshaw <benjamin.burtenshaw@huggingface.co>
Includes #1194 capture CLI no-leak (loopback default; refuse non-loopback without an explicit admin key). Co-authored-by: benjamin.burtenshaw <benjamin.burtenshaw@huggingface.co>
Re-trigger for post-#1194 0.5.0 candidate (no-leak CLI). Co-authored-by: benjamin.burtenshaw <benjamin.burtenshaw@huggingface.co>
There was a problem hiding this comment.
Alignment Review Report
Security-hardening follow-up to #1193 on the standalone capture CLI (python -m openenv.core.harness.capture.server). Reviewed the diff 32b782a...a1ea71d against OpenEnv principles, invariants, and open RFCs.
Automated Checks
- Lint: PASS —
ruff format --check,ruff check, andusort checkall pass on the two changed files and the fullsrc/ tests/scope. Removingsecrets.token_urlsafe(...)does not orphan thesecretsimport (still used bysecrets.compare_digestatserver.py:704). - Debug code: CLEAN — nothing new introduced; this PR actually removes a
print(...)that logged a minted credential. (check-debug.shonly surfaces pre-existing prints/TODOs elsewhere, none from this diff.) - Tests: PASS — all 11 tests in
tests/envs/test_harbor_capture_server.pypass, including the 4 rewritten CLI tests that pin the new fail-closed behavior and assert nothing is printed.
Open RFCs Context
- RFC 012 – Harbor capture providers (In Review): the closest RFC (capture proxy control plane), but it scopes capture purpose / provider fidelity / session ownership — it does not specify the CLI bind default or admin-key handling. No conflict.
- RFC 002 – Env spec (In Review), clause S4 (Least privilege & secret hygiene): "Providers do not put secrets, tokens, or ingress URLs in logs or exceptions." This change supports S4.
Tier 1: Fixes Required
None.
Tier 2: Alignment Discussion
Principle Conflicts
None identified — the change strengthens alignment:
- "No credential exposure" invariant (INVARIANTS.md → Security: never log secrets, never include credentials in error messages). The old path minted a random admin key and printed it to stdout (a logged credential); the new path fails closed with a message that contains no secret value (only the env-var name and the host).
- "Network access must be explicitly configured" invariant — flipping the default bind to loopback makes external exposure an explicit opt-in (secure by default).
RFC Conflicts
None identified.
Minor (non-blocking) notes
Two optional polish items left as inline comments — neither blocks merge:
- Default
--hostmoves0.0.0.0→127.0.0.1: a behavior change for the standalone CLI worth a release-note callout (pre-1.0 breaking changes are acceptable per INVARIANTS.md). - The admin-key precondition is evaluated after the
validate_llmupstream probe; consider validating right after arg-parsing so a missing key fails fast before any network call.
Summary
- 0 mechanical issues to fix (Tier 1)
- 0 alignment points requiring human review (Tier 2) — the change is well-aligned and improves credential hygiene
- 0 RFC conflicts
- 2 minor, optional polish notes (inline)
Suggested reviewer for the credential-hygiene invariant this touches: @Darktex (author of the "No credential exposure" invariant).
Sent by Cursor Automation: Pre-review
| default="0.0.0.0", | ||
| help="bind address (default: 0.0.0.0). Non-loopback binds mint an admin key when none is " | ||
| "set so /sessions* is never left open on a reachable interface.", | ||
| default="127.0.0.1", |
There was a problem hiding this comment.
Minor / non-blocking. This flips the standalone CLI's default bind from 0.0.0.0 to loopback-only 127.0.0.1 — a sensible secure-by-default that aligns with the "network access must be explicitly configured" invariant.
Do note it's a behavior change for anyone running python -m openenv.core.harness.capture.server who relied on the all-interfaces default (e.g. inside a container); they'll now need --host 0.0.0.0 --admin-key ... explicitly. Pre-1.0 breaking changes are acceptable per INVARIANTS.md, so no code change required — just worth a release-note callout.
| print( | ||
| f"capture admin key (minted for --host={args.host}; " | ||
| f"set --admin-key or $OPENENV_CAPTURE_ADMIN_KEY to pin): {admin_key}" | ||
| raise SystemExit( |
There was a problem hiding this comment.
Minor / non-blocking. This admin-key precondition runs after the validate_llm upstream probe above (the if not level: block), so a non-loopback invocation without a key will make a network round-trip and print the capture level before failing here. Consider moving this check to just after args = parser.parse_args() so a config error fails fast before any network call.
Note the tests pass --capture-level, which bypasses the probe, so this ordering isn't currently exercised.


Summary
Follow-up to #1193 / release hold for 0.5.0.
#1193 closed the ungated
/sessions*control plane, but the CLI path minted a random admin key and printed it to stdout — that violates the repo invariant "Never log API keys, tokens, or secrets" (CI/service managers commonly persist stdout).Change
--hostto127.0.0.1(private local port).0.0.0.0, etc.) require--admin-keyor$OPENENV_CAPTURE_ADMIN_KEYand exit otherwise.SystemExit; explicit key on non-loopback is accepted and not printed.run_batch()is unchanged (still always resolves a non-empty key in-process without printing it).Test plan
PYTHONPATH=src:envs uv run pytest tests/envs/test_harbor_capture_server.py tests/envs/test_harbor_runner_lifecycle.py -vDo not cut
v0.5.0until this lands onmain.Note
Medium Risk
Changes secure defaults and startup behavior for the capture CLI; misconfiguration now fails at launch instead of minting a printed key, while programmatic
run_batch/Harbor paths are unchanged.Overview
Hardens the capture server CLI so admin credentials are never written to stdout, aligning with the repo rule against logging secrets.
Default bind is now
127.0.0.1instead of0.0.0.0, so a bareservestays on a private local port. For non-loopback hosts (0.0.0.0, etc.), the process exits unless--admin-keyor$OPENENV_CAPTURE_ADMIN_KEYis set—it no longer auto-generates a key and prints it. Loopback may still run with no admin key for local debugging.Help text and tests were updated: default loopback leaves
admin_keyunset with no key in output; non-loopback without a key raisesSystemExit; an explicit key on non-loopback is accepted and not echoed.Reviewed by Cursor Bugbot for commit a1ea71d. Bugbot is set up for automated code reviews on this repo. Configure here.