Security
View as MarkdownNucleus provides defense-in-depth security: row-level security (RLS), tenant-aware encryption, and immutable audit logging.
Row-Level Security (RLS)
Control which rows each user can see and modify.
Enable RLS
ENABLE RLS ON patients;
Create Policies
-- Doctors can see all patients
CREATE POLICY doctor_read ON patients
FOR SELECT
TO doctor
USING (true);
-- Patients can only see their own records
CREATE POLICY patient_read ON patients
FOR SELECT
TO patient
USING (patient_id = current_user_id());
-- Nurses can only see patients in their department
CREATE POLICY nurse_read ON patients
FOR SELECT
TO nurse
USING (department = current_tenant());
Policy Types
| Type | Behavior | |------|----------| | Permissive (default) | At least one permissive policy must pass | | Restrictive | All restrictive policies must pass |
Policies combine: (any permissive passes) AND (all restrictive pass).
Predicate Types
| Predicate | Description |
|-----------|-------------|
| column = 'value' | Column equals constant |
| column = current_user() | Column matches session user |
| column = current_tenant() | Column matches session tenant |
| has_role('admin') | Session must have role |
| Combined with AND / OR | Compose predicates |
Superuser Bypass
Users with the superuser role bypass all RLS policies.
Column-Level Data Masking
Mask a column's value for a role, without changing what is stored:
CREATE MASKING POLICY ON people (ssn) TO analyst USING REDACT '***';
CREATE MASKING POLICY ON people (email) TO analyst USING EMAIL;
CREATE MASKING POLICY ON cards (pan) TO support USING PARTIAL (0, 4, '*');
CREATE MASKING POLICY ON people (name) TO analyst USING HASH;
SHOW MASKING POLICIES;
DROP MASKING POLICY ON people (ssn) TO analyst;
| Rule | Effect |
|------|--------|
| REDACT '<text>' | Replace the value with a constant |
| EMAIL | ada@example.com → a**@example.com |
| PARTIAL (first, last [, 'char']) | Keep the first and last N characters, mask the middle |
| HASH | Replace with a stable digest, for pseudonymisation |
| NONE | Pass through |
A policy is identified by (table, column, role); creating one for the same
triple replaces it. Masking DDL requires superuser authority, participates in
transactions with the rest of the security catalog, and survives restart.
Masks apply to a role the session has, including through role membership,
and they apply on every path a row leaves by — not just plain SELECT.
Superusers are not masked.
Two limits worth knowing. Masking is not a substitute for RLS: it changes the
value returned, so a masked column can still be used in a WHERE clause to
narrow rows. And there is no column-level GRANT — to remove a column from a
role's reach entirely, expose a view that omits it.
Encryption
Tenant-Aware Key Management
Nucleus supports per-tenant encryption keys with rotation:
-- Register a tenant key
SELECT register_tenant_key('tenant_42', decode('...base64key...', 'base64'));
-- Rotate to a new key (old key archived for decryption)
SELECT rotate_key('tenant_42', decode('...newkey...', 'base64'));
Key Rotation
Key rotation is a managed process:
- Begin — New key registered, old key marked for archival
- Re-encrypt — Pages re-encrypted in background (progress tracked)
- Complete — Old key archived, new key active
Old keys are retained for decrypting data written before rotation.
Encrypted Indexes
Encrypted index modes are unavailable. CREATE INDEX ... USING ENCRYPTED,
ENCRYPTED_OPE, ENCRYPTED_RANDOM, and ENCRYPTED_LOOKUP refuse with SQLSTATE
0A000. The retired prototype did not provide secure cryptography. Recovery
keeps historical catalog definitions and base rows but does not rebuild those
sidecars. Base rows remain plaintext; this boundary is separate from page-level
encryption at rest.
Audit Logging
Immutable, append-only audit log for compliance and forensics.
What's Logged
Every query execution records:
| Field | Description |
|-------|-------------|
| timestamp | When the query ran |
| user | Who ran it |
| action | Query type (SELECT, INSERT, UPDATE, DELETE, DDL) |
| table | Affected table |
| query | Full SQL text |
| rows_affected | Number of rows modified |
| success | Whether the query succeeded |
Querying the Audit Log
-- Recent activity by user
SELECT * FROM nucleus_audit_log WHERE user = 'alice' ORDER BY timestamp DESC LIMIT 100;
-- All changes to a table
SELECT * FROM nucleus_audit_log WHERE table_name = 'patients' AND action IN ('INSERT', 'UPDATE', 'DELETE');
Audit entries are immutable — they cannot be updated or deleted.
Session Context
Security features use session context to evaluate policies:
-- Set session properties
SET SESSION user = 'alice';
SET SESSION role = 'doctor';
SET SESSION tenant_id = 'hospital_42';
Context properties available to RLS predicates and masking rules:
| Function | Returns |
|----------|---------|
| current_user() | Session user |
| current_tenant() | Session tenant ID |
| has_role('name') | Whether session has role |
Cluster Security
Inter-Node TLS
In clustered deployments, node-to-node traffic — Raft and replication — uses mutual TLS: each node presents its certificate and requires its peer to present one signed by the cluster CA, in both directions. A node without a valid certificate cannot join, and a node will not talk to a peer whose certificate the CA did not sign.
Configure it with environment variables (there is no [cluster.tls] TOML
section — this page previously showed one that does not exist):
NUCLEUS_INTERNAL_TLS=1
NUCLEUS_INTERNAL_TLS_CERT=/path/to/node.crt
NUCLEUS_INTERNAL_TLS_KEY=/path/to/node.key
NUCLEUS_INTERNAL_TLS_CA=/path/to/ca.crt
NUCLEUS_INTERNAL_TLS_SERVER_NAME=nucleus.internal # optional, default "localhost"
NUCLEUS_CLUSTER_TOKEN is still enforced at connection setup and is a second
factor rather than a replacement. Node identity within an authenticated cluster
is not yet bound to the certificate: a peer that completes the handshake is
trusted for the node id it claims.
All Raft messages, WAL streaming, and gossip use encrypted channels.