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), oruser 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), orinvalid 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, orWithChainwas 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 sameExtensionNameon 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), ormessage token chain required(401): the receiver got no message token, or not the provenance chain it needs.payload checksum mismatch(reasonchecksum_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 mismatchedgoogle.golang.org/protobufproduces different bytes and fails here.message token audience "...", expected "..."(reasonwrong_audience): the token was issued for a different destination, or carries none while the receiver expects one. SetExpectAudienceto the destination you verify for.message token expired: a stored message was verified atnowinstead of its receipt instant. Passvaliss.At(receivedAt).message token carries no chain and none was supplied (WithChainTokens)(reasonno_chain): issue withWithChainto embed the provenance, or supply it out of band on the verify side withWithChainTokens.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, orunsupported token type: the token comes from a wire version or envelope this build does not implement.veris read from the header; the current version is 1.token signature verification failed, ormalformed 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.