Skip to content

Security

Tellstone supports TLS 1.3 transport encryption for both the binary protocol and the optional RESP2 listener. TLS is off by default and activated by providing a certificate and private key.

Terminal window
./bin/tellstone \
--tls-cert /path/to/server.crt \
--tls-key /path/to/server.key

This enables server-only TLS. Clients must verify the server certificate using the issuing CA.

Add the --tls-ca flag to require clients to present a certificate:

Terminal window
./bin/tellstone \
--tls-cert /path/to/server.crt \
--tls-key /path/to/server.key \
--tls-ca /path/to/ca.crt

When --tls-ca is set, Tellstone verifies every client certificate against the provided CA. Connections without a valid client certificate are rejected.

Flags providedBehaviour
(none)Plaintext — no encryption
--tls-cert + --tls-keyServer-only TLS on both listeners
--tls-cert + --tls-key + --tls-caMutual TLS (mTLS)
--tls-cert + --tls-key + --resp-starttlsBinary listener is implicit TLS; RESP listener stays plaintext and upgrades on STARTTLS

Environment variables follow the same pattern: TSD_TLS_CERT, TSD_TLS_KEY, and TSD_TLS_CA, plus TSD_RESP_STARTTLS for the RESP upgrade flag. Flags take precedence when both are set.

The RESP2 listener supports two TLS modes, chosen with --resp-starttls:

  • Implicit TLS (default when certs are configured) — every RESP connection is encrypted from the moment it is accepted.
  • STARTTLS (with --resp-starttls) — the listener stays plaintext and each client upgrades in place with a STARTTLS command, keeping the port usable by plaintext clients and load balancer health checks.
Terminal window
./bin/tellstone \
--tls-cert /path/to/server.crt \
--tls-key /path/to/server.key \
--resp-starttls

--resp-starttls requires --tls-cert and --tls-key; the server refuses to start without them. The flag only affects the RESP listener — the binary protocol remains implicit TLS.

A client connects in plaintext, issues STARTTLS (no arguments), receives +OK, and then performs a standard TLS 1.3 handshake on the same connection:

> STARTTLS
+OK
[Client → Server: TLS 1.3 ClientHello]

STARTTLS precedes the authentication gate, so AUTH credentials are sent only after the connection is encrypted. The upgrade also honours certificate rotation — the handshake uses the latest rotated certificate.

SituationReply
Too many arguments (STARTTLS now)-ERR wrong number of arguments for 'starttls' command
Connection already encrypted (repeat STARTTLS)-ERR connection is already encrypted
Implicit-TLS mode (no --resp-starttls)-ERR unknown command 'STARTTLS'

Pipelining is rejected for safety: a plaintext command sharing a buffer with STARTTLS causes the connection to close without a reply, so no command is ever executed before the upgrade completes.

For development and testing, generate a self-signed CA and server certificate:

Terminal window
# Generate CA
openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 \
-keyout ca.key -out ca.crt -nodes \
-subj "/CN=Tellstone Dev CA"
# Generate server key + CSR
openssl req -newkey rsa:2048 -nodes \
-keyout server.key -out server.csr \
-subj "/CN=localhost"
# Sign server certificate
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out server.crt -days 3650

For mTLS, generate client certificates the same way (replace server with client in the steps above), then sign them with the same CA.

When TLS is enabled, Tellstone watches the parent directories of the certificate, private key, and optional client CA. A complete replacement is validated and applied automatically — no restart or config reload required.

  • Zero downtime — existing TLS connections keep their original certificate state; only connections accepted after the rotation use the replacement
  • 500 ms debounce — bursts of filesystem events settle into a single reload
  • Failure-safe — a malformed or mismatched certificate/key pair is rejected and the last valid configuration stays active, logged at ERROR
  • Atomic rename support — works with direct writes, atomic rename(2) swaps, and Kubernetes projected Secret ..data symlink replacement

Replace the certificate and key in place (or atomically rename new files over them):

Terminal window
cp new-server.crt server.crt
cp new-server.key server.key

Tellstone picks up the change, validates the pair, and publishes it for new connections within ~500 ms. Rotating only the client CA (--tls-ca) is also supported.

With --enable-metrics, certificate rotation state is exposed on the /metrics endpoint:

MetricTypeDescription
tellstone_tls_cert_reload_totalcounterSuccessful certificate reloads
tellstone_tls_cert_reload_errors_totalcounterFailed reloads or watcher errors
tellstone_tls_cert_expiry_secondsgaugeActive leaf certificate NotAfter as Unix epoch seconds

For certificate-manager and projected Secret workloads, mount the Secret and point --tls-cert/--tls-key at the projected files:

volumeMounts:
- name: tls
mountPath: /etc/tellstone/tls
Terminal window
./bin/tellstone \
--tls-cert /etc/tellstone/tls/tls.crt \
--tls-key /etc/tellstone/tls/tls.key

When the Secret rotates, the projected ..data symlink swap is detected and the new certificate is published automatically.

Tellstone uses a forked TLS 1.3 implementation optimised for gnet’s epoll-based event-loop:

  • TLS 1.3 only — no support for TLS 1.0, 1.1, or 1.2
  • Cipher suites — AES-128-GCM-SHA256, AES-256-GCM-SHA384, ChaCha20-Poly1305-SHA256
  • Zero-allocation fast path — pre-allocated read buffer avoids per-record allocations
  • Single binary — no external OpenSSL dependency; TLS is compiled in

The repository includes a TLS-capable example client:

Terminal window
go run ./cmd/example/tls \
--addr localhost:9988 \
--tls-cert /path/to/ca.crt \
--tls-server-name localhost

This demonstrates both server-only TLS and mTLS connections.

TLS adds approximately one extra allocation per operation compared to plaintext. Benchmarks on AMD Ryzen 9 9950X:

ModeAllocs/op
Plaintext2
TLS 1.33

The single extra allocation originates from the TLS encryption layer and cannot be eliminated from the application side.

Beyond transport, Tellstone can encrypt data written to disk with ChaCha20-Poly1305, off by default. See At-Rest Encryption for key sourcing with --encryption-key (base64) and --encryption-key-file (mounted Kubernetes Secrets), key generation, and rotation.

Tellstone supports optional password-based authentication for both the binary protocol and the RESP2 listener. Auth is disabled by default; enable it with --require-pass:

Terminal window
./bin/tellstone --require-pass "hunter2"

For per-user authentication with role-based access control, use --rbac-config instead — it supersedes --require-pass when both are set.

When a password is configured, every connection must authenticate before issuing data commands (GET, SET, DEL). PING and QUIT are allowed before auth so that load balancers and health checks can reach the server.

Passwords are hashed with bcrypt at startup and verified against the hash on each AUTH attempt — the plaintext password never resides in server memory.

AUTH <password>
AUTH <username> <password>
  • Single-password mode: AUTH <password>
  • ACL-style mode: AUTH default <password> (only the default user exists)

There is exactly one principal in single-password mode — the default user — so any other username is rejected. For multiple named users with distinct credentials, enable RBAC instead.

The reply is +OK on success and -ERR invalid password on failure.

The Go client package exposes Auth:

c, _ := client.Dial("127.0.0.1:9988", 2*time.Second)
defer c.Close()
scratch := make([]byte, 4096)
if err := c.Auth("hunter2", scratch); err != nil {
log.Fatal(err)
}
// connection is now authenticated — issue Get/Set/Delete as normal

The binary wire format uses MsgAuth with payload:

[2B usernameLen][username bytes][2B passwordLen][password bytes]

The client.Auth() sends with usernameLen = 0 (single-password mode). The server responds with MsgAuthOk (OK) on success or MsgAuthErr (ERR INVALID_AUTH) on failure.

bcrypt verification runs on a dedicated worker pool (4 workers) off the event loop, so the hashing cost never blocks I/O.

After 3 failed AUTH attempts the server closes the connection automatically. The counter resets on a successful authentication. Failed attempts are logged at WARN level with the remote address and attempt count, and every rejected attempt is recorded in the ACL LOG audit buffer — ACL LOG (or c.AclLog over the binary protocol) shows who failed, when, from where, and why. See the ACL command page for details.

When --require-pass is not set, every connection starts authenticated and AUTH is a no-op that replies +OK. This preserves backward compatibility with clients that always send AUTH.

See the dedicated AUTH command page for the full reference, examples, and wire format details for both protocols.

  • Default cipher suites are not marked insecure
  • No legacy protocol versions or cipher suites are compiled in
  • Renegotiation and downgrade attacks are structurally impossible
  • Server-only TLS protects against passive eavesdropping
  • mTLS additionally protects against unauthorised clients