0xMowgli.

/ 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.

.env FILEPRIVATE_KEY=0x… · PLAIN TEXT ON DISKGIT PUSHDEPENDENCYBACKUPSCAST WALLET IMPORTPASSWORDENCRYPTED KEYSTORE~/.foundry/keystores/deployer · CIPHERTEXT ONLYFORGE --ACCOUNTSIGNED TXTHE FIXKEYKDFNO PASSWORD, NO SIGNATURE
Explainer · From .env to an encrypted keystore
  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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 .env is 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 .env with 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:

Explainer · Moving a key into the keystore
  1. 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.

  2. Command 02Check it is there

    The account is listed by name. Nothing about the key itself is printed.

  3. 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.

  4. 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 (--ledger or --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=value lines 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