Skip to main content

Authentication and authorization

Authentication checks who a client is. Authorization checks what that client can do. With both enabled, each application, service, and person connects as a named user, and each user can read or change only the databases and tables that you grant to it.

Why enable authentication​

Authentication is off by default, so a local node is easy to start. Without it, any client that can reach a node's network port can read, change, and drop every database on that node.

Enable authentication when:

  • The node is reachable from a network that you do not fully control, such as a shared office network, a cloud network, or the internet.
  • More than one application or team uses the same server, and each one must see only its own data.
  • You want least privilege. For example, a reporting service gets read access only, and only an administrator can run DDL.
  • A security review or a compliance rule requires named users and passwords.

You can leave it off for local development and for a test node on a private machine. An existing development deployment keeps working until an operator enables authentication.

What changes when you enable it​

While authentication is enabled, CamusDB fails closed:

  • The server refuses to start with an empty user catalog. It starts only when you supply bootstrap credentials for an administrator.
  • Every request must include a valid bearer token.
  • CamusDB checks every statement against the privileges its tables and databases require.
  • CamusDB refuses a request that carries a credential over a plaintext connection, except for requests from loopback and deployments where you disabled the TLS requirement.

CamusDB stores a password only as a salted PBKDF2-HMAC-SHA256 verifier. Ordinary client requests use a short-lived, opaque bearer token instead of sending the password again.

Enable authentication​

Configure the authentication switch and the secrets through environment variables or a secret provider, not in config.yml. Do not put a token key or a bootstrap password in YAML. The TLS topology setting is not a secret, and TLS covers it.

Environment variableRequiredMeaning
CAMUSDB_AUTH_ENABLEDyesSet it to true to enable authentication and authorization. Any other value keeps them disabled.
CAMUSDB_AUTH_TOKEN_KEYyes, while auth is enabledThe server-side key that applies an HMAC to access-token secrets at rest. It must be at least 32 bytes. Use a random value such as openssl rand -hex 32. Every node in the cluster must use the same value.
CAMUSDB_BOOTSTRAP_USERat the first start with auth onlyThe name of the first superuser, used when the auth catalog is empty.
CAMUSDB_BOOTSTRAP_PASSWORDat the first start with auth onlyThe bootstrap superuser's initial password.
CAMUSDB_NODE_SECRETin a cluster deployment with authThe shared secret for internal routes between nodes, used while auth is enabled. When set, it must be at least 32 bytes. Every node in the cluster must use the same value.

For example:

export CAMUSDB_AUTH_ENABLED=true
export CAMUSDB_AUTH_TOKEN_KEY="$(openssl rand -hex 32)"
export CAMUSDB_BOOTSTRAP_USER=admin
export CAMUSDB_BOOTSTRAP_PASSWORD="$(openssl rand -base64 24)"
export CAMUSDB_NODE_SECRET="$(openssl rand -hex 32)"

At startup, these rules apply:

  1. If the auth catalog is empty, CamusDB creates one bootstrap superuser from the supplied values.
  2. Startup fails if the auth catalog is empty and the bootstrap values are absent.
  3. CamusDB ignores the bootstrap values as soon as one user exists.

Startup also fails when CAMUSDB_AUTH_TOKEN_KEY, or a configured CAMUSDB_NODE_SECRET, is shorter than 32 bytes. An unset node secret is still a valid single-node configuration: peer routes are refused instead of guarded by a weak shared secret.

There is no default user and no default password.

Login and logout​

For application code, configure the user and password in your .NET, TypeScript, or Python driver. The library handles login, token reuse and renewal, and authentication headers. An access token supplied by your own token service has its own lifecycle; see the driver's options.

The operator still creates users, grants privileges, and configures TLS. Applications use their normal query and transaction methods after connecting.

Login and token protocol for custom clients

Use /login to exchange a user name and password for a bearer token:

POST /login
Content-Type: application/json

{ "user": "admin", "password": "secret" }

A successful response looks like this:

{
"status": "ok",
"token": "camus_<id>.<secret>",
"expiresAtUnixMs": 1785270000000,
"expiresInSeconds": 900
}

Send the token on each later HTTP request:

POST /execute-sql-query
Authorization: Bearer camus_<id>.<secret>
Content-Type: application/json

{
"databaseName": "app",
"sql": "SELECT * FROM orders"
}

Use /logout with the same Authorization header to revoke the current token:

POST /logout
Authorization: Bearer camus_<id>.<secret>

By default, a token has an absolute lifetime of 15 minutes. There is no refresh token, so a client logs in again after its token expires.

The login response reports the token's deadline in two forms:

  • expiresAtUnixMs is the absolute UTC Unix epoch millisecond at which CamusDB stops accepting the token.
  • expiresInSeconds is the same deadline, as a duration measured by the server.

Renew based on those fields rather than assuming a fixed lifetime, because an operator can change access_token_ttl (milliseconds, default 900000). A client must renew before the reported deadline.

A password change, a DROP USER, or a call to /logout invalidates an outstanding token.

gRPC tokens​

A gRPC client sends the same bearer token in the request metadata:

authorization: Bearer camus_<id>.<secret>

Use the CamusAuth gRPC service to obtain and revoke tokens:

RPCPurpose
Login(LoginRequest)Exchanges user and password for token, expires_at_unix_ms, and expires_in_seconds.
Logout(LogoutRequest)Revokes the token in the authorization metadata. A second logout has no further effect.

So a gRPC-only deployment does not need the HTTP API, and its clients never need to call /login.

Authentication and authorization errors map to ordinary gRPC status codes:

ConditiongRPC status
A token is absent, invalid, or expiredUNAUTHENTICATED
An authenticated caller lacks a privilegePERMISSION_DENIED
The login rate limit or the KDF limit is reachedRESOURCE_EXHAUSTED

See gRPC API for the service reference.

TLS​

While auth is enabled, CamusDB by default refuses a request that carries a credential over a plaintext connection. Requests from loopback are exempt, so development on one host works without a certificate.

Use HTTPS in production, or terminate TLS in front of the HTTP API. Deploy gRPC over TLS as well.

TLS can terminate in front of the node: at an ingress, a sidecar, or a service mesh. The CamusDB process then sees only the trusted plaintext hop. In that topology, disable the transport check, but keep authentication and grants enabled:

require_tls_when_auth_enabled: false

You can also use the command-line flag:

camusdb --require-tls-when-auth-enabled false

This setting is not a secret, so it belongs in the ordinary configuration. Do not turn it off for a node that clients can reach directly.

Manage users​

User management statements are server-level statements. They need no database context, but they require the superuser attribute.

CREATE USER myapp IDENTIFIED WITH sha256_password BY 'app-password';
CREATE USER myapp IDENTIFIED BY 'app-password';
CREATE USER IF NOT EXISTS myapp IDENTIFIED BY 'app-password';
CREATE USER grant_target;

ALTER USER admin IDENTIFIED BY 'new-password' REPLACE 'current-password';
ALTER USER admin IDENTIFIED WITH sha256_password BY 'new-password' REPLACE 'current-password';
ALTER USER myapp IDENTIFIED BY 'new-password';

DROP USER myapp;
DROP USER IF EXISTS myapp;

CamusDB supports only sha256_password, and uses it by default when you omit IDENTIFIED WITH.

CREATE USER grant_target creates a grant target without a password. That user cannot log in until you set a password with ALTER USER.

Use a bound parameter for passwords:

CREATE USER myapp IDENTIFIED BY @password;
ALTER USER admin IDENTIFIED BY @new_password REPLACE @current_password;

A parameter keeps the cleartext secret out of shell history, traces, and query logs. A password can be at most 1 KiB long. Both password literals in an ALTER USER ... REPLACE ... statement are redacted from server logs.

When users change their own password, the statement must include REPLACE with the current password. That rule applies to a superuser changing their own password too. A superuser can reset another user's password without knowing the old password. Any password change invalidates that user's existing tokens, including the token that made the request.

List accounts​

Use SHOW USERS to inventory the user catalog:

SHOW USERS;
SHOW USERS LIKE 'app_%';

The result has one row per account, ordered by user name:

ColumnMeaning
userAccount name, in the case used at creation.
idImmutable account id, or NULL for older accounts created before ids existed.
superuserWhether the account bypasses privilege checks.
has_passwordWhether the account can log in.
grantsCount of grant records held by the account.
created_atUTC creation time.

The output never includes password material: no hash, salt, iteration count, or algorithm. SHOW USERS requires a superuser because it names every account on the server.

Grant privileges​

Use GRANT and REVOKE to manage privileges on objects:

GRANT SELECT, INSERT ON app.* TO myapp;
GRANT SELECT ON app.orders TO reader;
GRANT ALTER, INDEX ON app.orders TO migrator;
GRANT ALL PRIVILEGES ON app.* TO poweruser;
GRANT MONITOR ON *.* TO monitoring;

REVOKE INSERT ON app.* FROM myapp;

SHOW GRANTS FOR myapp;
SHOW GRANTS FOR *;
SHOW GRANTS;

SHOW GRANTS FOR <user> returns the named user's grants. SHOW GRANTS without FOR returns the authenticated user's grants.

SHOW GRANTS FOR * returns all grants for all accounts, ordered by account name and object. An account with no grants produces no row, so use SHOW USERS to list the accounts themselves. The statement requires a superuser.

With authentication enabled, reading another user's grants requires a superuser. A non-superuser can always inspect their own grants. A refused SHOW GRANTS FOR <other> does not reveal whether that user exists.

CamusDB supports these privileges:

PrivilegeAllows
SELECTRead a table's data, and inspect its metadata with SHOW COLUMNS and SHOW CREATE TABLE.
INSERTInsert a row.
UPDATEUpdate a row.
DELETEDelete a row.
CREATE TABLECreate a table in the target database scope.
DROPDrop a database or a table in the scope.
ALTERAlter table or database metadata in the scope.
INDEXCreate, alter, or drop an index in the scope.
CREATECreate a database, or another object with a create scope where that applies.
MONITORRead node resource statistics. It is valid on *.* only and grants no access to application data.
ALL PRIVILEGESThe union of the concrete privileges that CamusDB knows today.

Creating a foreign key also requires SELECT on the referenced parent table. A child write can reveal whether a parent key exists, so CamusDB checks that privilege when the constraint is created. There is no separate REFERENCES privilege.

A grant has one of three scopes, from broadest to narrowest:

ScopeExampleMeaning
Global*.*Every database and every table.
Databaseapp.*Every table in one database.
Tableapp.ordersOne table.

Grants add together, and repeating a grant is harmless: granting a privilege that the user already holds does nothing. REVOKE subtracts a privilege from the matching scope.

A grant binds to the immutable identity of a database or table, so a rename keeps the grant. A table that you drop and create again does not inherit the old table's grants.

GRANT never creates a user, and it cannot make a user a superuser. Only the bootstrap sets the superuser attribute.

Apply authorization changes​

Most privilege changes need no manual flush:

FLUSH PRIVILEGES;
FLUSH SESSIONS;

Both statements require a superuser and return no rows.

Each node caches authorization decisions for at most the authorization cache TTL, which is 1 second by default. A GRANT, REVOKE, DROP USER, or password change applies to the next request on the node where it was made. Other nodes observe the change when their cached decision expires and they read the catalog again. Setting the TTL to 0 makes cross-node changes immediate, at the cost of a catalog lookup on every request. Long-lived gRPC batch streams re-resolve authorization on the same schedule.

gRPC batch streams and token expiry​

A BatchExecute stream presents its bearer token once, when the stream opens. The token lifetime controls how long that token can be presented to open a new stream or make a unary call. It does not by itself end an already-open batch stream that authenticated while the token was valid.

After ordinary token expiry, the stream keeps resolving authorization from the account it authenticated as, using the same authorization cache TTL described above. Grant and revoke changes still reach the next operation within that bound.

Early session revocation still ends the stream. /logout, FLUSH SESSIONS, a password change, and DROP USER all invalidate the session. A stream that ends this way returns UNAUTHENTICATED with CADB0516. The client should discard the token, log in again, and open a new stream.

The server keeps expired session records briefly so an open stream can tell ordinary expiry apart from revocation. A new stream still needs a token that resolves, so clients should renew before expiresAtUnixMs.

FLUSH PRIVILEGES makes this node drop cached authorization decisions and re-read the user and grant catalog. It also bumps a cluster-wide version counter for the authorization cache, so other nodes discard decisions derived from the older catalog within the same TTL bound. It does not revoke sessions or force clients to log in again.

FLUSH SESSIONS deletes all stored sessions, so every client on every node must log in again. Use it when you need to invalidate credentials held outside the server, not after ordinary grant changes.

Enforcement rules​

While auth is enabled, CamusDB checks every statement before running it.

Reading a table needs SELECT on every table that the statement references. That covers joins, derived tables, subqueries, semi-joins (filters that keep a row when a match exists in another table), EXISTS, IN, and EXPLAIN for a query that reads a table.

SELECT ... FOR UPDATE also requires UPDATE on its table, including when wrapped in plain EXPLAIN. See row-locking reads.

A write needs the matching write privilege:

  • INSERT for an insert.
  • UPDATE for an update.
  • DELETE for a delete.

INSERT ... ON CONFLICT DO UPDATE and UPSERT require both INSERT and UPDATE, checked before execution even if nothing conflicts. DO NOTHING requires INSERT only. See conflict handling.

A write with RETURNING also needs SELECT on the target table, because the RETURNING list reads stored row values. The count-only request flag (discard_returning_rows in gRPC, discardReturningRows over HTTP and in the TypeScript connector) does not remove that privilege check. The .NET driver's ExecuteNonQueryAsync() sets that flag.

TRUNCATE needs both DELETE and DROP on the target table. It removes every row, as a DELETE does, and it retires the table's whole key space (the stored keys that hold its rows and indexes), as a DROP does.

CamusDB checks the two privileges separately, so they can come from two separate grants.

DDL needs the relevant DDL privilege, or superuser status. Two areas need a superuser: user and grant administration, and database lifecycle DDL. The HTTP routes /create-db, /drop-db, and /close-db also require a superuser.

The typed gRPC rows API checks the table named by the request. InsertRow needs INSERT, Query and QueryById need SELECT, UpdateRows and UpdateById need UPDATE, and DeleteRows and DeleteById need DELETE.

Cluster routes that expose topology or change cluster state require a superuser. /v1/cluster/health answers without a credential so an orchestrator can probe the node, but only a superuser sees the detailed fields: the node's role, the partitions it hosts, and the partitions whose replication has stalled. Membership, placement, backfill, snapshot, leave, and replication-factor routes are superuser-only. Leave and replication-factor routes are loopback-only when authentication is off.

Database names are hidden from users who hold no grant that reaches into that database. A statement that runs in such a database fails with CADB0010 DatabaseDoesntExist, the same error returned for an unknown name. This applies to SQL over HTTP and gRPC, the typed rows API, and batch streams. A global grant, a db.* grant, or any grant on one table in the database is enough to make the database visible, but the statement still needs its normal table privilege.

Catalog statements follow the same visibility rule. SHOW DATABASES lists only databases the caller can reach. SHOW TABLES and SHOW TABLES FROM db list only tables on which the caller has any privilege. An invisible target of SHOW TABLES FROM db is reported as DatabaseDoesntExist. SHOW BRANCHES, SHOW ANCESTORS, and SHOW ORPHAN DATABASES omit databases the caller cannot see.

Some statements open no table, such as SHOW TABLES and a SELECT without a FROM clause. An authenticated user may run them when the current database is visible to that user.

Database-level inspection, such as SHOW DATABASE and SHOW ORPHAN TABLES, needs SELECT at database scope: a db.* or *.* grant, or superuser status. A grant on one table makes the database visible, but it does not authorize these database-level reads.

Inspecting a single table needs SELECT on that table, for example SHOW COLUMNS, SHOW CREATE TABLE, and SHOW STATISTICS. SHOW STATISTICS reports bounds taken from real column values, so CamusDB holds it to the same requirement as reading those columns, and nothing higher.

SHOW ENGINE STATS inspects how one node operates. It needs a superuser and is not scoped to a database or table grant.

The REST and gRPC node resource stats calls accept either a superuser or an account with MONITOR. MONITOR is outside ALL PRIVILEGES and is intentionally narrower than superuser access.

The configuration surface has the same requirement. SHOW VARIABLES, SHOW CLUSTER SETTINGS, and SET and RESET CLUSTER SETTING all need a superuser. The last two also change the behavior of every node, and several of the settings they reach bound memory, concurrency, and background work.

One behavior is conservative today. An UPDATE or DELETE with a subquery that reads another table needs the write privilege on that second table, even though SELECT would be enough. The rule is too restrictive, not too permissive.

Runtime defaults​

These defaults are process-level security settings:

SettingKeyDefaultMeaning
Access token lifetimeaccess_token_ttl900000 ms (15 minutes)The absolute lifetime of a bearer token.
Authorization cache TTLauthentication_cache_ttl1000 ms (1 second)The maximum staleness of a cached token or privilege decision on another node. A change made on this node applies to the next request. 0 forces a catalog lookup on every request.
Expired session retentionexpired_session_retention_ms300000 ms (5 minutes)How long an expired session record is kept so a live gRPC batch stream can tell ordinary token expiry apart from revocation.
Password hash iterationspassword_hash_iterations600,000The PBKDF2-HMAC-SHA256 work factor, stored with each credential.
Login KDF concurrencylogin_kdf_max_concurrency8The maximum number of concurrent password verifications on one node.
Login attempts per minutelogin_max_attempts_per_minute20The login rate limit for one account from one source address.
Login attempts per source per minutelogin_max_attempts_per_source_per_minute200The login rate limit across all accounts from one source address.
Login rate limiter entrieslogin_rate_limit_max_entries100,000The maximum number of keys that each login rate-limit counter tracks.
Session reaper intervalsession_reaper_interval_ms300000 ms (5 minutes)How often expired dashboard and bearer-token sessions are deleted. 0 or less turns the sweep off.
Principal cache max entriesauthentication_cache_max_entries10,000The maximum size of the authenticated principal cache on one node.
TLS requirementrequire_tls_when_auth_enabledtrueRefuse a plaintext request that carries a credential, except from loopback. The command-line form is --require-tls-when-auth-enabled true|false.

Errors​

Authentication errors intentionally do not tell the caller which part was wrong: the user, the password, or the token.

CodeMeaning
CADB0512 UserAlreadyExistsCREATE USER targets an existing user, and the statement has no IF NOT EXISTS.
CADB0513 UserDoesNotExistALTER USER, DROP USER, GRANT, or REVOKE targets an unknown user.
CADB0514 UnsupportedAuthPluginIDENTIFIED WITH names an auth plugin that CamusDB does not support.
CADB0515 InvalidPrivilegeGRANT or REVOKE uses an unknown or invalid privilege.
CADB0516 AuthenticationFailedThe credentials are absent, invalid, expired, or revoked, or the user name or password is wrong.
CADB0517 InsufficientPrivilegeThe authenticated caller lacks the privilege that the statement needs.
CADB0518 TooManyAuthAttemptsThe caller passed the login rate limit, or the limit on concurrent password verifications.
CADB0519 InsecureTransportA request with a credential arrived over plaintext while TLS is required.

See Error Codes for the HTTP status code mapping.