Skip to main content

Access Management

Caller access management authenticates the caller of a StackQL session and authorises each statement against a policy. It is independent of provider credentials, which continue to be supplied through --auth, environment variables or provider sessions. It is disabled by default, and deployments that do not enable it behave as before. Available in StackQL v1.0 and later.

See also:
[ Global Flags ] [mcp] [whoami] [groups]


Enabling access management​

Access management is switched on by starting StackQL with the --access.config global flag pointing at a JSON configuration file. The example starts an interactive shell in protected mode; exec and mcp are enabled the same way.

stackql --access.config '{"path": "/etc/stackql/access.json"}' shell

--access.config takes a JSON envelope with a single path key; a bare file path is also accepted. The configuration is loaded once at startup, so rotate keys or policy through a controlled restart.

While access management is enabled only exec, shell, mcp and version run; other commands, including registry, refuse rather than bypass the protected path. srv and the MCP reverse_proxy server type also refuse, because the PostgreSQL wire dependency does not yet expose per-connection authentication evidence. The SQL AUTH and AUTH REVOKE statements refuse in protected mode; provision provider credentials through host configuration instead. A protected exec that fails returns exit status 1 after presenting the error.

Authentication modes​

The configuration file selects one type: jwt (the default when omitted), bearer, mtls or local_os. Configuration is strict JSON: unknown fields, and fields belonging to another mode, are rejected. All modes share the same rules and the same principal directory.

JWT​

The caller presents a signed token, which is verified offline against the keys pinned in the configuration. A minimal configuration names the issuer and audience the token must carry, lists one public key, and grants the readers group read access:

{
"type": "jwt",
"issuer": "https://identity.example",
"audience": "stackql",
"keys": [
{"kid": "primary", "algorithm": "EdDSA", "public_key_pem": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n"}
],
"rules": [
{"subject": "group:readers", "resource": "host/sql/read", "action": "execute"}
]
}

The issuer, audience and public keys are pinned in the configuration; there is no OIDC discovery. Accepted algorithms are EdDSA, RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384 and ES512, and RSA keys must be at least 2048 bits. Each key is bound to its configured algorithm. Tokens with an unknown key ID, an invalid signature, a missing or past expiry, a future nbf or iat, or a wrong issuer or audience are rejected. Group claims and URLs carried in the token are not used for trust or directory membership.

Static bearer token​

The caller presents an opaque token, and the configuration lists the SHA-256 digest of each accepted token with the subject it identifies. This suits a small, fixed set of service identities where running a token issuer is not warranted:

{
"type": "bearer",
"issuer": "local-tokens",
"tokens": [{"subject": "alice", "sha256": "<64 hexadecimal characters: the SHA-256 of the token>"}],
"rules": [{"subject": "group:readers", "resource": "host/sql/read", "action": "execute"}]
}

Only the SHA-256 digest of each token is configured, and digests must be unique. Changing the configured digests requires a restart.

Mutual TLS​

The caller is identified by the client certificate it presents on the HTTPS MCP transport. The configuration carries the CA that must have issued the certificate and says which subject alternative name holds the identity:

{
"type": "mtls",
"issuer": "client-certificates",
"client_ca_pem": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n",
"certificate_identity": "uri_san",
"rules": [{"subject": "group:readers", "resource": "host/sql/read", "action": "execute"}]
}

The client certificate chain is verified against client_ca_pem and the identity subject is taken from exactly one SAN of the configured kind, uri_san or dns_san. There is no Common Name fallback. Mutual TLS is available over the HTTPS MCP transport only.

Local OS identity​

No credential is exchanged: the subject is the operating system identity that the StackQL process runs as. The configuration needs only an issuer label and the rules:

{
"type": "local_os",
"issuer": "this-host",
"rules": [{"subject": "group:readers", "resource": "host/sql/read", "action": "execute"}]
}

The identity subject is the effective Unix UID or Windows SID of the StackQL process, including numeric container UIDs with no passwd entry. No credential is presented and STACKQL_CALLER_TOKEN is ignored. This mode is local only: it is refused for HTTP callers. Operating system group membership does not grant access; only directory membership does.

Session deadline​

Queries have no authentication-derived deadline by default. Credentials are verified and directory membership is refreshed for each new statement, so an expired credential cannot start new work, but expiry does not interrupt a statement that is already authorised. Set "enforce_session_deadline": true to cancel at max_session_seconds (default 300, maximum 86400), shortened by JWT or certificate expiry. This does not renew tokens.

Principal directory​

Identities are mapped to principals, and principals to groups, in four tables in the configured SQLite or PostgreSQL backend:

TableContents
stackql_access_principalsPrincipal ID and a JSON object of string attributes. A tenant attribute namespaces preview IaC state.
stackql_access_identitiesVerified (issuer, subject) pair to principal ID.
stackql_access_groupsGroup IDs.
stackql_access_membershipsPrincipal to group bindings.

Provision the tables, identities and memberships through a trusted database administration connection before serving protected queries:

INSERT INTO stackql_access_principals (id, attributes) VALUES ('alice', '{}');
INSERT INTO stackql_access_groups (id) VALUES ('readers');
INSERT INTO stackql_access_identities (issuer, subject, principal_id)
VALUES ('https://identity.example', 'alice', 'alice');
INSERT INTO stackql_access_memberships (principal_id, group_id) VALUES ('alice', 'readers');

Use the mode's actual issuer and subject: for local OS identity this might be ('this-host', '1000'), and for mutual TLS ('client-certificates', 'spiffe://example/alice'). Select the backend explicitly:

stackql --access.config '{"path":"/etc/stackql/access.json"}' \
--sqlBackend '{"dbEngine":"postgres_tcp","dsn":"postgres://runtime@localhost/stackql?sslmode=require","sqlDialect":"postgres"}' \
exec 'SELECT whoami(), groups();'

The directory produces an immutable snapshot for each statement. Revocation is a DELETE FROM stackql_access_memberships on the administration connection; the next statement sees the new snapshot. Unknown identities, an absent schema, malformed data and directory outages all deny access. Ordinary protected SQL cannot address the directory tables. Keep membership writes on the administration connection, protect the database file, and use separate administrative and runtime database privileges where the backend supports them.

Policy rules​

Each rule grants one action on one resource to a subject of the form group:<group id>. Use the compiled method and operation identifiers from trusted provider documents; an operation wildcard grants mutation methods as well as reads.

The readers group implicitly holds the host's safe-read SQL capability. Reads by principals outside that group require host/sql/read, and other statements require execute on host/sql/<statement kind>. Provider operations and credential use always require explicit rules, and a collection grant does not grant its underlying methods. Additional host checks:

ResourceActionScope
host/sql/readexecuteRead statements outside the implicit reader group
host/sql/execexecuteNative EXEC, including nested EXEC
host/iac/run, host/iac/destroyexecuteThe preview IaC relations, despite their SELECT syntax
host/function/<name>executeFunctions outside the small allowlist of known-pure functions
host/backendreadOpaque stored and external backend relations
host/credentialsreloadThe MCP reload_credentials tool
session/variablesreadDereferencing session variables and secrets
session/configwriteSET and UNSET

Stored query expansion is checked before execution, and plan and row cache reuse is disabled on protected paths.

Entry points​

Authenticationexec, shell, MCP stdioHTTPS MCP
JWTSTACKQL_CALLER_TOKEN environment variableAuthorization: Bearer request header
Static bearerSTACKQL_CALLER_TOKEN environment variableAuthorization: Bearer request header
Local OSEffective process UID or SID; no credentialRefused
Mutual TLSRefused: no verified TLS connectionVerified client chain and one configured SAN

Supply local token credentials through the process environment, never as a SQL literal or a command-line argument. Over HTTPS the caller verifier replaces the shared auth_token_env_var bearer gate described in HTTP client authentication; server TLS certificate and key configuration is mandatory, and a request without credentials never falls back to the server's environment token.

Inspecting the caller​

In an authenticated session, whoami() returns the principal ID and groups() returns the directory groups as a JSON array:

SELECT whoami() AS principal, groups() AS memberships;
|-----------|-------------|
| principal | memberships |
|-----------|-------------|
| alice | ["readers"] |
|-----------|-------------|

Both functions require a session started with --access.config.

Walkthroughs​

The StackQL repository ships four executable walkthroughs, one per authentication mode, driven by a Python script that provisions a demonstration directory and verifies a query. They need no cloud account and are run by the project's test suite.