04 · DATABASE MANAGER
Production access: requested, approved by someone else, set to expire.
Your organization sets, database by database, who approves, when a change may go in and how long access lasts. The platform enforces that rule: the password is created in the Agent, sensitive data comes back masked, and a guardrail refuses a DELETE without WHERE.
Opens a WhatsApp chat. Prefer email? heimdall@yops.cloud
What it does
The request
Whoever needs access requests a database, the operations — SELECT, INSERT, UPDATE, DELETE — the tables or schema, and a reason, which is mandatory. A request that mixes reads with writes is treated entirely as a write.
Who approves, named database by database
Each database has three approver lists: access, command and migration. A database with nobody on a list refuses every request of that kind, with NO_NAMED_APPROVERS. A database can require two approvals. Nobody approves their own request, whatever their permissions, and the approver's role is read from the organization at that moment, not from their token.
How long it lasts
Your organization sets, per database, a default duration and a ceiling for reads and another for writes — for example, writes for 2 hours, up to 8. The clock starts at approval. Above any configuration, no access exceeds 30 days, and a request nobody decides lapses after 7.
A password nobody sees
With provisioning on, approval creates a PostgreSQL role for that access alone, with only what was requested and VALID UNTIL set to the end of the term. The password is created in the Agent and stays there; whoever connects goes through the proxy, and the Agent authenticates on their behalf.
Access ends, in three layers
PostgreSQL itself refuses the role when the term ends, even with all of Heimdall down. The proxy asks the API, on every new connection, whether access still holds — and in an open session, the statement after a revocation is refused. And a sweep drops the role through the Agent.
Inside the session
Every result comes back with sensitive columns masked, and every statement goes through your organization's guardrails before it reaches the database. The next two sections show how: masking and guardrail.
Schema changes
A migration is submitted, analyzed, approved by someone else and scheduled inside the database's time window, in an IANA time zone. The Agent runs it inside your network. The window is checked at approval, at scheduling, and at the moment it runs.
In an emergency: break-glass
Whoever is on call gets in without waiting for approval, for up to 4 hours. It is a permission of its own, which no role has by default. Approvers are notified first — if the notice fails, entry is refused — masking and the guardrails still apply — except a guardrail your organization marked as bypassable, which warns instead of refusing — and someone else reviews the event afterwards.
What is recorded
Every request, approval, refusal, revocation and break-glass, and the text of every SQL statement run through the proxy, with its type, outcome, rows affected and duration. Every column classification and every masking rule created or deleted.
How it works
One access, from request to expiry:
- The person requests through the CLI. That database's named approvers are notified.
- Someone else approves. The API checks the list, the role and the duration, and records the decision.
- The API queues the work. The Agent bound to that database's
environmentpicks it up and creates the role, with the administrative credential only it holds. - The person connects through the edge, with their personal certificate. The proxy asks the API whether access holds and authenticates to the database with the password that never left the Agent.
- On every statement, the proxy checks the
guardrails before sending it to the database. On every result, it replaces the values of masked columns before returning it. The database doesn't change, and the result never goes to the API. - Whatever a
guardrailrefused becomes a command: someone else approves it, and it runs once, in the requester's session. - When the term ends, PostgreSQL refuses the role, and the Agent drops it.
heimdall db access request pg-orders-orders --operations SELECT,UPDATE --tables public.orders,public.customers --reason "corrigir pedidos com total errado" --task-key OPS-101
✓ Access request submitted for database 'pg-orders-orders'
Operations: SELECT, UPDATE
Reason: corrigir pedidos com total errado
Task: OPS-101
Expires: 2026-10-10T08:13:58.978413Z
Duration: this database's default for those operations
('heimdall db expiry show --database pg-orders-orders' shows the defaults and the ceilings.)
Status: pending approval
An approver decides it with 'heimdall db access approve <grant-id>'.
heimdall db access approve 05512517-3276-4918-af87-79ef234fbd9d
✓ Access request 05512517-3276-4918-af87-79ef234fbd9d approved
Status: active
The requester can connect now with 'heimdall db connect <database>'.
IN THE DATABASE: THE ROLE, WITH VALID UNTIL, AND ONLY WHAT WAS REQUESTED
psql -h 127.0.0.1 -U orders_admin -d orders -tA -c "select rolname, rolvaliduntil from pg_roles where rolname like 'hd\_%'" -c "select table_name, privilege_type from information_schema.role_table_grants where grantee like 'hd\_%' order by 1, 2"
hd_pg_orders_orders_7cyjmwi7_hdm|2026-10-10 08:14:06+00
customers|SELECT
customers|UPDATE
orders|SELECT
orders|UPDATE
heimdall db access request and heimdall db access approve, and the role created in the database.masking by source column
Whoever queries production through the proxy gets [CPF] instead of the tax ID. The rule follows the column the value came from, not the name it arrives under.
Classify, then mask
Classifying a column records what it holds — personal, financial, health data, credentials — in a closed vocabulary your organization can extend. Masking is a separate act: the rule that replaces the value. It names a column, a strategy and a scope — the whole organization, an instance, a database, a schema or a table — and applies only inside it.
By source column
For every field in a result, PostgreSQL says which table and which column it came from, and the proxy matches the rule against that column. Renaming it with AS, or going through a subquery, a CTE or a view, doesn't change the origin. A field that comes from no column — an expression, a function, a UNION, the whole row — comes back under the strictest strategy in force in the session, even a count(*): without knowing what a function reveals, the proxy doesn't let it reveal anything.
The other doors
In a masked session, COPY … TO STDOUT is refused, with an instruction to read the rows with SELECT. The server's error message arrives with only its code, because it can quote the value. Copying the column into a temporary table doesn't launder it: reading it back comes masked. And the decision doesn't depend on the tool: the proxy asks the database to describe each result before running it, and a row with no decision drops the connection instead of passing.
Where it happens
In the Agent, on the way back. The database doesn't change — no extra view or column — and the API sees neither the original value nor the masked one.
NOTICE: Heimdall authenticated this connection as PostgreSQL role "hd_pg_orders_orders_7cyjmwi7_hdm" under access grant 05512517-3276-4918-af87-79ef234fbd9d; any credentials you supplied were not used and were not sent to the database.
psql (18.6, server 17.11)
Type "help" for help.
=> select cpf as documento, upper(email) as e, name from customers limit 1;
documento | e | name
-----------+---------+-----------
[CPF] | [EMAIL] | Ana Souza
(1 row)
cpf under another name and email inside a function, both masked.What the person querying gets
| STRATEGY | WHAT ARRIVES |
|---|---|
full |
[REDACTED], or *** for a value of up to three characters |
partial |
the first two and last two characters: an***st |
redact |
a marker by the value's shape: [EMAIL], [CPF], [PHONE], [CREDIT_CARD] or [REDACTED] |
hash |
the first 16 characters of the SHA-256, unsalted: it lets you compare two values, not hide one with few possible forms, such as a tax ID |
When two rules match the same column, the strictest wins.
How far it goes
masking protects against accidental exposure and against the tool: the screen, the exported file, the screenshot that ends up in a ticket. It does not protect against someone who holds the access and writes SQL to extract the value. For someone who must not see a column, the control is the scope of the access: the request names the tables, and SELECT-only access can't copy the value into another column. masking applies to sessions through the proxy — heimdall db connect, heimdall tunnel and break-glass; the application, with its service user, and the migration connect to the database directly.
guardrails that refuse before the database
A DELETE without WHERE never reaches the database. To run what a guardrail refuses, someone else approves that statement, and it runs once.
The rule is yours
No guardrail ships installed. Your organization writes each one and assigns it to the whole organization, an instance or a database: forbidden statement types, such as DROP TABLE, TRUNCATE and DROP SCHEMA; UPDATE and DELETE only with WHERE; indexes only CONCURRENTLY; NOT NULL columns only with a DEFAULT; no DROP COLUMN; and a row ceiling above which a table can't be wiped or rewritten whole.
Refuse, warn or record
Each guardrail has a severity: block refuses, warn lets it through and records it, audit_only only records it. It is evaluated in three places: in the proxy, on every statement in the session, before access is even checked; when a migration is submitted, which is refused; and when a command is submitted, where it doesn't refuse — it annotates the command for the approver.
Read the way the database reads it
The statement is read without comments and without the contents of literals, under PostgreSQL's own rules for whitespace and line breaks: a WHERE inside a comment doesn't count, and a \r doesn't hide a DROP. EXPLAIN ANALYZE is judged by what it runs, and a write inside a WITH counts as a write. What can't be judged from the text — DO, CALL, EXECUTE, COPY, MERGE — is refused by every block guardrail, and only runs once approved.
The refusal shows the way
The refusal names the guardrail and hands over the command that asks for approval. Submitting doesn't refuse: it records every guardrail the text crosses — including in a statement hidden after a harmless one — and that is what the approver reads before deciding. Once recorded, the annotation doesn't change.
Approved, it runs once
Approval doesn't execute anything: the requester runs the statement in their own session, within 4 hours. It runs once, compared by what the server would execute rather than by the text — moving the WHERE into a comment makes it a different command. The second run is refused again. And approval doesn't widen access: the tables and operations are still those of the request.
UPDATE without WHERE refused, submitted with the guardrails it crosses, run once after approval and refused the second time.1 · IN THE SESSION, THE REFUSAL
NOTICE: Heimdall authenticated this connection as PostgreSQL role "hd_pg_orders_orders_7cyjmwi7_hdm" under access grant 05512517-3276-4918-af87-79ef234fbd9d; any credentials you supplied were not used and were not sent to the database.
psql (18.6, server 17.11)
Type "help" for help.
=> UPDATE customers SET name = 'cliente de teste';
ERROR: HEIMDALL GUARDRAIL: UPDATE without WHERE clause is blocked by guardrail "DELETE and UPDATE need a WHERE" [require-where]. To run it, get it approved first, then run it again: heimdall db command submit 'b4bc0108-1814-47df-a9a9-7e591423dae6' --reason '<why this must run>' --sql 'UPDATE customers SET name = '\''cliente de teste'\'';'
2 · THE SUBMISSION ANNOTATES IT FOR THE APPROVER
heimdall db command submit 'b4bc0108-1814-47df-a9a9-7e591423dae6' --reason 'OPS-48: renomear todos os clientes de teste' --sql 'UPDATE customers SET name = '\''cliente de teste'\'';'
✓ Command 2fb2533e-b875-452f-bbd5-50f3f0bab8b8 submitted for database 'pg-orders-orders'
Status: pending_review
It runs only after somebody else approves it.
Guardrails it crosses, shown to the approver:
block: UPDATE without WHERE clause is blocked by guardrail "DELETE and UPDATE need a WHERE" [require-where]
3 · APPROVED, IT RUNS ONCE
NOTICE: Heimdall authenticated this connection as PostgreSQL role "hd_pg_orders_orders_7cyjmwi7_hdm" under access grant 05512517-3276-4918-af87-79ef234fbd9d; any credentials you supplied were not used and were not sent to the database.
psql (18.6, server 17.11)
Type "help" for help.
=> UPDATE customers SET name = 'cliente de teste';
UPDATE 2
=> UPDATE customers SET name = 'cliente de teste';
ERROR: HEIMDALL GUARDRAIL: UPDATE without WHERE clause is blocked by guardrail "DELETE and UPDATE need a WHERE" [require-where]. To run it, get it approved first, then run it again: heimdall db command submit 'b4bc0108-1814-47df-a9a9-7e591423dae6' --reason '<why this must run>' --sql 'UPDATE customers SET name = '\''cliente de teste'\'';'
How far it goes
A guardrail judges the text, without opening the database catalog: a function that writes, called inside a SELECT, is left to the role's privileges — which hold only what was requested. It applies to sessions through the proxy and to migration and command submissions; the application, with its service user, doesn't go through it. During break-glass, a guardrail your organization marked as bypassable warns instead of refusing.
Where the data lives
Everything runs in your infrastructure. The split below matters inside your company: it says who on your team can reach what.
| IN THE AGENT | IN THE API, WHICH IS YOURS |
|---|---|
Each instance's administrative credential, in the secrets store. Each access role's password, generated there. Each | The name of every database, schema, table and column, with the row estimate and each column's classification. Every role name. The |
Who approves what
| ACTION | WHO |
|---|---|
Request access, submit a migration or a command |
every member |
Approve access, commands and migrations |
owner and admin, by default — never the requester. The named list decides who is notified and whether the request can be made |
Write guardrails, approvers, windows and durations |
owner and admin |
Classify columns and write masking rules |
owner and admin |
Invoke break-glass |
only whoever a policy names, such as the on-call group. A database can forbid it |
Review a break-glass |
owner and admin — never the invoker |
| Revoke an open access | owner and admin |
With the other modules
-
03
Secret Manager
Each database's administrative credential is a
secretin the Agent; everyservice userpassword is created and rotated there. -
05
Internal app access
A database session arrives through the same edge; whoever connects needs both the network grant and the database approval.
-
07
Incident Manager
A
break-glassinvoked while an incident is open on the database is tied to it, with nobody typing a number. The incident names the entry; it never grants it.
Talk to the people who build Heimdall.
Tell us how your engineers store secrets and reach the database today. We'll answer with what Heimdall would put under rules first, and how the deployment would get there.
Opens a WhatsApp chat. Prefer email? heimdall@yops.cloud