In the world of cryptocurrency, private keys are king. Mathematical mechanics treat correctly signed transactions as valid regardless of intent, which makes account keys and passphrases a particularly lucrative target for malicious actors. While end-user best practices for securing keys tend to be plentiful, the development side of blockchain work takes place amid a sea of API tokens, key files, and service credentials that can sprawl out of control if not managed correctly.
Ensuring that these sensitive values remain secure is critical and outdated solutions such as .env files are no longer sufficient.
This tutorial will start from the basics of interacting with blockchain APIs from joining as a peer upwards. There are numerous third-party services and APIs that sit atop various blockchains, but establishing secure management practices for primitives such as private keys is fundamental.
No prior experience interacting with blockchain or web3 services is assumed, and we'll use a testnet to limit the potential exposure of sensitive information. Doppler will be used to store and retrieve credentials associated with private key material, which is a secure and convenient way to manage secrets without the risks of storing them on disk in unencrypted text files.
Before downloading any programs or generating account keys, there are a few concepts to establish heading into the unique paradigms that accompany working with web3 networks.
While plenty of unique blockchains and products abound in the web3 space, this guide will focus on the Ethereum blockchain, which is one of the more commonly used networks. Like most cryptocurrency blockchains, Ethereum operates in a decentralized way with a vast network of computers that can be interacted with as a peer. However, any sort of operation that requires some proof of ownership, e.g. signing a transaction, does require the authoritative stamp of a private key.
This is the core of how the network operates, which makes the method of communication and management of privileged information a chief concern.
This tutorial will follow a basic outline to work directly with the Ethereum blockchain and its API:
Let's begin!
When choosing how to connect to the Ethereum blockchain, there are two general approaches, each with distinct advantages and disadvantages.
The first is by connecting to a third-party API that serves as a frontend to low-level blockchain network protocols. Services like Infura offer a web-based JSON RPC API, which is widely compatible with a range of libraries and languages and allows developers to interact with the blockchain without downloading the entire ledger, which can take up significant compute resources and disk space (around 500GB at the time of this writing).
Using a third-party API means that your code can run from a variety of environments without being tethered to a running node and the accompanying resource requirements that are necessary. The downside to this approach is reliance upon a third party when one of the core advantages of blockchain technology is decentralisation. Routing communication with the aid of an intermediary party is convenient, but means that clients must trust the provider and are subject to the inherent properties of using a hosted API instead of communicating directly via peer protocols (such as availability and uptime). These tradeoffs are often the right choice, but are tradeoffs nonetheless.
The second approach is to run a native node to communicate to the chosen blockchain directly. As previously mentioned, there are significant computational costs involved, but this means that the client speaks directly to the chosen peer network which brings with it benefits like smaller risk exposure, high availability of the decentralised network, and the ability to self-validate the ledger.
There are additional considerations to take into account when operating a dedicated node, such as choosing whether to download the entire ledger during the initial loading process or instead electing a more lightweight method that incurs a smaller disk usage cost.
This guide will opt for the self-hosted node technique, though in practice, either approach is a reasonable choice.
Finally, a node operator must select which network to connect to when initialising their connection.
Normally, users will choose to connect to the live, "production" chain when interacting with a public ledger, but during local development and testing, a testnet is a better choice. In addition to less traffic (resulting in smaller ledgers and transaction costs), acquiring tokens to send and receive is usually free and does not require any initial funds. This is a significant advantage when sending and receiving tokens without risk of making mistakes that could incur significant consequences.
We'll use geth (Go Ethereum) to connect to the Ethereum network, which is a golang implementation that can function not only as a client but a fully featured node as well.
First, follow the instructions for installing geth for your operating system. Once installed, the geth command should be available from the terminal.
The command-line flags passed to the geth CLI dictate which network it will connect to. There are a number of testnets within the Ethereum network to choose from, and this guide will use the Goerli proof-of-authority network as it’s newer than some of the others, and is not coupled with the computationally-expensive proof-of-work networks.
The --syncmode flag controls the method that geth will use when downloading the historical ledger of the blockchain. On one end of the extreme, the full method will download the entirety of the blockchain ledger which includes all the relevant transactional data. On the other, the light method will only fetch recent block headers and retrieve other information on-demand. When communicating with the production Ethereum network, the full method offers the most comprehensive assurance that the local copy of blockchain data is valid and legitimate. However, in this exploratory case on a testnet, the light method is sufficient.
Begin the geth process in a background terminal on your machine. The initial syncing process will take time, but using the light method on the goerli testnet will not use excessive space (at the time of this writing, the entirety of the chain requires about 500MB of disk space). The following command will store the Goerli chain data in the ~/.goerli directory:
After some time, the daemon will stop importing historical blocks and the latest blocks will be available.
The logging output from geth will include the age of a block when performing initial synchronisation. When you see blocks being imported with output such as age=4m04w1d, this indicates old blocks being retrieved to construct the local chain. Once older blocks stop appearing, this indicates that the initial synchronisation is complete.
You can continue with the rest of this guide while the blockchain synchronises; full synchronisation will only be necessary once we reach the portion that covers transactions.
At the beginning of this guide, we spoke at length about the importance of keys when interacting with the blockchain.
Public-key cryptography forms the backbone of the cryptographic methods for forming and authenticating transactions on public ledgers. Private keys prove ownership of public addresses and sign transactions to send tokens to other public addresses.
While high-level, user-facing cryptocurrency applications may leverage account information for wallets in the form of traditional username/password combinations, the core values that comprise an "account" on a blockchain like Ethereum is a cryptographic key - tokens belong to an addressed coupled to a private key. The geth CLI includes utilities to generate keys, which it calls an account.
To begin, create a new account. Remember that this "account" is really a locally encrypted file and not an account that can be "logged into" from another machine - if you are using an account tied to a production blockchain, it's critical to ensure that this account is secured by a strong passphrase backed up regularly.
In our case, the local account and keypair will be associated with a testnet, but we'll still use best practices for managing the passphrase by storing it in encrypted secrets store (Doppler) and treat the account as sensitive. Because the running geth process is active, we'll connect to the running daemon via attach instead of using the geth account subcommand.
Enter the geth console:
You'll be greeted by the command prompt for geth javascript commands:
To create a new account, run the following command where you'll be asked to enter a passphrase:
After following the prompts, geth will create a keypair within the ~/.goerli/keystore/ directory. You can view the public address of this account at any time by using the following command from the geth console:
At this point, your local machine will have a light copy of the Ethereum Goerli testnet blockchain and a keypair ready to send and receive tokens. Let's begin working with transactions!
A simple task to confirm the functionality of this local testnet setup is to receive currency. Unlike currency on the live, production blockchain, currency on a testnet is often given away freely to permit developers to test and iterate on projects and products.
Providers for free currency - often called "faucets" - may sometimes be unavailable or lacking sufficient funds to distribute tokens freely. At the time of this writing, the faucet mentioned here is functional and has sufficient funds to distribute them to anyone who requests them, but if you encounter problems requesting funds at a later date than when this tutorial was written, you may also seek out alternate sources (remember that they must be operating on the Goerli Ethernet testnet).
We’ll be using the Goerli Faucet to acquire initial funds to begin experimenting with the blockchain.
From the geth console, find your public address using listAccounts:
Copy the address, navigate to the Goerli Faucet, paste your address into the Wallet Address field, then click Send Me ETH.
Blocks are minted frequently on this chain, so it's time to check your balance! We'll use the Web3 Python library to interact with the local node.
This guide assumes that you have Python 3 installed on your local machine. We’ve provided a repository with sample code as a quick start to getting up to speed with how to use the Web3 library.
Clone the GitHub repository then enter the repository directory:
Create a Python virtual environment and install the dependencies:
Feel free to open web3-app.py and take a look! The Python web3 library makes accessing blockchain information fairly straightforward, so this example code should be a good place to start if you want to experiment later.
Let’s verify the script is working by fetching the timestamp of the latest block on the chain:
If you see a timestamp that is within 30 seconds or so, congratulations! You've successfully interacted with your local instance of the testnet blockchain. The next task is to check the balance of the address that your faucet tokens were sent to.
First, confirm that Python is able to see the local account:
Your account number should match the address that you previously found within the geth console using personal.listAccounts. Finally, check the account balance.
The account balance for the address entered in the Ethereum faucet site should be 0.05 ETH. You've checked your account balance from the python web3 library. Well done!
Until now we've only interacted with the local blockchain with APIs that do not require private account keys. In order to finish this experiment and successfully sign a transaction with your account, we'll practice by sending an amount of ETH back to the Goerli faucet. Doing so will help demonstrate the process as well as replenish funds to future developers on the blockchain!
To send ETH, we’ll use the send_transaction method:
Remember when we created our local Geth account and provided the passphrase?
The passphrase is required here to unlock the private key and sign the transaction in order to validate it as a legitimate transfer of funds out of our account. However, entering a passphrase into a REPL or a shell is a security concern: most REPL and shell histories (like bash or the python interpreter) retain commands that are entered, so it's wise to avoid entering a passphrase manually in plaintext.
What other options exist? One approach is to read secret values - whether it be a passphrase, API key, or otherwise from a file on disk such as .env or JS file but this is a dangerous option as we’ll explain.
Storing secrets in plaintext files is a huge security nightmare are as they can be read by other user processes, and risk being committed during work with tools like git if a developer makes a mistake such as Nat Eliason did when he lost $30,000 by accidentally pushing a file containing his passphrase to GitHub.
Reading the value from an environment variable is a good idea, but that begs the question of how the environment variable containing the passphrase will be securely populated.
This is where a SecretOps platform such as Doppler can help by using the Doppler CLI to inject secrets as environment variables into an application process. This allows an environment variable to be populated with the passphrase securely without the risks of unencrypted local file storage.
To begin, first sign up for a Doppler account. The sign up process will walk through a few helpful tips to get started, and once your account has been configured, proceed to install the Doppler CLI, then authenticate your machine by running:
Create a new project called python-web3 which will hold the secrets for this project and keep things organised as well as within their own environment scope:
Ensure that you run Doppler setup in the python-web3 directory as the Doppler CLI scopes secrets access to specific directories. Eventually, you should have a setup similar to the one below: with your project directory coupled to the python-web3 project in Doppler.
Run the following command to check you can access secrets from the python-web3 directory:
The next step is to store the keychain passphrase in Doppler where it can be retrieved by the doppler CLI.
Open the python-web3 Doppler Project and create a ETH_PASSPHRASEsecret with your passphrase value, then click Save.
Heading back to the terminal, can verify Doppler saved your passphrase successfully by running:
With the passphrase managed by Doppler, we can now run the python script with secrets populated by the doppler run command.
You may inspect the accompanying python code from the cloned repository, but the key lines are those that retrieve your passphrase from the ETH_PASSPHRASE environment variable.
When you invoke the doppler run command, Doppler will set the ETH_PASSPHRASE environment variable for python to use and sign the transaction with the wallet information from the unlocked key. Although the noted address is that of the faucet you originally received your balance from, you may choose an arbitrary amount to send with your command (as long as you have sufficient funds!).
You can find a random account to send funds to from the Goerli top accounts page. Copy one of the “to “ addresses (starting with 0x) and replace {ADDRESS} in the command below to begin the funds transfer:
The transaction once complete will return a unique transaction hash like the following:
Your transaction has been submitted to the blockchain! At this point the running geth light node will broadcast this transaction and the transfer of funds will propagate through the network. You may use a few approaches to view the transaction details.
The provided python script supports a command to introspect an arbitrary transaction ID – for example, the following command will display the transaction details of the aforementioned transaction ID:
In addition to viewing details from a local client, using another method to introspect the public ledger is a good idea in order to confirm that the transaction is visible from any client.
For example, you can either use a generally-available web-based block explorer to view transactions associated with a specific address (replace the given hash with yours that is under the variable in your python repl as my_account):
Or you may alternatively view the transaction directly by entering your transaction hash into a URL like the following:
In either case you should be able to see the transaction data including the amount and associated gas price.
Congratulations! You just created a transaction on an Ethereum blockchain!
This guide has covered one of the most basic operations when working with a blockchain - signing and broadcasting a transaction - using Doppler to manage your passphrase securely so crypto keys and passphrases aren’t inadvertently exposed along with any other sensitive account credentials.
Recall that in this tutorial, we generated a keychain using the geth CLI so the associated public and private key exist only on the local machine.
To write code that could potentially run on any host with a keychain stored remotely, you could elect to store key material in Doppler and instantiate a local keychain based upon the retrieval of that information from a similar Doppler secret.
The web3 library provides the web3.geth.personal.import_raw_key method which accepts a private key and passphrase so that your transaction operations can occur on any machine with a properly setup Doppler environment. The local blockchain can be used for more experiments by virtue of the flexibility of the testnet environment, so you can continue to receive currency from a Goerli testnet faucet and send funds to other addresses.
Secrets Management and rotation automated for developers like you >
For additional information, these resources provide more documentation and information about the libraries and tools used in this guide:
Trusted by the world’s best DevOps and security teams. Doppler is the secrets manager developers love.