Security Hardening¶
This guide documents RockLake's security posture, IAM credential isolation, SQL injection defenses, TLS configuration, authentication, and the excision audit trail.
IAM Credential Isolation¶
RockLake separates the catalog storage prefix from the data file prefix, enabling precise IAM policy isolation in object storage.
Prefix Layout¶
bucket/
catalogs/ ← SlateDB SST files, manifests, WAL entries
my-catalog/
sst-001.sst
MANIFEST-001
...
data/ ← User Parquet/Arrow/Avro data files
schema_a/
table_b/
part-001.parquet
...
IAM Policy Design¶
Catalog sidecar (rocklake serve) requires: - s3:GetObject, s3:PutObject, s3:DeleteObject on catalogs/* - No access to data/*
DuckDB data plane (reads/writes Parquet files) requires: - s3:GetObject, s3:PutObject on data/* - No access to catalogs/*
This ensures that a compromise of the DuckDB data-plane credentials cannot read or tamper with the catalog, and a compromise of the catalog sidecar cannot access user data files.
Expected Error: SQLSTATE 42501¶
When the catalog sidecar attempts to access data/* (or vice versa), the expected SQLSTATE is 42501 (insufficient_privilege). The PG-wire layer maps object_store::Error::NotAuthorized to this code.
MinIO IAM Setup¶
# Create catalog-only policy.
mc admin policy create myminio catalog-policy - <<'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:ListBucket"],
"Resource": ["arn:aws:s3:::mybucket/catalogs/*", "arn:aws:s3:::mybucket"]
}
]
}
EOF
# Create data-only policy.
mc admin policy create myminio data-policy - <<'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:ListBucket"],
"Resource": ["arn:aws:s3:::mybucket/data/*", "arn:aws:s3:::mybucket"]
}
]
}
EOF
# Attach to service accounts.
mc admin user add myminio rocklake-sidecar <password>
mc admin policy attach myminio catalog-policy --user rocklake-sidecar
mc admin user add myminio duckdb-plane <password>
mc admin policy attach myminio data-policy --user duckdb-plane
SQL Injection Defenses¶
The PG-wire SQL classifier defends against injection at multiple layers:
Input Validation¶
| Guard | Description | SQLSTATE |
|---|---|---|
| NUL byte rejection | Queries containing \x00 are rejected immediately | 42601 |
| Overlong query rejection | Queries > 1 MiB are rejected | 42000 |
| Non-ASCII keyword bypass | Unicode lookalikes do not match SQL keywords | N/A |
| Parameterized queries | All user-supplied values are bound as parameters, never interpolated | N/A |
Fuzz Test Results¶
The sql_injection_fuzz_zero_panics_zero_wrong_results test in crates/rocklake-pgwire/tests/v040_security_tests.rs verifies: - Zero panics across 40+ adversarial inputs (NUL bytes, overlong strings, nested quotes, Unicode lookalikes, SQL keywords in various cases) - Zero wrong results — every input returns a valid classified outcome or a well-defined error
SQLSTATE Mapping¶
| Condition | SQLSTATE | Description |
|---|---|---|
| NUL byte in query | 42601 | syntax_error |
| Query exceeds 1 MiB | 42000 | syntax_error_or_access_rule_violation |
| Write to read-only catalog table | 25006 | read_only_sql_transaction |
| IAM permission denied | 42501 | insufficient_privilege |
| Writer epoch fencing | 40001 | serialization_failure |
TLS Configuration¶
Minimum Protocol Version¶
RockLake enforces a minimum TLS version of TLS 1.2. TLS 1.0, TLS 1.1, and SSL 3.0 are rejected at the server level.
The TLS configuration in crates/rocklake-pgwire/src/server.rs uses rustls::ServerConfig with rustls::server::WebPkiClientVerifier and defaults to the rustls::crypto::ring provider, which enforces TLS 1.2+.
CLI Flags¶
| Flag | Description |
|---|---|
--tls-cert <path> | Path to PEM-encoded TLS certificate |
--tls-key <path> | Path to PEM-encoded TLS private key |
--tls-required | Reject plaintext connections |
--insecure-no-tls-warning-suppress | Suppress auth-without-TLS warning |
Auth Without TLS Warning¶
When --auth-user / --auth-password are configured without --tls-cert / --tls-key, the server emits this warning at startup:
WARN Password authentication is enabled without TLS.
Credentials will be sent in plaintext.
Use --tls-cert / --tls-key to enable TLS, or pass
--insecure-no-tls-warning-suppress if this is intentional.
TLS Version Gating¶
The tls_audit_tls_11_and_older_rejected test in crates/rocklake-pgwire/tests/v040_security_tests.rs verifies that: - TLS 1.0 (protocol version 0x0301): rejected - TLS 1.1 (protocol version 0x0302): rejected - TLS 1.2 (protocol version 0x0303): accepted - TLS 1.3 (protocol version 0x0304): accepted
--require-tls Error Code¶
When --tls-required is set and a plaintext client connects, the server returns SQLSTATE 28000 (invalid_authorization_specification) in the PG ErrorResponse message.
Authentication Security¶
Constant-Time Password Comparison¶
RockLake uses constant-time byte comparison for password verification to prevent timing side-channel attacks. The implementation:
- Never short-circuits on length mismatch — the full comparison is always performed, even when the lengths differ.
- XOR-folds all byte comparisons into a single accumulator, returning
trueonly if all bytes match. - Does not reveal whether the user exists or the password length is correct.
The auth_timing_constant_time_comparison_no_early_exit and auth_timing_wrong_length_no_fast_path_exit tests in crates/rocklake-pgwire/tests/v040_security_tests.rs verify this contract.
SCRAM-SHA-256¶
For production deployments, use --auth-method scram-sha-256 (the default when TLS is enabled) instead of plaintext password comparison. SCRAM-SHA-256 provides: - Mutual authentication (server also proves identity to client) - Salted, iterated hashing (prevents offline dictionary attacks) - Channel binding (prevents MITM when combined with TLS)
Excision Audit Trail¶
Every rocklake excise --apply invocation writes an audit record under the 0xFF | "excised" key prefix. This record is:
- Immutable — written once, never overwritten
- Accumulating — each excision adds a new record
- Visible to
rocklake diagnose— the diagnose report includes a count of excision events
Audit Record Structure¶
Key: 0xFF | "excised" | <timestamp_millis_big_endian>
Value: ExciseAuditEntry {
timestamp_millis: u64,
before_snapshot: u64,
keys_deleted: u64,
keys_failed: u64,
operator: String,
}
Viewing Excision History¶
# Show diagnose report including excision audit events.
rocklake diagnose --catalog s3://bucket/catalogs/my-catalog
# The report includes a line like:
# [P2] 3 excision events recorded (latest: 2026-01-15T10:30:00Z by admin@company.com)
Test Coverage¶
The excision_audit_trail_* tests in crates/rocklake-pgwire/tests/v040_security_tests.rs verify: - The audit key prefix follows the 0xFF | "excised" convention - The audit entry contains all required fields - The audit entry is visible to list_audit_entries() - excise_plan on a fresh catalog returns a safe plan
Security CI Jobs¶
The following CI jobs enforce security requirements on every pre-release:
| Job | Description | Test File |
|---|---|---|
security-tests | PG-wire security tests (existing) | security_tests.rs |
security-v040 | v0.40.0 new security tests | v040_security_tests.rs |
tls-compat | TLS protocol-version gating | tls_compat_tests.rs |
fault-injection | Catalog fault injection (existing) | fault_injection_tests.rs |
fault-injection-v040 | v0.40.0 new fault injection tests | v040_fault_injection_tests.rs |