Why a remote signer
The consensus key signs every prevote, precommit, and proposal. Whoever holds it can sign a conflicting block. The chain punishes that with a 5 percent slash and a permanent tombstone of the validator (Validator overview). The key cannot be rotated on gaia v28.3.1. A leaked key therefore means retiring the validator. By defaultgaiad init writes the key to config/priv_validator_key.json on the node host. That is the same host that holds open TCP connections to peers and runs a large codebase. A remote signer moves the key to a separate host, or a hardware security module (HSM), that exposes nothing but the signing protocol. The node asks the signer for signatures over an authenticated connection. The validator host can then be rebuilt, snapshotted, or compromised without the key leaving the signer.
How CometBFT privval works
CometBFT asks the signer for signatures over a small protocol called privval. Oneconfig.toml key switches it on:
priv_validator_laddr key (default empty) is the address the node listens on for a signer, tcp:// or unix://. Once it is set, the node ignores priv_validator_key_file and does not start consensus until a signer connects and answers a public key request. On gaia v28.3.1 gaiad config set config needs --skip-validate. Confirm with gaiad config get config priv_validator_laddr.
The signer dials in over a CometBFT SecretConnection, encrypted and authenticated with an Ed25519 identity on each side. The node generates its identity fresh at every start (privval/utils.go in CometBFT v0.38), so neither side pins the other across restarts. The network boundary is the control: bind the listener to a private interface and firewall the port to the signer host. The example uses loopback for a signer on the same host.
Double-sign protection is a state file. A local signer keeps data/priv_validator_state.json:
priv_validator_state.json stays frozen at the last locally signed height.
One behaviour to know: a CometBFT v0.38 node logs SignerListener: Error accepting connection ... i/o timeout with an increasing failures counter while a signer is connected. It is the listener’s periodic accept timeout, not a lost connection. Watch the signed vote lines on the signer and the height instead.
Cosmos KMS
Cosmos KMS is the remote signer maintained under the Cosmos organisation. Status: Maintained by Cosmos. Latest release v0.1.0 (2026-07-27), source build only. Last change on the main branch 2026-08-20. Checked 2026-10-15.Cosmos KMS is an early release (v0.1.0) that builds from source. On 2026-10-15 it was run against gaia v28.3.1 (CometBFT v0.38.23) on a local devnet over
tcp://, where it signed heights 11 to 105 and resumed from its state file after a restart. It has not been run on the Hub testnet by its maintainers or through an independent security review. Rehearse the switch on the testnet and check the repository for newer releases and security notices before mainnet.- Build: Go 1.26 (
go.mod),git clone https://github.com/cosmos/kms, thenmake build(binary atbuild/kms) ormake install. There are no release binaries. A Dockerfile is included. - Commands:
kms init --home <dir>writes akms.yamlstub and an identity key;kms start --home <dir>runs the signer;kms state init --chain <id> --height <h> [--round <r>] [--step <s>]seeds the double-sign floor;kms peer-idprints the identity;kms version. - Transports:
tcp://(CometBFT SecretConnection, what a CometBFT v0.38 node accepts) andnoise://(libp2p Noise, which needs a node that supports it; gaia v28.3.1 does not). - Key backends:
file(reads a CometBFTpriv_validator_key.json; the README says this is not for production custody),pkcs11(an HSM module, withtoken_labelorslot,key_labelorkey_id, andpin,pin_envorpin_file), andawskms(anECC_NIST_EDWARDS25519key bykey_id,region,profile). There is no Google Cloud KMS backend. Onlyed25519applies to the Hub. - Configuration (
kms.yaml): achainslist (chain id and a state file, default<home>/state/<chain-id>.json), avalidatorslist (chain id, the node’spriv_validator_laddraddress, the identity key file, reconnect settings), and akeyslist (chain ids, backend, algorithm). An optionalgrpcblock exposes a signer service that has no caller authentication, so it must be behind TLS or stay off. - Double-sign protection reuses the CometBFT state-file format per chain. The
kms startcommand refuses to run with a missing state file unless you pass--allow-fresh-state <chain-id>. Thestate initcommand refuses to overwrite an existing floor. - Existing validators: kms does not import keys. The
filebackend reads a copy ofpriv_validator_key.json. An HSM needs the Ed25519 key imported with the vendor’s tooling, and AWS KMS accepts an imported Ed25519 private key (key specECC_NIST_EDWARDS25519, PKCS #8 encoding, checked 2026-10-19). The Hub cannot rotate consensus keys, so keep the original file offline and intact until the signer is proven.
tcp:// and ignore anything noise:// or --libp2p.
Operational rules for any remote signer
These apply whichever signer you run.- One signer instance per consensus key, ever. Not an active and a standby, not a container that might restart twice. High-availability setups need a signer built for it, with a shared or threshold state, or a strict manual failover.
- Never restore the signer’s state file from a backup, a snapshot, or a cloned disk. If the file is lost, stop the node. Wait until the chain is several heights past the last signed block. Then seed the state at the current height (
kms state init, or the equivalent for your signer) before starting. - Never place the consensus key where an unauthenticated service can use it: no unauthenticated gRPC signer endpoints, no key file readable by the node’s user, no shared volumes.
- Pin the peer identity where the signer supports it, and pin the network path everywhere: the signer dials exactly one node address, and the node’s privval port accepts connections from the signer host alone. One node, one signer.
- Put the state file on durable local storage that survives a crash (not tmpfs, not an ephemeral container layer) and fsyncs.
- Test the whole switch on the testnet first, including stopping and restarting the signer, before touching mainnet.
- When you switch from local signing, start the signer first, then the node. Seed the signer’s state at or above the height in the node’s
data/priv_validator_state.json. Once the signer is confirmed working, remove or replace the key in the node’spriv_validator_key.jsonso the node can never fall back to it.
External signers
Two other signers are widely used on the Hub. Neither is maintained under the Cosmos organisation, so Cosmos does not support them. Status and maintenance are as their repositories showed on the date checked. TMKMS: Maintained by iqlusion. Latest release v0.15.0 (2025-11-07). Repository active, last push 2026-09-17. Checked 2026-10-15. Rust, built withcargo install tmkms --features=yubihsm (or --features=ledger, --features=softsign). Backends: YubiHSM 2, Ledger, Fortanix DSM, software keys. It dials the node’s priv_validator_laddr over tcp:// or unix:// and keeps a per-chain state file. It is a single-instance signer. TMKMS repository.
Horcrux: Maintained by amygdala-labs (formerly strangelove-ventures). Latest release v3.3.2 (2025-03-07). Repository archived on 2026-08-17. Checked 2026-10-15. Go, a threshold signer that splits the key across several cosigners. No single host holds the key, and one host can fail without stopping signing. The archive means no further releases or fixes. Existing deployments keep working, but plan a migration. Horcrux repository.
Verify
After switching to any remote signer:- The node starts and
curl -s localhost:26657/status | jq .result.validator_info.pub_keyprints the key held by the signer. - The signer logs a signature for every block, and its state file height advances with the chain.
- The node’s
data/priv_validator_state.jsonstops advancing. - The query
gaiad query slashing signing-info $(gaiad comet show-validator)showsmissed_blocks_counterflat. If you replaced the local key with a throwaway, pass the signer’s public key or thecosmosvalconsaddress instead ofshow-validator. - Stopping the signer on the testnet stops your votes within a block. Starting it resumes them without a
rejectedorpanicline in either log.