/ Writing · · Updated · 6 min read
Why you should never store private keys in .env files
By Oussama Allamou · Foundry · Security · Private Keys · Keystore
A private key in a .env file is stored in plain text, and anything that can read the file can sign as you: a mistaken git commit, a dependency's install script, a backup. Foundry can keep the same key in an encrypted keystore instead. You import it once with cast wallet import, and from then on you deploy with --account and type a password. The key is never written to disk unencrypted.
Walk through both setups. Use the buttons, the arrow keys, or tap the diagram.
- Step 01The key sits in plain text
A .env file is a text file. PRIVATE_KEY=0x... is readable by every program that runs as your user, and by everyone who ever gets a copy of the folder.
- Step 02It leaks sideways
Nobody has to break in. One git add without a .gitignore entry, one dependency whose install script reads files, one backup or synced folder, and the key is somewhere you do not control.
- Step 03Import the key once
cast wallet import deployer --interactive asks for the private key and a password with hidden input. Nothing lands in your shell history and nothing is written to the project folder.
- Step 04Foundry encrypts it
The password goes through a deliberately slow key derivation function, and the result encrypts the private key. Only the ciphertext is saved, under ~/.foundry/keystores.
- Step 05Sign at deploy time
forge script --account deployer asks for the password, decrypts the key in memory, signs the transactions and exits. The key never touches the disk in plain text.
- Step 06The same leaks, now harmless
If the keystore file gets committed, copied or backed up, whoever finds it holds ciphertext. Without the password it does not sign anything. The password is now the secret, so make it long.
Why is a private key in .env a problem?
The file is plain text. The operating system protects it exactly as much as it protects your README. That makes every ordinary way files travel a way for the key to travel:
- Git. One
git add .in a repository where.envis not ignored, and the key is in the history. Deleting the file in a later commit does not remove it from earlier ones. - Dependencies. Install scripts and build tools run as your user. They can read any file you can.
- Copies you forgot. Cloud-synced folders, backups, zipped project folders sent to a colleague, a screen share with the editor open.
- Tools that read your project. Anything that indexes or uploads the working directory picks up
.envwith the rest.
None of these need an attacker to target you. They only need the key to be readable, and it is.
How do you store a key in Foundry's encrypted keystore?
Here is the whole session. Run it one command at a time.
cast wallet import deployer --interactive
Enter private key:
Enter password:
`deployer` keystore was saved successfully. Address: 0x…
cast wallet list
deployer (Local)
cat ~/.foundry/keystores/deployer
{ "crypto": { "cipher": "aes-128-ctr", "ciphertext": "…", "kdf": "scrypt", … }, "version": 3 }
forge script script/Deploy.s.sol --rpc-url $RPC_URL --broadcast --account deployer
Enter keystore password:
- Command 01Import the key
deployer is a name you choose. Both prompts hide what you type, so the key never appears on screen or in your shell history.
- Command 02Check it is there
The account is listed by name. Nothing about the key itself is printed.
- Command 03Look at what is on disk
This is everything a thief gets from the file: a cipher name, key derivation parameters and ciphertext. Output shortened here.
- Command 04Deploy with the account
You pass the account name, not a key. Foundry asks for the password, signs in memory and exits.
Run this in a terminal:
cast wallet import deployer --interactive
deployer is a name you choose. The command asks for the private key and then for a password. Both prompts hide what you type, so the key is not in your shell history. Foundry encrypts the key and writes the result to ~/.foundry/keystores/deployer.
Check that it is there:
cast wallet list
Then delete the key from your .env.
How do you deploy with it?
Pass the account name instead of a key:
forge script script/Deploy.s.sol --rpc-url $RPC_URL --broadcast --account deployer --sender <address of the key>
Foundry asks for the password, decrypts the key in memory, signs and broadcasts, and exits. The .env file can still hold things that are not secrets worth a wallet, like an RPC URL.
Keeping the deployer key safe is one half. The other half is what the deployed contract lets that key do, and whether its checks hold. See assert vs require for how I separate input checks from invariants.
What is actually in the keystore file?
A JSON file with the encrypted key and the parameters needed to decrypt it. Your password is fed through a key derivation function that is slow on purpose, which makes guessing passwords expensive. The derived key encrypts the private key. The file contains the ciphertext, never the key.
A correction to the earlier version of this post, which called this format ERC-2335. Foundry writes the Web3 Secret Storage format, the same JSON keystore geth uses for account keys. ERC-2335 is a closely related keystore standard built on the same idea, defined for the BLS keys that validators use. The principle in both is identical: password in, slow key derivation, encrypted key on disk.
What does this not protect against?
- A weak password. Someone holding the keystore file can try passwords offline for as long as they like. A long passphrase is what makes that hopeless.
- A compromised machine. Malware that records your keystrokes gets the password when you type it.
- Keys that already leaked. Encrypting a key that was pushed to a repository does not un-leak it. Generate a new one and move the funds.
- High-value keys. For a deployer that owns contracts with real money in them, use a hardware wallet (
--ledgeror--trezor) and a multisig as the owner. The keystore is the floor, not the ceiling.
What do the terms mean?
- Private key: the secret number that signs transactions for an address. Whoever has it controls the address.
- .env file: a plain text file of
NAME=valuelines that tools load into environment variables. - Keystore: a file that holds a private key encrypted with a password.
- Key derivation function (KDF): a function that turns a password into an encryption key and is deliberately slow, to make guessing costly.
- cast / forge: Foundry's command line tools for talking to a chain and for building, testing and deploying contracts.
FAQ
Is .env safe if it is in .gitignore?
It is safer than not ignoring it, and still plain text. .gitignore only stops git. It does nothing about dependencies, backups, synced folders or any other program running as your user.
Where does Foundry store the encrypted key?
In your home directory under .foundry/keystores, one file per account name. It is outside the project folder, so it does not travel with the repository.
What happens if I forget the keystore password?
The keystore cannot be decrypted without it and there is no reset. Keep the original key or seed phrase backed up offline, and you can import it again with a new password.
I already pushed a private key to GitHub. What now?
Treat it as public. Move everything out of that address to a fresh one right away and stop using it. Rewriting git history does not help, because the key may already have been copied.
Can I use this in CI?
A password prompt does not fit an unattended pipeline. For CI, use the platform's secret store with a key that holds only what a deployment needs, or keep signing out of CI and have a person or a multisig approve the transaction.
Further reading
- Foundry Book: reference for
cast walletandforge script. - ERC-2335: BLS12-381 keystore