Skip to main content

Production hardening checklist

By the end of this page you will have gone through every default that matters before ScramDB takes production traffic, and you'll know exactly which ones are one-line config changes versus real, named limitations to plan around. Each item links to the page that shows the full how-to.

1. Turn off --pg-no-auth​

The shipped Docker image and the Kubernetes StatefulSet both run with --pg-no-auth by default: every connection is trusted, no password, no hba_file. Override the container command and drop the flag, or set pg_no_auth = false explicitly in your config TOML (the engine itself already defaults it off; this item is about the shipped image's startup command, not the engine). See Docker: Securing a node for the exact command.

2. Set hba_file to rules that match your network​

The built-in default (localhost trusted, everything else password) is a reasonable starting point but is rarely the final posture for a deployment behind a load balancer or on a shared network. Write an explicit rule file scoped to your actual CIDR ranges, and pair hostssl with a password method to require TLS from any address you don't outright trust:

# /etc/scramdb/pg_hba.conf
host all all 10.0.0.0/8 trust
hostssl all all 0.0.0.0/0 password
hostssl all all ::/0 password

See Authentication for the full grammar.

3. Set tls_cert and tls_key​

TLS is off by default: both fields default to an empty path, and an empty path disables encryption entirely. Set both under [general] to a real certificate and key. See TLS encryption for generating a certificate and verifying the connection is actually encrypted.

4. Bind pg_address to the interface you actually need​

pg_address defaults to 127.0.0.1:5432 (loopback only) in the engine itself; the shipped image overrides it to 0.0.0.0:5432. If your deployment doesn't need to be reachable from every interface, bind to the specific one it does need. A container behind a load balancer or in its own network namespace is the normal exception to this, not a violation of it: 0.0.0.0 inside a container that's otherwise firewalled is a common and reasonable setup, just make sure something is actually doing that firewalling.

5. Firewall the metrics endpoint​

metrics_port (default 9090) always binds 0.0.0.0, independent of whatever pg_address is set to, and it has no authentication of any kind. This is true even if you've locked pg_address down to loopback: the metrics listener is a separate bind that doesn't inherit that restriction. Firewall port 9090 to your Prometheus scraper's address only, or disable it entirely if you're not scraping it:

[general]
metrics_port = 0 # disables the metrics endpoint

6. Least privilege for application roles​

Don't connect application traffic as the bootstrap superuser (scramdb). Create a role scoped to exactly the tables and columns the application touches, and use row-level security for per-tenant isolation instead of relying on the application layer to filter correctly on every query:

CREATE ROLE app_svc LOGIN PASSWORD 'a-real-password';
GRANT SELECT, INSERT, UPDATE, DELETE ON orders TO app_svc;

See Roles and privileges for table/column grants and Row-level security for per-tenant policies.

7. Secrets​

Prefer SCRAMDB_LICENSE_KEY over license_key in the config TOML: the environment variable wins if both are set, and it keeps the token out of an operator-readable file on disk. See Configuration: Environment variables.

If you use the UDF package registry with a bearer token, [udf.registry].token_file must be mode 0600 or tighter. This is enforced, not a suggestion: ScramDB refuses to start if the token file is group- or world-readable.

chmod 600 /etc/scramdb/registry-token

8. Know what is actually enforced​

CONNECT, TEMPORARY, USAGE, CREATE, and TRIGGER privilege grants parse and store but don't gate anything yet. VALID UNTIL and CONNECTION LIMIT on a role parse and store but aren't checked at connection time. Don't design a security boundary around any of these until they're enforced. The full table is in Roles and privileges: Parsed and stored, not enforced today.

9. The UDF sandbox guarantee​

Every installed user-defined function, in every language ScramDB supports, runs sealed: no filesystem access, no network, no subprocess, no environment access. This holds regardless of what a package's own manifest declares wanting; ScramDB doesn't widen what a module can do based on its own stated capabilities today, every module is sealed the same way no matter what it asks for.

Resource caps bound a runaway call:

CapDefaultBounds
fuel_per_batch50,000,000Compute per batch
memory_bytes64MBLinear memory per invocation
timeout_ms1000Wall-clock time per invocation
output_bytes16MBResult size per invocation

Breaching any of these surfaces as a loud, typed error naming the function and the specific limit it hit, never a silent truncation.

The honest gap: this fuel/memory/timeout/output governance applies to the JS/TS afterburner path, functions compiled to WebAssembly. LANGUAGE python and LANGUAGE ruby inline functions run through a separate interpreted path that has no per-call fuel, memory, or timeout cap today. A runaway Python or Ruby function body is not bounded the way a JS/TS one is; keep that in mind if you're accepting untrusted function bodies in those languages. If you don't need UDFs at all, set [udf].enabled = false to disable the substrate entirely, including the Wasm engine startup cost.

10. Registry egress control for UDF packages​

For an air-gapped deployment, set [udf.registry].offline = true. Every name-form install verb (scram.install('ns/pkg'), scram.upgrade, scram.versions) then refuses loudly instead of dialing out. The bytes-upload install form (scram.install(bytea) / scram.register) always works either way: it makes no network call, so it's the escape hatch for offline installs.

Use allow and deny glob lists to control which package names may install at all, independent of offline mode:

[udf.registry]
offline = true
allow = ["mycompany/*"]
deny = []

deny wins over allow on a match.