eco-server.service: stop leaking userToken JWT via ExecStart argv #2
Labels
No labels
burndown-2026-06
coherence-core
consult
headless
interactive
P0
P1
P2
P3
P4
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
coilyco-flight-deck/infrastructure#2
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally filed by @coilysiren on 2026-05-23T19:19:13Z - https://github.com/coilysiren/infrastructure/issues/297
Problem - The eco-server systemd unit invokes
EcoServer -userToken="$eco_server_api_token"(seescripts/eco-server-start.sh), which puts the JWT from SSM/eco/server-api-tokeninto/proc/<pid>/cmdline. Anysystemctl status eco-server,ps, or journald scrape that captures the cmdline (including agent transcripts) leaks the token in cleartext.Surfaced - 2026-05-23, mid-incident chat transcript captured the JWT verbatim from
systemctl statusoutput during the LFS-pointer mod recovery (sibling issue: install-eco-mod-source.sh hardening).Fix shape - Pass the token via env var or stdin instead of argv. Eco supports
userTokenvia config file (Configs/Network.eco->ServerAPIToken) as well; writing it there at ExecStartPre time and dropping the argv flag is the cleanest path. Rotate/eco/server-api-tokenonce the unit no longer exposes it.Out of scope - The DiscordLink token already gets injected via
jqintoConfigs/DiscordLink.ecorather than argv. Mirror that pattern for the user token.🔒 Reserved by
ward agent --driver claude— containerengineer-claude-infrastructure-2on hostKAI-DESKTOP-TOWERis carrying this issue (reserved 2026-07-04T17:25:53Z). Concurrentward agentruns are blocked until it finishes or the reservation goes stale (2h0m0s TTL);--forceoverrides.Do not comment on or edit this issue to steer the run while it is reserved. The engineer seeded the body once at launch and never re-reads it, so a comment or edit reaches only human readers, never the running engineer. A correction goes to a new issue, dispatched fresh — that is the only channel that reaches a run in flight. Where the forge supports it, ward locks this conversation to make that a road-block rather than a convention (ward#494).
The pre-flight judged this issue GO for an unattended run. Its justification:
pre-flight read (GO)
Context to front-load: In the fresh clone I will read, before any edit: (1)
scripts/eco-server-start.sh— the current SSM fetch and-userTokenargv wiring; (2) the eco-server systemd unit definition wherever the repo keeps it (service file / ansible / cloud-init) to see ExecStart/ExecStartPre structure; (3) the existing DiscordLinkjq-into-Configs/DiscordLink.ecoinjection, since the issue says to mirror it exactly; (4) how/whereConfigs/Network.ecolives on the host relative to the script (path conventions, ownership/permissions on the written file); and (5) repo plumbing —.ward/ward.yamlverbs, pre-commit hooks, and the Forgejo-main push flow from AGENTS.md — so the land step doesn't stall.Main risk: verification is indirect — the container can't run the real Eco server or systemd, so correctness rests on faithfully mirroring the proven DiscordLink jq pattern and confirming
Network.ecois jq-editable JSON with the rightServerAPITokenkey path; if the file turns out to be non-JSON or the key nesting differs from the issue's sketch, I'd have to infer it from repo fixtures or upstream docs. The token rotation step is operational (needs the live server/SSM), so I'd land the code fix and file a follow-up issue for rotation rather than block the merge on it. Everything else — the fix shape, the file to touch, the pattern to copy — is fully specified by the issue.GO
— Claude (she/her), via
ward agent🔓 Reservation released by
ward container reap— containerengineer-claude-infrastructure-2(--driver claude) exited without launching the agent (smoke-test death, ward#222/#264), so the hold it took is retracted. A plainward agentretry no longer needs--force.— Claude (she/her), via
ward agent🔒 Reserved by
ward agent --driver claude— containerengineer-claude-infrastructure-2on hostKAI-DESKTOP-TOWERis carrying this issue (reserved 2026-07-04T17:27:08Z). Concurrentward agentruns are blocked until it finishes or the reservation goes stale (2h0m0s TTL);--forceoverrides.Do not comment on or edit this issue to steer the run while it is reserved. The engineer seeded the body once at launch and never re-reads it, so a comment or edit reaches only human readers, never the running engineer. A correction goes to a new issue, dispatched fresh — that is the only channel that reaches a run in flight. Where the forge supports it, ward locks this conversation to make that a road-block rather than a convention (ward#494).
The pre-flight judged this issue GO for an unattended run. Its justification:
pre-flight read (GO)
Context to front-load: (1) the eco-server systemd unit definition in the clone (ExecStart/ExecStartPre wiring and how the unit file is deployed — ansible, cloud-init, or checked-in unit), (2)
scripts/eco-server-start.sh— how the SSM token is fetched and the argv is built today, (3) the existing DiscordLinkjq-into-Configs/DiscordLink.ecoinjection, since the issue names it as the pattern to mirror, (4) the repo's SSM fetch conventions (instance role vs.coily ops aws) so/eco/server-api-tokenis read the house way, and (5).ward/ward.yamlplus the pre-commit hook set so validation runs through ward. I will read each of these in the fresh clone before making my first edit.The main unknown is
Configs/Network.ecolifecycle: Eco may generate that file only on first server boot, so an ExecStartPrejqinjection needs create-or-merge handling and the exact key path (ServerAPIToken) verified against the DiscordLink precedent rather than assumed — I can resolve that from the existing injection code and Eco config structure in the clone. The change itself is shell/unit-file editing with no live Eco server to smoke-test in the container, so verification will be shellcheck/inspection-level, which is acceptable for this shape of change. The token-rotation step is an operational action against SSM and the live server that shouldn't happen from an unattended container; I'd land the leak fix and file a follow-up issue for rotation, which matches the issue's "once the unit no longer exposes it" sequencing. The only comment is a reservation-release notice, so nothing supersedes the original description.GO
— Claude (she/her), via
ward agentWARD-OUTCOME: done - eco-server-start.sh now jq-injects the API token into Configs/Network.eco (ServerAPIToken) and launches EcoServer with no -userToken argv, so the JWT is off /proc//cmdline. Landed on main as
bc5a48b.The fix was smaller than the leak was scary: the DiscordLink BotToken injection sitting right above in the same script was an exact template, so this was really just "do that, for the other token, into the other config." I added a first-boot fallback that creates Network.eco with
jq -nwhen the file isn't there yet, so a fresh Steam install doesn't dead-end on a missing config.What actually fought back was the pre-commit suite, not the code. The doc-size cap (80 lines / 4000 chars) on docs/ops-eco.md was already near its ceiling, so my ops note kept blowing it. I moved the durable explanation into the eco-server-setup skill doc and left the script comment pointing there instead. Separately, pylint couldn't run at first because uv wanted to download a managed Python into a root-owned /opt/uv/python. Redirecting UV_PYTHON_INSTALL_DIR to a writable path let it pass 10.00/10 without --no-verify.
Confidence: high on the shell logic (it mirrors a pattern already running in prod, and .eco files are JSON that jq handles cleanly). One caveat I couldn't close from inside the container: no AWS creds here, so I couldn't verify the live CI run or confirm the exact ServerAPIToken key name against a running Network.eco - I trusted the issue's stated field name. Worth an eyeball on the next restart that the token lands and the server authenticates.
Follow-ups: (1) rotate /eco/server-api-token now that the old value rode argv and was captured in transcripts - I can't touch the play.eco account or SSM from here; (2) confirm the first restart picks up ServerAPIToken from the config and drops the argv path cleanly.