feat(sdk): gate mTLS lifecycle identity on KEYLESS_DIR env var
When KEYLESS_DIR is set, the SDK acquires a lifecycle identity and presents a client certificate. When unset, the SDK operates in token-only mode — no hard-fail on missing cert. Listener TLS configs conditionally include Certificates only when controlPlaneCert is non-empty. AGENTS.md updated to document implicit mTLS semantics and admission order.
cognitive committed
Mar 4, 2026 at 21:22 UTC
76e4132bc5334d971aad1fadf5bf056cb0ae3470
3 files changed
+21
-14
AGENTS.md
+4
-2
@@ -40,8 +40,10 @@ Source of truth for architecture decisions: `docs/adr/README.md` and linked ADRs
40
1. **Relay holds the TLS private key; SDK/tunnel never does.** SDK calls `/v1/sign` on the relay via `RemoteSigner` for all private key operations.
41
- Why: prevents key material leakage to untrusted tunnel endpoints.
42
43
-2. **mTLS is mandatory for all `/sdk/*` control-plane paths.** No token-only fallback; hard-fail on missing client cert.
44
- - Why: ADR-0003 admission order (IP ban → Lease → CertBind → Token) requires mTLS at the CertBind stage.
43
+2. **mTLS is implicit (optional) for `/sdk/*` control-plane paths.** When a client cert is presented, the relay validates it (CertBind stage). When absent, CertBind is skipped and token auth alone is used.
44
+ - `KEYLESS_DIR` env var presence triggers SDK lifecycle identity issuance and client cert presentation. When unset, the SDK operates in token-only mode.
45
+ - Keyless TLS (`RemoteSigner` for `/v1/sign`) is independent of mTLS — always used for TLS termination regardless of client cert presence.
46
+ - Why: ADR-0003 admission order is IP ban → Lease → [CertBind if cert present] → Token. Invalid certs are still rejected; absent certs skip CertBind.
47
48
3. **All relay URLs must be `https://`.** `NormalizeRelayAPIURL` rejects non-HTTPS. SDK and tunnel hard-fail on `http://`.
49
- Why: enforces transport security without opt-out.
sdk/client.go
+6
-3
@@ -108,9 +108,12 @@ func (c *Client) Listen(name string, options ...types.MetadataOption) (net.Liste
108
if err != nil {
109
return nil, err
110
}
111
- controlPlaneIdentity, err := acquireLifecycleIdentity(lease.ID)
112
- if err != nil {
113
- return nil, err
111
+ var controlPlaneIdentity tls.Certificate
112
+ if strings.TrimSpace(os.Getenv(keylessDirEnvVar)) != "" {
113
+ controlPlaneIdentity, err = acquireLifecycleIdentity(lease.ID)
114
+ if err != nil {
115
+ return nil, err
116
+ }
117
}
118
119
listeners := make([]net.Listener, 0, len(relayAddrs))
sdk/listener.go
+11
-9
@@ -122,22 +122,21 @@ func NewListener(relayAddr string, lease *portal.Lease, tlsConfig *tls.Config, c
122
if tlsConfig == nil {
123
return nil, errors.New("tls config is required")
124
}
125
- if len(controlPlaneCert.Certificate) == 0 {
126
- return nil, errors.New("control plane client certificate is required")
127
- }
128
-
125
apiURL, err := netutil.NormalizeRelayAPIURL(relayAddr)
126
if err != nil {
127
return nil, err
128
}
129
host := netutil.PortalRootHost(apiURL)
130
clientTransport := http.DefaultTransport.(*http.Transport).Clone()
135
- clientTransport.TLSClientConfig = &tls.Config{
131
+ transportTLSConfig := &tls.Config{
132
MinVersion: tls.VersionTLS12,
133
ServerName: host,
134
InsecureSkipVerify: netutil.IsLocalhost(host),
139
- Certificates: []tls.Certificate{controlPlaneCert},
135
}
136
+ if len(controlPlaneCert.Certificate) > 0 {
137
+ transportTLSConfig.Certificates = []tls.Certificate{controlPlaneCert}
138
+ }
139
+ clientTransport.TLSClientConfig = transportTLSConfig
140
141
if reverseWorkers <= 0 {
142
reverseWorkers = defaultReverseWorkers
@@ -407,12 +406,15 @@ func (l *Listener) openReverseConnection() (net.Conn, error) {
406
_ = rawConn.Close()
407
return nil, errors.New("reverse connect URL missing TLS server name")
408
}
410
- tlsConn := tls.Client(rawConn, &tls.Config{
409
+ reverseTLSConfig := &tls.Config{
410
MinVersion: tls.VersionTLS12,
411
ServerName: serverName,
412
InsecureSkipVerify: netutil.IsLocalhost(serverName),
414
- Certificates: []tls.Certificate{l.controlPlaneCert},
415
- })
413
+ }
414
+ if len(l.controlPlaneCert.Certificate) > 0 {
415
+ reverseTLSConfig.Certificates = []tls.Certificate{l.controlPlaneCert}
416
+ }
417
+ tlsConn := tls.Client(rawConn, reverseTLSConfig)
418
err = tlsConn.HandshakeContext(ctx)
419
if err != nil {
420
_ = rawConn.Close()