TLS termination + backend mTLS#

This is the operator playbook for putting Strata on the wire securely. The gateway can terminate TLS itself (recommended for prod) or sit behind a load balancer that handles termination upstream. Backend connections (Cassandra, TiKV, S3-upstream) each take an opt-in mutual-TLS bundle so a network intruder cannot impersonate the gateway. RADOS is the one exception: it authenticates via Ceph’s native cephx protocol and ignores the TLS layer entirely (see RADOS uses cephx below).

The four shapes below are arranged shallowest to deepest. Pick the shallowest one that satisfies the threat model — every shape adds an opt-in env block on top of the previous one and the defaults from any shape remain valid.

Deploy shapes#

Shape A — plain HTTP behind an ingress#

The simplest shape. A load balancer / ingress controller terminates TLS; Strata speaks plain HTTP on the cluster network. Operator-supplied STRATA_TRUSTED_PROXIES lets the gateway honor the ingress’s X-Forwarded-Proto for the Secure cookie flag.

flowchart LR
  C[S3 client] -- TLS --> I[Ingress / LB]
  I -- HTTP --> S[Strata gateway]
  S --> M[Meta backend]
  S --> D[Data backend]
EnvValue
STRATA_LISTEN:9000
STRATA_TRUSTED_PROXIESingress source CIDR (e.g. 10.0.0.0/8)
STRATA_TLS_CERT_FILEempty

Pros: certificate lifecycle owned entirely by the ingress. Cons: traffic between the ingress and Strata is unencrypted; only fits a private network.

Shape B — Strata-terminated, single port#

Strata terminates TLS and serves S3 + admin + console + metrics on the same port. Fits a small deployment behind a network policy.

flowchart LR
  C[S3 client] -- HTTPS --> S[Strata gateway :9000]
  S --> M[Meta backend]
  S --> D[Data backend]
EnvValue
STRATA_LISTEN:9000
STRATA_TLS_CERT_FILE/etc/strata/tls/tls.crt
STRATA_TLS_KEY_FILE/etc/strata/tls/tls.key
STRATA_TLS_MIN_VERSIONTLS1.2
STRATA_TLS_CIPHER_PROFILEmozilla-modern

Multi-tenant deployments swap the single-cert envs for STRATA_TLS_CERT_DIR and SNI-driven dispatch (US-003 of ralph/harden-gateway):

EnvValue
STRATA_TLS_CERT_DIR/etc/strata/tls/
STRATA_TLS_RELOAD_INTERVAL60s
STRATA_TLS_CLIENT_CA_FILE(optional) PEM CA for client-cert auth

The cert dir is walked at boot for *.crt + matching *.key pairs; the tls.Config.GetCertificate callback dispatches per-handshake via SAN lookup. Reload is fsnotify-driven with a 60s re-stat fallback so kubelet ConfigMap / Secret atomic-symlink swaps are picked up without a gateway restart.

Shape C — Strata-terminated, split admin + S3 listeners#

The admin / console / metrics endpoints bind to a separate listener (loopback or RFC1918) so a public S3 client cannot reach the admin API even if NetworkPolicy fails open. Recommended for prod as defense-in-depth.

flowchart LR
  C[S3 client] -- HTTPS --> S[Strata :9000 — S3 only]
  O[Operator / Prometheus] -- HTTP --> A[Strata :9001 — admin/console/metrics]
  S --> M[Meta backend]
  S --> D[Data backend]
EnvValue
STRATA_LISTEN:9000
STRATA_TLS_CERT_FILE/etc/strata/tls/tls.crt
STRATA_TLS_KEY_FILE/etc/strata/tls/tls.key
STRATA_ADMIN_LISTEN127.0.0.1:9001
STRATA_ADMIN_HTTP_WRITE_TIMEOUT2m (default)

When set, the admin listener receives /admin/v1/*, /console/*, /metrics, /healthz, /readyz. The S3 catch-all (/) stays on the primary listener. Both listeners share the same OTel tracer, audit middleware instance, structured logger, and access-log chain — only the mux + *http.Server pair is duplicated.

Optional admin-side mTLS pins access to operator client certs:

EnvValue
STRATA_ADMIN_TLS_CERT_FILE/etc/strata/admin-tls/tls.crt
STRATA_ADMIN_TLS_KEY_FILE/etc/strata/admin-tls/tls.key
STRATA_ADMIN_TLS_CLIENT_CA_FILE/etc/strata/admin-tls/operators-ca.crt

When STRATA_ADMIN_TLS_CLIENT_CA_FILE is set, every connection to the admin listener must present a client cert signed by the CA (ClientAuth=RequireAndVerifyClientCert).

Shape D — Strata-terminated + backend mTLS#

Builds on Shape C. Every backend (Cassandra, TiKV, S3-upstream) gets an mTLS bundle so the gateway authenticates itself to the backend on every connection. RADOS continues to use cephx (see the dedicated section); the data-tier confidentiality story is covered by Ceph’s ms_cluster_mode=secure.

flowchart LR
  C[S3 client] -- HTTPS --> S[Strata gateway]
  S -- mTLS --> M[Meta backend — Cassandra / TiKV]
  S -- mTLS --> U[S3 upstream]
  S -- cephx --> R[(RADOS)]

Combine the per-backend env blocks from Backend mTLS below with Shape C.

Cert provisioning recipes#

cert-manager (Kubernetes)#

cert-manager issues a Secret mounted onto the Strata pod. kubelet’s atomic-symlink-swap rotation is picked up automatically by the fsnotify + 60s re-stat fallback.

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: strata-tls
  namespace: strata
spec:
  secretName: strata-tls
  dnsNames:
    - s3.example.com
    - "*.s3.example.com"
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer

Mount the Secret onto the Strata container at /etc/strata/tls/ and set:

env:
  - name: STRATA_TLS_CERT_FILE
    value: /etc/strata/tls/tls.crt
  - name: STRATA_TLS_KEY_FILE
    value: /etc/strata/tls/tls.key
  - name: STRATA_TLS_RELOAD_INTERVAL
    value: 60s

For SNI multi-cert, point STRATA_TLS_CERT_DIR=/etc/strata/tls/ at a directory volume that aggregates several Secrets via projected.

Vault PKI#

Vault’s PKI secrets engine issues short-lived certs (24h TTL is common). A side-car renewer like vault-pki-renewer writes the rotated cert + key into /etc/strata/tls/ and the gateway picks the new pair up via the periodic re-stat (no SIGHUP / restart needed).

vault write pki/issue/strata-gateway \
  common_name=s3.example.com \
  ttl=24h \
  format=pem \
  | jq -r .data.certificate > /etc/strata/tls/tls.crt

openssl (lab / dev)#

For local labs, generate a self-signed P-256 ECDSA cert. The smoke script scripts/smoke-tls.sh uses the same recipe:

openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \
  -keyout /etc/strata/tls/tls.key \
  -out /etc/strata/tls/tls.crt \
  -days 30 -nodes \
  -subj "/CN=localhost" \
  -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"

openssl s_client is too interactive for unattended health checks — prefer curl --cacert /etc/strata/tls/tls.crt https://host:port/healthz.

Backend mTLS#

Every backend takes an opt-in mTLS bundle. The shape is identical across all three backends: CA file, client cert file, client key file, skip-verify toggle. Empty all four = plain-TCP / plain-gRPC / plain-HTTP (the pre-ralph/harden-gateway default).

A boot-time WARN + the gauge strata_backend_tls_skip_verify{backend,cluster}=1 fires on any backend whose SKIP_VERIFY is true. Alert on sum(strata_backend_tls_skip_verify) > 0 to catch a stray-prod knob.

Cassandra#

EnvNotes
STRATA_CASSANDRA_TLS_CA_FILEPEM CA bundle for server-cert validation.
STRATA_CASSANDRA_TLS_CERT_FILEPEM client cert for mTLS. Half-pair with KEY_FILE rejected at boot.
STRATA_CASSANDRA_TLS_KEY_FILEPEM private key paired with CERT_FILE.
STRATA_CASSANDRA_TLS_SKIP_VERIFYtrue flips tls.Config.InsecureSkipVerify AND gocql.SslOptions.EnableHostVerification=false. Never set in production.

The matched-pair flip on SKIP_VERIFY is intentional — gocql’s historical disclaimer warns that setting either alone leaves the driver in a half-checked state.

TiKV#

EnvNotes
STRATA_TIKV_TLS_CA_FILERequired when any other STRATA_TIKV_TLS_* knob is set (tikv-client-go’s Security.ToTLSConfig short-circuits on empty ClusterSSLCA, silently downgrading to plain-gRPC).
STRATA_TIKV_TLS_CERT_FILEPEM client cert. Paired with KEY_FILE.
STRATA_TIKV_TLS_KEY_FILEPEM private key paired with CERT_FILE.
STRATA_TIKV_TLS_SKIP_VERIFYDisables PD HTTP control-plane validation. The gRPC data plane is bound to tikv-client-go’s global SecuritySKIP_VERIFY is honored on PD HTTP only.

PD endpoints accept https:// scheme when TLS is configured. Bare pd-server.example.com:2379 is auto-prefixed with https when any TiKV TLS env is set.

S3 upstream#

EnvNotes
STRATA_S3_TLS_CA_FILEGlobal default PEM CA bundle.
STRATA_S3_TLS_CERT_FILEGlobal default client cert.
STRATA_S3_TLS_KEY_FILEGlobal default client key.
STRATA_S3_TLS_SKIP_VERIFYDisable server-cert validation; bumps strata_backend_tls_skip_verify{backend="s3",cluster=<id>}=1 for every cluster that resolves to this bundle.

Per-cluster overrides on the STRATA_S3_CLUSTERS JSON schema replace the global block outright for that cluster — no per-field merge. Setting any single per-cluster tls.* key locks in the per-cluster bundle. Example:

[
  {
    "id": "primary",
    "endpoint": "https://s3-a.internal:9000",
    "region": "us-east-1",
    "credentials_ref": "primary"
  },
  {
    "id": "secondary",
    "endpoint": "https://s3-b.internal:9000",
    "region": "us-east-1",
    "credentials_ref": "secondary",
    "tls": {
      "ca_file": "/etc/strata/s3-secondary/ca.pem",
      "cert_file": "/etc/strata/s3-secondary/client.pem",
      "key_file": "/etc/strata/s3-secondary/client.key"
    }
  }
]

The first cluster falls back to the global STRATA_S3_TLS_* block (or the Go-default HTTP client if both are empty); the second uses its own per-cluster bundle and ignores the global block.

RADOS uses cephx, not TLS#

Strata does not ship an mTLS layer for the RADOS data backend. librados authenticates via Ceph’s native cephx mutual-auth protocol, which is the production-supported path: clients prove possession of the keyring (STRATA_RADOS_KEYRING / [rados].keyring) to the MON cluster on every connection, and the MON cluster signs the OSD-level capability tokens that gate every read + write. Adding a TLS layer on top would re-encrypt an already-mutually-authenticated channel and add a second key-distribution surface that operators would have to keep in sync.

If you need wire-level confidentiality on a public-network Ceph cluster, configure ms_cluster_mode=secure + ms_client_mode=secure in your ceph.conf — the cephx handshake then encrypts the messenger payload end-to-end. See the upstream Ceph networking notes; no Strata-side knob is required.

See also#