Skip to content

Break-glass access

Break-glass access lets chosen operators read a masked column unmasked. Analysts read email masked. A member of the operator group sre reads the real value, and the audit log shows who did.

Example

Start the server with examples/break-glass/serve.sh. The mask covers the analytics group. Its exclude takes the sre group out.

apiVersion: sluice.bino.bi/v1alpha1
kind: ColumnMaskPolicy
metadata:
  name: mask-email
  priority: 50
spec:
  match:
    any:
      - subjects:
          groups: ["analytics"]
        resources:
          catalogs: ["shop"]
          schemas: ["main"]
          tables: ["customers"]
          columns: ["email"]
  # Subjects in the sre group are not masked.
  exclude:
    any:
      - subjects:
          groups: ["sre"]
  mask:
    type: "null"

The SRE key is in both groups.

apiVersion: sluice.bino.bi/v1alpha1
kind: SubjectBinding
metadata:
  name: break-glass-sre
spec:
  claims:
    subjectId: "on-call-sre"
  apiKeys:
    - id: "sl_demo_sre"
      hashRef: "secret://env/SLUICE_APIKEY_SRE_HASH"
      # The sre group lifts the email mask.
      groups: ["analytics", "sre"]

The analyst key reads email.

curl -s http://localhost:8080/v1/query \
  -H "X-Api-Key: sl_demo_analyst.supersecret" \
  -H "Content-Type: application/json" \
  -d '{"sql": "SELECT id, email FROM shop.main.customers ORDER BY id"}'
{
  "query_id": "<query-id>",
  "columns": ["id", "email"],
  "rows": [
    [1, null],
    [2, null],
    [3, null]
  ],
  "row_count": 3,
  "truncated": false
}

The SRE key sends the same SQL.

curl -s http://localhost:8080/v1/query \
  -H "X-Api-Key: sl_demo_sre.rotatedtoken" \
  -H "Content-Type: application/json" \
  -d '{"sql": "SELECT id, email FROM shop.main.customers ORDER BY id"}'
{
  "query_id": "<query-id>",
  "columns": ["id", "email"],
  "rows": [
    [1, "alice@acme.example"],
    [2, "bob@acme.example"],
    [3, "carol@acme.example"]
  ],
  "row_count": 3,
  "truncated": false
}

Find the SRE query in the audit log.

grep '"event_type":"query"' data/audit/*.jsonl | grep on-call-sre
{
  "catalogs": ["shop"],
  "decision": "allow",
  "event_type": "query",
  "origin": "rest",
  "policies_applied": [
    {
      "kind": "SqlAccessPolicy",
      "name": "allow-analytics",
      "priority": 100
    },
    {
      "kind": "ColumnMaskPolicy",
      "name": "mask-email",
      "priority": 50
    }
  ],
  "prior_hash": "<hash>",
  "query_id": "<query-id>",
  "remote_ip": "<remote-ip>",
  "rewritten_fingerprint": "f90a934da548078a",
  "sluice_version": "<version>",
  "sql_fingerprint": "f90a934da548078a",
  "sql_sample": "SELECT id, email FROM shop.main.customers ORDER BY id",
  "subject": {
    "groups": ["analytics", "sre"],
    "id": "on-call-sre",
    "issuer": "break-glass-sre",
    "method": "api_key",
    "remote_ip": "<remote-ip>"
  },
  "tables": ["shop.main.customers"],
  "timestamp": "<time>",
  "hash": "<hash>"
}

What happened: The allow matches both keys through the analytics group. The exclude drops the mask for every subject in sre. The audit record names the subject and its groups.

More examples

The sre group alone gives no access. This suite case proves it.

- name: the sre group alone gives no access
  identity: { subject: on-call-sre, groups: [sre] }
  sql: "SELECT id, email FROM shop.main.customers"
  expect:
    outcome: deny

Limits

  • The audit record does not say that the mask was lifted. policies_applied still lists mask-email, and Sluice writes no separate record.
  • The only sign is that rewritten_fingerprint equals sql_fingerprint: Sluice ran the SQL unchanged.
  • To find such queries, search the query records for the group.
  • The exclude is always on. For access that a person accepts each time, use an approval.

See also