❯envpilot
featurespricingdocs❯blogchangelogwishlistfaq
sign-inget-started
❯envpilot

Encrypted environment variables for teams that live in the terminal. No .env files, no secrets in Slack.

$npm install -g @envpilot/cli

// product

  • Features
  • Pricing
  • Changelog
  • Wishlist

// resources

  • Getting Started
  • CLI Reference
  • VS Code Extension
  • Security

// compare

  • vs Doppler
  • vs Infisical
  • vs .env files

// support

  • FAQ
  • Support
  • Contact
  • Docs
  • Status

// legal

  • Privacy Policy
  • Terms of Service

© 2026 Envpilot · Built at Syntax Lab Technology · Abdul Rafay

ENVPILOT

❯envpilot
featurespricingdocs❯blogchangelogwishlistfaq
sign-inget-started
❯envpilot

Encrypted environment variables for teams that live in the terminal. No .env files, no secrets in Slack.

$npm install -g @envpilot/cli

// product

  • Features
  • Pricing
  • Changelog
  • Wishlist

// resources

  • Getting Started
  • CLI Reference
  • VS Code Extension
  • Security

// compare

  • vs Doppler
  • vs Infisical
  • vs .env files

// support

  • FAQ
  • Support
  • Contact
  • Docs
  • Status

// legal

  • Privacy Policy
  • Terms of Service

© 2026 Envpilot · Built at Syntax Lab Technology · Abdul Rafay

ENVPILOT

back to blogblog// blog

Where Does the Service-Account JSON Go? Secrets That Aren't Environment Variables

A
Abdul Rafay
September 5, 20266 min read
#guides#security

Ask any secrets manager how to share DATABASE_URL with a new teammate and you get a clean answer. Ask it where the Google service-account JSON goes and the answer gets quiet. Ask where the shared Grafana login goes and it usually points you at a password manager, which is a different product with different roles and no idea what a project is.

That gap is not an edge case. On a small team the non-variable secrets are the ones that leak, because there is nowhere to put them. The .jks gets emailed. The id_ed25519 gets pasted into a DM "just this once". The vendor login lives in a pinned Slack message that survives three offboardings.

This post is about those three things. What they are, why a KEY=value store cannot hold them well, and how Envpilot stores each one next to the variables they belong with.

The three that never fit#

Files. A Google service-account JSON, an Apple .p8 for push, an Android upload keystore, a .p12 certificate chain, an SSH private key for the deploy box. Binary or multi-line, usually with a fixed path the build expects (android/app/upload.jks), and a file mode that matters (0600, or SSH refuses it).

Logins. A username and password for something that never shipped an API. The Grafana dashboard, the Postmark account, the domain registrar, the vendor support portal. Not a token you inject into a process. A pair a human types into a form.

The explanation. The runbook that says which key rotates when, and why STRIPE_SECRET_KEY in staging is a restricted key. Not a secret, but it belongs beside them, and it goes stale when it lives in a wiki two clicks away.

Every tool in this category was designed around the first thing on the list, environment variables, and then asked to hold the rest. You can base64 a keystore into a variable. People do. Then the CI step has to decode it, write it to the right path, chmod it, and remember to delete it. Every one of those steps is written by hand, per project, and one of them is wrong.

Files: a path and a mode, not a string#

A secret file in Envpilot is the bytes plus the two facts that make it reproducible: where it goes, and what mode it gets.

❯terminal
envpilot files add ./service-account.json \
  --path config/service-account.json \
  --env production \
  --mode 0600 \
  --name "GCP service account (prod)"

From then on a fresh clone plus one command materialises everything a build needs:

❯terminal
envpilot files pull -e production

The path is written relative to the project root, the mode is applied on write, and the path is gitignored before the file lands. If a local copy differs from the server, the pull refuses and lists the conflict instead of overwriting someone's debug keystore.

Storage is envelope encryption. Each file is sealed with its own AES-256-GCM key; the ciphertext goes to file storage and the key goes to WorkOS Vault. Neither store alone can read it.

Listing is free and quiet. envpilot files list shows path, size, mode and checksum without decrypting anything. Fetching contents is a different act, and every fetch is written to the audit log against whoever asked.

The same object works from the GitHub Action and the Docker image, so the Android keystore your release workflow needs is the same keystore a developer pulls locally, with the same grant deciding who gets it. There is a full walkthrough in Android keystores in CI.

Logins: a credential with a project, not a vault entry#

A shared account is a username and password pair that belongs to a project and one or more of its environments. Same roles, same per-item grants, same revoke button as a variable.

The difference from a password manager is scope. A Team Lead on checkout-api can see the Grafana login for checkout-api. A Developer sees that the account exists and needs a grant to reveal it. A contractor who is not on the project sees nothing. When someone leaves, removing them from the organization removes the login from their reach, without anyone rotating the Grafana password on their way out.

Every reveal is audited with who and when. Rotating means editing the account; the previous credentials are snapshotted, and if the update fails halfway the old ones are restored so the record and the vault never drift apart.

Machines get read access through the accounts resource on an API key, so an agent or a script can look one up over the REST API or the MCP tool envpilot_list_accounts. Nothing machine-driven can create or rotate a login. That is a human's job by design.

The runbook: docs that live beside the secrets#

Project docs are markdown pages stored on the project. They can be shared with a teammate who is not on the project, or through an expiring public link, and a page is only ever shared once a human has published it.

The agent story is where this earns its place. Over MCP, envpilot_create_doc lets Claude Code or Cursor draft the rotation runbook after it has read the variables. The draft is invisible to everyone, including other agents, until a person holding project.docs.publish reads it and publishes. An agent can explain the secrets. It cannot decide what the team believes about them.

One grant model for all four#

The point of holding all of this in one place is not tidiness. It is that one role decision covers everything.

You need to hand a contractorVariablesFilesLoginsDocs
Read access to one itemGrantGrantGrantShare
Access that expires on FridayYesYesYesYes
A record of every readYesYesYesn/a
Removal on offboardingInstantInstantInstantInstant

Per-item grants carry an optional expiry, so "the contractor needs the staging keystore this week" is a grant that ends itself. Protected environments extend the same idea to writes: a change to a production file or variable becomes a change request a second person approves.

What to do this week#

  1. grep -rl "BEGIN.*PRIVATE KEY\|private_key_id" ~/Downloads ~/Desktop on your own machine. Whatever comes back is a secret file with no owner.
  2. Search Slack for .p12, .jks, .json attachments in your engineering channels. Those are the files that need a path and a mode.
  3. Find the pinned message with the shared logins. Each line in it is a shared account waiting to be created, with a grant list instead of a channel membership.

Variables were the easy part. The .env in Slack was never the whole problem; it was just the part every tool agreed to solve. Envpilot holds all four. Import them once, and stop asking.

Back to all posts