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
- 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.
- 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.
- Rotate the exposed key in the Google project once the file path is deployed, and delete the old key id.
- 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.
- 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
- Complete deployment of
ENGINE_FCM_SERVICE_ACCOUNT_FILE; do not expose the document through the engine environment.
- Rotate and revoke the historically exposed Firebase service-account key.
- Verify
vogt-dev and vogt-prod use the file-backed credential and that child sessions cannot read it from /proc or inherited environment.
- Add a regression test that rejects inline service-account JSON in the shipped deployment path.
- 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.
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 ofENGINE_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:
The value is the complete
service_accountdocument (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
ENGINE_FCM_SERVICE_ACCOUNT_FILE, bind-mounted at/run/secrets/fcm_service_account, mode 0400) and stop accepting the JSON via env. Mirror howVOGT_CORE_TOKEN_FILEis handled.*_JSON/*_KEY/*_SECRETenv var holds a value longer than a token (or a denylist for this specific name) so the regression cannot come back quietly./proc/<engine>/environis unreadable regardless of what it holds.Related: #265 (committed Firebase config rotated), #511 / #566 (brokered credential contract).
Implementation/closure plan
ENGINE_FCM_SERVICE_ACCOUNT_FILE; do not expose the document through the engine environment.vogt-devandvogt-produse the file-backed credential and that child sessions cannot read it from/procor inherited environment.This remains a private-estate security task; no FCM credential or estate configuration belongs in the public AIO image.