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.
Quick Start
Section titled “Quick Start”./bin/tellstone \ --tls-cert /path/to/server.crt \ --tls-key /path/to/server.keyThis enables server-only TLS. Clients must verify the server certificate using the issuing CA.
Mutual TLS (mTLS)
Section titled “Mutual TLS (mTLS)”Add the --tls-ca flag to require clients to present a certificate:
./bin/tellstone \ --tls-cert /path/to/server.crt \ --tls-key /path/to/server.key \ --tls-ca /path/to/ca.crtWhen --tls-ca is set, Tellstone verifies every client certificate against
the provided CA. Connections without a valid client certificate are rejected.
Flag Semantics
Section titled “Flag Semantics”| Flags provided | Behaviour |
|---|---|
| (none) | Plaintext — no encryption |
--tls-cert + --tls-key | Server-only TLS on both listeners |
--tls-cert + --tls-key + --tls-ca | Mutual TLS (mTLS) |
--tls-cert + --tls-key + --resp-starttls | Binary 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.
STARTTLS for RESP
Section titled “STARTTLS for RESP”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 aSTARTTLScommand, keeping the port usable by plaintext clients and load balancer health checks.
./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.
Upgrade flow
Section titled “Upgrade flow”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.
Error cases
Section titled “Error cases”| Situation | Reply |
|---|---|
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.
Generating Certificates
Section titled “Generating Certificates”For development and testing, generate a self-signed CA and server certificate:
# Generate CAopenssl req -x509 -newkey rsa:4096 -sha256 -days 3650 \ -keyout ca.key -out ca.crt -nodes \ -subj "/CN=Tellstone Dev CA"
# Generate server key + CSRopenssl req -newkey rsa:2048 -nodes \ -keyout server.key -out server.csr \ -subj "/CN=localhost"
# Sign server certificateopenssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \ -CAcreateserial -out server.crt -days 3650For mTLS, generate client certificates the same way (replace server with
client in the steps above), then sign them with the same CA.
Certificate Rotation
Section titled “Certificate Rotation”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..datasymlink replacement
Replace the certificate and key in place (or atomically rename new files over them):
cp new-server.crt server.crtcp new-server.key server.keyTellstone 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.
Observability
Section titled “Observability”With --enable-metrics, certificate rotation state is exposed on the
/metrics endpoint:
| Metric | Type | Description |
|---|---|---|
tellstone_tls_cert_reload_total | counter | Successful certificate reloads |
tellstone_tls_cert_reload_errors_total | counter | Failed reloads or watcher errors |
tellstone_tls_cert_expiry_seconds | gauge | Active leaf certificate NotAfter as Unix epoch seconds |
Kubernetes
Section titled “Kubernetes”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./bin/tellstone \ --tls-cert /etc/tellstone/tls/tls.crt \ --tls-key /etc/tellstone/tls/tls.keyWhen the Secret rotates, the projected ..data symlink swap is detected and the
new certificate is published automatically.
Protocol Details
Section titled “Protocol Details”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
Connecting with the TLS Example Client
Section titled “Connecting with the TLS Example Client”The repository includes a TLS-capable example client:
go run ./cmd/example/tls \ --addr localhost:9988 \ --tls-cert /path/to/ca.crt \ --tls-server-name localhostThis demonstrates both server-only TLS and mTLS connections.
Performance
Section titled “Performance”TLS adds approximately one extra allocation per operation compared to plaintext. Benchmarks on AMD Ryzen 9 9950X:
| Mode | Allocs/op |
|---|---|
| Plaintext | 2 |
| TLS 1.3 | 3 |
The single extra allocation originates from the TLS encryption layer and cannot be eliminated from the application side.
At-Rest Encryption
Section titled “At-Rest Encryption”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.
Authentication
Section titled “Authentication”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:
./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 thedefaultuser 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.
Binary protocol
Section titled “Binary protocol”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 normalThe 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.
Failure rate-limiting
Section titled “Failure rate-limiting”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.
No-password (no-op)
Section titled “No-password (no-op)”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.
Command reference
Section titled “Command reference”See the dedicated AUTH command page for the full reference, examples, and wire format details for both protocols.
Security Model
Section titled “Security Model”- 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