This is an automated email from the ASF dual-hosted git repository.
hanahmily pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git
The following commit(s) were added to refs/heads/main by this push:
new 54eb7309 feat: add TLS support between Liaison and Data nodes with
integration… (#646)
54eb7309 is described below
commit 54eb73096cccea0750c316c0c9a8b86408ac41fe
Author: wesamabed <[email protected]>
AuthorDate: Wed Apr 23 09:01:16 2025 +0300
feat: add TLS support between Liaison and Data nodes with integration…
(#646)
* feat: add TLS support between Liaison and Data nodes with integration test
Co-authored-by: Gao Hongtao <[email protected]>
Co-authored-by: 吴晟 Wu Sheng <[email protected]>
---
banyand/queue/pub/client.go | 15 ++-
banyand/queue/pub/pub.go | 28 ++++
banyand/queue/pub/pub_tls_test.go | 142 +++++++++++++++++++++
banyand/queue/pub/testdata/certs/README.md | 54 ++++++++
banyand/queue/pub/testdata/certs/ca.crt | 29 +++++
banyand/queue/pub/testdata/certs/ca.key | 52 ++++++++
.../queue/pub/testdata/certs/cert.conf.template | 18 +++
banyand/queue/pub/testdata/certs/server.crt | 26 ++++
banyand/queue/pub/testdata/certs/server.key | 28 ++++
docs/operation/configuration.md | 12 ++
docs/operation/security.md | 24 +++-
11 files changed, 422 insertions(+), 6 deletions(-)
diff --git a/banyand/queue/pub/client.go b/banyand/queue/pub/client.go
index c180c086..739d19a5 100644
--- a/banyand/queue/pub/client.go
+++ b/banyand/queue/pub/client.go
@@ -23,7 +23,6 @@ import (
"time"
"google.golang.org/grpc"
- "google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/health/grpc_health_v1"
"github.com/apache/skywalking-banyandb/api/common"
@@ -108,7 +107,12 @@ func (p *pub) OnAddOrUpdate(md schema.Metadata) {
if _, ok := p.evictable[name]; ok {
return
}
- conn, err := grpc.NewClient(address,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithDefaultServiceConfig(retryPolicy))
+ credOpts, err := p.getClientTransportCredentials()
+ if err != nil {
+ p.log.Error().Err(err).Msg("failed to load client TLS
credentials")
+ return
+ }
+ conn, err := grpc.NewClient(address, append(credOpts,
grpc.WithDefaultServiceConfig(retryPolicy))...)
if err != nil {
p.log.Error().Err(err).Msg("failed to connect to grpc server")
return
@@ -252,7 +256,12 @@ func (p *pub) checkClientHealthAndReconnect(conn
*grpc.ClientConn, md schema.Met
for {
select {
case <-time.After(backoff):
- connEvict, errEvict :=
grpc.NewClient(node.GrpcAddress,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithDefaultServiceConfig(retryPolicy))
+ credOpts, errEvict :=
p.getClientTransportCredentials()
+ if errEvict != nil {
+ p.log.Error().Err(errEvict).Msg("failed
to load client TLS credentials (evict)")
+ return
+ }
+ connEvict, errEvict :=
grpc.NewClient(node.GrpcAddress, append(credOpts,
grpc.WithDefaultServiceConfig(retryPolicy))...)
if errEvict == nil &&
p.healthCheck(en.n.String(), connEvict) {
func() {
p.mu.Lock()
diff --git a/banyand/queue/pub/pub.go b/banyand/queue/pub/pub.go
index 8cfc0ea1..77972843 100644
--- a/banyand/queue/pub/pub.go
+++ b/banyand/queue/pub/pub.go
@@ -27,6 +27,7 @@ import (
"github.com/pkg/errors"
"go.uber.org/multierr"
+ "google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
@@ -41,6 +42,7 @@ import (
"github.com/apache/skywalking-banyandb/banyand/metadata/schema"
"github.com/apache/skywalking-banyandb/banyand/queue"
"github.com/apache/skywalking-banyandb/pkg/bus"
+ "github.com/apache/skywalking-banyandb/pkg/grpchelper"
"github.com/apache/skywalking-banyandb/pkg/logger"
"github.com/apache/skywalking-banyandb/pkg/run"
"github.com/apache/skywalking-banyandb/pkg/timestamp"
@@ -49,6 +51,7 @@ import (
var (
_ run.PreRunner = (*pub)(nil)
_ run.Service = (*pub)(nil)
+ _ run.Config = (*pub)(nil)
)
type pub struct {
@@ -60,7 +63,24 @@ type pub struct {
active map[string]*client
evictable map[string]evictNode
closer *run.Closer
+ caCertPath string
mu sync.RWMutex
+ tlsEnabled bool
+}
+
+func (p *pub) FlagSet() *run.FlagSet {
+ fs := run.NewFlagSet("queue-client")
+ fs.BoolVar(&p.tlsEnabled, "internal-tls", false, "enable internal TLS")
+ fs.StringVar(&p.caCertPath, "internal-ca-cert", "", "CA certificate
file to verify the internal data server")
+ return fs
+}
+
+func (p *pub) Validate() error {
+ // simple sanity‑check: if TLS is on, a CA bundle must be provided
+ if p.tlsEnabled && p.caCertPath == "" {
+ return fmt.Errorf("TLS is enabled (--internal-tls), but no CA
certificate file was provided (--internal-ca-cert is required)")
+ }
+ return nil
}
func (p *pub) Register(topic bus.Topic, handler schema.EventHandler) {
@@ -339,3 +359,11 @@ func isFailoverError(err error) bool {
}
return s.Code() == codes.Unavailable || s.Code() ==
codes.DeadlineExceeded
}
+
+func (p *pub) getClientTransportCredentials() ([]grpc.DialOption, error) {
+ opts, err := grpchelper.SecureOptions(nil, p.tlsEnabled, false,
p.caCertPath)
+ if err != nil {
+ return nil, fmt.Errorf("failed to load TLS config: %w", err)
+ }
+ return opts, nil
+}
diff --git a/banyand/queue/pub/pub_tls_test.go
b/banyand/queue/pub/pub_tls_test.go
new file mode 100644
index 00000000..6cc8c9c2
--- /dev/null
+++ b/banyand/queue/pub/pub_tls_test.go
@@ -0,0 +1,142 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build unit
+// +build unit
+
+package pub
+
+import (
+ "context"
+ "crypto/tls"
+ "github.com/onsi/ginkgo/v2"
+ "github.com/onsi/gomega"
+ "github.com/onsi/gomega/gleak"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/credentials"
+ health "google.golang.org/grpc/health"
+ healthv1 "google.golang.org/grpc/health/grpc_health_v1"
+ "google.golang.org/protobuf/types/known/anypb"
+ "io"
+ "net"
+ "path/filepath"
+ "testing"
+
+ "github.com/apache/skywalking-banyandb/api/data"
+ clusterv1
"github.com/apache/skywalking-banyandb/api/proto/banyandb/cluster/v1"
+ streamv1
"github.com/apache/skywalking-banyandb/api/proto/banyandb/stream/v1"
+ "github.com/apache/skywalking-banyandb/pkg/bus"
+ "github.com/apache/skywalking-banyandb/pkg/test/flags"
+)
+
+func TestPubTLS(t *testing.T) {
+ gomega.RegisterFailHandler(ginkgo.Fail)
+ ginkgo.RunSpecs(t, "queue‑pub TLS dial‑out Suite")
+}
+
+type mockService struct {
+ clusterv1.UnimplementedServiceServer
+}
+
+func (m *mockService) Send(stream clusterv1.Service_SendServer) error {
+ _, err := stream.Recv()
+ if err != nil {
+ return err
+ }
+ anyBody, err := anypb.New(&streamv1.QueryResponse{})
+ if err != nil {
+ return err
+ }
+ if err := stream.Send(&clusterv1.SendResponse{Body: anyBody}); err !=
nil {
+ return err
+ }
+ return io.EOF
+}
+
+func tlsServer(addr string) func() {
+ crtDir := filepath.Join("testdata", "certs")
+ cert, err := tls.LoadX509KeyPair(
+ filepath.Join(crtDir, "server.crt"),
+ filepath.Join(crtDir, "server.key"),
+ )
+ gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+
+ creds := credentials.NewTLS(&tls.Config{Certificates:
[]tls.Certificate{cert}})
+ lis, err := net.Listen("tcp", addr)
+ gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+
+ srv := grpc.NewServer(grpc.Creds(creds))
+ clusterv1.RegisterServiceServer(srv, &mockService{})
+
+ hs := health.NewServer()
+ hs.SetServingStatus("", healthv1.HealthCheckResponse_SERVING)
+ healthv1.RegisterHealthServer(srv, hs)
+
+ go func() { _ = srv.Serve(lis) }()
+ return func() { srv.Stop() }
+}
+
+func newTLSPub() *pub {
+ p := NewWithoutMetadata().(*pub)
+ p.tlsEnabled = true
+ p.caCertPath = filepath.Join("testdata", "certs", "ca.crt")
+
gomega.Expect(p.PreRun(context.Background())).ShouldNot(gomega.HaveOccurred())
+ return p
+}
+
+var _ = ginkgo.Describe("Broadcast over one‑way TLS", func() {
+ var before []gleak.Goroutine
+
+ ginkgo.BeforeEach(func() {
+ before = gleak.Goroutines()
+ })
+ ginkgo.AfterEach(func() {
+ gomega.Eventually(gleak.Goroutines, flags.EventuallyTimeout).
+ ShouldNot(gleak.HaveLeaked(before))
+ })
+
+ ginkgo.It("establishes TLS and broadcasts a QueryRequest", func() {
+ addr := getAddress()
+ stop := tlsServer(addr)
+ defer stop()
+
+ p := newTLSPub()
+ defer p.GracefulStop()
+
+ node := getDataNode("node-tls", addr)
+ p.OnAddOrUpdate(node)
+
+ gomega.Eventually(func() int {
+ p.mu.RLock()
+ defer p.mu.RUnlock()
+ return len(p.active)
+ }, flags.EventuallyTimeout).Should(gomega.Equal(1))
+
+ futures, err := p.Broadcast(
+ flags.EventuallyTimeout,
+ data.TopicStreamQuery,
+ bus.NewMessage(bus.MessageID(1),
&streamv1.QueryRequest{}),
+ )
+ gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+ gomega.Expect(futures).Should(gomega.HaveLen(1))
+
+ msgs, err := futures[0].GetAll()
+ gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
+ gomega.Expect(msgs).Should(gomega.HaveLen(1))
+
+ _, ok := msgs[0].Data().(*streamv1.QueryResponse)
+ gomega.Expect(ok).To(gomega.BeTrue())
+ })
+})
diff --git a/banyand/queue/pub/testdata/certs/README.md
b/banyand/queue/pub/testdata/certs/README.md
new file mode 100644
index 00000000..fc1c9723
--- /dev/null
+++ b/banyand/queue/pub/testdata/certs/README.md
@@ -0,0 +1,54 @@
+# TLS Test Certificates
+
+This folder contains everything you need to generate a self‑signed CA and a
server certificate that’s valid for both `localhost` and your machine’s real
hostname.
+
+## Prerequisites
+
+- Bash (or any POSIX shell)
+- OpenSSL (v1.1.1 or later)
+- `envsubst` (part of GNU gettext)
+
+## Steps
+
+1. **Set the HOSTNAME variable**
+ ```bash
+ export HOSTNAME=$(hostname)
+2. **Generate your test CA (valid for 10 years)**
+
+ ```bash
+ openssl req \
+ -x509 -nodes \
+ -newkey rsa:4096 \
+ -days 3650 \
+ -keyout ca.key \
+ -out ca.crt \
+ -subj "/CN=MyTestCA"
+
+3. Render the OpenSSL config with your hostname, and create a CSR + key:
+ ```bash
+ sed "s/\${HOSTNAME}/$HOSTNAME/g" cert.conf.template > cert.conf
+ ```
+ ```bash
+ openssl req \
+ -newkey rsa:2048 -nodes \
+ -keyout server.key \
+ -out server.csr \
+ -config cert.conf
+
+4. Sign the server CSR with your CA (valid for 1 year):
+ ```bash
+ openssl x509 \
+ -req \
+ -in server.csr \
+ -CA ca.crt \
+ -CAkey ca.key \
+ -CAcreateserial \
+ -out server.crt \
+ -days 365 \
+ -sha256 \
+ -extensions v3_req \
+ -extfile cert.conf
+
+5. Clean up (optional):
+ ```bash
+ rm cert.conf server.csr ca.srl
\ No newline at end of file
diff --git a/banyand/queue/pub/testdata/certs/ca.crt
b/banyand/queue/pub/testdata/certs/ca.crt
new file mode 100644
index 00000000..adbf99aa
--- /dev/null
+++ b/banyand/queue/pub/testdata/certs/ca.crt
@@ -0,0 +1,29 @@
+-----BEGIN CERTIFICATE-----
+MIIFBzCCAu+gAwIBAgIUJvo1azZUBE4aJN+NNF6qilrnNK8wDQYJKoZIhvcNAQEL
+BQAwEzERMA8GA1UEAwwITXlUZXN0Q0EwHhcNMjUwNDE3MTUxNzE0WhcNMzUwNDE1
+MTUxNzE0WjATMREwDwYDVQQDDAhNeVRlc3RDQTCCAiIwDQYJKoZIhvcNAQEBBQAD
+ggIPADCCAgoCggIBAM8BVCx141tLNBSPfa66kAinFrqhQKhYHjd/ArVvJrHt1UnR
+aw/anUAkKdiZ15DTQYg4SU1q3CwbAsN2CvxKxmH+AZLkrSawHcaHtTYK7VGgYgfa
+WobWpZoN6yE3toib/qQvjpIt9+0SJEUqe/QgF/H1bUbYXcAxCGqrNyN88CEZo67f
+1M6CmlkWaKgPTnSfUHRy6CgEeBWo5fdYUaJc+ppU0DvSVz3MpGMqD/iZLEj8wQ+M
+NsJ8+j5lcFvkHQGoz0/0gmqalCWNTQPGDtIHtFVlAr7QR8Im6Jw4A9o4m/HxF7dq
+Dg1GmHcuUiFLiHSprIsfXFA1y468DR/Ygvzfw+xjfLHhi/933O9iqCY73dGynlom
+iBCQD8MpqFCWf6KsJk+r1ZisbVHhveyA/JL0CK/ozGy6u3W3STpo2RMEu+lPeXii
+id6OZfV8FVkYrNQtq/AxrhG+zQKhmKfbprclO6LmSXfME21ht5k9RPkUEhLHIuvw
+bZE6LHz44cLsx84AgISlxU/9D6Yk2ZwL7mRQirh7aU+i+UqDF79zR3B0MSbgq++I
+WgnK5XDiQJQ8+OUvDVJUvaM5tZMKq6brjO/y9jN9QmEo2zJ/rXqbuHiiX/CdgLKH
+XLmK/xYqWCVW6Sx0Tf6Tk0CcMZ8SeSAzPV1bUOXiRj2Jpa/lzCsXjzZaq3z7AgMB
+AAGjUzBRMB0GA1UdDgQWBBTfaziiuqucrZTHeGdpq3u7JwzbtzAfBgNVHSMEGDAW
+gBTfaziiuqucrZTHeGdpq3u7JwzbtzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3
+DQEBCwUAA4ICAQCf52UGWLHp43ApOZ1jV7ehzpL0FFp75fzN3WhWx4Zc8pigfU6Y
+kVQhPUXId4WQOsD1wiEwaSNDj+lrA4WZmcUWHDAVhoFXwNZRekpXKgV9jU4ejjVO
+ithk70UufFwM6jUiE7Y0fqe56ZHcsl6WRUnDPwfZYDmhJu8L4/cDlyhXLLZWOCX3
+TAMgn9vrq2XomgJ2pVQz+uh/5vCWAISm9CtU5av4+h5jD3kj36zTQmj7QJVa0wr+
+WjGoioxY8TbMQxMdd9xIFGEdvwQBAWjnmS0YOsPQsTX8I+IrrNAmgyqDgWpKRdh3
+I+1S1pdm0coH1KYbXxSiU+dbBw4gTn72hblC8ktPF9uQtzOfZkX6VhghkhIQQawm
+A9XRj/sgfZ9xQrS0VHxpCPc0h4kTQsf7F4jrhMZ6YCPxoK5gnR1mafQ+5bWRTXhx
+BqOMVa/QvgjF/s35K9+M1MCDa4eiQ1FA35mr3CTycRkH6cv3VL48T9qxxtMc3anJ
+WdHkenrDjGOeiMFhxkhy/tpU4bbDnvjXdt2LHPX/VTKjnL2dUVbYYAubmqDmmJAP
+sfJsCIZ7CPxfxv/ml4X63QpijfirhwhJJV96nX/KLmIDum6ZdTLHiF+ZAbSv/emw
+z+pExHtVVQQwdFekqeawB79QDfVBDw8XGDh7kKZRXToudWuHmoNlXT88TA==
+-----END CERTIFICATE-----
diff --git a/banyand/queue/pub/testdata/certs/ca.key
b/banyand/queue/pub/testdata/certs/ca.key
new file mode 100644
index 00000000..5518c4b6
--- /dev/null
+++ b/banyand/queue/pub/testdata/certs/ca.key
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQDPAVQsdeNbSzQU
+j32uupAIpxa6oUCoWB43fwK1byax7dVJ0WsP2p1AJCnYmdeQ00GIOElNatwsGwLD
+dgr8SsZh/gGS5K0msB3Gh7U2Cu1RoGIH2lqG1qWaDeshN7aIm/6kL46SLfftEiRF
+Knv0IBfx9W1G2F3AMQhqqzcjfPAhGaOu39TOgppZFmioD050n1B0cugoBHgVqOX3
+WFGiXPqaVNA70lc9zKRjKg/4mSxI/MEPjDbCfPo+ZXBb5B0BqM9P9IJqmpQljU0D
+xg7SB7RVZQK+0EfCJuicOAPaOJvx8Re3ag4NRph3LlIhS4h0qayLH1xQNcuOvA0f
+2IL838PsY3yx4Yv/d9zvYqgmO93Rsp5aJogQkA/DKahQln+irCZPq9WYrG1R4b3s
+gPyS9Aiv6Mxsurt1t0k6aNkTBLvpT3l4oonejmX1fBVZGKzULavwMa4Rvs0CoZin
+26a3JTui5kl3zBNtYbeZPUT5FBISxyLr8G2ROix8+OHC7MfOAICEpcVP/Q+mJNmc
+C+5kUIq4e2lPovlKgxe/c0dwdDEm4KvviFoJyuVw4kCUPPjlLw1SVL2jObWTCqum
+64zv8vYzfUJhKNsyf616m7h4ol/wnYCyh1y5iv8WKlglVuksdE3+k5NAnDGfEnkg
+Mz1dW1Dl4kY9iaWv5cwrF482Wqt8+wIDAQABAoICADDIcuJBa/GFUQIxaKCHRc9M
+NS6JNtgVYBWbAHPqfFd9IYEScaUcU2ecviHV63etWWm1Pg0ZDEb2nJmclW0sYAES
+36MS8f1GjtXNAHb6b0AHyGJqYmAZcJBUDF/ZJdKe1I0zyekIHT/IwTRtlSWMdKgo
+OvbxoXJb+8xLiiR6WoqqZKkfBaMfIymwfrxwUwBn1QmEVNKtbvFHyt4V+bMGL2a0
+axhV8wpU0j6uWHIxAr82lXbJB9SgqEaRGEAHi5BrDGQezqc5w4Tv/kDP6Yk0mJpx
+QgrewbJvAe1ixAGmypVjSEAIpcQaKW8YwvEmwEMiA1AL9XDPpKeACKXDG+dlAmIj
+8ug1O9rtO0+4mPyXOSc/oBWo2vpzYOXGrwV3iRJdSWgH9JdM6hkXfqbLYHj7CkDJ
+TxOqVHgJM6iAX36PY1sW6LU4jH9mxnxBSxsUSc40loz2uy1cxMVfW9qN5UCiDvy8
+T4F6cTJ4edUhi2ocSEMEdmNMLkb50fRAHKBUcp5NdkRiPbL80vFqsjOOuCQsJwfE
+2BTKlGZbSEwD7JURoIo4Q28yr21WUs4uLczry70bwuh4ZNg86GBZlXLBQwnuuOl7
+/mZ5OwlpD9eMEMDUm4gu0A6Qf0vpKFHrYgwB8jUREhTtl7j1PW+S6kE++5pb0iRz
+XWtEbMtYB+bO6yXkBhbxAoIBAQDpcQAf743Wltw+qDcev1a2FlY/s7ybu9Q3vE4N
+1ojCgPpRy7isrj2eXTp41QFiYqLZDi2caCPotrKS96NjpxsbAAru0ii+mEEZxCpd
+sm6I9dnOQ8tccGwCdH+dqlLSjak9Pb/jcKN6SiNUMFW9fm20LVk6oELDWhc6nxfV
+v1Y2t3AuEqSVB+tHm/kSo/QP31NEYvxgUHNcdcgBRBLuw093YWKHjObdk2Q95z1h
+ppa6yw1yxu+QbB9gL+6kH7qHc6Eg/2eHqGPoeiOXwzhS3H0bKOelU42tCZaDvJMx
+SblbziZgg37AelUGvSTi6+6NIJA4Szy3IGu1LknfVvQCWh2xAoIBAQDjAlW8NNpN
+fTBjuqD7U25mqrg18z6TDIPur0y5iF/ljjSRVRT9oF1cCbo2HSt9F1bJx8LlI0k5
+7bV9+xzR8Fs7qcTNSjrgCoUBt6/6vNPY5RpafsTbJR2+KJN03Lt0ghY3Bv91KO98
+bYZuEr7w/vuPqp1M/nVfm+vDXiKF+16AQWdnIR3Rd80R8Me7MhuDjnbUFpEkbDVY
+qckHxV+F+4f8H19Pkv9gc5Cf37ksLq3WdcPnYw20ET+WIcaSZqIeHQkCXc2WIlhf
+Zz6EuDr1gsqqZMPN6D7lfRwR+/WInciWYSFT3Kd6oHnm1xJXaBgZsKpwbJ4eMeVB
+uhGsNzfTM1RrAoIBAGQJxejpqtn8GnRLeBuYGZu8pMd1yezfKEmeS8DIYCEiTqOA
+7yopHUThZO5lMcusw2bLGaa+Ri0zJgGvV/iboxUGBqljrIxJCRT2qMUwcwgEe4tW
+KC78Cn1e2VWCqS2MAau566KXIaeFX/BzKjsjk/WzvVpPVW0MDYpUpU03SsX2BH/q
+A1fOZfhxEeL4Gd43cSKMXOUVdOp5mvVX69kgH8zkEepO6pynzjxs/TP8xPlVjPTT
+5dP5UdTRla4F3tSvK6zNZtuOwQneVaRyx49S59YcyHBkBwHRawwXYuirVGDQTkfr
+8gSgKBHUOpt4Sel2u/cz3tgHb8DcDlJEnBrp7XECggEAHn/AzxF2xvRr3OpFGJ8j
+9Q04xJ7SqGUFELtinoaxA6FwdJouwMib4nK4Hu8aWasaEPASwkBUZUEHok5rl9Uo
+HML5Wu9/K3yeVBW+wfw+piRZvxDLF/pLnbHL5eaGFOgpXelFxxLh4iDA7+b62lwX
+pjyw95g0Ys0LBuuNzdxw3OBsqRFs9SiYV7G20/KueaVZV7NUesVDAY+GH9InvFOH
++JqqboF8aBP+uUwQj9wRpP+be2n2fFvY5C3ThPXfEBaskDHUHjitENxJLQGngja5
+Td1N5UsvsBt3+v6UBW/VdEbGeILryXDoD9iTcUTeAA2ZSJN/RuVDPOpn13BvwqNh
+0wKCAQB4A54Qx7nHNNQqGWIAowM0HDe5oH1q/1jUiaLNHrifXt5OVKIZC6xbaGGk
+x9yrx+tK0jEgbRq3WtgI9q7U4NZEEc+617gKHBPs/QP/p6T85b2vOoOehcK24PCE
+ZqU0J2pbYI79S4KWk0zhA1a67t2l6cTjm5KJRCjoyjMi4JrWmxfM9WIQNiKDu99u
+kOcpXD3gPL6ZkyCiIIfbq3S18Cio9rg+1zraXjyHExUlDKZt4BzmCDsou1gNkM+Z
+9eQzGLqmCj3W6JYb6uUkQHApgpuJIktmw35zxXTbomhOn7WgpExnRgAV1DjPvaX4
+H4qrDp+9Sx0BY/Fagycrrm52MiTT
+-----END PRIVATE KEY-----
diff --git a/banyand/queue/pub/testdata/certs/cert.conf.template
b/banyand/queue/pub/testdata/certs/cert.conf.template
new file mode 100644
index 00000000..bef4e434
--- /dev/null
+++ b/banyand/queue/pub/testdata/certs/cert.conf.template
@@ -0,0 +1,18 @@
+[ req ]
+distinguished_name = req_distinguished_name
+req_extensions = v3_req
+prompt = no
+
+[ req_distinguished_name ]
+# Common Name will be replaced with the local hostname at runtime
+CN = ${HOSTNAME}
+
+[ v3_req ]
+# add the SAN extension
+subjectAltName = @alt_names
+
+[ alt_names ]
+DNS.1 = localhost
+DNS.2 = ${HOSTNAME}
+IP.1 = 127.0.0.1
+IP.2 = ::1
diff --git a/banyand/queue/pub/testdata/certs/server.crt
b/banyand/queue/pub/testdata/certs/server.crt
new file mode 100644
index 00000000..9e213cf9
--- /dev/null
+++ b/banyand/queue/pub/testdata/certs/server.crt
@@ -0,0 +1,26 @@
+-----BEGIN CERTIFICATE-----
+MIIEUDCCAjigAwIBAgIUEG6jrP+X5pg7FNorVnQlGBIT1TYwDQYJKoZIhvcNAQEL
+BQAwEzERMA8GA1UEAwwITXlUZXN0Q0EwHhcNMjUwNDE3MTUxODU2WhcNMjYwNDE3
+MTUxODU2WjAjMSEwHwYDVQQDDBhXZXNhbXMtTWFjQm9vay1Qcm8ubG9jYWwwggEi
+MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDUGYdez3b3NGSwIliEJtEAINqt
+QpynwwUVIWkkuYqQEujX3M6s5N7xYP/7UCOV6OyZNbkt8GSXJb/03OvB/qVAKWXN
+Qx0wOKWSGxvK0wheXO3woIsLlrRYm5KeCiBCEhqu9wKDgQ2fy+ph3elNJMnayaJW
+GaizuvFEPYY52sae1iy/nyy976R5EHMWh6zafG7JuxEzAQaS04fB7IIMtcxA7XtF
+El00GtxVFVAGuUur9YLGDGRX0CR7z2eoqD04VoPzUMrP09y44LO0M1lIN5JbQW1p
+4Uojx5jWbBMz6wnK1gKEtAtyggJP0FYFES+f14/qoW8zNM7KKUcYe3SZoJ3lAgMB
+AAGjgYswgYgwRgYDVR0RBD8wPYIJbG9jYWxob3N0ghhXZXNhbXMtTWFjQm9vay1Q
+cm8ubG9jYWyHBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEwHQYDVR0OBBYEFLw6lkin
+ijLXLUJ+Rq+R8UD2865VMB8GA1UdIwQYMBaAFN9rOKK6q5ytlMd4Z2mre7snDNu3
+MA0GCSqGSIb3DQEBCwUAA4ICAQA9ptMjfg5ETA5eKxqXJ5AXxcFISmetSzJbAEsd
+c/toMfWxYSyt1+W6SDBI4S3vIHokh0uuK1K8/PiywQ6xQicNtKfY8R96ZP+ZcdsI
+Gj1VNWVY7FbbCaUHXX6oC5rpLrU7oeQJ0CEj+WNfPz9hfqRVz7WCHNIZGvJFdMLk
+Zm56vmXC4brTUSMasItgHt68iofWNBeeUpgyvhoAkzxDuJ7/5fp6BRo5p10Nb2qW
+FdYGnLiAyqX8bV/ZPjoUMWPn8Q3gtcfFhelEQqRiFdQyehjKkD1eIcgIgbJQokoo
+7SRqBEqdK5YDvd8+ZA8+O15nbAopRu+atWVoQaA0ho0KolzpqlsEvc4oTRpO5VAB
+4AA7RlrHmZamoBxlq+79tcbJ1277tFBxky/zT8TvuhmLUSYGzC9aLTsqpBuuHkm6
+am/AGAnwQsaVjie1FSTl7lNJkxSFpS6wUy/Xm4ynAtGtwUf1Nk0BreT4H11Fe42E
+jZrVYQruiU+qtchzwXnrOI8j1GPQHqbiZZPLgmo6TqFFJP9gCsrnqAKy0Fc3tzI8
+noTE8vTXQ2NNefMZz61Ad+8TETMGk/tQeOJG+EsJYb+Boj9t+1QGU4Wd9cI6jdX3
+1qp7DRBZFGzqVUgtXdgY2Dtuq7mpkANULqvD47iyhvnNY1XKGGfO1zgrbNXE7rCs
+pizyeQ==
+-----END CERTIFICATE-----
diff --git a/banyand/queue/pub/testdata/certs/server.key
b/banyand/queue/pub/testdata/certs/server.key
new file mode 100644
index 00000000..be3b02f1
--- /dev/null
+++ b/banyand/queue/pub/testdata/certs/server.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDUGYdez3b3NGSw
+IliEJtEAINqtQpynwwUVIWkkuYqQEujX3M6s5N7xYP/7UCOV6OyZNbkt8GSXJb/0
+3OvB/qVAKWXNQx0wOKWSGxvK0wheXO3woIsLlrRYm5KeCiBCEhqu9wKDgQ2fy+ph
+3elNJMnayaJWGaizuvFEPYY52sae1iy/nyy976R5EHMWh6zafG7JuxEzAQaS04fB
+7IIMtcxA7XtFEl00GtxVFVAGuUur9YLGDGRX0CR7z2eoqD04VoPzUMrP09y44LO0
+M1lIN5JbQW1p4Uojx5jWbBMz6wnK1gKEtAtyggJP0FYFES+f14/qoW8zNM7KKUcY
+e3SZoJ3lAgMBAAECggEAA8ZDavQEUehxnguy3siEDjLOlsSiT9RqzZIvXOFV9Ykv
+uQnrhTOXCaSExFeoeZKloeiUO1WOV2CyUvrwJuYIYAinOzAsn6JzUvaUNDP7/d0E
+DsMhFI03FEonIYZORhllYSrxYAJIihAt/DlRedPOfq+kBjRXBoGP/pfWXUbf6ozl
+p9JMZd8JRkKLhkAE+tWNEztiq6Z+Ay7r9ZEBPfgvhuR8ZvCmTzU5G5prAG9sZRVO
+jmggXrZJZE8DCcQuuVDxnW2yri1B40JTq6oqPIFON2/pONxIXdbkh6cMalgoBGns
+ju1n+/bmCuZOJvGLz6CI2mMaA71EBwAFTTPDyd9ORwKBgQD0tWuL+txW14TRDR/3
+1xZhCTtKvXGsB/lysv8TT9Z+rs9BYFTLdP3bss3rK6quYmcs4lfX1hh63N/nNu5c
+Zzb7OBlOOdsneGMlqvaS0Wi7w/QD3cJk5SRT2EDnSmid+ZH1FRTfU76csXQKlWZ0
+c+MITYSI9A5ruC/7ro7BjLZmNwKBgQDd4uvEI0GJkCvX6I46WysNUvCYnLFwMqzo
+It+Y40OtVFyx8M0+2txmUEPShE8yiOxfooJ9kTgJPeA+UCrD5EfT6g96E6yf0o7v
+b4yS2yXMMmBzGNg6IOMcuo6dg/+g5afiFJjpW+uTkCmzSpBuv0AXaq+ril2FXSeg
+6/b+XtROwwKBgQDoI+65ZSp/a4aAtr275JxDQ3mu5laehxYZvDqEPHnTxcuxTkxC
+kmJ8d3wm+064jXspNjN0+pJQg5YYhqDKodOjsE05S2NeZzNPOYceMm/zp+mlfUr7
+YfD7ZSv0/j6OloHjCLO7RHuPtDvMCnyePo2Cg98V+MhxYdKLJMYiUHV8MwKBgQDB
+oFIpeLKqPrtj0Da9Se2J1QTLyIE27aHheP0yR7A5Q1MYnJAe95I5XnWw8XDDIqVS
+11eUB/OkbPCQsBiBlWXw0WHsH9sWJQJCg09ioAad6KAuEFIwd5545XuqjRO37nDQ
+YzUE/wfWX4lkQf9EBXUCekEOKtJtnNsGHKQPWeVfTQKBgQCH8+TR70V5Etvpj9E7
+Hiup0iqcXdqm8XX5/pZcBws8UkKc97AkXJiXhdWo+Uj/r9e2xILGeTubxnENaWAR
+t6HTybF+NjKtQFJCzB3KMXD5ATKCjzOTKZYeuoS0O/N8xm9hre1THGUmGlNI3gb8
+MFoAbhIZDHbU/F0eb0ZCpqu8SA==
+-----END PRIVATE KEY-----
diff --git a/docs/operation/configuration.md b/docs/operation/configuration.md
index dd4a6c91..387ad9dc 100644
--- a/docs/operation/configuration.md
+++ b/docs/operation/configuration.md
@@ -77,6 +77,18 @@ If you want to enable TLS for the communication between the
client and liaison/s
- `--http-key-file string`: The TLS key file of the HTTP server.
- `--http-cert-file string`: The TLS certificate file of the HTTP server.
+#### Internal queue TLS (Liaison ↔ Data)
+
+Enable TLS on the internal gRPC queue that the Liaison uses to push data into
every Data‑Node:
+
+- `--internal-tls`: enable TLS on the queue client inside Liaison; if false
the queue uses plain TCP.
+- `--internal-ca-cert <path>`: PEM‑encoded CA (or bundle) that the queue
client uses to verify Data‑Node server certificates.
+
+#### Server certificates
+
+Each Liaison/Data process still advertises its certificate with the public
flags shown above (`--tls`, `--cert-file`, `--key-file`).
+The same certificate/key pair can be reused for both external traffic and the
internal queue.
+
### Data & Storage
If the node is running as a data server, you can configure the health check
server port:
diff --git a/docs/operation/security.md b/docs/operation/security.md
index df070992..fbddd5ea 100644
--- a/docs/operation/security.md
+++ b/docs/operation/security.md
@@ -20,7 +20,7 @@ For example, to enable TLS for gRPC communication, you can
use the following fla
banyand liaison --tls=true --key-file=server.key --cert-file=server.crt
--http-grpc-cert-file=server.crt --http-tls=true --http-key-file=server.key
--http-cert-file=server.crt
```
-If you only want to security gRPC connection, you can leave `--http-tls=false`.
+If you only want to secure the gRPC connection, you can leave
`--http-tls=false`.
```shell
banyand liaison --tls=true --key-file=server.key --cert-file=server.crt
--http-grpc-cert-file=server.crt
@@ -32,10 +32,28 @@ Also, you can enable TLS for HTTP connection only.
banyand liaison --http-tls=true --http-key-file=server.key
--http-cert-file=server.crt
```
-> Note: BanyanDB does not support TLS between liaison and data nodes.
-
The key and certificate files can be reloaded automatically when they are
updated. You can update the files or recreate the files, and the server will
automatically reload them.
+### Internal TLS (Liaison ↔ Data Nodes)
+
+BanyanDB supports enabling TLS for the internal gRPC queue between liaison and
data nodes. This secures the communication channel used for data ingestion and
internal operations.
+
+The following flags are used to configure internal TLS:
+
+- `--internal-tls`: Enable TLS on the internal queue client inside Liaison; if
false, the queue uses plain TCP.
+- `--internal-ca-cert <path>`: PEM‑encoded CA (or bundle) that the queue
client uses to verify Data‑Node server certificates.
+
+Each Liaison/Data process still advertises its certificate with the public
flags (`--tls`, `--cert-file`, `--key-file`). The same certificate/key pair can
be reused for both external traffic and the internal queue.
+
+**Example: Enable internal TLS between liaison and data nodes**
+
+```shell
+banyand liaison --internal-tls=true --internal-ca-cert=ca.crt --tls=true
--cert-file=server.crt --key-file=server.key
+banyand data --tls=true --cert-file=server.crt --key-file=server.key
+```
+
+> Note: The `--internal-ca-cert` should point to the CA certificate used to
sign the data node's server certificate.
+
## Authorization
BanyanDB does not have built-in authorization mechanisms. However, you can use
external tools like [Envoy](https://www.envoyproxy.io/) or
[Istio](https://istio.io/) to manage access control and authorization.