Database Access
Let a sandboxed agent query your databases over ODBC without ever holding a connection string or credential
Database access lets the agent query your databases without ever holding a password or connection string. By default the agent can reach no database. IT maps a short name (an alias) to a data source that already exists on the computer, and decides which statements and tables the agent may use. The agent writes ordinary SQL; Levee checks each statement, runs it as the signed-in user, and records it in the audit trail.
What does the agent see?
An ordinary database driver and data source names such as levee-sales. It never sees the connection string, the user name, the password, or any data source it was not granted. Each query is checked against policy and then run outside the sandbox, under the signed-in user's own database identity, through a data source the user already has.
Two consequences matter for your security reviewers:
- Policy can only narrow access, never widen it. The user's existing data sources and the database's own permissions are the ceiling. A Levee rule cannot give the agent more than the user could already do.
- Levee does not store database credentials. Sign-in uses whatever the data source already uses (Windows integrated authentication, a stored credential, and so on).
How does a query flow?
| Step | Where | What happens |
|---|---|---|
| 1 | Sandbox | The agent connects to a Levee data source by alias, for example levee-sales. |
| 2 | Computer | Levee maps the alias to a real host DSN named in policy. An unknown alias is refused. |
| 3 | Computer | Each statement is classified and checked against the alias rule (verbs, tables, dangerous patterns). |
| 4 | Computer | The decision is written to the audit trail before the query runs. If the audit record cannot be accepted, the query is refused. |
| 5 | Computer | The query runs through the host DSN with the rule's row cap and timeout, and the rows are returned to the agent. |
For your IT team: prerequisites on the computer
You need a data source on the computer for each database, a database login with only the rights the agent should have, and a policy rule.
-
A 64-bit ODBC data source for each database you want to expose, created for the Windows user who runs Levee. Any database with a 64-bit ODBC driver works: SQL Server, PostgreSQL, Oracle, MySQL, SQLite, Snowflake, and so on. Levee does not ship or require a particular database driver on the host.
# Example: a per-user 64-bit DSN for SQL Server with Windows authentication Add-OdbcDsn -Name 'Sales-Prod' -DriverName 'ODBC Driver 18 for SQL Server' ` -DsnType 'User' -Platform '64-bit' ` -SetPropertyValue @('Server=sql01.corp.example', 'Database=Sales', 'Trusted_Connection=Yes', 'Encrypt=Yes') # Confirm it exists Get-OdbcDsn -Name 'Sales-Prod' -Platform '64-bit' -
A least-privilege database identity behind that DSN. See Scope the host DSN, not just the policy.
-
A
proxies.odbcpolicy section that maps an alias to the DSN. Without one, every connection is refused.
The Levee ODBC driver is already installed in the Levee container image. You do not install anything inside the sandbox.
Connecting from inside the sandbox
The agent connects by alias, with no password. The agent (or any tool it runs) connects with either form of connection string:
DSN=levee-<alias>
Driver={LeveeODBC};Alias=<alias>
For an alias sales, both DSN=levee-sales and Driver={LeveeODBC};Alias=sales connect to the host DSN mapped to sales.
- Aliases may contain letters, digits,
_and-, up to 64 characters. - Use
SQLDriverConnect(the connection-string form). The olderSQLConnectcall with a separate DSN, user and password is not supported and returns SQLSTATEIM001. - Anything after the alias (user, password, server) is ignored. The agent cannot redirect the connection.
A quick test from the container's PowerShell:
$conn = New-Object System.Data.Odbc.OdbcConnection 'DSN=levee-sales'
$conn.Open()
$cmd = $conn.CreateCommand()
$cmd.CommandText = 'SELECT TOP 5 * FROM dbo.Customers'
$reader = $cmd.ExecuteReader()
while ($reader.Read()) { $reader[0] }
$conn.Close()
Policy reference: proxies.odbc
Each rule names an alias, the data source behind it, and what the agent may do there. Database rules live in the proxies.odbc section of the Levee policy, either in %ProgramData%\Vediram\Levee\policy.json or in the ODBC section of a Portal policy. See How Levee Policy Works.
{
"proxies": {
"odbc": {
"default": "deny",
"rules": [
{
"alias": "sales",
"hostDsn": "DSN=Sales-Prod",
"allowedVerbs": ["SELECT"],
"maxRowsPerQuery": 1000,
"queryTimeoutSeconds": 10,
"tables": { "allow": ["dbo.*"], "deny": ["dbo.CreditCards"] }
},
{
"alias": "hr",
"hostDsn": "DSN=HR-Reporting",
"allowedVerbs": ["SELECT"],
"tables": { "allow": ["*"], "deny": ["salary", "payroll.*"] }
}
],
"dangerousPatterns": ["sp_configure", "OPENQUERY"]
}
}
}
Top-level keys
| Key | Type | Default | Meaning |
|---|---|---|---|
default | "allow" or "deny" | deny | Applies to an alias with no rule. A missing, blank or misspelled value is treated as deny (with a warning in the log). Keep this at deny. |
rules | array | none | One rule per alias. |
dangerousPatterns | string array | baseline list | Extra patterns to block. They add to the built-in baseline and can never remove from it. |
Rule keys
| Key | Type | Default | Meaning |
|---|---|---|---|
alias | string | required | The name the agent uses (levee-<alias>). Letters, digits, _, -; max 64 characters. |
hostDsn | string | required | The host connection string, usually DSN=<your DSN name>. The agent never sees it. |
allowedVerbs | string array | none | Statement types permitted. If present, anything not listed is refused. |
deniedVerbs | string array | none | Statement types always refused. Checked before allowedVerbs. |
tables | object | all tables | { "allow": [...], "deny": [...] }. Only these two keys are accepted. |
maxRowsPerQuery | integer | 10000 | Maximum rows returned for one statement. |
queryTimeoutSeconds | integer | 30 | The query is terminated after this many seconds. |
A malformed or non-positive maxRowsPerQuery or queryTimeoutSeconds falls back to the default and logs a warning.
Statement types (verbs)
Levee recognizes these verbs: SELECT, INSERT, UPDATE, DELETE, MERGE, CREATE, DROP, ALTER, TRUNCATE, EXEC (covers EXECUTE), GRANT, REVOKE, DENY.
- A statement that starts with a
WITHcommon table expression is classified by the statement that follows it (WITH ... SELECTis aSELECT). - Any statement Levee cannot classify is refused, whatever the policy says. That includes
WAITFOR,BACKUP,USE,DECLARE, and statements wrapped in parentheses.
Table patterns
| Pattern | Matches |
|---|---|
* | Every table |
schema.* | Every table in that schema |
name or schema.name | That table, case-insensitively |
- Deny always wins over allow.
- An empty
allowlist means all tables are allowed (subject todeny). - A table is checked by both its bare name and its
schema.name, so denyingsalaryalso covershr.salary.
Dangerous patterns
These patterns are always blocked, even if your policy omits dangerousPatterns:
xp_cmdshell OPENROWSET OPENDATASOURCE xp_regread sp_oacreate BULK INSERT
Add your own to extend the list. Matching ignores string literals and comments, so a pattern mentioned inside a quoted value does not trigger a block.
Validation behavior
Levee validates each rule when policy is applied:
- With
default: "deny", a malformed rule is skipped, so its alias simply has no access. - With
default: "allow", a malformed rule is turned into deny-all for that alias. If Levee cannot even identify the alias, the whole update is rejected and the previous database policy stays in force. Anallowdefault with noruleskey is also rejected. tablesaccepts onlyallowanddeny. A misspelling such asdeniedmakes the rule malformed.- Unknown rule keys (for example
hostdsnordenyVerbs) are accepted but logged as warnings, and have no effect. Check the log after editing policy by hand.
How is a statement checked?
One statement at a time, against a fixed order of checks. The first check that refuses stops evaluation. Checks run in this order:
- More than one statement in the request is refused, including batches stacked without semicolons.
- Dangerous pattern found: refused.
- Unclassifiable statement: refused.
- No rule for the alias:
defaultapplies. - Verb in
deniedVerbs: refused. - Verb not in
allowedVerbs(when the list is present): refused. - A referenced table is denied or not allowed: refused.
- Otherwise the statement is permitted, audited, and run with the rule's row cap and timeout.
The alias itself is also checked when the connection opens. An alias with no rule under a deny default cannot connect at all.
Can the agent see tables it may not query?
No. Catalog calls (SQLTables, SQLColumns) are subject to the same table rules. Tables the agent may not query do not appear in listings, so the agent cannot discover their names or columns through metadata.
What does the agent see when something is refused?
A normal database (ODBC) error, so the agent and its tools report it like any other database error. Common ones:
| SQLSTATE | Message | Meaning |
|---|---|---|
08004 | Connection refused: unknown or unauthorized alias | No rule for this alias, or it is not permitted. |
08004 | Connection refused: audit unavailable (fail closed) | The connection could not be recorded, so it was refused. |
42000 | Multiple SQL statements in a single request are not allowed | Send one statement per call. |
42000 | Dangerous SQL pattern blocked: <pattern> | A baseline or policy pattern matched. |
42000 | Unrecognized SQL statement denied (verb could not be classified) | Rewrite as a plain supported statement. |
42000 | No policy rule for alias '<alias>' and default is deny | Add a rule for the alias. |
42000 | Verb <VERB> is explicitly denied | The verb is in deniedVerbs. |
42000 | Verb <VERB> is not in allowed verbs [...] | The verb is not in allowedVerbs. |
42000 | Table '<t>' is denied by policy / Table '<t>' is not allowed by policy | A table rule refused the statement. |
42000 | No policy configured; query denied (fail closed) | No database policy has reached the project yet. |
HY000 | Query refused: audit channel unavailable (fail closed) | The audit record could not be accepted. |
HYT00 | Query timed out and was terminated by policy. | queryTimeoutSeconds was reached. |
HY000 | Query execution failed. See host logs for details. | The database itself returned an error. Details are in the host log, not shown to the agent. |
01000 (warning) | Result set truncated by row-cap policy; not all rows were returned | maxRowsPerQuery was reached. |
Database errors are deliberately not passed through verbatim, so the agent cannot use error text to learn about the schema. See Database queries refused for fixes.
What happens when something is not available?
The query is refused. In detail:
- No policy yet: when a project starts, connections, queries and metadata calls wait up to 30 seconds for the database policy to arrive, then are refused.
- Levee service unavailable: if the project loses contact with the Levee service, database policy is dropped after a 45-second grace period and everything is refused until the service pushes policy again.
- Audit unavailable: a connection is refused unless its audit record is confirmed stored; a query or metadata call is refused unless its audit record is accepted.
ODBC feature support
Plain SQL tools work; tools that rely on prepared or parameterized statements do not. The Levee driver is a 64-bit Unicode ODBC 3.8 driver. It reports the DBMS name LeveeODBC.
| Supported | Not supported |
|---|---|
SQLDriverConnect with DSN=levee-<alias> or Driver={LeveeODBC};Alias=<alias> | SQLConnect (DSN, user, password form) |
SQLExecDirect, SQLFetch, SQLGetData, SQLMoreResults | SQLPrepare, SQLExecute, SQLBindParameter (parameterized queries) |
SQLDescribeCol, SQLColAttribute, SQLNumResultCols | SQLBindCol |
SQLTables, SQLColumns (filtered by policy) | SQLPrimaryKeys, SQLStatistics, SQLGetTypeInfo |
SQLGetInfo, SQLGetFunctions, diagnostics |
Behavior worth knowing:
- Rows arrive in one response, up to
maxRowsPerQuery. Rows beyond the cap cannot be fetched; narrow the query instead. SQLRowCountalways returns-1, so affected-row counts forINSERT,UPDATEandDELETEare not available.- Transactions follow the host DSN's default (usually autocommit).
SQLEndTranhas no effect. DECIMALandNUMERICvalues are returned as double-precision numbers,BITas a small integer, and binary or unknown types as text.- Up to 20 simultaneous connections per alias.
Tools that rely on unsupported calls (for example, some ORMs that always prepare statements) will fail. Plain SQL through pyodbc cursor.execute(sql) without parameters, PowerShell System.Data.Odbc, and similar direct-execution clients work.
Audit
Every connection, statement and decision is recorded in the Levee audit trail. See Audit Event Reference.
| Event | Recorded when |
|---|---|
odbc.connect / odbc.connect.denied | A connection is opened or refused |
odbc.query | A statement passed policy and is about to run |
odbc.query.denied | A statement was refused (the reason is included) |
odbc.query.timeout | A statement hit queryTimeoutSeconds |
odbc.security.dangerous_pattern | A dangerous pattern was matched |
odbc.metadata | A catalog call was made |
odbc.disconnect | A connection closed |
odbc.policy.partial / odbc.policy.rejected | A policy update was partly or wholly rejected |
Audit records include the SQL text (truncated to 500 characters), because auditors need to see what was run. Treat the audit store accordingly. The diagnostic log, by contrast, never contains SQL text or connection strings.
Scope the host DSN, not just the policy
Give the agent's data source a database login that can do no more than the rule allows. That is the strongest control.
Levee's statement checks are a policy guard, not a full SQL parser. They classify the leading statement and look for table names and known-dangerous constructs. That is effective against ordinary misuse, but a determined attacker can hide intent inside constructs a guard cannot fully understand, such as dynamic SQL built from string fragments.
The strongest control is the database's own permission system. For every hostDsn:
- Use a database login that has no more privilege than the rule is meant to allow. For a read-only rule, use a login that can only
SELECTfrom the intended schemas. - Do not point a Levee alias at a DSN that uses a DBA or owner account.
- Prefer a dedicated reporting replica for exploratory agent work.
With that in place, the Levee rule and the database grant back each other up: the rule gives you fast, audited, human-readable control, and the database guarantees the ceiling.