Skip to content
Troubleshooting

Troubleshooting

valiss verification fails closed, and every rejection has one specific cause. This page maps the symptom you see (an HTTP status, a gRPC code, an error string) back to what the verifier decided and what to check. The strings quoted here come from the Go reference; the Python and TypeScript ports raise ValissError whose reason is the same spec section 7 reason code, so the causes apply to all three. Switch on reason rather than matching message text: the code is stable, the message is illustrative.

The transports translate a verifier failure to a status once. httpauth answers 401 for an authentication failure and 403 for an extension denial; grpcauth maps the same split to Unauthenticated and PermissionDenied. The response body or status message carries the underlying cause.

The client is rejected 401 but does send a token

Missing credentials. Symptom: 401 with missing credentials (gRPC Unauthenticated: missing credentials). It means neither the account nor the user token header reached the server, so the middleware rejects before verifying anything. Check that the client uses a signing transport (httpauth.NewTransport, grpcauth.NewCredentials, or the Python and TypeScript auth hooks), and that no proxy strips the valiss-account-token and valiss-user-token headers.

Account token not recognized. Symptom: 401, account token not recognized (reason not_allowlisted). The account token verified against the pinned operator key, but its jti is not in the server’s allowlist, which fails closed. Check that the jti the minter printed is the one loaded, that the allowlist file was reloaded after any re-issue (issuing with different claims produces a different jti), and that you did not mean to run AllowAll.

Wrong operator pinned. Symptom: 401, account token not signed by the expected issuer. The account token was signed by a different operator than the public key the server pinned. Check that the operator public key passed to NewVerifier is the one that issued these account tokens, and in a multi-operator deployment that the credential’s trust domain is in the keyring.

A token is expired or out of its window

Symptom: 401 with account token expired, user token expired, account token not yet valid, or operator token expired: the trust domain is closed. A token’s exp or nbf fell outside the skew window (2 minutes by default); an expired operator token closes the whole trust domain until a fresh one is published. Check the clocks on the issuing and verifying sides, the TTL you issued with, and whether an operator-token rotation ceremony was allowed to lapse. WithSkew widens the tolerance when drift is unavoidable.

Epoch mismatch after a rotation

Symptom: 401 with account token epoch N, trust domain epoch M, the same for a user token, or no trusted operator <key> at epoch N (reason epoch_mismatch or unknown_operator). The verifier enforces an operator token or keyring at one epoch and the presented token carries another. This is the intended outcome of an epoch bump: every earlier-epoch token is rejected cryptographically. To resolve it, re-issue the account and user tokens at the current epoch. For a grace period, register both the outgoing and incoming epochs in a Keyring so the old epoch keeps verifying until its operator token’s exp lapses. Unstamped tokens are epoch 0.

The request signature does not check out

Signature required, not a bearer token. Symptom: 401, request signature required: not a bearer token (reason not_bearer). The request carried no per-request signature and the effective token is not a bearer user token, so possession is unproven. Check that the client creds carry a seed and sign through the transport, or, for a client that cannot hold a key, issue the user token with WithBearer() and pair it with TLS.

Signature verification failed. Symptom: 401, request signature verification failed (reason bad_request_signature). The Ed25519 check over the reconstructed request context failed, almost always because the server rebuilt different context bytes than the client signed. The signature binds the method, host, and path exactly. Check that no proxy rewrote the Host header or the path, and that the host the client signed is the one the wire carries (an explicit Host header wins, otherwise the URL host keeping any non-default port). A captured signature cannot be reused across a different method or path.

Timestamp outside the skew window. Symptom: 401, request timestamp outside the 2m0s skew window (reason skew). The client’s request timestamp is too far from the server’s clock, or is unparsable. Check client clock sync, or widen the verifier with WithSkew.

Nonce required, or replay. Symptom: 401, request nonce required or request nonce already seen (replay). The verifier runs a replay cache. The first says the signed request carried no nonce; the second says the nonce was already spent. Enable the nonce on the client (httpauth.WithNonce(), or nonce=True in Python), and make each retry use a fresh nonce rather than resend the identical request.

The client is denied 403 (PermissionDenied)

Request outside the extension. Symptom: 403, token does not permit GET /admin, or the gRPC form token does not permit /pkg.Svc/Method. Authentication succeeded, but the request falls outside the token’s HTTP or gRPC extension bounds. Check the Methods, Paths, and (HTTP) Hosts on the token. Each dimension is an AND-filter that constrains only when populated, and paths take a trailing * prefix wildcard. Extensions on both the account and the user token are enforced together, so an account-level cap bounds every user beneath it.

Token carries no extension. Symptom: 403, token carries no http extension or token carries no grpc extension. Transport enforcement fails closed, and a token somewhere in the chain does not carry the transport extension at all. Issue every token in the chain with the extension (the zero-value Ext{} grants nothing; allow-all is the explicit wildcard []string{"*"}). Only when authorization lives entirely outside the transport, build the middleware with AllowMissingExtension().

User-only credentials are rejected

Symptom: 401, request carries no account token and the server has no account token resolver (reason no_resolver). The client sent only a user token and the server cannot supply the matching account token. Configure WithAccountTokenResolver (for example StaticAccountTokens(...)), or have the client send a bundle that embeds the account token.

A credentials file will not load

Symptom: creds: no token markers found, creds seed: ..., creds token: ..., or creds: unsupported version N. The file is not a valiss credentials file, a token or seed block is corrupt, or it declares a format version this build does not implement. Check that the file is the one the minter wrote, whole and untruncated, that the seed and token blocks are intact base32, and that the library is not older than the format that produced the file.

Issuing a token errors

Issuance validates the key roles up front, so these fire at Issue*, not at verify time:

  • account tokens must be signed by an operator-type nkey (expected an SO... seed), or user tokens must be signed by an account-type nkey (expected an SA... seed): the signing key is the wrong level. The signer must sit exactly one level above the subject.
  • invalid tenant public key (expected an A... nkey), or invalid user public key (expected a U... nkey): the subject public key is the wrong role.
  • audience, checksum, and chain apply only to message tokens: WithAudience, WithChecksum, or WithChain was passed to an identity issuer.
  • message tokens must carry an expiry (WithTTL or WithExpiry): message tokens are short-lived by rule and cannot be issued without one.
  • duplicate extension "name": two extensions with the same ExtensionName on a single token.

A message token will not verify

A message token is a proof of origin, verified offline; a Verifier never accepts one as a credential. Its own failures:

  • missing message token (httpsig, 401), or message token chain required (401): the receiver got no message token, or not the provenance chain it needs.
  • payload checksum mismatch (reason checksum_mismatch): the body the receiver hashed differs from the one the emitter signed. Anything that re-encodes the payload in transit breaks it. For gRPC the checksum is over the deterministic protobuf encoding, so both ends must share the same message definition; a schema drift or a mismatched google.golang.org/protobuf produces different bytes and fails here.
  • message token audience "...", expected "..." (reason wrong_audience): the token was issued for a different destination, or carries none while the receiver expects one. Set ExpectAudience to the destination you verify for.
  • message token expired: a stored message was verified at now instead of its receipt instant. Pass valiss.At(receivedAt).
  • message token carries no chain and none was supplied (WithChainTokens) (reason no_chain): issue with WithChain to embed the provenance, or supply it out of band on the verify side with WithChainTokens.
  • message checksum requires a proto.Message, got T (grpcsig): the value handed to the gRPC message signer is not a protobuf message.

A token decodes but is rejected outright

  • unsupported wire version N, or unsupported token type: the token comes from a wire version or envelope this build does not implement. ver is read from the header; the current version is 1.
  • token signature verification failed, or malformed token: the token bytes are corrupt or were re-serialized. The signing input is the header and payload exactly as they appear on the wire, so a reparsed-and-re-encoded token no longer verifies.