Customer-Controlled Scope
Scope is an allowlist of database objects (schema.table patterns) declared in the agent config. The agent filters introspected schema before reporting it to the control plane, rejects grants on objects outside the list, and refuses to load policies that reference out-of-scope objects in masking rules or row filters. Scope is the boundary between your network and the daam cloud. It is declared in a file you control and enforced inside your network.
What Scope Does
Without scope set, the agent reports the full introspected schema upstream and trusts the control plane to author sane policies. The control plane does not validate policy grants against the introspected schema, so the agent's grant-time check is the only barrier. With scope set, the agent becomes the source of truth for what daam can see and act on.
- Data minimization. Out-of-scope relation names, comments, columns, and types never enter the schema report sent upstream.
- Defense in depth. Even if a stale or compromised control plane pushes a policy referencing an out-of-scope object, the agent rejects the grant locally.
- Auditable. The pattern list is in a config file in your repo. The introspect subcommand can diff config against live upstream. Violations are persisted as a 7-day hygiene signal on the agent detail page.
Scope is a trust-boundary feature, not an agent-process-compromise mitigation. A compromised agent retains the upstream DSN and can introspect the full schema locally regardless of scope. See What Scope Does Not Protect Against.
Configuration
Declare scope under upstream.scope in the agent config file:
upstream:
dsn_file: /etc/daam-agent/dsn
scope:
- public.users
- public.orders
- analytics.*
- reporting.daily_* Field semantics
The presence of the scope field is the signal that scope is active. The empty-list case is meaningfully different from the absent case: it represents an explicit operator choice rather than an unconfigured default.
| Form | Meaning |
|---|---|
| Field absent | Allow-all. Existing behavior; back-compat for upgrades. |
scope: [] | Deny-all. Explicit "this agent reports and grants nothing." |
scope: [patterns] | Restricted. Allow only objects matching at least one pattern. |
Pattern syntax
Patterns are case-folded schema.table strings with * as a wildcard matching any sequence of characters within a single segment. The wildcard does not cross the . separator.
| Pattern | Matches |
|---|---|
public.users | Exactly public.users |
public.* | All tables in public |
*.users | A table named users in any schema |
analytics.events_* | analytics.events_clicks, analytics.events_views, ... |
*.* | All user objects (equivalent to allow-all, but explicit) |
- Identifiers are compared case-folded, mirroring how PostgreSQL handles unquoted identifiers.
- Quoted identifiers containing dots (e.g.
"weird.name") are not supported and treated as unmatchable. - System schemas (
pg_catalog,information_schema,pg_toast,pg_temp_*) are implicitly excluded and cannot be matched by any pattern, not even*.*. - Negation (
!public.secrets) and regular expressions are not supported. See Limitations.
Strict config decoding. The agent rejects unknown YAML keys at startup. A typo such as scopes: or upstream.scopes: fails fast rather than silently falling back to allow-all, which would be a security-critical regression.
Enforcement Points
Scope is enforced at four independent sites. A stale policy or compromised control plane cannot route around a single check: every downstream pathway that touches an object name goes through the matcher.
- Schema introspection filter. On every introspection (initial connect, schema_request from the control plane, SIGHUP), out-of-scope relations are removed from the payload before it is sent. The control plane has no API to request the unfiltered result.
- Grant issuance. When the agent translates a policy into
GRANTstatements on an ephemeral role, every(schema, table)target is checked. Out-of-scope targets are dropped and ascope_violationis emitted. - Policy-load validation. When the agent receives a
policy_update, it walks masking-rule match patterns and row-filter expressions (AST) and rejects the entire policy if any referencedschema.tableis out of scope. Partial-apply is intentionally not supported: silently dropping a masking rule would leave residual access via the still-applied grant. - Schema refresh on request. When the control plane sends a
schema_request, the agent re-introspects and applies the filter before responding. There is no unfiltered code path.
Discovery: daam-agent introspect
A read-only utility for discovering the upstream schema locally without involving the control plane. It does not require a running daemon, does not contact the control plane, and does not write any state to disk.
Default output
Print a ready-to-paste scope: block sorted deterministically:
$ daam-agent introspect --config config.yaml
scope:
- analytics.daily_rollup
- analytics.events
- public.orders
- public.users Customers paste it into their config and edit down. An upstream with zero user tables renders as scope: [] (the deny-all encoding), so raw output cannot accidentally produce allow-all.
--diff mode
Compares live introspection against the scope: value already present in the config:
$ daam-agent introspect --config config.yaml --diff
+ public.payments_v2
+ public.refunds
- analytics.deprecated_metrics - Lines prefixed
+are upstream relations not covered by current scope (potential additions). - Lines prefixed
-are scope patterns that match no upstream relation (potential typos or removed tables).
Exit codes
| Code | Meaning |
|---|---|
0 | Success. In --diff mode, also indicates no drift. |
1 | Connection or introspection error. |
2 | --diff only: drift detected. Useful for cron exit-status alerting. |
Cron daam-agent introspect --diff daily. A non-zero exit means upstream has changed; review and update scope. This is the operational drift-detection signal.
Reload via SIGHUP
The agent reloads upstream.scope on SIGHUP without a restart. The sequence:
- Re-read config from disk under strict decoding. An invalid edit causes the reload to fail with an error logged locally; the previous scope remains in effect.
- Re-introspect the upstream database and rebuild the filtered schema.
- Send a fresh
schema_reportupstream. The control plane treats this as a replacement: relations no longer reported are evicted from its schema cache. - Diff active grants on ephemeral roles. For each live connection, the agent recomputes the in-scope grant set from the cached policy and issues
REVOKEon grants that are no longer in scope. - Log the change with
addedandremovedpattern lists. The log stays local; nothing about the scope value itself is sent upstream.
Widening scope is a no-op on existing connections by design; new connections naturally pick up the wider scope. Tightening scope revokes in real time.
What the Control Plane Sees
With scope active, the control plane's view of your database is the in-scope subset and nothing else. Concretely:
- Schema report. Only in-scope relations appear. Out-of-scope schemas, tables, column names, types, and comments never leave the agent process.
- Scope violation events. When the agent rejects a grant or policy-load, it emits a
scope_violationmessage naming the rejected target:policy_id,schema,table,site. The pattern list itself is never echoed; events are deduplicated by(policy_id, schema, table)over a 5-minute sliding window to bound traffic when a hot-path policy is stale. - Persisted hygiene signal. Scope violations are persisted to an audit table and surface on the agent detail page with a 7-day rolling count badge, plus a chip on the offending policy's detail page linking back. Related event types are documented in the Audit Log.
What the control plane does not see: the pattern list, the filtered relation count, whether scope is active at all, or any out-of-scope object name. There is no control-plane API or message type that returns scope state. The only upstream signal is the per-rejection scope_violation.
Local Health Output
The agent's /health/detailed endpoint exposes the local scope state. It stays local; the control plane never receives this payload.
{
"scope": {
"active": true,
"patterns": [
"public.users",
"public.orders",
"analytics.*"
],
"in_scope_object_count": 24
}
} | Field | Meaning |
|---|---|
scope.active | true when upstream.scope is set (including empty list); false when absent. |
scope.patterns | Raw pattern list from config. Always an array (empty when deny-all or allow-all). |
scope.in_scope_object_count | Number of upstream relations matching scope after the most recent introspection. |
Adopting Scope on an Existing Deployment
- Discover the upstream surface. Run
daam-agent introspect --config config.yamland review the output. - Cross-reference current policies. In the control plane UI, list the policies on this database and the relations they reference. The union of those references is the minimum scope that preserves existing access.
- Author the scope value. Edit
upstream.scopeto cover the required relations. Prefer wildcards (analytics.*) over enumerations when an entire schema is intentional. - Reload.
SIGHUPthe agent process. Verify the structured log shows the expected scope change. - Observe. Watch the agent detail page in the control plane for
scope_violationevents over the next week. Each event is either a policy to clean up or a missing scope entry to add. - Schedule drift detection. Cron
daam-agent introspect --diff. A non-zero exit means upstream has changed; review and update.
What Scope Does Not Protect Against
Scope is a boundary-of-trust mechanism between your network and the daam-hosted control plane. It does not harden the agent process itself.
- A compromised agent process retains the upstream DSN credential and can introspect the full schema by running queries directly. Scope filtering happens after introspection; it shapes what leaves the process, not what the process can see.
- The privileged DSN role still has whatever privileges you granted it (typically
CREATEROLEplusGRANTauthority). A compromised agent can use that role to its full extent on the upstream database, irrespective of scope. - The
daam-agent introspectsubcommand discloses the full unfiltered schema by design, to operators with file-system access to the config. It is a local tool and assumes a trusted local context.
Mitigations for agent-process compromise are separate concerns (least-privilege DSN role provisioning, container/systemd hardening, binary integrity, pgaudit on the upstream). Scope and those mitigations are complementary; neither subsumes the other.
Limitations
- Glob patterns only. The wildcard is
*; regular expressions are not supported. - Positive patterns only. Negation (e.g.
!public.secrets) is not supported. To exclude a relation, omit it from the pattern list. - Metadata on in-scope relations is reported in full. Once a relation is in scope, its comments, column names, types, and nullability are included in the schema report. Scope is a relation-level boundary, not a column-level or metadata-level one.
Trust Boundary
The daam control plane never sees data, or metadata, about tables you have not explicitly opted in. Scope is declared in a config file you control, inside your network. The control plane has no API or message type that returns out-of-scope information, and the agent has no command from the control plane that bypasses scope.
The boundary is enforced in code at four sites (Enforcement Points) and verifiable from your side via local logs and the introspect subcommand.
Related
- Agent: deploying and configuring the agent.
- Policies: per-identity access control composed with scope.
- Data Masking: column-level protection inside in-scope relations.