Protected public API requests require authentication and an operation-specific permission. Key permissions can be scoped to a key id.
Authentication modes
| Type | Header | Use |
|---|---|---|
dev |
X-DEV-TOKEN |
explicit opt-in deployments |
jwt |
X-JWT-TOKEN |
production deployments |
Use JWT for production. Developer authentication requires an explicit build feature and runtime setting. If a deployment uses it outside development, protect the token and configuration as production credentials and restrict its permissions.
Developer authentication
Developer authentication reads its token and permissions from keeper.dev.config.location. Build with auth-dev, which is excluded from keeper.features=all.
Example:
keeper.dev {
token = "dev-token"
permissions = [
"tkeeper.system.init",
"tkeeper.system.unseal",
"tkeeper.dkg.create",
"tkeeper.key.*.public",
"tkeeper.key.*.sign",
"tkeeper.key.*.verify"
]
}
Build it into an artifact explicitly:
./gradlew :build -Pkeeper.features=auth-dev -Pkeeper.platforms=ecc
Then enable it at runtime:
-Dkeeper.dev.enabled=true
-Dkeeper.dev.config.location=/etc/tkeeper
JWT authentication
JWT auth verifies token signature and claims against configured issuer metadata.
auth {
type = "jwt"
jwt {
jwks-location = "https://issuer.example/.well-known/jwks.json"
issuer = "https://issuer.example"
audience = "tkeeper"
refresh = 15m
clock-skew = 15s
}
}
Tokens must contain:
subaudexppermissionsas a string list claim
If auth.jwt.issuer is configured, iss must match it. Configure issuer in production.
aud may be a single value or an array. TKeeper checks that it contains auth.jwt.audience.
nbf is optional. When present, TKeeper rejects the token before that time.
Permission model
The optional mandate module authorizes a specific
operation for later execution. Issue it with a primary credential, then submit the
operation with X-KEEPER-MANDATE and any required approvals. The mandate replaces
the primary credential for that request; current policies are checked again.
Permissions are explicit strings:
tkeeper.key.{keyId}.sign
tkeeper.key.{keyId}.verify
tkeeper.key.{keyId}.public
Wildcards are supported:
tkeeper.key.*.sign
tkeeper.key.prefix-*.sign
tkeeper.key.*.*
Wildcards match dot-separated permission segments. tkeeper.key.*.sign matches tkeeper.key.wallet.sign, but not tkeeper.key.team.wallet.sign.
Negative permissions remove access granted by a broader permission:
tkeeper.key.*.sign
-tkeeper.key.hot-wallet.sign
This grants signing on all one-segment key ids except hot-wallet.
Permission groups
| Permission | Allows |
|---|---|
tkeeper.system.init |
initialize keeper |
tkeeper.system.unseal |
unseal |
tkeeper.system.seal |
seal |
tkeeper.dkg.create |
create key identity |
tkeeper.dkg.rotate |
rotate key identity |
tkeeper.dkg.refresh |
refresh generation |
tkeeper.key.{keyId}.public |
read public key |
tkeeper.key.{keyId}.sign |
sign |
tkeeper.key.{keyId}.verify |
verify |
tkeeper.key.{keyId}.encrypt |
ECIES encrypt |
tkeeper.key.{keyId}.decrypt |
ECIES decrypt |
tkeeper.key.{keyId}.destroy |
destroy key generation |
tkeeper.storage.write |
trusted-dealer import |
tkeeper.consistency.fix |
run consistency fix |
tkeeper.expired.view |
read key-expiration indexes |
tkeeper.integrity.rotate |
rotate audit integrity key |
tkeeper.audit.log.verify |
verify signed audit log lines |
tkeeper.compliance.inventory |
read asset inventory |
tkeeper.quorum.promote |
promote mono to threshold |
tkeeper.control.system |
read control-plane system state |
tkeeper.control.sinks |
read control-plane audit sink state |
Keep destructive and lifecycle permissions narrower than signing permissions.
Internal peer authentication
Peer-to-peer calls use TKeeper's internal request signing. This is separate from public API authentication.
Each internal request carries:
X-INSTANCE-ID
X-INTENDED-FOR
X-TIMESTAMP
X-NONCE
X-KEY-ID
X-PUBLIC-KEY
X-BOOT-PROOF
X-SIGNATURE
The request signature binds the HTTP method, path, canonical query, body hash, intended peer, timestamp, nonce, boot proof, and any forwarded actor token. The nonce must be unique and the timestamp must be fresh. Accepted nonces share the persistent RocksDB replay store with four-eye approvals and are retained for keeper.nonce-ttl (12 hours by default). The configured retention must exceed the full 60-second internal request acceptance window; physical FIFO cleanup can occur later.
The external JWT or dev token is forwarded on protected internal entrypoints. Signing, DKG initialization, trusted-dealer import, destroy prepare and consistency mutations independently enforce actor permissions. Registry management entrypoints also check their respective Approvers permissions. WebAuthn registration finish requires a valid primary token and invitation, with no additional permission; the writer validates the forwarded token independently. Protocol rounds, certified registry delivery and marker-bound commit or abort calls use peer authentication and stored state.
On first contact, a peer proves its integrity key with the shared bootstrap token. After that, the integrity key is pinned for the lifetime of the process. If keeper.peers[].tls-spki-sha256 is configured, TKeeper first verifies that the mTLS client certificate matches the claimed peer id; this removes network-first bootstrap enrollment from the trust decision.
Authenticated internal responses carry the peer identity, request hash and nonce, response timestamp, body hash, boot proof, and X-RESPONSE-SIGNATURE. The caller verifies the status, content type, and body before accepting the response. Unsigned, replayed, cross-peer, or request-substituted responses are rejected.
Protected internal routes require TLS outside dev mode. Mutual TLS with per-peer SPKI binding authenticates the transport peer; signed requests and responses separately bind protocol content and session intent.
Protect the bootstrap token as cluster enrollment authority. Without mTLS SPKI binding, an attacker that can reach an unenrolled peer and knows the token may be able to pin an attacker-controlled peer identity before the legitimate peer connects.
Forwarded bearer credentials remain visible to a malicious recipient peer. Use short-lived tokens, restrict protected internal paths, and enable internal mTLS. Sender-constrained external credentials require deployment-specific mTLS, DPoP, or request-signing support and are not inferred from an ordinary bearer token.
Common failures
UNAUTHENTICATED
The token is missing, invalid, expired, has the wrong issuer or audience, or is signed by a key not present in JWKS.
ACCESS_DENIED
The token is valid, but the permissions claim does not allow the operation. Check negative permissions too; a matching negative permission wins over a broad grant.