Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ class DynamoRuntimeConfig(ConfigBase):
# default; when set, these surface env vars that the Rust runtime reads
# directly (see lib/runtime/src/pipeline/network/ingress/shared_tcp_endpoint.rs).
engine_request_limit: Optional[int] = None
tcp_tls_cert_path: Optional[str] = None
tcp_tls_key_path: Optional[str] = None
tcp_tls_ca_cert_path: Optional[str] = None
tcp_tls_insecure: bool = False
tcp_tls_server_name: Optional[str] = None
tcp_tls_handshake_timeout_secs: Optional[int] = None

def validate(self) -> None:
self.namespace = get_worker_namespace(self.namespace)
Expand Down Expand Up @@ -92,6 +98,28 @@ def validate(self) -> None:
f"--engine-request-limit must be a positive integer, got {self.engine_request_limit}"
)

# Propagate TCP TLS CLI flags to env vars so the Rust runtime picks them up.
if self.tcp_tls_cert_path:
os.environ["DYN_TCP_TLS_CERT_PATH"] = self.tcp_tls_cert_path
if self.tcp_tls_key_path:
os.environ["DYN_TCP_TLS_KEY_PATH"] = self.tcp_tls_key_path
if self.tcp_tls_ca_cert_path:
os.environ["DYN_TCP_TLS_CA_CERT_PATH"] = self.tcp_tls_ca_cert_path
if self.tcp_tls_insecure:
os.environ["DYN_TCP_TLS_INSECURE"] = "1"
else:
os.environ.pop("DYN_TCP_TLS_INSECURE", None)
if self.tcp_tls_server_name:
os.environ["DYN_TCP_TLS_SERVER_NAME"] = self.tcp_tls_server_name
if self.tcp_tls_handshake_timeout_secs is not None:
if self.tcp_tls_handshake_timeout_secs <= 0:
raise ValueError(
f"--tcp-tls-handshake-timeout must be a positive integer, got {self.tcp_tls_handshake_timeout_secs}"
)
os.environ["DYN_TCP_TLS_HANDSHAKE_TIMEOUT_SECS"] = str(
self.tcp_tls_handshake_timeout_secs
)
Comment thread
walkoss marked this conversation as resolved.

def _validate_output_modalities(self) -> None:
"""Validate --output-modalities values."""
if not self.output_modalities:
Expand Down Expand Up @@ -350,3 +378,53 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
"semaphore size). Enables worker-side request rejection when set. "
"Disabled by default.",
)

add_argument(
g,
flag_name="--tcp-tls-cert-path",
env_var="DYN_TCP_TLS_CERT_PATH",
default=None,
help="Path to PEM certificate for the TCP server.",
)

add_argument(
g,
flag_name="--tcp-tls-key-path",
env_var="DYN_TCP_TLS_KEY_PATH",
default=None,
help="Path to PEM private key for the TCP server certificate.",
)

add_argument(
g,
flag_name="--tcp-tls-ca-cert-path",
env_var="DYN_TCP_TLS_CA_CERT_PATH",
default=None,
help="Path to PEM CA certificate used by this node to verify the TCP peer's certificate.",
)

add_negatable_bool_argument(
g,
flag_name="--tcp-tls-insecure",
env_var="DYN_TCP_TLS_INSECURE",
default=False,
help="Disable TCP TLS certificate verification. For local development only.",
)

add_argument(
g,
flag_name="--tcp-tls-server-name",
env_var="DYN_TCP_TLS_SERVER_NAME",
default=None,
help="Override TLS SNI server name for TCP connections (useful when connecting by IP).",
)

add_argument(
g,
flag_name="--tcp-tls-handshake-timeout",
env_var="DYN_TCP_TLS_HANDSHAKE_TIMEOUT_SECS",
default=None,
arg_type=int,
dest="tcp_tls_handshake_timeout_secs",
help="TLS handshake timeout in seconds (default: 3).",
)
27 changes: 27 additions & 0 deletions components/src/dynamo/frontend/frontend_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ class FrontendConfig(RouterConfigBase, KvRouterConfigBase, AicPerfConfigBase):
http_port: int
tls_cert_path: Optional[pathlib.Path]
tls_key_path: Optional[pathlib.Path]
tcp_tls_cert_path: Optional[str] = None
tcp_tls_key_path: Optional[str] = None
tcp_tls_ca_cert_path: Optional[str] = None

namespace: Optional[str] = None
namespace_prefix: Optional[str] = None
Expand Down Expand Up @@ -248,6 +251,30 @@ def add_arguments(self, parser) -> None:
arg_type=pathlib.Path,
)

add_argument(
g,
flag_name="--tcp-tls-cert-path",
env_var="DYN_TCP_TLS_CERT_PATH",
default=None,
help="Path to PEM certificate for the TCP server.",
)

add_argument(
g,
flag_name="--tcp-tls-key-path",
env_var="DYN_TCP_TLS_KEY_PATH",
default=None,
help="Path to PEM private key for the TCP server certificate.",
)

add_argument(
g,
flag_name="--tcp-tls-ca-cert-path",
env_var="DYN_TCP_TLS_CA_CERT_PATH",
default=None,
help="Path to PEM CA certificate used to verify the TCP peer's certificate.",
)
Comment thread
walkoss marked this conversation as resolved.

# Router options (shared with dynamo.router)
RouterArgGroup().add_arguments(parser)

Expand Down
6 changes: 6 additions & 0 deletions components/src/dynamo/frontend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,12 @@ def signal_handler():
kwargs["tls_cert_path"] = config.tls_cert_path
if config.tls_key_path:
kwargs["tls_key_path"] = config.tls_key_path
if config.tcp_tls_cert_path:
os.environ["DYN_TCP_TLS_CERT_PATH"] = config.tcp_tls_cert_path
if config.tcp_tls_key_path:
os.environ["DYN_TCP_TLS_KEY_PATH"] = config.tcp_tls_key_path
if config.tcp_tls_ca_cert_path:
os.environ["DYN_TCP_TLS_CA_CERT_PATH"] = config.tcp_tls_ca_cert_path
Comment thread
walkoss marked this conversation as resolved.
Comment thread
walkoss marked this conversation as resolved.
if config.namespace:
kwargs["namespace"] = config.namespace
if config.namespace_prefix:
Expand Down
2 changes: 2 additions & 0 deletions docs/fern/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -993,6 +993,8 @@ navigation:
contents:
- page: Runtime Configuration
path: pages/reference/components/runtime-configuration.mdx
- page: TLS Configuration
path: pages/reference/components/tls-configuration.mdx
- page: Frontend Configuration
path: pages/reference/components/frontend-configuration.mdx
- page: Planner Configuration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ Additional TCP-specific environment variables:
- `DYN_TCP_CONNECT_TIMEOUT`: Connect timeout for TCP client (default: 3 seconds)
- `DYN_TCP_CHANNEL_BUFFER`: Request channel buffer size for TCP client (default: 100)

**Encryption:**

TCP request-plane traffic can be encrypted with TLS. See the
[TLS reference](../../../../reference/components/tls-configuration.mdx) for the
`DYN_TCP_TLS_*` environment variables, CLI flags, and setup.

### Using NATS

NATS provides a brokered request plane and can also carry KV events and router replica synchronization over NATS Core.
Expand Down
147 changes: 147 additions & 0 deletions docs/fern/pages/reference/components/tls-configuration.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
---
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
title: TCP TLS
subtitle: Encrypt TCP streaming connections between frontend and workers
---

Dynamo supports opt-in TLS encryption on the TCP call-home streaming transport
(implemented by `TcpStreamServer` and `TcpClient`, handling both response
streams and request streams between frontends and workers). When enabled, all TCP
connections on this path are upgraded to TLS using
[rustls](https://github.com/rustls/rustls) with the `ring` cryptographic
provider. When no TLS configuration is provided, the transport operates in
plaintext exactly as before.

## Environment variables

All TLS configuration is driven by environment variables. The Rust runtime
reads these directly at first connection (lazy initialization).

Both frontends and workers act as TCP server and client depending on the
stream direction (response streams: worker dials frontend; request streams:
frontend dials worker). All TLS env vars should be set on every pod.

### Server role (accepting connections)

| Variable | Description |
|---|---|
| `DYN_TCP_TLS_CERT_PATH` | Path to the PEM certificate file. When set together with `DYN_TCP_TLS_KEY_PATH`, TLS is enabled on the TCP server. |
| `DYN_TCP_TLS_KEY_PATH` | Path to the PEM private key for the server certificate. |

### Client role (dialing connections)

| Variable | Description |
|---|---|
| `DYN_TCP_TLS_CA_CERT_PATH` | Path to the PEM CA certificate used to verify the peer's server certificate. |
| `DYN_TCP_TLS_INSECURE` | Set to `1` or `true` to skip certificate verification. For local development only. |
| `DYN_TCP_TLS_SERVER_NAME` | Override the TLS SNI hostname. Useful when connecting by IP to a server whose certificate has a DNS SAN. |
| `DYN_TCP_TLS_HANDSHAKE_TIMEOUT_SECS` | TLS handshake timeout in seconds (default: 3). |

## CLI flags

The same configuration is available via command-line flags on all backends
(vllm, sglang, trtllm, tokenspeed) through `DynamoRuntimeArgGroup`:

```
--tcp-tls-cert-path PATH Server certificate (PEM)
--tcp-tls-key-path PATH Server private key (PEM)
--tcp-tls-ca-cert-path PATH CA certificate for server verification (PEM)
--tcp-tls-insecure Disable certificate verification
--tcp-tls-server-name NAME Override TLS SNI hostname
--tcp-tls-handshake-timeout N Handshake timeout in seconds (default: 3)
```

The frontend (`dynamo.frontend`) also accepts `--tcp-tls-cert-path`,
`--tcp-tls-key-path`, and `--tcp-tls-ca-cert-path`.

## Quick start

Generate a self-signed certificate for local testing:

```bash
# Generate CA
openssl req -x509 -newkey rsa:2048 -keyout ca-key.pem -out ca-cert.pem \
-days 365 -nodes -subj "/CN=DynamoCA"

# Generate server cert with SAN
openssl req -newkey rsa:2048 -keyout server-key.pem -out server-csr.pem \
-nodes -subj "/CN=localhost" \
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1"

openssl x509 -req -in server-csr.pem -CA ca-cert.pem -CAkey ca-key.pem \
-CAcreateserial -out server-cert.pem -days 365 -copy_extensions copyall
```

Both frontend and worker need the same flags (both act as server and client):

```bash
python -m dynamo.vllm \
--tcp-tls-cert-path server-cert.pem \
--tcp-tls-key-path server-key.pem \
--tcp-tls-ca-cert-path ca-cert.pem \
--tcp-tls-server-name localhost \
...

python -m dynamo.frontend \
--tcp-tls-cert-path server-cert.pem \
--tcp-tls-key-path server-key.pem \
--tcp-tls-ca-cert-path ca-cert.pem \
--tcp-tls-server-name localhost \
...
```

## Kubernetes deployment

In Kubernetes, TLS certificates are typically delivered by a certificate
management system (e.g., cert-manager) and mounted into pods. Set the
environment variables on each component's pod template in the
`DynamoGraphDeployment` spec:

```yaml
spec:
components:
- name: Frontend
podTemplate:
spec:
containers:
- name: main
env:
- name: DYN_TCP_TLS_CERT_PATH
value: /etc/certs/server/cert.pem
- name: DYN_TCP_TLS_KEY_PATH
value: /etc/certs/server/key.pem
- name: DYN_TCP_TLS_CA_CERT_PATH
value: /etc/certs/ca/ca.pem
- name: VllmWorker
podTemplate:
spec:
containers:
- name: main
env:
- name: DYN_TCP_TLS_CERT_PATH
value: /etc/certs/server/cert.pem
- name: DYN_TCP_TLS_KEY_PATH
value: /etc/certs/server/key.pem
- name: DYN_TCP_TLS_CA_CERT_PATH
value: /etc/certs/ca/ca.pem
```

Both components need the same TLS env vars because each acts as both TCP
server and client depending on the stream direction.

> **Note:** A future PR ([#10809](https://github.com/ai-dynamo/dynamo/issues/10809))
> will add operator-level TLS configuration via `InfrastructureConfiguration`,
> allowing TLS to be configured once at the platform level and auto-injected
> into all DGD pods without per-component env var setup.

## Design notes

- TLS configuration is cached after the first TCP connection via `OnceCell`.
Certificate rotation requires a process restart.
- The TLS handshake is spawned per-connection on the server side so the accept
loop is never blocked by a slow handshake.
- When server and client TLS configurations are mismatched (e.g., server has TLS
but client does not), a warning is logged at startup.
- An empty CA certificate file is detected at load time and rejected with a
clear error message.
3 changes: 3 additions & 0 deletions lib/bindings/kvbm/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions lib/bindings/python/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions lib/runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,11 @@ rmp-serde = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }
tokio-rustls = { workspace = true }
tokio-stream = { workspace = true }
tokio-util = { workspace = true }
rustls = { workspace = true }
rustls-pemfile = { workspace = true }
tower-http = { workspace = true }
tracing = { workspace = true }
mio = { workspace = true }
Expand Down Expand Up @@ -111,6 +114,7 @@ k8s-openapi = { version = "0.26.0", features = ["v1_32"] }

[dev-dependencies]
criterion = { version = "0.5", features = ["async_tokio"] }
rcgen = { workspace = true }
rstest = { version = "0.23.0" }
temp-env = { version = "0.3.6" , features=["async_closure"] }
stdio-override = {version= "0.2.0"}
Expand Down
Loading
Loading