Skip to main content

Transport security

The gRPC TCP listener supports TLS and mTLS. UDS connections are always plaintext. Filesystem permissions are the authz boundary, and TLS on a unix socket is just overhead.

Modes

SettingBehaviour
cert_file + key_file onlyTLS-only. Server presents the cert; client validates it.
Add client_ca_file + require_client_cert: truemTLS. Every client must present a cert chained to client_ca_file.
Add client_ca_file only (no require)"If a client presents a cert, validate it; otherwise allow."

The listener log reports the active mode:

{"msg":"listening","transport":"tcp","addr":"...","security":"mtls"}

Config

server:
grpc:
tcp: "0.0.0.0:7777"
tls:
cert_file: /etc/mindd/tls/server.crt
key_file: /etc/mindd/tls/server.key
client_ca_file: /etc/mindd/tls/client-ca.crt
require_client_cert: true

MinVersion is forced to TLS 1.3. NextProtos is set to ["h2"] so grpc-go's ALPN check is satisfied (without it the handshake fails with "missing selected ALPN property"; see grpc/grpc-go#434).

Generating a self-signed cert for dev

A 30-line Go program in internal/server/tls_test.go produces an ECDSA P-256 self-signed cert valid for an hour. For quick local testing, generate one with:

// gen.go
package main

import (
"crypto/ecdsa"; "crypto/elliptic"; "crypto/rand"
"crypto/x509"; "crypto/x509/pkix"; "encoding/pem"
"math/big"; "net"; "os"; "time"
)

func main() {
priv, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
serial, _ := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
tmpl := x509.Certificate{
SerialNumber: serial,
Subject: pkix.Name{CommonName: "localhost"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
BasicConstraintsValid: true, IsCA: true,
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
DNSNames: []string{"localhost"},
}
der, _ := x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, &priv.PublicKey, priv)
os.WriteFile("server.crt", pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), 0o600)
keyDER, _ := x509.MarshalECPrivateKey(priv)
os.WriteFile("server.key", pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}), 0o600)
}
go run gen.go

Then dial with grpcurl:

grpcurl -cacert server.crt -authority localhost \
-H "x-mindd-capability: Bearer $TOKEN" \
-d '{"namespace":"scratchpad","key":"hello"}' \
127.0.0.1:7777 mindd.kv.v1.KV/Get

In Kubernetes

The Helm chart accepts either an inline cert + key (rendered into a kubernetes.io/tls Secret) or a pre-existing Secret name:

# values.yaml
tls:
enabled: true
existingSecret: mindd-server-tls # managed by cert-manager

The Deployment mounts that Secret at /etc/mindd/tls, where the referenced cert_file / key_file live.

TLS and capability tokens are orthogonal

mTLS authenticates the transport peer; the capability token still scopes what they can do. A connection authenticated by mTLS but missing a valid token is still rejected with Unauthenticated at the auth interceptor.

Known limits

  • The HTTP/JSON gateway runs on its own listener; chart support for terminating TLS on the gateway itself is not built in. Terminate at the ingress.
  • The OTLP exporter to a remote collector uses its own TLS settings under observability.tracing.otlp; the server-side TLS config above doesn't affect outbound exports.