Skip to main content

Authentication and authorization

CamusDB can enforce SQL authentication and per-object authorization for clients. Authentication is off by default. An existing development deployment therefore continues to work until an operator enables authentication.

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.
  • A request must include a valid bearer token.
  • CamusDB checks every statement against the privileges that its tables and its databases require.
  • CamusDB refuses a request that carries a credential over a plaintext connection. There are two exceptions: a request from loopback, and a deployment where you disabled the TLS requirement.

CamusDB stores a password only as a salted PBKDF2-HMAC-SHA256 verifier. An ordinary client request uses a short-lived opaque bearer token. It does not send the password again.

Enable authentication​

Configure the authentication switch and the secrets through environment variables, or through a secret provider. Do not configure them in config.yml. Do not put a token key or a bootstrap password in YAML. The TLS topology setting is not a secret. 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 the access-token secrets at rest. It must be at least 32 bytes. Use a random value such as openssl rand -hex 32. Every node of 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 initial password of the bootstrap superuser.
CAMUSDB_NODE_SECRETin a cluster deployment with authThe shared secret for the internal routes between nodes, used while auth is enabled. When set, it must be at least 32 bytes. Every node of the cluster must use the same value.

Here is an 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, three rules apply:

  1. CamusDB creates one bootstrap superuser from the supplied values, if the auth catalog is empty.
  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 remains a valid single-node configuration; it leaves peer routes refused instead of guarded by a weak shared secret.

There is no default user, and there is no default password.

Login and logout​

Use /login to exchange a user name and a 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>

A token has an absolute lifetime of 15 minutes by default. There is no refresh token. A client logs in again after a token expires.

The login response reports the deadline of the token in two forms:

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

Renew from those two fields. Do not assume a fixed lifetime. An operator can change AccessTokenTtl. A client must renew before the reported deadline.

Three events invalidate an outstanding token: a change of the password, a DROP USER, and a call to /logout.

gRPC tokens​

A gRPC client uses the same bearer token, in the request metadata:

authorization: Bearer camus_<id>.<secret>

Use the CamusAuth gRPC service to obtain a token and to revoke one:

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.

A gRPC-only deployment therefore does not need the HTTP API. Its clients do not need a call to /login.

An authentication error and an authorization error map to an ordinary gRPC status code:

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. A request from loopback is exempt. Development on one host therefore 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, at a sidecar, or in a service mesh. The CamusDB process then sees only the trusted plaintext hop. In that topology, disable the transport check. Keep authentication and the 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. It therefore belongs in the ordinary configuration. Do not turn it off for a node that a client can reach directly.

Manage users​

The statements for user management are server-level statements. They need no database context. They need 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 sha256_password only. It uses that plugin by default when you omit IDENTIFIED WITH.

CREATE USER grant_target creates a target for a grant, without a password. That user cannot log in. Set a password with ALTER USER first.

Use a bound parameter for a password:

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

A parameter keeps a cleartext secret out of the shell history, out of the traces, and out of the query logs. A password has a maximum length of 1 KiB. Both password literals in an ALTER USER ... REPLACE ... statement are redacted from server logs.

When a user changes 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 the privileges on an object:

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;

REVOKE INSERT ON app.* FROM myapp;

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

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

SHOW GRANTS FOR * returns all grants for all accounts, ordered by account name and object. An account with no grants produces no row; use SHOW USERS to list 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 the data of a table. Inspect the metadata of a table, 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 the metadata of a table or of a database 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.
ALL PRIVILEGESThe union of the concrete privileges that CamusDB knows today.

A grant has one of three scopes. The list starts with the broadest scope:

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

Grants add together, and a repeated grant is harmless. A grant of 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 of a table. A rename therefore keeps the grant. A table that you drop and create again does not inherit the grants of the old table.

GRANT never creates a user. It also 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 continues to resolve 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 distinguish ordinary expiry 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 advances a replicated coherence generation, 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. 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 it runs the statement.

A read of a table needs SELECT on every table that the statement references. That rule covers a join, a derived table, a subquery, a semi-join, EXISTS, IN, and EXPLAIN for a query that reads a table.

A write needs the matching write privilege:

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

TRUNCATE needs both DELETE and DROP on the target table. It removes every row, which is an effect of a DELETE. It also retires a whole key space, which is an effect of a DROP.

CamusDB checks the two privileges one at a time. They can therefore come from two separate grants.

DDL needs the relevant DDL privilege, or superuser status. Two areas need a superuser: the administration of users and grants, and the DDL for the lifetime of a database. 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 detailed role, partition, and stalled-range fields. Membership, placement, backfill, snapshot, leave, and replication-factor routes are superuser-only; leave and replication-factor routes are loopback-only when authentication is off.

Some statements open no table. Any authenticated user may run them. Examples are SHOW TABLES, SHOW DATABASE, and a SELECT without a FROM clause.

Inspection of one table needs SELECT on that table. Examples are SHOW COLUMNS, SHOW CREATE TABLE, and SHOW STATISTICS. SHOW STATISTICS reports bounds taken from real column values. CamusDB therefore holds it to the same requirement as a read of those columns, and to nothing higher.

SHOW ENGINE STATS inspects the operation of one node. It needs a superuser. It is not scoped to a grant on a database or on a table.

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. Several of the settings that they reach bound memory, concurrency, and background work.

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

Runtime defaults​

These defaults are security settings at process level:

SettingDefaultMeaning
Access token lifetime15 minutesThe absolute lifetime of a bearer token.
Authorization cache TTL1 secondThe maximum staleness of a cached token or privilege decision on another node. A change made on this node applies to the next request.
Expired session retention5 minutesHow long an expired session record is kept so a live gRPC batch stream can distinguish ordinary token expiry from revocation.
Password hash iterations600,000The PBKDF2-HMAC-SHA256 work factor, stored with each credential.
Login KDF concurrency8The maximum number of concurrent password verifications on one node.
Login attempts per minute20The login rate limit, per account.
Login attempts per source per minute200The login rate limit across all accounts from one source address.
Session reaper interval5 minutesHow often expired dashboard and bearer-token sessions are deleted.
Principal cache max entries10,000The bound of the cache of authenticated principals, on one node.
TLS requirementenabledRefuse a plaintext request that carries a credential, except from loopback. Configure it as require_tls_when_auth_enabled, or as --require-tls-when-auth-enabled true|false.

Errors​

An authentication error does not reveal which part was wrong. It does not tell the caller whether the user, the password, or the token was wrong. That behavior is intentional.

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. The user name or the password is also wrong in some cases.
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 map to HTTP status codes.