Redis Integration Guide

This guide covers how to connect a Redis instance to BurnLedger, configure key patterns, and set up a read-only user.


Connection Configuration

BurnLedger connects to Redis using a host:port address with optional ACL username and password authentication.

Configuration fields:

Field Required Description
addr Yes Redis address in host:port format. The host must be publicly resolvable — localhost, .local/.internal names, and private/loopback addresses are rejected at registration.
username No ACL username (Redis 6.0+). Omit it and Redis authenticates as the default user — so if you created a dedicated ACL user such as burnledger_ro, you must set this or the connector will connect as default and be checked against default's privileges.
password No Redis password (AUTH).
db No Redis database number (default: 0).
tls No Transport encryption. Defaults to true — omit it and the connector uses TLS 1.2+ with the certificate verified. "tls": false selects a plaintext link, which production refuses to build; see Transport security.
ca_cert No Base64-encoded PEM CA bundle used to verify the server certificate. Replaces the system trust store; only meaningful when TLS is on.
read_only Yes Your assertion that the credential is read-only. It must be true — BurnLedger does not claim to verify this for Redis, so without it construction is refused. See Read-Only User Setup.

TypeScript

const system = await bl.registerSystem({
  name: "session-cache",
  connectorType: "redis",
  connectionConfig: {
    addr: "redis.example.com:6379",
    username: "burnledger_ro",
    password: process.env.REDIS_PASSWORD,
    db: 0,
    read_only: true,
  },
  subjectQuery: "user:{identifier}:*",
});

Python

system = bl.register_system(
    name="session-cache",
    connector_type="redis",
    connection_config={
        "addr": "redis.example.com:6379",
        "username": "burnledger_ro",
        "password": os.environ["REDIS_PASSWORD"],
        "db": 0,
        "read_only": True,
    },
    subject_query="user:{identifier}:*",
)

Connection requirements:

Requirement Details
TLS On by default, and required. The connector negotiates TLS 1.2+ and verifies the certificate unless the config sets "tls": false, which a production deployment then refuses to connect at all. The endpoint must be TLS-enabled (or fronted by a TLS proxy). Supply ca_cert for a private CA. See Transport security.
Network access Add BurnLedger's egress address to your firewall or security group.
Permissions Read-only, and asserted by you with "read_only": true. BurnLedger refuses to connect if the ACL rules prove the credential can write, and refuses to connect at all without the assertion. See Read-Only User Setup.
Timeout Each attestation and health check must complete within the system's query_timeout (default 30 seconds, set at registration). The dial itself has no separately configurable connect timeout — it is bounded by BurnLedger's own dialer.

Transport security

BurnLedger signs the number of keys it counted. That number is read over this connection, so the connection is classified before the connector is built and anything below verified TLS is refused — see connector transport security.

What reaches verified: omit tls, or set "tls": true. The connector then negotiates TLS 1.2+ and verifies the server certificate against the system trust store, or against ca_cert when you supply one. The hostname checked against the verification record is the host in addr, pinned explicitly — BurnLedger's SSRF-safe dialer may hand the handshake a resolved IP, and in enclave mode an egress proxy terminates the TCP leg, so nothing else on the dial path carries the real name. addr must therefore be host:port; a bare host fails at connection time with CONNECTION_FAILED.

What is refused: "tls": false classifies plaintext and the connector is not built (health check CONNECTION_FAILED, and every attestation against the system fails). Development and test deployments can lower the floor with DP_ALLOW_UNVERIFIED_TRANSPORT=true; do not set it in production.

Redis systems registered before the transport-security fix (ADR-012). Until that fix this connector sent everything in cleartext no matter what tls said: go-redis ignores Options.TLSConfig whenever a custom dialer is set, and BurnLedger sets one on every production path. A config saying "tls": true connected in the clear, and the attestation still completed and certified. Any verification record issued for a Redis system before that fix was therefore issued on evidence gathered over an unauthenticated link — the key count could have been altered in transit, and the ACL username and password crossed the network in cleartext. Rotate those credentials, re-run the attestation, and re-issue the verification record.


Key Pattern Format

For Redis, the query template is a key pattern using {identifier} as the placeholder for the data subject. BurnLedger uses SCAN with MATCH to find keys — it never uses KEYS (which blocks the server).

Pattern syntax:

  • {identifier} is replaced with the subject value at attestation time. The literal token {identifier} — spelled exactly that way — is required. A pattern without it is rejected at registration, and rejected again at attestation time with INVALID_QUERY_TEMPLATE ("contains no {identifier} placeholder"). This is deliberate: a pattern that never substitutes would match nothing, report zero keys, and produce a signed verification record asserting the subject has no data.
  • * matches any sequence of characters (standard Redis glob).
  • ? matches a single character.

Scan budget. Matching runs as SCAN … MATCH <pattern> COUNT 100, capped at 10,000 iterations (~1M keys examined). A pattern that is not anchored on the identifier can blow through that and fail with RECORD_LIMIT_EXCEEDED ("Redis SCAN exceeded the maximum iteration budget"). Anchor the pattern on a fixed prefix wherever you can — user:{identifier}:* scans far better than *{identifier}*.

Examples:

# All keys for a user
user:{identifier}:*

# Session keys
session:{identifier}

# Keys with user ID prefix
{identifier}:*

# Specific hash key
profile:{identifier}

Supported key types:

Type Hashing behavior
string Hash the string value
hash Hash all field-value pairs (sorted by field name)
list Hash all elements in order
set Hash all members (sorted lexicographically)
zset Hash all members with scores, sorted by score ascending, ties broken by member name; each element encoded as score:member
stream Not supported — returns UNSUPPORTED_REDIS_TYPE
anything else Not supported — any type outside the five above (module types such as ReJSON-RL, for example) also returns UNSUPPORTED_REDIS_TYPE

A single unsupported key anywhere in the matched set fails the whole attestation — the connector does not skip it.


Read-Only User Setup

BurnLedger does not claim to verify that a Redis credential is read-only. You assert it, with "read_only": true, and BurnLedger records that the guarantee rests on your assertion. Without the flag, construction is refused — closed, not open. BurnLedger never writes to check.

Why an assertion and not a check

The connector still reads ACL WHOAMI and ACL GETUSER <user>, and it still refuses a credential those rules prove can write. What it will no longer do is call the opposite answer a verification.

Redis reaches write access by several independent routes, and a rule set that grants none of the obvious ones can still hold one of the others: CONFIG, ACL, DEBUG, MODULE and SCRIPT each get there on their own. Three successive attempts to enumerate the safe side of that line each closed one route and opened another — most recently config|get, which writes nothing but, on an instance started with requirepass, reads the password of the stock default user, who holds +@all. A rule string also does not show key patterns, selectors, or what a module added to the command table. "These rules name nothing I recognise as a write" is not the same statement as "this credential cannot write", and only the second one is worth putting on a verification record.

So the classifier survives as a refusal signal only: it can say definitely writable, never definitely safe.

What this means on the record

A Redis system reports read_only_enforcement: operator_asserted — the same value BurnLedger reports for HBase, MarkLogic and Azure Blob Storage, whose APIs expose no privilege introspection at all. It is deliberately distinguishable from verified_by_introspection, which PostgreSQL, MySQL and the other catalogue-queryable engines report: California's proposed CPPA data-broker audit rule (11 CCR § 7632(d)) does not let an audit finding rest primarily on assertions by the broker's own management, so the two are not interchangeable evidence and BurnLedger will not print one when it has the other.

Point the connector at a read-only replica, or at an ACL user built by the recipe below, so that the assertion you are making is actually true.

BurnLedger now reports which one you pointed it at. POST /v1/systems/test-connection returns replication.role from INFO replicationprimary for role:master, replica for role:slave — with master_last_io_seconds_ago beside it and a note when master_link_status is not up. A replica whose link to its master broke keeps serving its last known keyspace, so a SCAN over it can report zero for a subject the master still holds; the disclosure is what lets you see that before the count is signed. It needs +info, which the recipe below already grants. See endpoint role.

What is still refused

The ACL rules are evaluated left to right (later rules win, as Redis applies them), and the connection is refused with WRITE_ACCESS_DETECTED when they grant anything outside a measured Redis 7 zero-write set: the @read, @pubsub, @connection and @transaction categories, every command in them, and the few control commands that cannot write, cannot grant, and cannot disclose a credential that does either (info, object, acl|whoami, acl|getuser). Any other category, any unrecognised command, and any selector carrying its own grants is treated as write.

Two revocations are believed to strip a grant: -@all and nocommands. Every other revocation, -@write included, only cancels the identical grant. -@write removes the write-flagged commands, which is not the same as removing write access: +@all -@write still holds @admin, and a credential holding ACL SETUSER can grant itself +set in one command. So -@write does not rescue a broad grant — start from -@all and add what you need.

Grant ACL WHOAMI and ACL GETUSER even though construction no longer depends on them. The ACL command lives in the @admin, @dangerous and @slow categories, so a rule set containing -@admin -@dangerous denies it. A denial there is no longer fatal on its own — your read_only assertion carries the connection — but the refusal signal goes blind with it, and a credential you meant to be read-only and mis-typed is then registered rather than rejected. Grant the two subcommands explicitly, as the recipe below does. (-@admin -@dangerous also takes back +info, which is fatal; see below.)

On Redis 5.x and below there are no ACLs to read at all, so nothing is refused and everything rests on the assertion. +info is required on every version: without it the connector cannot tell a standalone node from a cluster node and refuses to connect at all.

Redis 6.0+ (ACL)

# Connect to Redis as admin
redis-cli

# Create a read-only user
ACL SETUSER burnledger_ro on >your-secure-password ~* &* -@all +@read +ping +info +scan +type +object +dbsize +acl|whoami +acl|getuser

# Verify
ACL GETUSER burnledger_ro

Remember to set "username": "burnledger_ro" in the connector config — without it Redis authenticates the connection as the default user, reads the keyspace with default's privileges, and the write check inspects default's rules instead. And set "read_only": true: the recipe above makes the assertion true, it does not make it unnecessary.

ACL breakdown:

Rule Purpose
on Enable the user
>your-secure-password Set the password
~* Access all key patterns
-@all Start from no commands at all, so the + rules below are the whole grant
+@read Allow all read commands
+ping +info +scan +type Needed for connection validation and key discovery
+object +dbsize Key introspection; both containers are read-only
+acl\|whoami +acl\|getuser Let BurnLedger read the rules it refuses on; without them a write-capable credential is registered on your assertion instead of rejected

A new ACL user starts with no commands, so the leading -@all is redundant on ACL SETUSER burnledger_ro for a user that does not exist yet — but it is not redundant when you re-run the recipe against an existing user, and it is what makes the rule list mean what it reads as.

config|get is deliberately not on that list. It grants no write, but on an instance started with requirepass it reads the password in plaintext — and that password authenticates as the stock default user, who holds +@all. A credential that can run CONFIG GET can therefore write as somebody else, so BurnLedger counts it as write access and refuses. The connector never issues CONFIG GET, and the recipe above does not grant it. This is also the last of the three escalation routes that ended the practice of calling any of this a verification: see Why an assertion and not a check.

Do not append -@write -@admin -@dangerous. Earlier revisions of this guide did, and it breaks the connector: ACL and INFO live in @admin/@dangerous, so those revocations take back +info, +acl|whoami and +acl|getuser — the connector then fails with CONNECTION_FAILED on INFO cluster, before it ever reaches the write check. They buy nothing either, because -@all already granted nothing to take away. Do not add +@dangerous, +@admin, or +@all anywhere; any of them is refused with WRITE_ACCESS_DETECTED.

Persist ACLs

# Save to the ACL file so they survive restarts
ACL SAVE

Or add to your redis.conf or ACL file:

user burnledger_ro on >your-secure-password ~* &* -@all +@read +ping +info +scan +type +object +dbsize +acl|whoami +acl|getuser

Redis 5.x and below

Redis 5.x has no ACLs, so there is nothing to read: ACL WHOAMI fails, the refusal signal never runs, and the connection rests entirely on your assertion. The config is otherwise the same, minus the username:

{ "addr": "redis.example.com:6379", "password": "…", "read_only": true }

On any version, read_only: true is an assertion, not a check — BurnLedger logs a warning naming it and takes your word for it. Point it at a read-only replica so your word is good — and read the endpoint role the test connection now reports, which says whether that replica is still in contact with its master.

Verify permissions

This is the check BurnLedger cannot do for you, and the evidence behind the read_only you are about to set. Run it before you register the system.

# Connect as the read-only user
redis-cli -u redis://burnledger_ro:your-secure-password@redis.example.com:6379

# These should succeed
SCAN 0 MATCH user:* COUNT 10
GET user:test:profile
TYPE user:test:profile

# These should fail with "NOPERM"
SET test_key "value"
DEL user:test:profile
FLUSHDB

# So should these — each is a separate route to writing as somebody else,
# and none of them is write-flagged, so none is covered by `-@write`
CONFIG GET requirepass
ACL SETUSER burnledger_ro +set
SCRIPT LOAD x
MODULE LIST

Verified on redis:7-alpine started with --requirepass: the ACL above answers NOPERM to every command in the second block. DEBUG is the fifth route and answers differently — on a stock server it is refused as disabled (enable-debug-command) rather than by the ACL, so check ACL GETUSER burnledger_ro rather than the error text if you have enabled it.


Hash Scope Options

Hashes the complete value of every matching key. For compound types (hash, list, set, zset), the value is canonicalized: fields are sorted deterministically before hashing.

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

existence

Records only how many keys matched the pattern. Key values are never read. Faster for large datasets where you only need to prove keys existed and were removed. (The TYPE of each matched key is still checked, so an unsupported type still fails the attestation.)

const system = await bl.registerSystem({
  // ...
  hashScope: "existence",
});

End-to-End Example

TypeScript

import { BurnLedger } from "burnledger";
import fs from "node:fs/promises";

const bl = new BurnLedger({
  apiKey: process.env.BURNLEDGER_API_KEY,
});

// Register the Redis instance
const system = await bl.registerSystem({
  name: "user-sessions",
  connectorType: "redis",
  connectionConfig: {
    addr: "redis.example.com:6379",
    username: "burnledger_ro",
    password: process.env.REDIS_PASSWORD,
    db: 0,
    read_only: true,
  },
  subjectQuery: "user:{identifier}:*",
});

// Attest that keys exist
const attestation = await bl.attest("user-4821", {
  systemIds: [system.id],
  proofMode: "merkle",
});

for (const s of attestation.systems) {
  console.log(`Found ${s.recordCount} keys for user-4821`);
}

// Delete the keys in your application
// await redis.del(...keys);

// Verify 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, "user-4821", { timeout: 60 });

if (result.certificate) {
  await bl.savePdf(result.certificate.id, `redis-deletion-cert-${result.certificate.id}.pdf`);
  console.log(`Certificate issued: ${result.certificate.id}`);
} 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 Redis instance
system = bl.register_system(
    name="user-sessions",
    connector_type="redis",
    connection_config={
        "addr": "redis.example.com:6379",
        "username": "burnledger_ro",
        "password": os.environ["REDIS_PASSWORD"],
        "db": 0,
        "read_only": True,
    },
    subject_query="user:{identifier}:*",
)

# Attest that keys exist
attestation = bl.attest(
    "user-4821",
    system_ids=[system.id],
    proof_mode="merkle",
)

for s in attestation.systems:
    print(f"Found {s.record_count} keys for user-4821")

# Delete the keys in your application
# redis_client.delete(*keys)

# Verify deletion
# The certificate is ISSUED BY verify() once the data is gone -- there is
# no separate "create certificate" step.
result = bl.verify(attestation.id, "user-4821", timeout=60)

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

Troubleshooting

Error Cause Fix
CONNECTION_FAILED with "connection refused" Redis not reachable at the given address Verify addr, check firewall rules, ensure BurnLedger IPs are allowlisted.
CONNECTION_FAILED with "NOAUTH" / "WRONGPASS" Password required but not provided, or the ACL username was omitted so Redis authenticated as default Add password and — for an ACL user — username to the connection config.
CONNECTION_FAILED with a TLS handshake error TLS is on by default and the endpoint is plaintext, or the certificate does not verify against the trust store / the host in addr Use a TLS-enabled endpoint whose certificate names that host, supply ca_cert for a private CA, or — for a local/dev instance only — set "tls": false and run with DP_ALLOW_UNVERIFIED_TRANSPORT=true.
CONNECTION_FAILED on attestation: "connector redis: transport security plaintext is below the required minimum verified". A health check reports only connection failed — read transport_security on the system "tls": false, so the link would be unencrypted and the record count unattributable to the real server Enable TLS on the endpoint and remove "tls": false. See Transport security.
CONNECTION_FAILED "Redis addr must be host:port to verify a TLS certificate" addr carries no port, so there is no host to bind the verification record to Write addr as host:port, e.g. redis.example.com:6379.
CONNECTION_FAILED "cannot verify the credential is read-only" read_only is missing or false. BurnLedger does not verify this for Redis, so there is nothing to fall back on Confirm the credential is read-only (Verify permissions), then set "read_only": true. See Why an assertion and not a check.
WRITE_ACCESS_DETECTED The ACL rules prove the credential can write — +@all, +@write, +@dangerous, +@admin, +config\|get, an individual write command, or a broad grant that only a -@write takes back (+@all -@write still holds ACL SETUSER) Configure a read-only ACL user starting from -@all. "read_only": true does not override this: a proof of write outranks the assertion. See Read-Only User Setup.
UNSUPPORTED_REDIS_TYPE A matching key is a stream or another unsupported type Only string/hash/list/set/zset can be hashed. Narrow the pattern so it excludes those keys, or register a separate system.
RECORD_LIMIT_EXCEEDED Too many keys match the pattern, or the SCAN sweep exceeded 10,000 iterations Narrow / anchor the key pattern, or raise max_records (it cannot exceed the plan default). The iteration cap is fixed and is not raised by max_records.
© 2026 ProChatFlow LLC Last updated present → absent → proven