> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cosmos.network/llms.txt
> Use this file to discover all available pages before exploring further.

# Remote signing

> How a validator operator moves the consensus key off the validator host into a remote signer, how CometBFT talks to that signer, and the rules that prevent a double sign.

Commands on this page were run on gaia v28.3.1 (Cosmos SDK v0.53.4, CometBFT v0.38.23) on 2026-10-15 by agent testing on a local devnet.

This page is for an operator whose validator exists on chain ([Create and manage a validator](https://docs.cosmos.network/hub/latest/validators/validator-setup.md)) and who wants the consensus key off the validator host. At the end the validator signs through a separate signer process, and the key never touches the node. You also know the handful of rules that keep one key from signing twice.

## 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](https://docs.cosmos.network/hub/latest/validators/overview.md#live-constraints)). The key cannot be rotated on gaia v28.3.1. A leaked key therefore means retiring the validator.

By default `gaiad 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. One `config.toml` key switches it on:

```bash theme={"theme":{"light":"github-light-high-contrast","dark":"github-dark-high-contrast"}}
gaiad config set config priv_validator_laddr tcp://127.0.0.1:26659 --skip-validate
```

The `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`:

```json theme={"theme":{"light":"github-light-high-contrast","dark":"github-dark-high-contrast"}}
{"height":"0","round":0,"step":0}
```

Before every signature the signer compares the request's height, round, and step with the file, refuses anything equal or lower, and otherwise writes the new values and signs. This is the only thing standing between a restart and a double sign, so the file must never be restored from a backup or shared. A remote signer keeps its own equivalent file, and the node's local `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](https://github.com/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.

> [!NOTE]
>
> 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.

What the README and the v0.1.0 release state:

* Build: Go 1.26 (`go.mod`), `git clone https://github.com/cosmos/kms`, then `make build` (binary at `build/kms`) or `make install`. There are no release binaries. A Dockerfile is included.
* Commands: `kms init --home <dir>` writes a `kms.yaml` stub 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-id` prints the identity; `kms version`.
* Transports: `tcp://` (CometBFT SecretConnection, what a CometBFT v0.38 node accepts) and `noise://` (libp2p Noise, which needs a node that supports it; gaia v28.3.1 does not).
* Key backends: `file` (reads a CometBFT `priv_validator_key.json`; the README says this is not for production custody), `pkcs11` (an HSM module, with `token_label` or `slot`, `key_label` or `key_id`, and `pin`, `pin_env` or `pin_file`), and `awskms` (an `ECC_NIST_EDWARDS25519` key by `key_id`, `region`, `profile`). There is no Google Cloud KMS backend. Only `ed25519` applies to the Hub.
* Configuration (`kms.yaml`): a `chains` list (chain id and a state file, default `<home>/state/<chain-id>.json`), a `validators` list (chain id, the node's `priv_validator_laddr` address, the identity key file, reconnect settings), and a `keys` list (chain ids, backend, algorithm). An optional `grpc` block 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 start` command refuses to run with a missing state file unless you pass `--allow-fresh-state <chain-id>`. The `state init` command refuses to overwrite an existing floor.
* Existing validators: kms does not import keys. The `file` backend reads a copy of `priv_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 spec `ECC_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.

The configuration and backend detail is in the SDK docs: [Remote signing with Cosmos KMS](https://docs.cosmos.network/sdk/latest/kms/remote-signing.md), [Configure a backend](https://docs.cosmos.network/sdk/latest/kms/configure-backend.md), [Configuration reference](https://docs.cosmos.network/sdk/latest/kms/configuration-reference.md), [Best practices](https://docs.cosmos.network/sdk/latest/kms/best-practices.md), and [Migrate from TMKMS](https://docs.cosmos.network/sdk/latest/kms/migrate-from-tmkms.md). Those pages target the CometBFT release kms pins. The Hub runs CometBFT v0.38.23, so use `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](https://docs.cosmos.network/hub/latest/hub-tutorials/join-testnet.md) 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's `priv_validator_key.json` so 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 with `cargo 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](https://github.com/iqlusioninc/tmkms).

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](https://github.com/amygdala-labs/horcrux).

## Verify

After switching to any remote signer:

* The node starts and `curl -s localhost:26657/status | jq .result.validator_info.pub_key` prints 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.json` stops advancing.
* The query `gaiad query slashing signing-info $(gaiad comet show-validator)` shows `missed_blocks_counter` flat. If you replaced the local key with a throwaway, pass the signer's public key or the `cosmosvalcons` address instead of `show-validator`.
* Stopping the signer on the testnet stops your votes within a block. Starting it resumes them without a `rejected` or `panic` line in either log.

## Next

Put the validator behind sentries and close the ports: [Validator security](https://docs.cosmos.network/hub/latest/validators/security.md).
