Skip to content

security(engine): FCM service-account private key is a plain env var readable by every agent session #583

Description

@thedancingdeveloper

Summary

The engine container's environment carries the Firebase Cloud Messaging service account JSON, including its RSA private key, as a plain environment variable (MYDEVENV2_FCM_SERVICE_ACCOUNT_JSON, legacy name of ENGINE_FCM_SERVICE_ACCOUNT_JSON). The engine runs as the same uid as every agent session it spawns, so any process in a session can read it from /proc/<engine pid>/environ.

Verified on prod 2026-09-05 from inside an ordinary Claude Code session:

$ ls -l /proc/10/environ          # pid 10 = vogt-engine
-r-------- 1 sprooty sprooty 0 ... /proc/10/environ
$ tr '\0' '\n' < /proc/10/environ | grep -c FCM_SERVICE_ACCOUNT
1

The value is the complete service_account document (private_key_id, private_key, client_email, ...). I have not copied it anywhere; it was seen while dumping the engine's environment during the 0.5.3 incident investigation, which is exactly the kind of routine action that leaks it.

Every other secret in the same environment is either a file secret under /run/secrets (vogt_core_token, github_token, vogt_engine_token) or a name-only breadcrumb (ENGINE_AGENT_AUTH_SECRETS, ..._TOKEN_PROJECT_ID). This one is the exception, and it is the one that grants push-notification signing for the whole project.

Why it matters

Proposed fix

  1. Load the FCM service account from a file secret (ENGINE_FCM_SERVICE_ACCOUNT_FILE, bind-mounted at /run/secrets/fcm_service_account, mode 0400) and stop accepting the JSON via env. Mirror how VOGT_CORE_TOKEN_FILE is handled.
  2. Have the engine read it once at startup and drop it from its own environment, or better, read it lazily only in the push sender.
  3. Rotate the exposed key in the Google project once the file path is deployed, and delete the old key id.
  4. Add a startup check that refuses to run if any *_JSON/*_KEY/*_SECRET env var holds a value longer than a token (or a denylist for this specific name) so the regression cannot come back quietly.
  5. Consider running sessions under a different uid from the engine, so /proc/<engine>/environ is unreadable regardless of what it holds.

Related: #265 (committed Firebase config rotated), #511 / #566 (brokered credential contract).

Implementation/closure plan

  1. Complete deployment of ENGINE_FCM_SERVICE_ACCOUNT_FILE; do not expose the document through the engine environment.
  2. Rotate and revoke the historically exposed Firebase service-account key.
  3. Verify vogt-dev and vogt-prod use the file-backed credential and that child sessions cannot read it from /proc or inherited environment.
  4. Add a regression test that rejects inline service-account JSON in the shipped deployment path.
  5. Send a real push after rotation and attach the evidence.

This remains a private-estate security task; no FCM credential or estate configuration belongs in the public AIO image.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingsecuritySecurity finding

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions