Apache HBase Integration Guide

This guide covers how to connect an Apache HBase cluster to BurnLedger, configure row-key query templates, supply a read-only principal, and understand exactly what gets hashed.

BurnLedger talks to HBase through the HBase REST gateway (Stargate) — not Thrift and not the native RPC protocol. The gateway must be running and reachable from BurnLedger's egress addresses.


Connection Configuration

Configuration fields:

Field Required Description
host Yes Hostname or address of the HBase REST gateway. Not a RegionServer, not ZooKeeper.
port No REST gateway port (default: 8080).
tls No Transport encryption. Defaults to true — omit it and the connector uses https://. "tls": false selects a plaintext gateway, which classifies plaintext and a production deployment then refuses to build at all; see Transport Security (TLS).
username No Reserved for gateway authentication. See A note on username / password before relying on it.
password No Reserved for gateway authentication. See the same note.
read_only Yes Must be true. Asserts that the principal the gateway uses cannot write. Construction is refused without it — see Read-Only Principal Setup.

There is no dsn, no table and no namespace field: the table is part of the query template, not the connection.

TypeScript

const system = await bl.registerSystem({
  name: "users-hbase",
  connectorType: "hbase",
  connectionConfig: {
    host: "hbase-rest.example.com",
    port: 8080,
    tls: true,
    read_only: true,
  },
  subjectQuery: "users/{identifier}",
});

Python

system = bl.register_system(
    name="users-hbase",
    connector_type="hbase",
    connection_config={
        "host": "hbase-rest.example.com",
        "port": 8080,
        "tls": True,
        "read_only": True,
    },
    subject_query="users/{identifier}",
)

Connection requirements:

Requirement Details
REST gateway Required. Start it with hbase rest start -p 8080 (or run it as a service). The native RPC port (16020) and the Thrift port (9090) will not work.
Connectivity probe On registration BurnLedger issues GET /status/cluster against the gateway. Anything other than 200 OK fails registration with CONNECTION_FAILED.
HTTP methods The gateway must accept PUT /<table>/scanner, GET on the returned scanner URL, and DELETE on it. Scanner creation is a read operation server-side, but it uses HTTP PUT — proxies or WAFs that strip non-GET methods will break scanning.
Network access BurnLedger connects from a fixed egress address. Allowlist it on the gateway host or its load balancer.
Permissions Read-only, asserted with read_only: true. See Read-Only Principal Setup.
Timeout The HTTP client uses a 30-second timeout per request; each attestation query is additionally bounded by the system's query timeout.

A note on username / password

username and password are accepted by the connector config, but the current HBase connector does not attach them to its HTTP requests — no Authorization header is sent to the gateway. In practice this means:

  • The gateway must accept BurnLedger's requests without HTTP-level credentials (typical for a gateway restricted at the network layer).
  • The effective HBase principal is whichever principal the REST gateway itself runs as (or impersonates by default), not a principal you pass here.

That makes the gateway principal the thing that must be read-only. Do not rely on these two fields to scope access; scope it on the gateway.


Transport Security (TLS)

TLS is on by default. Omitting tls uses https:// for the gateway base URL, which requires the gateway to be configured for SSL (hbase.rest.ssl.enabled=true, with hbase.rest.ssl.keystore.store / .password set), or fronted by a TLS-terminating proxy.

Changed: tls previously defaulted to false, so a config that never mentioned transport sent row keys and cell values in cleartext. Existing configs that relied on the old default now fail with CONNECTION_FAILED against a plaintext gateway — enable SSL on the gateway, or front it with a TLS-terminating proxy. "tls": false restores the old wire behaviour but is not a fix: see below.

The gateway's certificate is validated against the standard public trust store. There is no option to disable verification and no field for a custom CA bundle, so a self-signed or private-CA certificate will fail with CONNECTION_FAILED. If your gateway uses a private CA, terminate TLS on a proxy with a publicly trusted certificate.

"tls": false classifies plaintext, which is below the transport floor, so a production BurnLedger refuses to build the connector at all — the system registers, its health check reports connection failed, and every attestation against it fails. It is usable only on a deployment running with DP_ALLOW_UNVERIFIED_TRANSPORT=true, which is development and test only: traffic to the gateway, including row keys and cell values and the record count that gets signed, is unencrypted, and an on-path attacker chooses that count. See connector transport security.


Query Template Format

For HBase systems the query template is a row-key prefix scan, not a SQL query. It uses {identifier} as the placeholder for the data subject identifier.

Format:

<table>/<row-key-prefix>

Rules:

  • The template must contain {identifier}. A template without it is rejected at attestation time with INVALID_QUERY_TEMPLATE. This is deliberate: a template with no placeholder would expand to a fixed prefix, match nothing, and produce a signed verification record asserting "0 records" for a subject whose data is still there. {identifier} is the only placeholder this connector substitutes — ?, $1, :id and {{subject}} are all plain text to it.
  • Everything before the first / is the table name; everything after it is the row-key prefix. Namespaced tables use HBase's own namespace:table syntax (a colon), so analytics:users/{identifier} is a valid template.
  • Include the /. A template with no slash is read as a bare table name with an empty prefix, which scans the entire table — almost certainly not what you want, and it will trip RECORD_LIMIT_EXCEEDED on any real table.
  • The scan is a prefix match anchored at the start of the row key: BurnLedger scans from startRow = <expanded prefix> to endRow = <expanded prefix with its final byte incremented>. Row keys must therefore begin with the expanded prefix. There is no substring, regex, column-value or filter support.
  • Because your row keys must start with the subject identifier, this connector fits designs that key by subject (<user_id>#<event_ts>) and does not fit designs that salt or hash-prefix row keys for region spreading. See Row-key design below.
  • Avoid a prefix whose last byte is 0xFF; incrementing it wraps and the scan range becomes invalid. Keep a printable separator (#, |, -) at the end of the prefix and this never arises.

Examples:

# Row keys are exactly the subject identifier, or start with it
users/{identifier}

# Row keys are "<user_id>#<event_timestamp>" -- separator included in the prefix
user_events/{identifier}#

# Table in a namespace
analytics:page_views/{identifier}|

# Composite key with a fixed leading tenant, subject second: NOT supported
# (the prefix must start at byte 0 of the row key)
# events/tenant42#{identifier}   <- only works if row keys literally start with "tenant42#"

Row-key design

The prefix scan is the only access path, so the subject identifier has to be at the head of the row key. If your table salts row keys (e.g. md5(user_id)[0:2] + user_id) or leads with a timestamp bucket, a single template cannot reach the subject's rows. Options:

  • Register one BurnLedger system per salt bucket, each with its own template, and attest against all of them.
  • Maintain a secondary index table keyed by subject and register that table instead — but note that this proves deletion in the index, not in the base table.

What gets hashed

Each row returned by the scan is hashed as a canonical field set:

  • _rowkey — the raw row key bytes.
  • One field per cell, named family:qualifier, holding the raw cell value bytes.

Cell timestamps are not part of the hash, so a rewrite that changes only the version timestamp does not change the record hash. The scanner returns the latest version of each cell.


When a zero is refused

BurnLedger will not certify a zero it cannot account for. A prefix scan that returns no rows is the whole product, and there are three ways HBase produces that answer without the subject's rows being gone. So when a subject query returns nothing, BurnLedger asks two further read-only questions before it reports a zero.

1. Is the table still one this connection can read? BurnLedger asks the gateway for its table list (GET /) and looks for the table your template names. HBase's AccessController filters that list to the tables the principal holds a permission on, so one call answers both halves of the question.

  • The table is listed → carry on to question 2.
  • The table is not listed → refused, UNDETERMINED. Either the table was renamed or dropped, or the principal has no grant on it.

This one matters more than it looks. The REST gateway reports a scan it was not allowed to run as a scan that finished. ScannerResultGenerator catches the AccessDeniedException, logs it server-side, and the scanner answers 204 No Content — the same status an exhausted scan gets. Without the table-list check, a template pointed at a table your principal cannot read produces a clean zero from every scan BurnLedger can make, over rows that are sitting right there. Check your gateway's log for AccessDeniedException if you hit this refusal and believe the grant is correct.

2. Does the row-key prefix still address rows? The literal text your template puts before {identifier}u# in users/u#{identifier} — is a naming convention your application owns and the template merely copies. Renaming it leaves the template scanning a range nothing writes to.

  • Rows exist under the prefix (other subjects') → the zero is a real absence. Certified.
  • The table holds no rows at all → there is nowhere else in it the subject could be. Certified.
  • The table holds rows, but none under the prefix → refused, UNDETERMINED, naming the prefix. Either the convention moved, or it is real but now entirely empty; point-in-time those are the same observation and BurnLedger refuses both.

A template whose row key starts with {identifier} has no convention above the subject, so question 2 does not apply to it.

Neither question is asked when the scan finds rows — a non-zero count fails the deletion claim on its own. Both run under the same table-scoped READ grant the scan uses; neither needs a wider permission.

What is not checked: KEEP_DELETED_CELLS

An HBase Delete writes a tombstone. On a column family configured KEEP_DELETED_CELLS=TRUE, that tombstone hides the cell from ordinary reads but does not make it unreadable: a Get whose time range ends before the delete marker still returns the value, with no raw scan and no operator involved. On such a family, a deleted subject's data is one ordinary read away from being back.

BurnLedger does not check this, and cannot. The only endpoint that reports column-family attributes is GET /<table>/schema, which HBase serves through getTableDescriptor — and its AccessController check requires ADMIN or CREATE on the table, not READ. A correctly least-privileged BurnLedger principal gets 403 AccessDeniedException … action=CREATE. Granting C or A to make the check possible would break the read-only assertion the connector is built on, which is a worse trade.

This is a template- and schema-review obligation on your side. Run describe '<table>' in the HBase shell with an administrative account and confirm KEEP_DELETED_CELLS => 'FALSE' on every family before relying on an HBase deletion certificate. VERSIONS and MIN_VERSIONS do not need checking: measured against HBase 2.1.3, neither VERSIONS=5 nor MIN_VERSIONS=2 with a TTL keeps a deleted cell readable — MIN_VERSIONS protects cells from TTL expiry, not from a Delete.


Read-Only Principal Setup

BurnLedger cannot verify read-only access for HBase, and it says so rather than guessing.

HBase enforces authorization in the server-side AccessController coprocessor. Those ACLs are not exposed over the REST gateway's wire API, so there is no read-only call BurnLedger can make to learn whether a principal may write. The only way to prove write access over this API is to perform a write, which BurnLedger must never do against your data.

So the connector falls back to an explicit operator assertion:

  • Set "read_only": true in the connection config to assert, on your authority, that the principal cannot write.
  • Omit it (or set it to false) and registration is refused with CONNECTION_FAILED and the reason HBase exposes no permission introspection over its wire API. It fails closed; it never assumes read-only.

What BurnLedger actually checks for HBase:

Check Performed?
Gateway is reachable and healthy (GET /status/cluster returns 200) Yes
Principal's ACLs inspected for write grants No — not possible over the REST API
Any write, probe write, or dry-run write to your cluster Never
read_only: true present in config Yes — required, refuses construction otherwise

Because the assertion is yours, the credential behind it must genuinely be read-only. As noted above, the connector sends no credentials of its own, so the principal to lock down is the one the REST gateway runs as or impersonates.

Create a read-only principal

With the AccessController coprocessor enabled (hbase.security.authorization=true and AccessController in hbase.coprocessor.region.classes, .master.classes and .regionserver.classes), grant only R (READ) in the HBase shell:

# Grant READ on a single table
hbase> grant 'burnledger_ro', 'R', 'users'

# Grant READ on every table in a namespace
hbase> grant 'burnledger_ro', 'R', '@analytics'

# Grant READ on one column family only -- NOT sufficient for BurnLedger, see below
hbase> grant 'burnledger_ro', 'R', 'users', 'profile'

Grant READ at table or namespace scope, not column-family scope. BurnLedger's scanner sends only batch, startRow and endRow — it never sets a column filter — so every scan reads all column families of the table. A principal granted READ on a single family will have those scans rejected (AccessDeniedException, surfaced as CONNECTION_FAILED). This also means every family's cell values end up in the record hash; if a family must not be read, put it in a separate table.

Grant R and nothing else. W (write), C (create), A (admin) and X (exec) must all be absent — a single W anywhere in the chain makes the assertion false.

Verify the permissions

# List the effective grants -- expect READ only
hbase> user_permission 'users'
hbase> user_permission '@analytics'

# As burnledger_ro, this must succeed
hbase> scan 'users', {LIMIT => 1}

# As burnledger_ro, these must all fail with AccessDeniedException
hbase> put 'users', 'test-row', 'profile:email', 'test@test.com'
hbase> delete 'users', 'test-row', 'profile:email'
hbase> disable 'users'

Also confirm the scan path BurnLedger uses actually works, straight against the gateway:

# Health probe -- must return 200
curl -i -H "Accept: application/json" https://hbase-rest.example.com:8080/status/cluster

# Create a scanner (note: HTTP PUT) -- must return 201 with a Location header
curl -i -X PUT -H "Content-Type: application/json" \
  -d '{"batch": 10}' \
  https://hbase-rest.example.com:8080/users/scanner

Revoke write privileges if they exist

# Remove all grants for the principal on a table, then re-grant READ only
hbase> revoke 'burnledger_ro', 'users'
hbase> grant 'burnledger_ro', 'R', 'users'

# Same at namespace scope
hbase> revoke 'burnledger_ro', '@analytics'
hbase> grant 'burnledger_ro', 'R', '@analytics'

Passwords in URIs. BurnLedger's HBase config takes host and port as separate fields, so no connection URI is involved here. If you build a gateway URL by hand anywhere else (a curl check, a proxy config), remember a URI must percent-encode special characters in credentials — @%40, :%3A, /%2F, ?%3F, #%23 — or the host is parsed wrongly. Using a password limited to letters, digits, - and _ sidesteps the problem entirely.


Hash Scope Options

The hashScope parameter controls what BurnLedger hashes for each record a system returns.

HBase does not currently implement the distinction. The connector hashes the row key plus every cell value returned for the row, whatever hashScope is set to — there is no existence-only path in the HBase code. Set proofMode: "merkle" explicitly on HBase systems so the scope recorded on the attestation matches what is actually hashed. If you omit hashScope, the API defaults it to existence, which would label the system inaccurately even though the hashing does not change.

full (the effective behavior)

Hashes the row key plus every cell value returned for that row. Any change to any cell value produces a different hash, giving you the strongest proof that data has or has not been modified.

const system = await bl.registerSystem({
  // ...
});

Use when: You need to prove the exact data that existed, or prove that data was deleted rather than merely blanked out (nulling PII cells is not the same as deletion under some interpretations of GDPR).

existence

On the object-store connectors this chooses between hashing content and hashing only existence. HBase ignores hash_scope entirely — it hashes the row key and every cell value regardless, so existence does not avoid hashing sensitive content and does not speed up wide rows.

Registration refuses hash_scope: "full" here, since the field is inert and would only put an unbacked claim on the verification record. Use proof_mode: "merkle" on the attestation for content-level proof.

Behavior

HBase (either scope)
Detects row deletion Yes
Detects row insertion Yes
Detects cell value changes Yes
Detects cell timestamp-only rewrites No
Hash includes cell content (PII) Yes
Recommended for GDPR Art. 17 Yes

End-to-End Example

This example walks through the full process: preparing the read-only principal, registering the system, attesting before deletion, deleting, verifying, and generating a verification record.

Assume a table users whose row keys are <user_email>#<record_type>, e.g. jane.doe@example.com#profile.

1. Cluster setup (run once)

# In the HBase shell, as an admin
hbase> grant 'burnledger_ro', 'R', 'users'
hbase> user_permission 'users'

# Confirm the REST gateway is up (on the gateway host)
$ hbase rest start -p 8080

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: "myapp-users-hbase",
  connectorType: "hbase",
  connectionConfig: {
    host: "hbase-rest.example.com",
    port: 8080,
    tls: true,
    read_only: true,
  },
  // Row keys are "<email>#<record_type>", so the prefix ends at the separator
  subjectQuery: "users/{identifier}#",
});

// --- 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} rows found`);
}

// Step 2: Delete the rows in your application
// (e.g. a scan for the same prefix followed by Delete mutations, from a
//  principal that *does* have write access -- not this one)

// 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} rows`);
}

if (result.certificate) {
  await bl.savePdf(result.certificate.id, `erasure-cert-${subjectEmail}.pdf`);
  console.log(`Certificate issued: ${result.certificate.id}`);
  console.log(`Transparency status: ${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="myapp-users-hbase",
    connector_type="hbase",
    connection_config={
        "host": "hbase-rest.example.com",
        "port": 8080,
        "tls": True,
        "read_only": True,
    },
    # Row keys are "<email>#<record_type>", so the prefix ends at the separator
    subject_query="users/{identifier}#",
)

# --- 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} rows found")

# Step 2: Delete the rows in your application
# (scan the same prefix and issue Delete mutations from a write-capable
#  principal -- never the one registered here)

# 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} rows")

if result.certificate:
    bl.save_pdf(result.certificate.id, f"erasure-cert-{subject_email}.pdf")
    print(f"Certificate issued: {result.certificate.id}")
    print(f"Transparency status: {result.certificate.transparency_status.value}")
else:
    print(f"Not certified: status {result.status.value}")

Troubleshooting

Error Cause Fix
CONNECTION_FAILED with "HBase ping failed" The REST gateway is not running, not reachable, or host/port point at the native RPC (16020) or Thrift (9090) port Start the gateway (hbase rest start -p 8080), point host/port at it, and allowlist BurnLedger's egress IPs.
CONNECTION_FAILED with "HBase ping returned status 401/403" The gateway requires HTTP authentication The connector sends no Authorization header. Restrict the gateway at the network layer instead, or front it with a proxy that authenticates on BurnLedger's behalf.
CONNECTION_FAILED with a TLS/certificate error tls: true against a self-signed or private-CA certificate Terminate TLS on a proxy with a publicly trusted certificate. There is no custom-CA or skip-verify option.
CONNECTION_FAILED on attestation: "connector hbase: transport security plaintext is below the required minimum verified". A health check reports only connection failed — read transport_security on the system "tls": false Enable SSL on the REST gateway (or front it with a TLS-terminating proxy) and omit tls.
CONNECTION_FAILED with "cannot verify the credential is read-only" read_only missing or false Confirm the gateway principal has R only, then set "read_only": true. HBase permissions cannot be introspected, so the assertion is mandatory.
WRITE_ACCESS_DETECTED Not produced by the HBase connector. It has no way to detect write access (no permission introspection over the REST API), so it never reaches this verdict — the read_only assertion above is the only read-only gate If you see this code, it came from another system in the same attestation, not from HBase.
INVALID_QUERY_TEMPLATE The template has no {identifier} placeholder Rewrite as <table>/<prefix>{identifier}.... No other placeholder syntax is substituted — a template without {identifier} would certify an empty result.
CONNECTION_FAILED with "create scanner returned status 404" The table in the template does not exist, or the namespace prefix is wrong Check the part before the first /; use namespace:table (colon) for namespaced tables.
CONNECTION_FAILED with "create scanner returned status 405" A proxy or WAF is blocking the PUT /<table>/scanner request Allow PUT and DELETE through to the gateway; scanner lifecycle depends on them.
RECORD_LIMIT_EXCEEDED The prefix matched more rows than the system's max_records Tighten the prefix (include the separator so it does not match neighbouring keys), or raise max_records for the system. A missing separator often turns {identifier} into an unintended wide prefix.
0 rows found for a subject you expect to have data Row keys do not begin with the expanded prefix (salted or timestamp-led keys), or the separator is missing from the template Verify with a manual scan against the gateway; see Row-key design.
QUERY_HASH_MISMATCH during verification Data changed between attestation and verification Expected if you deleted or modified rows. Review the changes array in the verification result.
© 2026 ProChatFlow LLC Last updated present → absent → proven