How to share API keys and .env files securely
For developers: scoped, short-lived keys, secret managers (Vault, AWS Secrets Manager, 1Password CLI, Doppler), when a one-time link fits, and rotating safely.
On this page
The best way to share an API key securely is not to share it. Give each person and each service its own key, scoped to what it needs and set to expire. When a value really has to be shared, such as a database password or a third-party key with one per account, keep it in a secret manager and grant people access to the manager, not to the value.
A one-time link is the right tool for the gaps: bootstrapping the first credential that opens the secret manager, or a one-off handover to someone outside your systems. Git, tickets and chat are never the right tool, and “I’ll just send you my .env” is how keys end up in all three.
Give each person and service its own key
A key that belongs to one person or one service can be revoked without breaking anyone else, and every call it makes is attributable. A shared key is a password with no audit trail.
-
Scope it. Stripe’s restricted keys limit a key to the resources it needs. GitHub’s fine-grained personal access tokens are limited to chosen repositories and permissions, and can be set to expire. AWS IAM policies should name actions and resources rather than
*. ShareShield’s own API keys work the same way: scoped (a CI job that only sends secrets needs onlysecrets:create), bound to one organisation, and able to expire after 1 to 365 days. -
Make it short-lived. The safest credential is one that’s useless by tomorrow. For AWS, use IAM Identity Center instead of long-lived access keys:
aws sso login --profile staging aws sts get-caller-identity --profile staging -
Let CI fetch its own credentials. GitHub Actions can authenticate to AWS, Azure and Google Cloud with OpenID Connect, so there’s no cloud key stored in the repository’s secrets at all:
permissions: id-token: write contents: read steps: - uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::123456789012:role/deploy-staging aws-region: eu-west-2 -
Use dynamic secrets where you can. Vault’s database secrets engine creates a database user per request with its own time to live, so nobody shares the database password because nobody has it.
Keep shared values in a secret manager
For the values that do have to be shared, “sharing” should mean adding someone to a policy, a vault or a project. Removing them later is then one change, and the value never travels.
| Tool | Good fit | How you give someone access |
|---|---|---|
| HashiCorp Vault | Self-hosted, many teams, dynamic secrets | A policy on the secret’s path, attached to their identity |
| AWS Secrets Manager | Workloads already on AWS, managed rotation | An IAM policy allowing secretsmanager:GetSecretValue on the secret |
| 1Password (with the CLI) | Small teams who already use 1Password for everything else | Add them to the vault |
| Doppler | Per-environment config for apps, synced to hosting platforms | Add them to the project, with access to the environments they need |
Writing a value in and reading it back, without it ending up in your shell history:
# Vault (KV v2). "api_key=-" reads the value from stdin.
vault kv put secret/payments/stripe api_key=-
vault kv get -field=api_key secret/payments/stripe
# AWS Secrets Manager. Write the JSON to a file, upload it, delete the file.
aws secretsmanager create-secret --name payments/stripe \
--secret-string file://stripe.json
aws secretsmanager get-secret-value --secret-id payments/stripe \
--query SecretString --output text
# 1Password CLI
op read "op://Payments/stripe/credential"
# Doppler
doppler secrets get STRIPE_API_KEY --plain
Share a .env file without sharing the values
The .env file itself is the problem: a plain-text file of every secret an app uses, which people copy between laptops, paste into chat and commit by accident. Replace it with something that’s safe to share.
-
Commit a
.env.examplewith every variable name and no values, and keep.envin.gitignore. -
With 1Password, put references in the
.envinstead of values. The file then contains nothing secret and can be shared or even committed.op runresolves the references when the app starts, for whoever is signed in and has access to the vault:# .env STRIPE_API_KEY=op://Payments/stripe/credential DATABASE_URL=op://Payments/staging-db/url op run --env-file=.env -- npm run dev -
With Doppler, drop the file.
doppler setuplinks the directory to a project and environment, anddoppler run -- npm run devinjects the variables at start-up. -
With Vault, Vault Agent can render a
.envor config file from a template on the machine that needs it, so the file is produced in place rather than sent.
When a one-time link is the right tool
Every secret manager has a first credential: the token, key or password that opens it. And sometimes the recipient simply isn’t in your systems: a vendor’s support engineer who needs a key for a day, or a freelancer’s laptop that needs a working .env this afternoon. For those, a link that opens once and expires is the honest answer.
-
Vault has its own one-time mechanism. Response wrapping returns a single-use token instead of the secret. You send the token; they unwrap it. If their unwrap fails because the token has already been used, someone else got there first, and you know.
vault kv get -wrap-ttl=30m secret/payments/stripe # prints a wrapping_token vault unwrap <wrapping_token> # run by the recipient -
Vault unseal keys can be encrypted to each holder’s PGP key at
vault operator initwith-pgp-keys, which is better than sending them at all. -
For everything else, use a one-time link. Set one view, an expiry of hours rather than days, and if you can, lock it to the recipient’s email address. From a script, ShareShield’s API does it in one call:
jq -n --rawfile content .env.staging '{content: $content, expiryHours: 4, maxViews: 1}' \ | curl -sS --fail-with-body -X POST "https://app.shareshield.net/api/v2/secrets" \ -H "x-api-key: $SHARESHIELD_API_KEY" \ -H "content-type: application/json" \ --data @- \ | jq -r .urlThe share link is as sensitive as the secret until it’s opened, so keep it out of CI logs. For a certificate or key file rather than text, file secrets send the file itself. How to send a password securely covers the rest of the routine: passcodes, confirming receipt, and what to do if the link has already been opened.
If you hand a key to someone outside your systems, create a new one for them and revoke it when they’re done. Don’t send them yours.
Never in git, tickets or build output
-
Git. Once a key has been pushed, assume it’s compromised: rewriting history with
git filter-repodoesn’t reach existing clones, forks, CI caches or the provider’s own scanning. Rotate the key first, then clean up. To stop tracking a committed.env:echo ".env" >> .gitignore git rm --cached .env git commit -m "Stop tracking .env"Catch it before it happens with a pre-commit scan (
gitleaks git -vscans the repository’s history) and GitHub’s push protection, which is free on public repositories and part of GitHub Secret Protection for private ones. -
Tickets and chat. Jira and Linear comments are emailed to watchers and indexed for everyone on the project. A key pasted “just for a minute” stays for years.
-
CI logs. GitHub Actions masks registered secrets, but not values derived from them. Mask anything you compute with
echo "::add-mask::$TOKEN", and neverset -xin a step that handles secrets. -
Container images. A secret passed as a build
ARGorENVis visible in the image’s layers anddocker history. Use BuildKit secret mounts instead:RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ciand build with
docker build --secret id=npmrc,src=$HOME/.npmrc .
Rotate without breaking production
Rotate when something happens: a key appears anywhere it shouldn’t, someone with access leaves, or a vendor reports a breach. A rotation schedule nobody keeps is worse than none, because it gives false comfort.
- Create the new key alongside the old one. Most providers allow two at once: AWS IAM users can hold two access keys, and Stripe’s “roll key” lets the old key keep working for a period you choose.
- Deploy the new key through your secret manager, so every consumer picks it up.
- Watch for the old key still being used. AWS shows each access key’s last use; most providers have something similar.
- Revoke the old key once nothing uses it.
Where the provider supports it, automate the whole loop: AWS Secrets Manager can rotate RDS credentials on a schedule, and aws secretsmanager rotate-secret --secret-id payments/db runs one on demand. When someone leaves, offboarding shared credentials has the order to work through.
Send the next one as a link that expires.
ShareShield turns a password, key or file into a link that opens once and is then deleted. You can send a text secret without an account.
