Deploy a Hardhat contract to Sepolia with Google Cloud KMS
In this tutorial you create a Hardhat project, create a signing key in Google Cloud KMS, deploy a contract to Sepolia from that key and verify its source on block explorers. The private key never leaves Cloud KMS: Hardhat asks Cloud KMS for a signature each time it sends a transaction.
You need:
- Node.js 22.13.0 or later (see supported Node.js versions), and npm, pnpm or Yarn.
- A POSIX shell, such as bash or zsh; on Windows, use WSL. Step 3 also uses
curlto show which account the plugin signs in as. - The gcloud CLI, signed in with an identity that can create Cloud KMS keys and grant roles on them, and a project set:
gcloud config get-value projectprints it. The project Owner role is enough, and so is Cloud KMS Admin; Cloud KMS Admin cannot read a public key or sign, so step 3 grants the two roles that can. The project needs billing and the Cloud KMS API turned on;gcloud services enable cloudkms.googleapis.comturns the API on. - Application Default Credentials: run
gcloud auth application-default loginonce. The plugin signs in with these, not with the gcloud CLI's own sign-in, and the two can be different accounts; step 3 shows how to see which one the plugin uses. - A Sepolia RPC URL. The examples use the public
https://ethereum-sepolia-rpc.publicnode.com; a provider URL with an API key works too. - About 0.01 Sepolia ETH, from a faucet or another account.
NOTE
Audience: developers who have a Google Cloud project and the gcloud CLI signed in, and have not used Cloud KMS with Hardhat. It takes about 15 minutes, plus the time it takes to get Sepolia ETH.
This tutorial was followed from an empty directory on 2026-10-02, at commit ab3ee2a, with Hardhat 3.18.1 and @nomicfoundation/hardhat-verify 3.1.2. The commands took about 6 minutes, without the wait for Sepolia ETH. The run kept the 30-day default destroy schedule; the key-loss check covers the 24-hour schedule of step 2.
1. Create a Hardhat project with the plugin
Create a project from Hardhat's viem template in an empty directory:
mkdir kms-tutorial
cd kms-tutorial
npx --yes hardhat@latest --init --template node-test-runner-viemmkdir kms-tutorial
cd kms-tutorial
pnpm dlx hardhat@latest --init --template node-test-runner-viemmkdir kms-tutorial
cd kms-tutorial
yarn init -2
printf 'nodeLinker: node-modules\napprovedGitRepositories:\n - "https://github.com/foundry-rs/forge-std.git"\n' >> .yarnrc.yml
yarn dlx hardhat@latest --init --template node-test-runner-viem--yes lets npx download Hardhat without asking first; pnpm dlx and yarn dlx do not ask.
With Yarn, yarn init -2 pins Yarn 4 in package.json, so the template's install runs with Yarn 4 rather than Yarn 1. Hardhat does not run under Yarn 4's default Plug'n'Play linker, and the template installs forge-std from GitHub, which Yarn 4 refuses unless the repository is approved.
The template has a Counter contract, the Ignition module ignition/modules/Counter.ts that deploys it, and a sepolia network. It also installs @nomicfoundation/hardhat-verify, which verifies contracts on block explorers.
Install the core plugin, hardhat-kms, and the Google Cloud provider, @hardhat-kms/gcp, with the package manager you created the project with:
npm install --save-dev hardhat-kms @hardhat-kms/gcppnpm add --save-dev hardhat-kms @hardhat-kms/gcpyarn add --dev hardhat-kms @hardhat-kms/gcpIf the install stops with ERR_PNPM_IGNORED_BUILDS from pnpm or YN0016 from Yarn, If the install stops gives the fix for each package manager.
Register the provider: in hardhat.config.ts, import it and add it to plugins. It loads hardhat-kms itself. The rest of the file stays as the template made it:
import hardhatKmsGcp from "@hardhat-kms/gcp";
import hardhatToolboxViemPlugin from "@nomicfoundation/hardhat-toolbox-viem";
import { configVariable, defineConfig } from "hardhat/config";
export default defineConfig({
plugins: [hardhatToolboxViemPlugin, hardhatKmsGcp],
// The rest of the template's config, unchanged.
});Check that Hardhat finds the plugin:
npx hardhat kms --helppnpm hardhat kms --helpyarn hardhat kms --helpIt lists the kms tasks, such as kms accounts and kms address. If it prints Error HHE404: Task "kms" not found instead, hardhatKmsGcp is missing from plugins. You have created nothing in Google Cloud yet, so an install problem costs nothing to fix.
2. Create a key in Cloud KMS
Cloud KMS offers the secp256k1 curve only at protection level HSM, and an HSM key version costs about $2.50 a month ($0.003424658 an hour). Google bills it while the version is enabled, disabled or scheduled for destruction; only a destroyed version is free. Signing and reading the public key cost at most $0.15 per 10,000 calls. The free tier covers only keys made by Cloud KMS Autokey, so not this one. See Cloud KMS pricing. Step 8 schedules the version for destruction, and the 24 hours it waits are billed too, so the key costs about $0.10 in all.
Set the project and a location for the key. The examples use us-east1; any Cloud KMS region works. The project's config reads both variables in step 4:
export GCP_PROJECT_ID=$(gcloud config get-value project)
export GCP_LOCATION=us-east1Create a key ring, then a secp256k1 signing key in it, which Cloud KMS calls EC_SIGN_SECP256K1_SHA256:
gcloud kms keyrings create hardhat-kms-tutorial --location "$GCP_LOCATION"
gcloud kms keys create deployer \
--keyring hardhat-kms-tutorial \
--location "$GCP_LOCATION" \
--purpose asymmetric-signing \
--default-algorithm ec-sign-secp256k1-sha256 \
--protection-level hsm \
--destroy-scheduled-duration 24hThe key gets version 1, which the project signs with. At protection level software, the same command fails with ALGORITHM_NOT_SUPPORTED_FOR_PROTECTION_LEVEL.
A key ring and a key cannot be deleted, so their names stay in the project; they cost nothing. If you ran this tutorial before, the key ring exists already: skip its create, and give the key another name, here and in the rest of the page.
--destroy-scheduled-duration 24h sets the shortest wait Cloud KMS allows between scheduling a version's destruction and destroying it. The duration is fixed when the key is created. A short wait keeps the cost of this tutorial down, but it leaves you one day to undo the destruction in step 8. Give a key that will hold value a long duration, up to 120 days; Prevent and recover from losing a key explains why. If an organization policy, constraints/cloudkms.minimumDestroyScheduledDuration, refuses 24h, use the smallest value it allows.
3. Allow the key to sign, and nothing else
Hardhat signs as the identity of your Application Default Credentials (ADC), not as the account the gcloud CLI uses. gcloud auth login signs in the gcloud CLI, which runs the gcloud commands on this page. gcloud auth application-default login writes separate credentials, which Google's client libraries use, and so does the plugin (How Application Default Credentials works). Both can be the same account, but they do not have to be.
The ADC identity needs two permissions on this key: cloudkms.cryptoKeyVersions.viewPublicKey, to derive the address, and cloudkms.cryptoKeyVersions.useToSign, to sign. Creating the key does not give them. Cloud KMS Admin has neither: it "provides access to Cloud KMS resources, except for access to restricted resource types and cryptographic operations" (Cloud KMS permissions and roles). The project Owner role has both. The 0.10.1 acceptance run on 2026-10-10 signed and deployed with only the two key-scoped roles below, through impersonated ADC without a quota project (Set up a Google Cloud KMS key).
The predefined roles roles/cloudkms.publicKeyViewer and roles/cloudkms.signer hold one each. Neither can disable, destroy or restore a key version, nor change who has access; Optional: inspect the roles and the key's access lists what they hold, and Optional: give a deployer only the sign roles signs as a service account that holds these two roles and nothing else.
Find the ADC identity. First check that the gcloud CLI does not impersonate a service account:
gcloud config get auth/impersonate_service_accountIf it prints an account, note it, and run gcloud config unset auth/impersonate_service_account. That setting changes which token the next command gets, but the plugin does not read gcloud settings, so the check would name the wrong identity.
Then get an access token from your ADC and ask Google's token information endpoint whose it is. The token goes in the request body and is not printed:
curl -s -d "access_token=$(gcloud auth application-default print-access-token \
--scopes=https://www.googleapis.com/auth/cloud-platform,https://www.googleapis.com/auth/userinfo.email)" \
https://www.googleapis.com/oauth2/v1/tokeninfoThe email field of the response is the identity. The command asks for the userinfo.email scope because the response has email only when the token has that scope (Token types). With credentials from gcloud auth application-default login, the email is the account you chose in the browser. If it ends in .gserviceaccount.com, ADC holds a service account: the one you named with gcloud auth application-default login --impersonate-service-account, or the one whose key file GOOGLE_APPLICATION_CREDENTIALS names.
If the response still has no email, ADC holds a service account, and the azp field is its unique ID. Print its email with gcloud iam service-accounts describe <azp> --format='value(email)'.
Set DEPLOYER to that identity, with user: before a person's email and serviceAccount: before a service account's:
DEPLOYER="user:<email>"For a service account named hardhat-deployer, it is DEPLOYER="serviceAccount:hardhat-deployer@$GCP_PROJECT_ID.iam.gserviceaccount.com".
If you unset auth/impersonate_service_account above and your gcloud commands rely on it, set it again with the account you noted: gcloud config set auth/impersonate_service_account <account>.
Grant both roles on the key, not on the project, so the identity can use no other key. The gcloud CLI account runs the grant, which needs cloudkms.cryptoKeys.setIamPolicy; Owner and Cloud KMS Admin hold it:
for role in roles/cloudkms.publicKeyViewer roles/cloudkms.signer; do
gcloud kms keys add-iam-policy-binding deployer \
--keyring hardhat-kms-tutorial \
--location "$GCP_LOCATION" \
--member "$DEPLOYER" \
--role "$role"
doneA new grant typically takes 2 minutes to apply, and can take 7 minutes or longer (Access change propagation). If step 4 fails with permission denied (PERMISSION_DENIED) soon after the grant, wait and run it again.
If the deployer then gets PERMISSION_DENIED about serviceusage.services.use, look at its quota project. If the ADC account has serviceusage.services.use on the gcloud CLI's project, gcloud auth application-default login writes that project into the credentials as the quota project; without the permission, it writes none and says so (gcloud auth application-default login). The client then names the quota project in each request (the x-goog-user-project header), which needs serviceusage.services.use on it. Either set a project the deployer may use with gcloud auth application-default set-quota-project <project>, sign in again with gcloud auth application-default login --disable-quota-project, or grant the deployer roles/serviceusage.serviceUsageConsumer on the project (Set the quota project).
4. Add the key to the project
Replace hardhat.config.ts with the file below. Compared with the template, it keeps hardhatKmsGcp in plugins from step 1, adds a kms section with the key, gives the sepolia network kmsAccounts instead of accounts, so no private key is in the project, sets the network's chainId and turns Etherscan verification off. The key's project and location come from the variables you set in step 2, so the project ID stays out of the file:
import hardhatKmsGcp from "@hardhat-kms/gcp";
import hardhatToolboxViemPlugin from "@nomicfoundation/hardhat-toolbox-viem";
import { configVariable, defineConfig } from "hardhat/config";
export default defineConfig({
plugins: [hardhatToolboxViemPlugin, hardhatKmsGcp],
solidity: {
profiles: {
default: {
version: "0.8.34",
},
production: {
version: "0.8.34",
settings: {
optimizer: {
enabled: true,
runs: 200,
},
},
},
},
},
verify: {
etherscan: {
enabled: false,
},
},
kms: {
keys: {
deployer: {
provider: "gcp",
projectId: configVariable("GCP_PROJECT_ID"),
location: configVariable("GCP_LOCATION"),
keyRing: "hardhat-kms-tutorial",
keyName: "deployer",
keyVersion: "1",
},
},
},
networks: {
hardhatMainnet: {
type: "edr-simulated",
chainType: "l1",
},
hardhatOp: {
type: "edr-simulated",
chainType: "op",
},
sepolia: {
type: "http",
chainType: "l1",
chainId: 11155111,
url: configVariable("SEPOLIA_RPC_URL"),
kmsAccounts: ["deployer"],
},
},
});Etherscan needs an API key, and without one its verification fails. To verify on Etherscan too, get a key from Etherscan, store it with npx hardhat keystore set ETHERSCAN_API_KEY or export ETHERSCAN_API_KEY=…, and replace enabled: false with apiKey: configVariable("ETHERSCAN_API_KEY").
chainId: 11155111 is Sepolia's chain id. Before the plugin signs a transaction, it asks the node for its chain; if SEPOLIA_RPC_URL points at another chain, it stops with core.chain.mismatch and signs nothing.
Every command that uses the key needs GCP_PROJECT_ID and GCP_LOCATION. Run them in the same shell as step 2, or export the two variables again first.
Set the RPC URL, then ask Cloud KMS for the key's address:
export SEPOLIA_RPC_URL=https://ethereum-sepolia-rpc.publicnode.com
DEPLOYER_ADDRESS=$(npx hardhat kms address deployer)
echo "$DEPLOYER_ADDRESS"export SEPOLIA_RPC_URL=https://ethereum-sepolia-rpc.publicnode.com
DEPLOYER_ADDRESS=$(pnpm hardhat kms address deployer)
echo "$DEPLOYER_ADDRESS"export SEPOLIA_RPC_URL=https://ethereum-sepolia-rpc.publicnode.com
DEPLOYER_ADDRESS=$(yarn hardhat kms address deployer)
echo "$DEPLOYER_ADDRESS"It prints the key's address, 0x and 40 hex digits. Steps 5 and 6 use DEPLOYER_ADDRESS; in a new shell, run the same command again first. Pin the address: in hardhat.config.ts, add an address line to the deployer key, with the address in place of <deployer address>. The rest of the file stays the same:
kms: {
keys: {
deployer: {
provider: "gcp",
projectId: configVariable("GCP_PROJECT_ID"),
location: configVariable("GCP_LOCATION"),
keyRing: "hardhat-kms-tutorial",
keyName: "deployer",
keyVersion: "1",
address: "<deployer address>",
},
},
},With the pin, the plugin refuses to sign if the variables ever name another project, location or key with the same names. Rotate a key and pin its address explains why.
Before you send funds to the address, check that your credentials may sign with the key:
npx hardhat kms accounts --check-signpnpm hardhat kms accounts --check-signyarn hardhat kms accounts --check-signIt prints a table with one row for the key, with your address in place of <deployer address>:
NAME PROVIDER SOURCE ADDRESS PIN SIGN KEY ID
deployer gcp kms.keys <deployer address> matches ok gcp:projects/<GCP_PROJECT_ID>/locations/<GCP_LOCATION>/keyRings/hardhat-kms-tutorial/cryptoKeys/deployer/cryptoKeyVersions/1matches under PIN and ok under SIGN prove that your credentials may sign with the key and that its signatures recover to the pinned address. The check signs a random message, not a transaction, so it needs no funds. On FAILED, read the error: line under the row and fix the cause before step 5.
5. Fund the address
Send about 0.01 Sepolia ETH to the deployer address, from a Sepolia faucet or from another account. Deploying the Counter contract and calling it costs much less than that on Sepolia.
The address belongs to the key version alone. If the version is destroyed, any funds and contract roles at the address are lost for good, so step 8 sends the funds back before it destroys the version.
To see the balance, open the address on Sepolia Etherscan or Sepolia Blockscout.
To try the deployment on a local fork of Sepolia before you fund the address, add the sepoliaFork network and kms.simulatedBalance from Rehearse on a simulated network, then run step 6's command with --network sepoliaFork and without --verify. The rehearsal signs with the real key, so it also checks the key and its permissions. Ignition does not ask you to confirm, and it starts with "You are running Hardhat Ignition against an in-process instance of Hardhat Network": that is the fork, and the results are lost when the command ends. It ends with a Deployed Addresses list, as in step 6.
6. Deploy and verify
Deploy the module from the deployer address, and verify the contract, in one command:
npx hardhat ignition deploy ignition/modules/Counter.ts --network sepolia --verify --default-sender "$DEPLOYER_ADDRESS"pnpm hardhat ignition deploy ignition/modules/Counter.ts --network sepolia --verify --default-sender "$DEPLOYER_ADDRESS"yarn hardhat ignition deploy ignition/modules/Counter.ts --network sepolia --verify --default-sender "$DEPLOYER_ADDRESS"Ignition asks you to confirm the network; answer y. Each transaction usually costs one Cloud KMS AsymmetricSign call; retries can add more. Naming the sender by its address keeps the deployer the same when you add accounts or keys to the network later; Choose the sender by address explains why.
Ignition deploys Counter, calls incBy(5), then verifies the contract on Blockscout and Sourcify. The end of the output looks like this:
[ CounterModule ] successfully deployed 🚀
Deployed Addresses
CounterModule#Counter - 0xEFa2D146dC54546358157D816443Cd68C31DF95A
Verifying deployed contracts
Verifying contract "contracts/Counter.sol:Counter" for network sepolia...
=== Blockscout ===
The contract at 0xEFa2D146dC54546358157D816443Cd68C31DF95A has already been verified on Blockscout.
If you need to verify a partially verified contract, please use the --force flag.
Explorer: https://eth-sepolia.blockscout.com/address/0xEFa2D146dC54546358157D816443Cd68C31DF95A#code
=== Sourcify ===
The contract at 0xEFa2D146dC54546358157D816443Cd68C31DF95A has already been verified on Sourcify.
If you need to verify a partially verified contract, please use the --force flag.
Explorer: https://sourcify.dev/server/repo-ui/11155111/0xEFa2D146dC54546358157D816443Cd68C31DF95ABoth explorers may answer "already verified": the template's Counter is a common contract, and they have seen its code before. Blockscout may instead submit the source and end with ✅ Contract verified successfully on Blockscout!; the contract is then verified too. Open the Explorer: link that Blockscout printed. If the page shows the contract as verified, you are done. If it shows a "verified twin" or a "similar match" instead, your contract is not verified yet: Verify the source on block explorers has the command to run.
7. Open the contract on the explorer
Open the Explorer: links from the output, with your contract's address:
- Blockscout:
https://eth-sepolia.blockscout.com/address/<contract address>#codeshows the source, marked as verified, and the two transactions from your deployer address: the deployment and theincBycall. - Sourcify:
https://sourcify.dev/server/repo-ui/11155111/<contract address>shows the files and "Exact Match" or "Match". "Match", formerly "partial match", means the bytecode matches except the metadata hash at its end, so the source can differ in comments, variable names or file paths; the contract is verified either way (Exact Match vs Match). - Etherscan:
https://sepolia.etherscan.io/address/<contract address>#code. This tutorial does not verify on Etherscan, so the contract has no verified source of its own there. Etherscan may show the source of another contract with the same bytecode, marked "Similar Match".
The From field of each transaction is your deployer address. The signature came from Cloud KMS; Hardhat never held a private key.
Optional: inspect the roles and the key's access
Skip to step 8 if you do not need this section.
The tutorial does not need these commands. They read the two roles and the key's access and change nothing. Run them before step 8, in the shell from step 2, which has GCP_LOCATION.
List what the two roles of step 3 hold:
gcloud iam roles describe roles/cloudkms.publicKeyViewer --format='value(includedPermissions)'
gcloud iam roles describe roles/cloudkms.signer --format='value(includedPermissions)'The output:
cloudkms.cryptoKeyVersions.viewPublicKey;cloudkms.locations.get;cloudkms.locations.list;resourcemanager.projects.get
cloudkms.cryptoKeyVersions.useToSign;cloudkms.locations.get;cloudkms.locations.list;resourcemanager.projects.getNext to the one key permission, each role can only read the project and list the Cloud KMS locations. Neither can disable, destroy or restore a key version, nor change who has access.
To check who has access to the key, without changing anything:
gcloud kms keys get-iam-policy deployer --keyring hardhat-kms-tutorial --location "$GCP_LOCATION"Before any grant it prints only an etag line, since the key has no bindings of its own; your access comes from the project. After the grant it lists the two roles, each with the deployer as member. Set up a Google Cloud KMS key covers the roles in more detail.
Optional: give a deployer only the sign roles
Skip to step 8 if you do not need this section.
This section sets up a production deployer, which the tutorial does not need: a service account that holds the two roles of step 3 on this key and nothing else, and that the plugin signs as. Run it before step 8, while the key version is enabled, in the shell from step 3, which has GCP_PROJECT_ID, GCP_LOCATION and DEPLOYER. It assumes that DEPLOYER is your own account, user:<email>. Creating, granting and deleting the service account needs Service Account Admin (roles/iam.serviceAccountAdmin) on the project, which project Owner includes and Cloud KMS Admin does not. Step 8 removes the service account and its grants.
Create the service account. A new service account has no roles, in the project or anywhere else:
SA_EMAIL="hardhat-kms-deployer@$GCP_PROJECT_ID.iam.gserviceaccount.com"
gcloud iam service-accounts create hardhat-kms-deployer --display-name "hardhat-kms tutorial deployer"In a new shell, set SA_EMAIL again first, with the same line.
Grant it the two roles on the key, as step 3 did for you:
for role in roles/cloudkms.publicKeyViewer roles/cloudkms.signer; do
gcloud kms keys add-iam-policy-binding deployer \
--keyring hardhat-kms-tutorial \
--location "$GCP_LOCATION" \
--member "serviceAccount:$SA_EMAIL" \
--role "$role"
doneImpersonation goes through the IAM Service Account Credentials API, which a project may not have turned on (Use service account impersonation). Turn it on; doing so again does no harm:
gcloud services enable iamcredentials.googleapis.comTo sign as the service account without a key file, you impersonate it: your account asks Google for short-lived tokens of the service account. That needs iam.serviceAccounts.getAccessToken, from the Service Account Token Creator role, roles/iam.serviceAccountTokenCreator. Google says to grant it "even when you are working in a project that you created" (Use service account impersonation), so do not count on project Owner for it. Grant it to your account on this service account only, not on the project:
gcloud iam service-accounts add-iam-policy-binding "$SA_EMAIL" \
--member "$DEPLOYER" \
--role roles/iam.serviceAccountTokenCreatorThen point your Application Default Credentials at the service account. The command opens a browser: choose the account in DEPLOYER. It overwrites the credentials that gcloud auth application-default login wrote before, and step 8 writes them again. --disable-quota-project keeps a quota project out of the credentials, since the service account has no serviceusage.services.use permission to use one (step 3 explains that error):
gcloud auth application-default login --impersonate-service-account "$SA_EMAIL" --disable-quota-projectCheck whose tokens the plugin now gets, with step 3's command:
curl -s -d "access_token=$(gcloud auth application-default print-access-token \
--scopes=https://www.googleapis.com/auth/cloud-platform,https://www.googleapis.com/auth/userinfo.email)" \
https://www.googleapis.com/oauth2/v1/tokeninfoThe email field is the service account's email. If the command fails with a 403 from iamcredentials.googleapis.com that says the API is disabled, run the gcloud services enable command above. Any other 403 from it means the Token Creator grant has not taken effect yet: like the key's roles, it typically takes 2 minutes and can take 7 or longer. Wait and run it again.
Check that the service account may sign with the key:
npx hardhat kms accounts --check-signpnpm hardhat kms accounts --check-signyarn hardhat kms accounts --check-signIt prints the same row as in step 4, with matches under PIN and ok under SIGN, this time signed by a service account that holds only the two roles on this key. Until step 8 restores your credentials, every program that uses your Application Default Credentials, Hardhat included, runs as the service account, so step 6's command deploys as it too. A permission denied (PERMISSION_DENIED) soon after the grant means the roles have not taken effect yet: wait and run it again.
8. Clean up
When you are done, send the remaining Sepolia ETH back, then disable the key version and schedule its destruction. In a new shell, set SEPOLIA_RPC_URL, GCP_PROJECT_ID and GCP_LOCATION again first, as in steps 2 and 4: the script loads the config, which reads them.
Send the balance back with the script in Return the funds from a KMS address: save it as scripts/return-funds.ts, then run it with RETURN_TO set to an address with no code, such as your own wallet's:
RETURN_TO=<return address> npx hardhat run scripts/return-funds.tsRETURN_TO=<return address> pnpm hardhat run scripts/return-funds.tsRETURN_TO=<return address> yarn hardhat run scripts/return-funds.tsIt ends with sent in <transaction hash>. If it stops with a one-line message instead, What the script refuses explains each one, and nothing was sent unless the line names a transaction.
Before you remove the key, open the deployer address on Sepolia Etherscan or Sepolia Blockscout and check that its balance is close to zero. Once the key is gone, nothing can move what is left.
Then disable the key version and schedule its destruction. If you closed the shell since step 3, set GCP_PROJECT_ID, GCP_LOCATION and DEPLOYER again first, as in steps 2 and 3:
gcloud kms keys versions disable 1 --key deployer --keyring hardhat-kms-tutorial --location "$GCP_LOCATION"
gcloud kms keys versions destroy 1 --key deployer --keyring hardhat-kms-tutorial --location "$GCP_LOCATION"Disabling first is not required, but an organization policy can require it (constraints/cloudkms.disableBeforeDestroy). See when the version will be destroyed:
gcloud kms keys versions describe 1 --key deployer --keyring hardhat-kms-tutorial --location "$GCP_LOCATION" \
--format='value(state,destroyTime)'It prints DESTROY_SCHEDULED and a time 24 hours from now. Until the destroy time, you can undo it: restore the version, then enable it. Prevent and recover from losing a key covers it:
gcloud kms keys versions restore 1 --key deployer --keyring hardhat-kms-tutorial --location "$GCP_LOCATION"
gcloud kms keys versions enable 1 --key deployer --keyring hardhat-kms-tutorial --location "$GCP_LOCATION"The change takes a moment to reach every Cloud KMS server. A disabled version usually keeps working for up to a minute, and in exceptional cases for several hours (Cloud KMS resource consistency). In the recorded run, the version still signed 30 seconds after the destroy, and refused with the key version cannot be used (FAILED_PRECONDITION) a minute later.
After the 24 hours, the version is destroyed for good, and nothing can sign for the address again. The key ring and the key stay, with no cost.
Remove the two roles you granted in step 3:
for role in roles/cloudkms.publicKeyViewer roles/cloudkms.signer; do
gcloud kms keys remove-iam-policy-binding deployer \
--keyring hardhat-kms-tutorial \
--location "$GCP_LOCATION" \
--member "$DEPLOYER" \
--role "$role"
doneIf you followed Optional: give a deployer only the sign roles, remove the service account in this order: its two roles on the key, your Token Creator grant, then the service account itself. A deleted service account's bindings are not removed with it: they stay in the key's policy as deleted:serviceAccount: members for up to 60 days (Delete and undelete service accounts). The last command of the second block below, gcloud iam service-accounts delete, asks you to confirm; answer y. In a new shell, set SA_EMAIL again first:
SA_EMAIL="hardhat-kms-deployer@$GCP_PROJECT_ID.iam.gserviceaccount.com"for role in roles/cloudkms.publicKeyViewer roles/cloudkms.signer; do
gcloud kms keys remove-iam-policy-binding deployer \
--keyring hardhat-kms-tutorial \
--location "$GCP_LOCATION" \
--member "serviceAccount:$SA_EMAIL" \
--role "$role"
done
gcloud iam service-accounts remove-iam-policy-binding "$SA_EMAIL" \
--member "$DEPLOYER" \
--role roles/iam.serviceAccountTokenCreator
gcloud iam service-accounts delete "$SA_EMAIL"Then sign your Application Default Credentials in as yourself again, since they still point at the deleted service account, and choose your own account in the browser:
gcloud auth application-default loginTo keep the key instead, run only the disable command: a disabled version cannot sign, gcloud kms keys versions enable brings it back, and it still costs about $2.50 a month.
Next steps
- Deploy with Hardhat Ignition: choose the deployer, rehearse on a simulated network and verify the source on block explorers.
- Set up a Google Cloud KMS key: every option of a Cloud KMS key, and the errors you can meet.
- Errors: every error message, with its cause and fix.
- Prevent and recover from losing a key before the key holds anything of value.