Teradata Integration Guide
This guide covers how to connect a Teradata system to BurnLedger, configure query templates, set up a read-only Teradata user that BurnLedger will accept, and handle the TLS certificate situation that Teradata appliances ship with. It was verified end to end against a live ClearScape Analytics environment.
BurnLedger drives Teradata through its Query Service REST API — HTTPS on port 1443, not the native database port — so no ODBC driver, JDBC jar or Teradata client software is involved on either side. Two Teradata-specific things account for nearly every failed registration, and both fail before your query template ever runs. First, Teradata automatically grants a freshly created user write rights on its own database space, so a brand-new "read-only" user is rejected with WRITE_ACCESS_DETECTED until those automatic rights are revoked — the Teradata twin of Redshift's default PUBLIC grants. Second, ClearScape and Vantage Express serve Query Service behind a factory self-signed certificate with no subject alternative names, which no trust store can ever verify; the fix is the server_cert certificate pin, not a TLS downgrade. Read Read-Only User Setup and TLS and the server_cert pin before you register a system.
Connection Configuration
BurnLedger connects to Teradata using a structured connection config — there is no DSN form for this connector.
Connection config keys
| Key | Type | Required | Description |
|---|---|---|---|
host |
string | Yes | Hostname of the Query Service endpoint. For a ClearScape environment this is <name>-<random>.env.trial.teradata.com. |
username |
string | Yes | The read-only Teradata user. Sent as HTTP Basic auth over the TLS channel. |
password |
string | Yes | Its password. |
port |
number | No | Query Service port. Defaults to 1443 (its TLS port). |
system |
string | No | The Query Service system name — the {system} in its /systems/{system}/queries endpoint. Teradata installations name their systems; ClearScape environments expose exactly one, named local, which is the default. |
server_cert |
string | No | Certificate pin: base64 of the server's certificate (PEM or DER). See TLS. |
tls |
boolean | No | Defaults to on when omitted. "tls": false is a deliberate opt-out for a plaintext endpoint on a private network, and is refused by the production transport floor. |
There is no read_only flag for Teradata. BurnLedger introspects the credential's access rights in the data dictionary and refuses write-capable credentials — see What BurnLedger checks.
This config is typed JSON, not a string→string map.
portmust be a JSON number andtlsa JSON boolean —"port": "1443"or"tls": "false"(strings) fail the config decode withfailed to parse Teradata configbefore anything is dialed. This is the opposite of the Redshift/Oracle guides' warning: there, non-string values break the decode; here, wrongly stringified values do. Unknown extra keys are ignored harmlessly.
Example:
{
"host": "myenv-a1b2c3d4e5f6.env.trial.teradata.com",
"username": "burnledger_ro",
"password": "REPLACE_WITH_YOUR_PASSWORD",
"server_cert": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t..."
}
REPLACE_WITH_YOUR_PASSWORD is a placeholder, not a suggestion: choose your own password when you create the user below, and substitute it wherever the placeholder appears in this guide.
When registering the system via the SDK (the config keys stay snake_case in both SDKs — they are the keys the connector reads, not SDK parameters):
TypeScript
const system = await bl.registerSystem({
name: "warehouse-teradata",
connectorType: "teradata",
connectionConfig: {
host: "myenv-a1b2c3d4e5f6.env.trial.teradata.com",
username: "burnledger_ro",
password: "REPLACE_WITH_YOUR_PASSWORD",
server_cert: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t...",
},
subjectQuery: "SELECT user_id, email, full_name FROM appdb.users WHERE email = $1",
});
Python
system = bl.register_system(
name="warehouse-teradata",
connector_type="teradata",
connection_config={
"host": "myenv-a1b2c3d4e5f6.env.trial.teradata.com",
"username": "burnledger_ro",
"password": "REPLACE_WITH_YOUR_PASSWORD",
"server_cert": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t...",
},
subject_query="SELECT user_id, email, full_name FROM appdb.users WHERE email = $1",
)
Connection requirements:
| Requirement | Details |
|---|---|
| Port | 1443 (Query Service TLS port) unless overridden with port. This is the REST layer, not the native Teradata SQL port — nothing here speaks the Teradata wire protocol. |
| Endpoint | A hostname. Raw private, loopback and link-local addresses are rejected by the SSRF policy on a cloud deployment; a self-hosted deployment reaching a private endpoint needs DP_ALLOW_PRIVATE_NETWORKS=true. |
| TLS | On by default, TLS 1.2 minimum. The production floor requires verified transport: either a certificate that chains to a trusted root for the hostname, or a server_cert pin. "tls": false classifies as plaintext and is refused. See TLS. |
| Authentication | HTTP Basic over the TLS channel. Health checks execute a real authenticated query (SELECT 1) — Query Service serves some endpoints without authentication, so a mere status probe would prove nothing about the credential. |
| Permissions | Read-only. BurnLedger counts write-capable access rights in the DBC dictionary — granted directly or through roles, recursively — and refuses the credential if it holds any. See Read-Only User Setup. |
| Limits | max_records defaults to 1,000,000, which is also the hard cap — a larger value is rejected at registration with BAD_REQUEST. query_timeout defaults to 30s; for this connector, values above 30 seconds have no effect, because the REST client enforces its own 30-second ceiling per request. At most 10 MB of any Query Service response is read; a response truncated at that limit fails parsing rather than certifying a partial result. |
Network Access
BurnLedger needs to reach https://<host>:1443 from wherever it runs. There is no VPC peering or wire-protocol firewall subtlety here — it is one HTTPS endpoint — but two things about ClearScape environments cost real time:
1. The endpoint pattern
A ClearScape environment's hostname is <name>-<random>.env.trial.teradata.com — the random suffix is part of the hostname, so copy it from the ClearScape console rather than reconstructing it. The Query Service listens on 1443 and exposes a single system named local; leave system and port at their defaults.
# Reachability check from the BurnLedger host
curl -sk "https://myenv-a1b2c3d4e5f6.env.trial.teradata.com:1443/systems" | head -c 200
2. Hibernation: the first touch after idle can fail, and that is the wake-up call
ClearScape environments hibernate when idle. The first request against a hibernated environment can time out or return HTTP 504 — surfaced by BurnLedger as CONNECTION_FAILED ("Teradata query failed with status 504") — and that very request is what wakes the environment. Registration health-checks the system inline, so a first registration or health check against a sleeping environment may fail once and succeed on the retry ~20 seconds later. Treat one failure after an idle period as the wake-up, and two consecutive failures as a real signal: an environment that is stopped (not hibernated) does not wake on touch and must be started from the ClearScape console — the symptom there is a plain TCP timeout, not a 504.
Query Template Format
A query template is a SELECT statement with $1 as the placeholder for the data subject identifier. Under the hood, Query Service binds positional ? parameters from a params array in the request body; BurnLedger converts every $1 occurrence to ? and binds the subject identifier once per occurrence — as a bound parameter, never string interpolation, so the value can never alter the parsed statement. Write $1 in the template; the ? form is what travels on the wire, not what you register.
$1 is the only placeholder this connector substitutes. There is no universal placeholder across BurnLedger connectors: ? (Snowflake, Cassandra, MySQL), :1 (Oracle), $IDENTIFIER (MongoDB, Elasticsearch) and {identifier} (S3, Redis, HBase) belong to other connectors and mean nothing here. A Teradata template without $1 is rejected at registration with INVALID_QUERY_TEMPLATE. That rejection is deliberate: a template that never binds the subject matches nothing, and BurnLedger would issue a valid, signed verification record asserting "0 records" — a cryptographically signed false negative telling a compliance team there was nothing to delete while the subject's data sits untouched.
Rules (all enforced at registration):
- Must be a single statement beginning with
SELECTorWITH … SELECT. Teradata'sSELabbreviation is rejected — the validator requires the full keyword. - Must contain
$1. Referencing the subject more than once (WHERE email = $1 OR user_id = $1) is fine — each occurrence is converted and bound. - Must contain a
WHEREclause (a template without one would full-table-scan, and is rejected). - No embedded
;— a single trailing;is tolerated by the validator but will break execution, because the template is wrapped as a derived table. Leave the semicolon off. - No data-modifying keywords (
INSERT,UPDATE,DELETE,MERGE,TRUNCATE,CREATE,DROP,ALTER, …), including inside CTEs. - No dollar-quoted strings (
$$ … $$). - No
FOR UPDATE/FOR SHARElocking clauses.
SELECT * is allowed but produces a warning: adding or dropping a column changes the row hash. Listing columns explicitly is more stable.
Examples:
-- Single table, subject identified by email
SELECT user_id, email, full_name FROM appdb.users WHERE email = $1
-- Subject referenced twice: every $1 is bound
SELECT user_id, email FROM appdb.users WHERE email = $1 OR recovery_email = $1
-- Join across tables for a complete subject record
SELECT u.user_id, u.email, o.order_id, o.placed_at
FROM appdb.users u
LEFT JOIN appdb.orders o ON o.user_id = u.user_id
WHERE u.email = $1
At attestation time BurnLedger wraps your template as a derived table — SELECT COUNT(*) FROM (<your template>) AS _dp_inner SAMPLE <max_records+1> in count mode, SELECT * FROM (…) AS _dp_inner SAMPLE <max_records+1> in Merkle mode — so the template must be valid inside a derived table: no trailing semicolon, no statement-level keywords.
Read-Only User Setup
BurnLedger requires a dedicated read-only Teradata user and actively verifies it at connection time. This is a security property of the product, not a suggestion: BurnLedger must not be able to modify the data it attests to. Nothing is ever written to your system during the check — it is pure introspection of Teradata's data dictionary.
What BurnLedger checks
Immediately after connecting, the connector counts write-capable access rights from both places a right can come from, and rejects the credential if either count is non-zero:
-- 1. Rights granted directly to the user, on ANY object
SELECT COUNT(*) FROM DBC.AllRightsV
WHERE UserName = USER
AND AccessRight IN ('I','U','D','C','CT','DT','CV','DV','CM','DM','CF','DF',
'CP','DP','CG','DG','CE','DE','CA','DA','CO','DO','AS','NT','IX','RF');
-- 2. Rights carried by roles the user holds -- walked RECURSIVELY, because
-- roles nest: a role granted to a role confers its rights on the user
WITH RECURSIVE user_roles (RoleName) AS (
SELECT RoleName FROM DBC.RoleMembersV WHERE Grantee = USER
UNION ALL
SELECT rm.RoleName FROM DBC.RoleMembersV rm
JOIN user_roles ur ON rm.Grantee = ur.RoleName
)
SELECT COUNT(*) FROM DBC.AllRoleRightsV rr
JOIN user_roles ur ON rr.RoleName = ur.RoleName
WHERE rr.AccessRight IN ('I','U','D','C','CT','DT','CV','DV','CM','DM','CF','DF',
'CP','DP','CG','DG','CE','DE','CA','DA','CO','DO','AS','NT','IX','RF');
- Either count > 0 → the credential is refused with
WRITE_ACCESS_DETECTED, and registration is rejected outright (not merely marked unhealthy). - If the dictionary views cannot be read, BurnLedger fails closed: it cannot prove the credential is read-only, so it refuses with
CONNECTION_FAILED("failed to verify Teradata credential is read-only"). The user must be able to readDBC.AllRightsV,DBC.RoleMembersVandDBC.AllRoleRightsV— readable by default; a site that has revoked general DBC dictionary access must restore read on those three views.
The write-right codes, spelled out: I insert · U update · D delete · C/CT create table · DT drop table · CV/DV create/drop view · CM/DM create/drop macro · CF/DF create/drop function · CP/DP checkpoint/dump · CG/DG create/drop trigger · CE/DE create/drop external procedure · CA/DA create/drop authorization · CO/DO create/drop profile · AS abort session · NT nontemporal · IX index · RF reference.
Scope of the check, stated plainly:
'R'(RETRIEVE — Teradata's code for SELECT) is treated as read, deliberately. It is the one right the credential must hold; it appears inDBC.AllRightsVfor every readable grant and does not count against the user.- The check has no database filter: a write right on any object rejects the credential, including rights on the user's own default database. That is exactly what catches the automatic-rights trap below.
- Role-carried rights are found even through nested roles. Do not put the BurnLedger user in a role hierarchy that holds write rights anywhere.
Gotcha: Teradata grants a new user write rights on itself
This is the single most common Teradata rejection. In Teradata a user is a database, and CREATE USER automatically grants the new user a set of rights on its own space — several of them (CT, DT, I, U, D, …) are in the write set above. A user you just created with the intention of granting it nothing but SELECT therefore already holds write rights, and BurnLedger refuses it with WRITE_ACCESS_DETECTED before you have granted it anything at all.
The fix is to revoke the automatic self-rights after creating the user:
REVOKE ALL ON burnledger_ro FROM burnledger_ro;
Unlike Redshift's PUBLIC revoke, this touches only the one user — there is no cluster-wide side effect to coordinate.
Create the read-only user
Run as an administrative user (dbc on a ClearScape environment — it shares the password you set for demo_user). Note the Teradata CREATE USER shape: the clauses come after AS, unparenthesized. PERM is mandatory (omitting it fails with error 3796, "The user must specify a value for PERMANENT space"); set it to 0, since the account must never own space or create objects.
-- 1. The login principal
CREATE USER burnledger_ro FROM dbc AS PASSWORD = REPLACE_WITH_YOUR_PASSWORD, PERM = 0;
-- 2. REQUIRED: remove the automatic rights Teradata granted it on itself
-- (without this, registration fails with WRITE_ACCESS_DETECTED)
REVOKE ALL ON burnledger_ro FROM burnledger_ro;
-- 3. Read access to the tables the query template touches
GRANT SELECT ON appdb.users TO burnledger_ro;
GRANT SELECT ON appdb.orders TO burnledger_ro;
-- Or read access to the whole database:
-- GRANT SELECT ON appdb TO burnledger_ro;
Do not grant anything beyond SELECT, and do not add the user to any role — a role that (transitively) carries a write right anywhere disqualifies the credential.
On a ClearScape trial, do not register with
demo_user. It is the environment's write-capable owner and carries dozens of write rights, so BurnLedger rejects it — correctly. Keepdemo_user(or your application account) for performing the actual deletions; registerburnledger_rofor attesting to them. The separation is the point: the credential that proves deletion must not be a credential that could have performed it.
Verify before you register
Run exactly what BurnLedger runs, substituting the user name for USER. Both counts must be 0:
SELECT COUNT(*) FROM DBC.AllRightsV
WHERE UserName = 'burnledger_ro'
AND AccessRight IN ('I','U','D','C','CT','DT','CV','DV','CM','DM','CF','DF',
'CP','DP','CG','DG','CE','DE','CA','DA','CO','DO','AS','NT','IX','RF');
WITH RECURSIVE user_roles (RoleName) AS (
SELECT RoleName FROM DBC.RoleMembersV WHERE Grantee = 'burnledger_ro'
UNION ALL
SELECT rm.RoleName FROM DBC.RoleMembersV rm
JOIN user_roles ur ON rm.Grantee = ur.RoleName
)
SELECT COUNT(*) FROM DBC.AllRoleRightsV rr
JOIN user_roles ur ON rr.RoleName = ur.RoleName
WHERE rr.AccessRight IN ('I','U','D','C','CT','DT','CV','DV','CM','DM','CF','DF',
'CP','DP','CG','DG','CE','DE','CA','DA','CO','DO','AS','NT','IX','RF');
If the first count is not 0, see what trips it — after a fresh CREATE USER it is almost always the automatic self-rights:
SELECT DatabaseName, TableName, AccessRight
FROM DBC.AllRightsV
WHERE UserName = 'burnledger_ro'
AND AccessRight IN ('I','U','D','C','CT','DT','CV','DV','CM','DM','CF','DF',
'CP','DP','CG','DG','CE','DE','CA','DA','CO','DO','AS','NT','IX','RF')
ORDER BY DatabaseName, TableName;
Then confirm the user can actually read, and cannot write:
-- Connect as burnledger_ro, then:
SELECT COUNT(*) FROM appdb.users; -- succeeds
INSERT INTO appdb.users (email) VALUES ('probe'); -- fails: no INSERT access
DELETE FROM appdb.users WHERE email = 'probe'; -- fails: no DELETE access
CREATE TABLE burnledger_ro.probe (id INTEGER); -- fails: no space, no right
TLS and the server_cert pin
TLS is on by default for this connector, and the production transport floor requires verified TLS — an unverified or plaintext link would let an on-path attacker rewrite the record count BurnLedger signs into a certificate. See connector transport security for the level model.
There are two ways to reach verified:
- A certificate that chains to a trusted root for the Query Service hostname. If your site fronts Query Service with a properly issued certificate, omit
server_certand the system trust store verifies it normally. - The
server_certpin. Teradata appliances — Vantage Express, and every ClearScape trial environment — serve a factory self-signed certificate with no subject alternative names. No root store can verify it; chain verification is not inconvenient there, it is impossible. The pin replaces chain verification with something stronger: the handshake is accepted only if the presented leaf certificate is byte-identical to the pinned one. No CA is trusted at all, which is a stricter identity check than chain-plus-hostname — so a pinned config still classifies asverifiedand is admitted by the production floor.
Capture the pin from the live endpoint and base64-encode it:
# Fetch the Query Service certificate as PEM
openssl s_client -connect myenv-a1b2c3d4e5f6.env.trial.teradata.com:1443 </dev/null 2>/dev/null \
| openssl x509 -outform PEM > server.pem
# The pin: base64 of the PEM (base64 of the raw DER is also accepted)
base64 -w0 server.pem # macOS: base64 -i server.pem | tr -d '\n'
Put the output in the connection config as server_cert.
Things to know about the pin:
- Byte-identical means byte-identical. A renewed or reissued certificate — even one for the same key, hostname and environment — is a different byte string, and the handshake is refused with "presented certificate does not match pinned server_cert". If the environment is rebuilt or its certificate rotates, re-capture the pin and update the system's connection config.
server_certrequires TLS. Pairing it with"tls": falseasks for a plaintext connection to a certificate; the connector refuses to build (server_cert requires tls) rather than guessing which half you meant.- There is no
ca_certfield for Teradata. The pin is the private-trust mechanism for this connector. A value that is not a valid base64-wrapped certificate is rejected at construction (server_cert is not a valid certificate).
Without a pin and without a chain-verifiable record, the handshake fails with x509: certificate signed by unknown authority. A "tls": false config classifies as plaintext: the system registers but is stored unhealthy with the generic connection failed, and attestations against it fail with the verdict stated outright — "connector teradata: transport security plaintext is below the required minimum verified". Development and test deployments can lower the floor with DP_ALLOW_UNVERIFIED_TRANSPORT=true (a downgrade, one WARN per connector construction — never production); it is independent of DP_ALLOW_PRIVATE_NETWORKS, and a lab environment on a private network typically needs both.
Hash Scope Options
The hash_scope (hashScope) parameter declares what an attestation is meant to cover. If you omit it, it defaults to existence.
What actually runs is chosen by
proof_mode, not byhash_scope. On Teradata,hash_scopeis recorded on the system and echoed on every attestation, but the Teradata connector does not branch on it — only the object-store connectors vary their behavior byhash_scope. Theproof_modeyou pass toattest()is what selects the work:proof_mode: "count"(the SDK default) runs the boundedCOUNT(*)and records a record count only;proof_mode: "merkle"fetches the rows, canonicalizes each one and builds a Merkle tree. Sethash_scopeto state your intent, and setproof_modeto get the behavior — and keep the two consistent.
full
Declares that the attestation covers the complete content of every row returned. Pair it with proofMode: "merkle", which is what actually fetches and hashes the rows. Each row is canonicalized by column: fields are sorted by column name before hashing (so SELECT a, b and SELECT b, a hash identically, but renaming or aliasing a column changes the hash), and values hash as the text the Query Service returns for them. Alias computed expressions deterministically and cast exotic types to VARCHAR in the template if you need their representation stable.
const system = await bl.registerSystem({
// ...
});
Use when: you need to prove the exact data that existed, or prove data was deleted rather than merely nulled out (nullifying PII fields is not the same as deletion under some interpretations of GDPR).
existence
Declares that the attestation covers only whether rows exist and how many, not their content. Pair it with proofMode: "count" (the SDK default), which is materially cheaper here: a bounded COUNT(*) returns one row over REST, while Merkle mode returns every matched row inline in the HTTP response — subject to the Query Service's own inline row limit and BurnLedger's 10 MB response cap.
const system = await bl.registerSystem({
// ...
hashScope: "existence",
});
Use when: you only need to prove records existed and were later removed, the result set is large, or you would rather not read PII content at all.
Comparison
full |
existence |
|
|---|---|---|
| Detects row deletion | Yes | Yes |
| Detects row insertion | Yes | Yes |
| Detects column value changes | Yes (merkle root only; see the note below the table) |
No |
| Hash includes PII content | Yes | No |
| Cost over the REST transport | Higher (all rows returned inline) | Lower (single-row count) |
| Recommended for GDPR Art. 17 | Yes | Acceptable |
"Detects" here is bounded by what a merkle certificate actually carries: a signed Merkle root over the row hashes and nothing else — BurnLedger serves no per-record inclusion proof, so a changed value is demonstrable only to someone who already holds every matching row and rebuilds the tree, and from format 7.0 the leaves are keyed inside the enclave so no party, BurnLedger included, can open the root at all (see Certificate Scope §4).
Each column describes the scope paired with its matching proof_mode (full + merkle, existence + count). Because the Teradata connector ignores hash_scope, a mismatched pair gives you the behavior of the proof_mode.
Either way the result is bounded: a query matching more than max_records rows fails with RECORD_LIMIT_EXCEEDED rather than being silently truncated, and in Merkle mode a result set the Query Service itself truncated at its inline row limit is likewise refused (result set exceeded the server row limit) — hashing a truncated set would certify fewer records than exist. If the Query Service ever answers in a shape the connector does not understand (no inline result set, rows without column metadata), BurnLedger fails with CONNECTION_FAILED instead of treating the unparsed response as "0 records": an answer that cannot be read is never proof of absence.
End-to-End Example
1. Teradata setup (run once, as an administrative user)
CREATE USER burnledger_ro FROM dbc AS PASSWORD = REPLACE_WITH_YOUR_PASSWORD, PERM = 0;
-- Without this, registration fails with WRITE_ACCESS_DETECTED
REVOKE ALL ON burnledger_ro FROM burnledger_ro;
GRANT SELECT ON appdb.users TO burnledger_ro;
GRANT SELECT ON appdb.orders TO burnledger_ro;
-- Confirm: both verification counts in "Verify before you register" must be 0
Then capture the server_cert pin as shown in TLS.
2. Application code
TypeScript
import { BurnLedger } from "burnledger";
import fs from "node:fs/promises";
const bl = new BurnLedger({ apiKey: process.env.BURNLEDGER_API_KEY! });
// Register the system (run once, store the system ID)
const system = await bl.registerSystem({
name: "warehouse-teradata",
connectorType: "teradata",
connectionConfig: {
host: "myenv-a1b2c3d4e5f6.env.trial.teradata.com",
username: "burnledger_ro",
password: "REPLACE_WITH_YOUR_PASSWORD",
server_cert: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t...",
},
subjectQuery:
"SELECT u.user_id, u.email, u.full_name, o.order_id, o.placed_at " +
"FROM appdb.users u " +
"LEFT JOIN appdb.orders o ON o.user_id = u.user_id " +
"WHERE u.email = $1",
});
console.log(`registered ${system.id}: ${system.healthStatus}, transport ${system.transportSecurity}`);
// --- When a deletion request comes in ---
const subjectEmail = "jane.doe@example.com";
// Step 1: attest that the data currently exists
const attestation = await bl.attest(subjectEmail, {
systemIds: [system.id],
proofMode: "merkle",
});
for (const s of attestation.systems) {
console.log(`${s.systemName}: ${s.recordCount} records found`);
}
// Step 2: delete the data with your own tools (as your application user,
// never as burnledger_ro -- it cannot delete, by design)
// Step 3: verify the deletion. The certificate is ISSUED BY verify() once
// the data is gone -- there is no separate "create certificate" step.
const result = await bl.verify(attestation.id, subjectEmail, { timeout: 60 });
for (const s of result.systems) {
console.log(`${s.systemName}: now ${s.recordCount} records`);
}
// Step 4: download the certificate
if (result.certificate) {
const pdf = await bl.downloadPdf(result.certificate.id);
await fs.writeFile(`erasure-cert-${subjectEmail}.pdf`, pdf);
console.log(`Certificate ${result.certificate.id} (${result.certificate.transparencyStatus})`);
} else {
console.log(`Not certified: status ${result.status}`);
}
Python
import os
from burnledger import BurnLedger
bl = BurnLedger(api_key=os.environ["BURNLEDGER_API_KEY"])
# Register the system (run once, store the system ID)
system = bl.register_system(
name="warehouse-teradata",
connector_type="teradata",
connection_config={
"host": "myenv-a1b2c3d4e5f6.env.trial.teradata.com",
"username": "burnledger_ro",
"password": "REPLACE_WITH_YOUR_PASSWORD",
"server_cert": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t...",
},
subject_query=(
"SELECT u.user_id, u.email, u.full_name, o.order_id, o.placed_at "
"FROM appdb.users u "
"LEFT JOIN appdb.orders o ON o.user_id = u.user_id "
"WHERE u.email = $1"
),
)
# --- When a deletion request comes in ---
subject_email = "jane.doe@example.com"
# Step 1: attest that the data currently exists
attestation = bl.attest(
subject_email,
system_ids=[system.id],
proof_mode="merkle",
)
for s in attestation.systems:
print(f"{s.system_name}: {s.record_count} records found")
# Step 2: delete the data with your own tools (as your application user,
# never as burnledger_ro -- it cannot delete, by design)
# Step 3: verify the deletion. The certificate is ISSUED BY verify() once
# the data is gone -- there is no separate "create certificate" step.
result = bl.verify(attestation.id, subject_email, timeout=60)
for s in result.systems:
print(f"{s.system_name}: now {s.record_count} records")
# Step 4: download the certificate
if result.certificate:
bl.save_pdf(result.certificate.id, f"erasure-cert-{subject_email}.pdf")
print(f"Certificate issued: {result.certificate.id}")
else:
print(f"Not certified: status {result.status.value}")
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
WRITE_ACCESS_DETECTED right after creating a fresh read-only user |
Teradata automatically granted the new user write rights (CT, DT, I, U, D, …) on its own database space; the check counts write rights on any object |
REVOKE ALL ON burnledger_ro FROM burnledger_ro;, then re-run the verification queries |
WRITE_ACCESS_DETECTED but the direct-rights query returns 0 |
A role the user holds — possibly through a nested role — carries a write right; the check walks role grants recursively | Run the recursive role query from Verify before you register; remove the role membership or use a role-free user |
WRITE_ACCESS_DETECTED when registering with demo_user on ClearScape |
demo_user is the environment's write-capable owner |
Correct behavior. Register with a dedicated burnledger_ro; keep demo_user for performing the deletions themselves |
CONNECTION_FAILED "failed to verify Teradata credential is read-only; ensure the user can read DBC.AllRightsV…" |
The rights introspection failed — the user cannot read the DBC dictionary views, or the response was cut short. BurnLedger fails closed rather than assuming read-only | Restore read access to DBC.AllRightsV, DBC.RoleMembersV and DBC.AllRoleRightsV for the user and retry |
CONNECTION_FAILED "Teradata: authentication failed" |
Query Service returned 401 — wrong username/password (or a locked user) | Fix the credentials in the connection config; confirm them with a direct curl -u user:pass https://<host>:1443/systems/local/queries POST |
CONNECTION_FAILED "Teradata query failed with status 504" on the first touch after idle |
The ClearScape environment was hibernating; the failing request is what wakes it | Retry after ~20 seconds. One failure after idle is the wake-up; two consecutive failures are a real outage |
CONNECTION_FAILED, plain TCP timeout |
The environment is stopped (nothing listens on 1443) — a stopped environment does not wake on touch — or a firewall blocks the port | Start the environment from the ClearScape console; confirm with curl -sk https://<host>:1443/systems |
CONNECTION_FAILED wrapping "presented certificate does not match pinned server_cert" |
The Query Service certificate changed (environment rebuilt, certificate rotated) or the pin was captured from the wrong endpoint — the pin admits only a byte-identical leaf | Re-capture the pin from the live endpoint (TLS) and update the system's connection config |
CONNECTION_FAILED wrapping x509: certificate signed by unknown authority (no server_cert set) |
The factory self-signed, SAN-less appliance certificate can never chain-verify | Add the server_cert pin, or front Query Service with a certificate that chains to a trusted root |
CONNECTION_FAILED "Teradata config server_cert requires tls" |
The config pairs a certificate pin with "tls": false |
Remove "tls": false — a pin names a TLS identity and requires TLS on |
Attestation fails: "connector teradata: transport security plaintext is below the required minimum verified". A health check reports only connection failed — read transport_security on the system |
"tls": false in the connection config |
Remove it (TLS defaults to on) and satisfy verified via a chain-verifiable certificate or server_cert. A self-hosted dev deployment may instead run with DP_ALLOW_UNVERIFIED_TRANSPORT=true — a downgrade, never for production |
CONNECTION_FAILED "failed to parse Teradata config" |
A value has the wrong JSON type — port as a string, tls as a string |
port must be a JSON number and tls a JSON boolean; see Connection Configuration |
CONNECTION_FAILED "Teradata query failed with status 404" |
The system config key names a system the Query Service does not have |
ClearScape environments expose exactly one system, named local — omit the key and the default is used |
INVALID_QUERY_TEMPLATE "does not contain $1" |
The template uses another connector's placeholder (?, :1, $IDENTIFIER, {identifier}) or hard-codes the subject value |
Rewrite with $1 — it is converted to Query Service's ? binds on the wire. Without it the query would match nothing and certify "0 records" |
INVALID_QUERY_TEMPLATE "must be a SELECT (or WITH … SELECT) statement, got \"SEL\"" |
The template uses Teradata's SEL abbreviation |
Spell out SELECT |
INVALID_QUERY_TEMPLATE "no WHERE clause" / "single statement" / "disallowed keyword" |
Template would full-table-scan, contains an embedded ;, or contains a write keyword (including inside a CTE) |
Reduce the template to one read-only SELECT (or WITH … SELECT) with WHERE … = $1, no trailing semicolon |
RECORD_LIMIT_EXCEEDED "query returned more records than max_records limit" |
The query matched more than max_records rows |
Narrow the template, or register the system with a higher max_records (up to 1,000,000) |
RECORD_LIMIT_EXCEEDED "result set exceeded the server row limit" (Merkle mode) |
The Query Service truncated the inline result set at its own row cap; hashing a truncated set would certify fewer records than exist | Narrow the template, raise the Query Service's row limit, or use proof_mode: "count" if you do not need content hashes |
CONNECTION_FAILED "the response was not understood" (COUNT returned no rows / no result set / rows without column metadata) |
Query Service answered in a shape the connector refuses to certify from — an unparsed response is never treated as "0 records" | Check the system name and the Query Service version; run the query directly against /systems/local/queries to see what it returns |
CONNECTION_FAILED, query timed out |
The query exceeded the timeout. Note the REST client's own 30-second ceiling: raising query_timeout beyond 30s has no effect on this connector |
Index the predicate column or narrow the template so it completes within 30 seconds |
QUERY_HASH_MISMATCH during verification |
The registered query template no longer matches the one being verified | Verify against the same system registration; register a new system if the template must change |