Skip to content

Security: ZVN-DEV/powdb

SECURITY.md

Security Policy

Supported Versions

PowDB ships security fixes only for the latest minor series. Upgrade to the latest release to stay supported.

Version Supported
0.29.x ✅
0.28.x ❌ (superseded)
0.27.x ❌ (superseded)
0.26.x ❌ (superseded)
0.25.x ❌ (superseded)
0.24.x ❌ (superseded)
0.23.x ❌ (superseded)
0.22.x ❌ (superseded)
0.19.x ❌ (superseded)
0.18.x ❌ (superseded)
0.17.x ❌ (superseded)
0.16.x ❌ (superseded)
0.15.x ❌ (superseded)
0.14.x ❌ (superseded)
0.13.x ❌ (superseded)
0.12.x ❌ (superseded)
0.11.x ❌ (superseded)
0.10.x ❌ (superseded)
0.9.x ❌ (superseded)
0.8.x ❌ (superseded)
0.7.x ❌ (superseded)
0.6.x ❌ (superseded)
0.5.x ❌ (superseded)
0.4.4 – 0.4.9 ❌ (superseded)
0.4.1 – 0.4.3 ❌ (yanked)
≤ 0.4.0 ❌

v0.4.1, v0.4.2, and v0.4.3 are yanked for data-loss bugs in crash recovery and were replaced by v0.4.4, which added a permanent durability regression suite. If you are on any of those three versions, or any release older than the current minor series, upgrade to the latest release. See CHANGELOG.md for details.

Reporting a Vulnerability

If you discover a security vulnerability in PowDB, please report it responsibly.

Do not open a public issue. Instead, use GitHub's private vulnerability reporting:

https://github.com/ZVN-DEV/powdb/security/advisories/new

That link opens a report visible only to the maintainers. It works from any GitHub account, keeps the discussion attached to the repository, and lets us issue a security advisory and a patched release from the same place.

Include:

  • Description of the vulnerability
  • Steps to reproduce
  • Potential impact
  • Suggested fix (if any)

You should receive an acknowledgment within 48 hours. We aim to provide a fix or mitigation within 7 days for critical issues.

If the advisory form is unavailable to you for any reason, open a public issue containing only the words "security report, please open a private channel" and no technical detail, and we will follow up.

Scope

PowDB is a storage engine and query executor. Security-relevant areas include:

  • Wire protocol (crates/server/) — binary framing, authentication, connection limits
  • Query parser (crates/query/src/parser.rs) — input validation, nesting depth limits
  • Storage engine (crates/storage/) — WAL integrity, mmap safety, file I/O bounds
  • Network binding — server binds to 127.0.0.1 by default (not 0.0.0.0)

Transport Security (TLS)

PowDB supports native TLS for encrypted client-server connections. To enable TLS, set the following environment variables when starting the server:

  • POWDB_TLS_CERT — path to the PEM-encoded TLS certificate
  • POWDB_TLS_KEY — path to the PEM-encoded TLS private key

When both are set, the server requires TLS for all connections. When unset, the server accepts plaintext TCP connections. For production deployments, always enable TLS or use a reverse proxy / SSH tunnel. Setting POWDB_REQUIRE_TLS makes the server refuse to start if authentication is configured without TLS.

The bundled CLI supports TLS in remote mode (since v0.17.0):

  • --tls (or POWDB_TLS=1) encrypts the connection, verifying the server certificate against the built-in webpki (Mozilla) root store
  • --tls-ca <path> (or POWDB_TLS_CA) trusts a custom root CA PEM instead, for self-signed deployments; implies --tls
  • --tls-server-name <name> (or POWDB_TLS_SERVER_NAME) sets the hostname the certificate is verified against, for connecting by IP to a certificate issued for a hostname; implies --tls

Without any TLS flags the CLI behaves exactly as before and connects over plaintext TCP. On releases before v0.17.0, the bundled CLI has no TLS support and cannot reach a TLS-required server; use the TS client or a TLS-terminating tunnel instead.

Generating a self-signed certificate for testing

Run openssl version first. The recipe below is written so that LibreSSL (what stock macOS ships as /usr/bin/openssl), OpenSSL 1.1.1 and OpenSSL 3.x all produce the same certificate, and two details do that work:

  • -pkeyopt ec_param_enc:named_curve puts the P-256 object identifier in the key. Without it, LibreSSL writes the curve as explicit parameters, and the server's TLS stack (aws-lc) refuses such a key. The refusal surfaces as a key/certificate mismatch even though the pair does match, so the error text points nowhere near the cause.
  • The extensions come from a config file, not from -addext. OpenSSL 1.1.1's req -x509 applies its configuration's CA:TRUE basicConstraints as well as an -addext one, and the server refuses to start on a certificate carrying two basicConstraints extensions.
  • Extensions have to come from somewhere. A plain openssl req -x509 one-liner with no extension flags produces a CA:TRUE certificate with no subject alternative name. The server starts with it, and then every client handshake fails with CaUsedAsEndEntity, because a CA certificate is not a valid end-entity certificate.
cat > cert.cnf <<'EOF'
[req]
distinguished_name = dn
prompt = no
x509_extensions = ext
[dn]
CN = localhost
[ext]
subjectAltName = DNS:localhost, IP:127.0.0.1
basicConstraints = critical, CA:FALSE
keyUsage = critical, digitalSignature
extendedKeyUsage = serverAuth
EOF

openssl req -x509 -newkey ec \
  -pkeyopt ec_paramgen_curve:P-256 -pkeyopt ec_param_enc:named_curve -nodes \
  -keyout server.key -out server.crt -days 365 -config cert.cnf

# server: POWDB_TLS_CERT=server.crt POWDB_TLS_KEY=server.key powdb-server ...
# client: powdb-cli --remote 127.0.0.1:5433 --tls --tls-ca server.crt ...

Verified end to end (server starts, CLI connects with --tls, query returns rows) against LibreSSL 3.3.6, OpenSSL 1.1.1v, OpenSSL 3.4.1 and OpenSSL 3.6.3.

Authentication

PowDB supports two authentication modes:

  1. Shared password — set the POWDB_PASSWORD environment variable. All clients authenticate with the same shared secret. Applies only when no named users are defined.
  2. Named users with roles (since 0.4.5) — users with admin, readwrite, or readonly roles, managed via powdb-cli useradd / passwd / userdel. Passwords are stored as argon2id hashes only (auth.json in the data directory, 0600 on Unix). When POWDB_ADMIN_USER and POWDB_ADMIN_PASSWORD are both set, the server bootstraps an initial admin on startup without the CLI. Once any user is defined, the shared password is no longer used.

In both modes:

  • Rate limiting: failed authentication attempts are counted per peer address in a fixed 60-second window. 5 failures against one username from one peer lock that (peer, username) pair out; 50 failures across all usernames from one peer lock the peer out entirely. The two thresholds differ on purpose: a wrong username swept across a server must not be able to lock a real user out of their own account. A locked-out client is refused with too many auth failures, retry after 60s (wire error class 7, rate_limited) rather than being told whether the credentials were right. A successful authentication clears both counters for that peer. A server with no user store (open, or shared-password) never reads the username while authenticating, so it counts failures per peer alone: 5 per minute from one address, not 50. Unix-socket peers have no address and share one bucket between them.
  • The limiter itself is bounded. A failure bucket retains at most 64 bytes of the username the peer sent, and the table holds at most 4096 buckets. At capacity it evicts deterministically (lowest failure count first, then oldest window, then key order), so a spray of throwaway source addresses cannot clear a peer that is close to its bound. The expiry sweep runs at most once a second rather than on every handshake.
  • Pre-auth payload limits: the server enforces frame size limits on unauthenticated connections to prevent resource exhaustion.
  • Connection limits: the server accepts at most POWDB_MAX_CONNECTIONS concurrent connections (1024 by default). A peer past the ceiling is not refused: its connection is established and then waits, unserved, for a slot, and its 10-second pre-auth deadline does not start until it gets one.
  • Pre-auth deadline: the whole phase before a successful CONNECT, pings included, runs under one 10-second deadline. It is not configurable on the binary.
  • A Unix-domain socket (--socket / POWDB_SOCKET) is published at mode 0660 by an atomic rename from a staging name in the same directory, so the published path never names a socket at the process umask and is never briefly absent. Socket peers have no address, so they share one rate-limit bucket between them rather than being limited per address.

Note on the readonly role: in releases up to and including 0.4.5, role storage is in place but read-only restrictions are not enforced at the query layer — do not rely on the readonly role as a security boundary against writes on those versions. Read-only restrictions are enforced as of 0.4.6 at the server dispatch layer: write statements from readonly users are rejected with permission denied, and unknown roles fail closed.

Known Limitations

  • Roles are coarse (admin / readwrite / readonly). There are no per-table ACLs, row-level security, or multi-tenant isolation; readonly enforcement is absent in ≤0.4.5 and enforced from 0.4.6 (see note above).
  • The query parser has a nesting depth limit. Runaway queries are bounded by POWDB_QUERY_TIMEOUT (default 30s) and the per-query memory budget (POWDB_QUERY_MEMORY_LIMIT).

There aren't any published security advisories