feat: remove self tls mode

Kim committed Mar 3, 2026 at 12:08 UTC 2a3b1e742668982ecdf831c1b044ae6ac08b915e
18 files changed +215 -421
AGENTS.md
+5 -8
@@ -87,9 +87,7 @@ Portal uses environment variables for domain and TLS configuration:
87 | Variable | Description |
88 |----------|-------------|
89 | `RELAYS` | Relay API URLs for tunnel client (comma-separated) |
90 -| `TLS_MODE` | `no-tls`, `self`, or `keyless` |
91 -| `TLS_CERT_FILE` | Self TLS certificate chain path (self mode only) |
92 -| `TLS_KEY_FILE` | Self TLS private key path (self mode only) |
90 +| `TLS` | Enable TLS keyless mode (`1`/`true`) |
91 | `TLS_BASE_DOMAIN` | Base domain used for keyless certificate hostname validation (keyless mode) |
92
93 ### Domain Derivation
@@ -98,12 +96,11 @@ Portal uses environment variables for domain and TLS configuration:
96 - Base domain extracted from `PORTAL_URL` via `extractBaseDomain()` in `cmd/relay-server/utils.go`
97 - SNI routes registered in `portal/sni/router.go`
98
101 -### TLS Modes
99 +### TLS
100
103 -1. **`no-tls`**: HTTP proxy mode for development.
104 -2. **`self`**: Tunnel uses locally provided certificate and key (`TLS_CERT_FILE` + `TLS_KEY_FILE`).
105 -3. **`keyless`**: Tunnel uses SDK keyless mode with auto defaults.
106 - - Keyless signer endpoint defaults to relay URL unless explicitly overridden in SDK options.
101 +1. **`TLS` disabled**: HTTP proxy mode for development.
102 +2. **`TLS` enabled**: Tunnel uses SDK keyless TLS mode.
103 + - Keyless signer endpoint defaults to relay URL.
104 - Certificate chain/root trust are auto-discovered by SDK from signer endpoint when not explicitly provided.
105 - Auto-discovery requires an HTTPS signer endpoint.
106 - Relay signer key comes from `KEYLESS_DIR/privatekey.pem`; when missing and `CLOUDFLARE_TOKEN` is set, relay auto-issues via ACME DNS-01.
cmd/demo-app/main.go
+5 -19
@@ -34,9 +34,7 @@ var (
34 flagTags string
35 flagOwner string
36 flagHide bool
37 - flagTLSMode string
38 - flagTLSCert string
39 - flagTLSKey string
37 + flagTLS bool
38 )
39
40 func main() {
@@ -47,13 +45,8 @@ func main() {
45 flag.StringVar(&flagTags, "tags", "demo,connectivity,activity,cloud,sun,moning", "comma-separated lease tags")
46 flag.StringVar(&flagOwner, "owner", "PortalApp Developer", "lease owner")
47 flag.BoolVar(&flagHide, "hide", false, "hide this lease from listings")
50 - defaultTLSMode := os.Getenv("TLS_MODE")
51 - if defaultTLSMode == "" {
52 - defaultTLSMode = string(sdk.TLSModeNoTLS)
53 - }
54 - flag.StringVar(&flagTLSMode, "tls-mode", defaultTLSMode, "TLS mode: no-tls, self, or keyless [env: TLS_MODE]")
55 - flag.StringVar(&flagTLSCert, "tls-cert-file", os.Getenv("TLS_CERT_FILE"), "certificate chain PEM file for --tls-mode self [env: TLS_CERT_FILE]")
56 - flag.StringVar(&flagTLSKey, "tls-key-file", os.Getenv("TLS_KEY_FILE"), "private key PEM file for --tls-mode self [env: TLS_KEY_FILE]")
48 + defaultTLS := strings.EqualFold(strings.TrimSpace(os.Getenv("TLS")), "true") || strings.TrimSpace(os.Getenv("TLS")) == "1"
49 + flag.BoolVar(&flagTLS, "tls", defaultTLS, "Enable TLS (keyless) [env: TLS]")
50
51 flag.Parse()
52
@@ -65,15 +58,8 @@ func main() {
58 func runDemo() error {
59 // 1) Create SDK client and connect to relay(s)
60 opts := []sdk.ClientOption{sdk.WithBootstrapServers([]string{flagServerURL})}
68 - mode := sdk.TLSMode(flagTLSMode)
69 - switch mode {
70 - case sdk.TLSModeNoTLS:
71 - case sdk.TLSModeSelf:
72 - opts = append(opts, sdk.WithTLSSelfCertificateFiles(flagTLSCert, flagTLSKey))
73 - case sdk.TLSModeKeyless:
74 - opts = append(opts, sdk.WithTLSKeylessDefaults())
75 - default:
76 - return fmt.Errorf("unsupported tls mode: %s", flagTLSMode)
61 + if flagTLS {
62 + opts = append(opts, sdk.WithTLS())
63 }
64 client, err := sdk.NewClient(opts...)
65 if err != nil {
cmd/portal-tunnel/README.md
+9 -29
@@ -25,19 +25,17 @@ Enable TLS for end-to-end encryption from client to your local service:
25 ./bin/portal-tunnel --host localhost:8080 \
26 --relay https://portal.example.com \
27 --name myapp \
28 - --tls-mode keyless
28 + --tls
29 ```
30
31 -TLS modes:
32 -- No TLS mode (`--tls-mode no-tls`): plain TCP/HTTP proxying without TLS termination
33 -- Self mode (`--tls-mode self`): tunnel uses local certificate and key files
34 -- Keyless mode (`--tls-mode keyless`): tunnel auto-discovers certificate chain and delegates signing to external signer API
31 +TLS options:
32 +- TLS disabled (default): plain TCP/HTTP proxying without TLS termination
33 +- TLS enabled (`--tls`): keyless TLS mode with auto-discovered certificate chain and remote signing
34 - TLS is terminated at the tunnel, then proxied to your local service via TCP
35 - Access via `https://myapp.example.com` directly on port 443
36
37 **Requirements:**
39 -- Self mode: set `TLS_CERT_FILE` and `TLS_KEY_FILE` (or use `--tls-cert-file`, `--tls-key-file`)
40 -- Keyless mode: no local cert/key required in default mode (SDK auto-discovers signer certificate chain)
38 +- TLS enabled: no local cert/key required in default mode (SDK auto-discovers signer certificate chain)
39 - Keyless auto-discovery expects an HTTPS signer endpoint.
40
41 ## Flags
@@ -50,10 +48,7 @@ Options:
48 --relay Portal relay server API URLs (comma-separated, http/https) [default: http://localhost:4017] [env: RELAYS]
49 --host Target host to proxy to (host:port or URL) [env: APP_HOST]
50 --name Service name [env: APP_NAME]
53 - --tls-mode TLS mode: no-tls, self, or keyless [default: no-tls] [env: TLS_MODE]
54 - --tls-cert-file PEM certificate chain for --tls-mode self [env: TLS_CERT_FILE]
55 - --tls-key-file PEM private key for --tls-mode self [env: TLS_KEY_FILE]
56 - --tls-base-domain Base domain for keyless certificate validation (e.g., example.com) [env: TLS_BASE_DOMAIN]
51 + --tls Enable keyless TLS mode [env: TLS]
52 --description Service description metadata [env: APP_DESCRIPTION]
53 --tags Service tags metadata (comma-separated) [env: APP_TAGS]
54 --thumbnail Service thumbnail URL metadata [env: APP_THUMBNAIL]
@@ -80,21 +75,7 @@ $env:APP_HOST="localhost:3000"; $env:APP_NAME="myapp"; irm https://portal.exampl
75 export RELAYS=https://portal.example.com
76 export APP_HOST=localhost:3000
77 export APP_NAME=myapp
83 -export TLS_MODE=keyless
84 -export TLS_BASE_DOMAIN=example.com
85 -
86 -./bin/portal-tunnel
87 -```
88 -
89 -### Production (Self TLS)
90 -
91 -```bash
92 -export RELAYS=https://portal.example.com
93 -export APP_HOST=localhost:3000
94 -export APP_NAME=myapp
95 -export TLS_MODE=self
96 -export TLS_CERT_FILE=/etc/ssl/myapp/fullchain.pem
97 -export TLS_KEY_FILE=/etc/ssl/myapp/privkey.pem
78 +export TLS=1
79
80 ./bin/portal-tunnel
81 ```
@@ -105,8 +86,7 @@ export TLS_KEY_FILE=/etc/ssl/myapp/privkey.pem
86 export RELAYS=https://portal.example.com
87 export APP_HOST=localhost:3000
88 export APP_NAME=myapp
108 -export TLS_MODE=keyless
109 -export TLS_BASE_DOMAIN=example.com
89 +export TLS=1
90
91 ./bin/portal-tunnel
92 ```
@@ -138,5 +118,5 @@ Expected signer API contract (`/v1/sign`):
118 --host localhost:3000 \
119 --name myapp \
120 --relay https://portal1.example.com,https://portal2.example.com \
141 - --tls-mode keyless
121 + --tls
122 ```
cmd/portal-tunnel/main.go
+8 -23
@@ -28,9 +28,7 @@ var (
28 flagThumbnail string
29 flagOwner string
30 flagHide bool
31 - flagTLSMode string
32 - flagTLSCert string
33 - flagTLSKey string
31 + flagTLS bool
32 )
33
34 func main() {
@@ -45,13 +43,8 @@ func main() {
43 flag.StringVar(&flagHost, "host", os.Getenv("APP_HOST"), "Target host to proxy to (host:port or URL) [env: APP_HOST]")
44 flag.StringVar(&flagName, "name", os.Getenv("APP_NAME"), "Service name [env: APP_NAME]")
45
48 - defaultTLSMode := os.Getenv("TLS_MODE")
49 - if defaultTLSMode == "" {
50 - defaultTLSMode = string(sdk.TLSModeNoTLS)
51 - }
52 - flag.StringVar(&flagTLSMode, "tls-mode", defaultTLSMode, "TLS mode: no-tls, self, or keyless [env: TLS_MODE]")
53 - flag.StringVar(&flagTLSCert, "tls-cert-file", os.Getenv("TLS_CERT_FILE"), "PEM certificate chain for --tls-mode self [env: TLS_CERT_FILE]")
54 - flag.StringVar(&flagTLSKey, "tls-key-file", os.Getenv("TLS_KEY_FILE"), "PEM private key for --tls-mode self [env: TLS_KEY_FILE]")
46 + defaultTLS := strings.EqualFold(strings.TrimSpace(os.Getenv("TLS")), "true") || strings.TrimSpace(os.Getenv("TLS")) == "1"
47 + flag.BoolVar(&flagTLS, "tls", defaultTLS, "Enable TLS (keyless) [env: TLS]")
48
49 flag.StringVar(&flagDesc, "description", os.Getenv("APP_DESCRIPTION"), "Service description metadata [env: APP_DESCRIPTION]")
50 flag.StringVar(&flagTags, "tags", os.Getenv("APP_TAGS"), "Service tags metadata (comma-separated) [env: APP_TAGS]")
@@ -81,20 +74,12 @@ func runTunnel() error {
74 log.Info().Msg("Starting Portal Tunnel...")
75 log.Info().Msgf(" Local: %s", flagHost)
76 log.Info().Msgf(" Relays: %s", strings.Join(relayURLs, ", "))
84 - log.Info().Msgf(" TLS Mode: %s", flagTLSMode)
77 + log.Info().Msgf(" TLS: %t", flagTLS)
78
79 opts := []sdk.ClientOption{sdk.WithBootstrapServers(relayURLs)}
87 - mode := sdk.TLSMode(flagTLSMode)
88 - switch mode {
89 - case sdk.TLSModeNoTLS:
90 - case sdk.TLSModeSelf:
91 - opts = append(opts, sdk.WithTLSSelfCertificateFiles(flagTLSCert, flagTLSKey))
92 - case sdk.TLSModeKeyless:
93 - opts = append(opts, sdk.WithTLSKeylessDefaults())
94 - default:
95 - return fmt.Errorf("unsupported tls mode: %s", flagTLSMode)
80 + if flagTLS {
81 + opts = append(opts, sdk.WithTLS())
82 }
97 -
83 client, err := sdk.NewClient(opts...)
84 if err != nil {
85 return fmt.Errorf("service %s: failed to create client: %w", flagName, err)
@@ -125,7 +110,7 @@ func runTunnel() error {
110 if leaseAware, ok := listener.(interface{ LeaseID() string }); ok {
111 log.Info().Msgf("- Lease ID: %s", leaseAware.LeaseID())
112 }
128 - if flagTLSMode != string(sdk.TLSModeNoTLS) {
113 + if flagTLS {
114 log.Info().Msg("- TLS: Enabled")
115 }
116
@@ -160,7 +145,7 @@ loop:
145 connWG.Add(1)
146 go func(relayConn net.Conn) {
147 defer connWG.Done()
163 - tlsEnabled := flagTLSMode != string(sdk.TLSModeNoTLS)
148 + tlsEnabled := flagTLS
149 proxyType := "TCP"
150 if tlsEnabled {
151 proxyType = "TLS→TCP"
cmd/relay-server/frontend/src/components/TunnelCommandModal.tsx
+20 -130
@@ -1,4 +1,4 @@
1 -import { useState, useMemo, useRef, useEffect } from "react";
1 +import { useState, useMemo, useEffect } from "react";
2 import { Copy, Check, Terminal, X } from "lucide-react";
3 import { cn } from "@/lib/utils";
4 import {
@@ -16,8 +16,6 @@ interface TunnelCommandModalProps {
16 trigger?: React.ReactNode;
17 }
18
19 -type TLSMode = "no-tls" | "self" | "keyless";
20 -
19 export function TunnelCommandModal({ trigger }: TunnelCommandModalProps) {
20 const defaultHost = "localhost:3000";
21 const defaultName = "your-app-name";
@@ -36,11 +34,8 @@ export function TunnelCommandModal({ trigger }: TunnelCommandModalProps) {
34 const [urlInput, setUrlInput] = useState("");
35 const [copied, setCopied] = useState(false);
36 const [os, setOs] = useState<"unix" | "windows">("unix");
39 - const [tlsMode, setTlsMode] = useState<TLSMode>("keyless");
40 - const [tlsCertFile, setTlsCertFile] = useState("");
41 - const [tlsKeyFile, setTlsKeyFile] = useState("");
42 - const urlInputRef = useRef<HTMLInputElement>(null);
43 - const keylessAvailable = useMemo(() => {
37 + const [tls, setTls] = useState(true);
38 + const tlsAvailable = useMemo(() => {
39 if (relayUrls.length === 0) {
40 return false;
41 }
@@ -54,10 +49,10 @@ export function TunnelCommandModal({ trigger }: TunnelCommandModalProps) {
49 }, [relayUrls]);
50
51 useEffect(() => {
57 - if (!keylessAvailable && tlsMode === "keyless") {
58 - setTlsMode("no-tls");
52 + if (!tlsAvailable && tls) {
53 + setTls(false);
54 }
60 - }, [keylessAvailable, tlsMode]);
55 + }, [tlsAvailable, tls]);
56
57 const addRelayUrl = (url: string) => {
58 const trimmed = url.trim();
@@ -98,34 +93,13 @@ export function TunnelCommandModal({ trigger }: TunnelCommandModalProps) {
93 relayUrls.length > 0 ? relayUrls.join(",") : currentOrigin;
94
95 if (os === "windows") {
101 - let tlsEnv = `$env:TLS_MODE="${tlsMode}"; `;
102 - if (tlsMode === "self" && tlsCertFile.trim() !== "") {
103 - tlsEnv += `$env:TLS_CERT_FILE="${tlsCertFile}"; `;
104 - }
105 - if (tlsMode === "self" && tlsKeyFile.trim() !== "") {
106 - tlsEnv += `$env:TLS_KEY_FILE="${tlsKeyFile}"; `;
107 - }
96 + const tlsEnv = tls ? `$env:TLS="1"; ` : "";
97 return `$ProgressPreference = 'SilentlyContinue'; ${tlsEnv}$env:APP_HOST="${hostVal}"; $env:APP_NAME="${nameVal}"; $env:RELAYS="${relayUrlVal}"; irm ${currentOrigin}/tunnel?os=windows | iex`;
98 }
99
111 - let tlsEnv = `TLS_MODE=${tlsMode} `;
112 - if (tlsMode === "self" && tlsCertFile.trim() !== "") {
113 - tlsEnv += `TLS_CERT_FILE="${tlsCertFile}" `;
114 - }
115 - if (tlsMode === "self" && tlsKeyFile.trim() !== "") {
116 - tlsEnv += `TLS_KEY_FILE="${tlsKeyFile}" `;
117 - }
100 + const tlsEnv = tls ? "TLS=1 " : "";
101 return `curl -fsSL ${currentOrigin}/tunnel | ${tlsEnv}APP_HOST=${hostVal} APP_NAME=${nameVal} RELAYS="${relayUrlVal}" sh`;
119 - }, [
120 - currentOrigin,
121 - host,
122 - name,
123 - relayUrls,
124 - os,
125 - tlsMode,
126 - tlsCertFile,
127 - tlsKeyFile,
128 - ]);
102 + }, [currentOrigin, host, name, relayUrls, os, tls]);
103
104 const handleCopy = async () => {
105 try {
@@ -137,29 +111,6 @@ export function TunnelCommandModal({ trigger }: TunnelCommandModalProps) {
111 }
112 };
113
140 - const isNoTLS = tlsMode === "no-tls";
141 - const isSelfTLS = tlsMode === "self";
142 -
143 - const onNoTLSToggle = (checked: boolean) => {
144 - if (checked) {
145 - setTlsMode("no-tls");
146 - return;
147 - }
148 - if (tlsMode === "no-tls") {
149 - setTlsMode("keyless");
150 - }
151 - };
152 -
153 - const onSelfTLSToggle = (checked: boolean) => {
154 - if (checked) {
155 - setTlsMode("self");
156 - return;
157 - }
158 - if (tlsMode === "self") {
159 - setTlsMode("keyless");
160 - }
161 - };
162 -
114 return (
115 <Dialog>
116 <DialogTrigger asChild>
@@ -252,7 +203,6 @@ export function TunnelCommandModal({ trigger }: TunnelCommandModalProps) {
203 </span>
204 ))}
205 <input
255 - ref={urlInputRef}
206 type="text"
207 value={urlInput}
208 onChange={(e) => setUrlInput(e.target.value)}
@@ -307,87 +257,27 @@ export function TunnelCommandModal({ trigger }: TunnelCommandModalProps) {
257 <span className="flex items-center gap-2">
258 <input
259 type="checkbox"
310 - checked={isNoTLS}
311 - onChange={(e) => onNoTLSToggle(e.target.checked)}
312 - className="h-4 w-4"
260 + checked={tls}
261 + onChange={(e) => setTls(e.target.checked)}
262 + disabled={!tlsAvailable}
263 + className="h-4 w-4 disabled:opacity-50 disabled:cursor-not-allowed"
264 />
314 - No TLS
265 + TLS Enabled
266 </span>
267 <span className="text-xs text-text-muted text-right">
317 - Local-only testing mode.
318 - </span>
319 - </label>
320 - <label className="flex items-center justify-between gap-3 text-sm text-foreground">
321 - <span className="flex items-center gap-2">
322 - <input
323 - type="checkbox"
324 - checked={isSelfTLS}
325 - onChange={(e) => onSelfTLSToggle(e.target.checked)}
326 - className="h-4 w-4"
327 - />
328 - Self TLS
329 - </span>
330 - <span className="text-xs text-text-muted text-right">
331 - Bring your own cert and key.
268 + {tlsAvailable
269 + ? "Recommended: keyless TLS."
270 + : "TLS is not available on this relay."}
271 </span>
272 </label>
273 </div>
274 <p className="text-xs text-text-muted">
336 - {tlsMode === "no-tls"
337 - ? "Local testing only (no TLS)."
338 - : tlsMode === "keyless"
339 - ? "Recommended: Keyless TLS."
340 - : "Advanced: Self-managed TLS cert/key."}
275 + {tls
276 + ? "TLS is enabled (keyless mode)."
277 + : "TLS is disabled (no-tls mode)."}
278 </p>
279 </div>
280
344 - {tlsMode === "self" && (
345 - <div className="space-y-3 rounded-md border border-input/80 p-3">
346 - <p className="text-xs font-medium text-foreground">Self TLS Files</p>
347 - <div className="space-y-2">
348 - <label
349 - htmlFor="tls-cert-file"
350 - className="text-sm font-medium text-foreground"
351 - >
352 - TLS Cert File
353 - </label>
354 - <div className="flex items-center rounded-md bg-border">
355 - <span className="px-3 text-sm text-text-muted">TLS_CERT_FILE=</span>
356 - <Input
357 - id="tls-cert-file"
358 - type="text"
359 - value={tlsCertFile}
360 - onChange={(e) => setTlsCertFile(e.target.value)}
361 - placeholder="/path/to/fullchain.pem"
362 - className="rounded-l-none"
363 - />
364 - </div>
365 - </div>
366 - <div className="space-y-2">
367 - <label
368 - htmlFor="tls-key-file"
369 - className="text-sm font-medium text-foreground"
370 - >
371 - TLS Key File
372 - </label>
373 - <div className="flex items-center rounded-md bg-border">
374 - <span className="px-3 text-sm text-text-muted">TLS_KEY_FILE=</span>
375 - <Input
376 - id="tls-key-file"
377 - type="text"
378 - value={tlsKeyFile}
379 - onChange={(e) => setTlsKeyFile(e.target.value)}
380 - placeholder="/path/to/privkey.pem"
381 - className="rounded-l-none"
382 - />
383 - </div>
384 - </div>
385 - <p className="text-xs text-text-muted">
386 - Self TLS selected. These files are included in the command.
387 - </p>
388 - </div>
389 - )}
390 -
281 {/* Generated Command */}
282 <div className="space-y-2">
283 <label className="text-sm font-medium text-foreground">
cmd/relay-server/registry.go
+11 -11
@@ -20,18 +20,18 @@ type SDKRegistry struct{}
20
21 // HandleSDKRequest routes /sdk/* requests.
22 func (r *SDKRegistry) HandleSDKRequest(w http.ResponseWriter, req *http.Request, serv *portal.RelayServer) {
23 - route := strings.Trim(strings.TrimPrefix(req.URL.Path, "/sdk"), "/")
23 + path := strings.TrimSuffix(req.URL.Path, "/")
24
25 - switch route {
26 - case "register":
25 + switch path {
26 + case sdk.SDKPathRegister:
27 r.handleRegister(w, req, serv)
28 - case "unregister":
28 + case sdk.SDKPathUnregister:
29 r.handleUnregister(w, req, serv)
30 - case "renew":
30 + case sdk.SDKPathRenew:
31 r.handleRenew(w, req, serv)
32 - case "domain":
32 + case sdk.SDKPathDomain:
33 r.handleDomain(w, req, serv)
34 - case "connect":
34 + case sdk.SDKPathConnect:
35 r.handleConnect(w, req, serv)
36 default:
37 http.NotFound(w, req)
@@ -97,7 +97,7 @@ func (r *SDKRegistry) handleRegister(w http.ResponseWriter, req *http.Request, s
97 Name: registerReq.Name,
98 Metadata: registerReq.Metadata,
99 Expires: time.Now().Add(30 * time.Second),
100 - TLSMode: string(registerReq.TLSMode),
100 + TLS: registerReq.TLS,
101 ReverseToken: registerReq.ReverseToken,
102 }
103
@@ -114,7 +114,7 @@ func (r *SDKRegistry) handleRegister(w http.ResponseWriter, req *http.Request, s
114 serv.GetReverseHub().ClearDropped(registerReq.LeaseID)
115
116 // Only register SNI route for TLS leases.
117 - if registerReq.TLSMode != sdk.TLSModeNoTLS {
117 + if registerReq.TLS {
118 sniName := strings.ToLower(strings.TrimSpace(registerReq.Name)) + "." + serv.BaseHost
119 if err := serv.GetSNIRouter().RegisterRoute(sniName, registerReq.LeaseID, registerReq.Name); err != nil {
120 // Keep lease and route state consistent on partial failure.
@@ -130,7 +130,7 @@ func (r *SDKRegistry) handleRegister(w http.ResponseWriter, req *http.Request, s
130 log.Info().
131 Str("lease_id", registerReq.LeaseID).
132 Str("name", registerReq.Name).
133 - Str("tls_mode", string(registerReq.TLSMode)).
133 + Bool("tls", registerReq.TLS).
134 Msg("[Registry] Lease registered")
135
136 // Build public URL
@@ -230,7 +230,7 @@ func (r *SDKRegistry) handleRenew(w http.ResponseWriter, req *http.Request, serv
230
231 // Re-register route if needed (e.g., router restarted while lease remained active).
232 // Only TLS leases need SNI routes.
233 - if sdk.TLSMode(entry.Lease.TLSMode) != sdk.TLSModeNoTLS {
233 + if entry.Lease.TLS {
234 sniName := strings.ToLower(strings.TrimSpace(entry.Lease.Name)) + "." + serv.BaseHost
235 if err := serv.GetSNIRouter().RegisterRoute(sniName, entry.Lease.ID, entry.Lease.Name); err != nil {
236 log.Warn().
cmd/relay-server/serve.go
+4 -5
@@ -17,7 +17,6 @@ import (
17
18 "gosuda.org/portal/portal"
19 "gosuda.org/portal/portal/keyless"
20 - "gosuda.org/portal/sdk"
20 )
21
22 //go:embed dist/*
@@ -169,11 +168,11 @@ func shouldProxyHTTP(host string, serv *portal.RelayServer) (string, *portal.Lea
168 return leaseName, nil, true
169 }
170
172 - // If TLS mode is no-tls, we can proxy via HTTP.
173 - shouldProxy := sdk.TLSMode(entry.Lease.TLSMode) == sdk.TLSModeNoTLS
171 + // If TLS is disabled, we can proxy via HTTP.
172 + shouldProxy := !entry.Lease.TLS
173 log.Debug().
174 Str("lease_name", leaseName).
176 - Str("tls_mode", entry.Lease.TLSMode).
175 + Bool("tls", entry.Lease.TLS).
176 Msg("[proxy] shouldProxyHTTP")
177 return leaseName, entry, shouldProxy
178 }
@@ -189,7 +188,7 @@ func proxyToHTTP(w http.ResponseWriter, r *http.Request, serv *portal.RelayServe
188 return
189 }
190
192 - if sdk.TLSMode(entry.Lease.TLSMode) != sdk.TLSModeNoTLS {
191 + if entry.Lease.TLS {
192 http.Error(w, "TLS enabled requires HTTPS access", http.StatusBadRequest)
193 return
194 }
cmd/relay-server/tunnel.go
+3 -9
@@ -54,10 +54,8 @@ set -- "$BIN_PATH" --relay "$RELAYS" --host "${APP_HOST:-localhost:3000}"
54 if [ "${APP_HIDE:-}" = "1" ] || [ "${APP_HIDE:-}" = "true" ]; then
55 set -- "$@" --hide
56 fi
57 -[ -n "${TLS_MODE:-}" ] && set -- "$@" --tls-mode "$TLS_MODE"
58 -if [ "${TLS_MODE:-}" = "self" ]; then
59 - [ -n "${TLS_CERT_FILE:-}" ] && set -- "$@" --tls-cert-file "$TLS_CERT_FILE"
60 - [ -n "${TLS_KEY_FILE:-}" ] && set -- "$@" --tls-key-file "$TLS_KEY_FILE"
57 +if [ "${TLS:-}" = "1" ] || [ "${TLS:-}" = "true" ]; then
58 + set -- "$@" --tls
59 fi
60
61 echo "Starting portal-tunnel..." >&2
@@ -103,11 +101,7 @@ if ($env:APP_TAGS) { $ArgsList += "--tags", $env:APP_TAGS }
101 if ($env:APP_THUMBNAIL) { $ArgsList += "--thumbnail", $env:APP_THUMBNAIL }
102 if ($env:APP_OWNER) { $ArgsList += "--owner", $env:APP_OWNER }
103 if ($env:APP_HIDE -eq "1" -or $env:APP_HIDE -eq "true") { $ArgsList += "--hide" }
106 -if ($env:TLS_MODE) { $ArgsList += "--tls-mode", $env:TLS_MODE }
107 -if ($env:TLS_MODE -eq "self") {
108 - if ($env:TLS_CERT_FILE) { $ArgsList += "--tls-cert-file", $env:TLS_CERT_FILE }
109 - if ($env:TLS_KEY_FILE) { $ArgsList += "--tls-key-file", $env:TLS_KEY_FILE }
110 -}
104 +if ($env:TLS -eq "1" -or $env:TLS -eq "true") { $ArgsList += "--tls" }
105
106 Write-Host "Starting portal-tunnel..."
107 try {
cmd/relay-server/utils.go
+1 -2
@@ -16,7 +16,6 @@ import (
16 "gosuda.org/portal/cmd/relay-server/manager"
17 "gosuda.org/portal/portal"
18 "gosuda.org/portal/portal/keyless"
19 - "gosuda.org/portal/sdk"
19 )
20
21 func isSecureRequest(r *http.Request) bool {
@@ -386,7 +385,7 @@ func (r *leaseRow) fromLeaseEntry(entry *portal.LeaseEntry, admin *Admin, portal
385 }
386
387 kind := "http"
389 - if sdk.TLSMode(lease.TLSMode) != sdk.TLSModeNoTLS {
388 + if lease.TLS {
389 kind = "https"
390 }
391
portal/keyless/client.go renamed
+48
@@ -11,9 +11,57 @@ import (
11 "strings"
12 "time"
13
14 + keylesstls "github.com/gosuda/keyless_tls/keyless"
15 "github.com/rs/zerolog/log"
16 )
17
18 +// BuildClientTLSConfig builds a keyless TLS server config for tunnel-side TLS termination.
19 +// It returns the TLS config and a close callback for signer resources.
20 +func BuildClientTLSConfig(relayAddr, keylessServerName, domain string) (*tls.Config, func(), error) {
21 + if keylessServerName == "" {
22 + return nil, nil, fmt.Errorf("keyless server name is required")
23 + }
24 + if domain == "" {
25 + return nil, nil, fmt.Errorf("tls domain is required")
26 + }
27 + certPEM, rootCAPEM, err := ResolveMaterials(
28 + context.Background(),
29 + relayAddr,
30 + keylessServerName,
31 + nil,
32 + nil,
33 + )
34 + if err != nil {
35 + return nil, nil, fmt.Errorf("prepare keyless materials: %w", err)
36 + }
37 +
38 + if err := VerifyCertificateHostname(certPEM, domain); err != nil {
39 + return nil, nil, fmt.Errorf("keyless certificate does not cover %s: %w", domain, err)
40 + }
41 +
42 + remoteSigner, err := keylesstls.NewRemoteSigner(keylesstls.RemoteSignerConfig{
43 + Endpoint: relayAddr,
44 + ServerName: keylessServerName,
45 + KeyID: RelayKeyID,
46 + RootCAPEM: rootCAPEM,
47 + }, certPEM)
48 + if err != nil {
49 + return nil, nil, fmt.Errorf("create keyless remote signer: %w", err)
50 + }
51 +
52 + tlsConfig, err := keylesstls.NewServerTLSConfig(keylesstls.ServerTLSConfig{
53 + CertPEM: certPEM,
54 + Signer: remoteSigner,
55 + })
56 + if err != nil {
57 + _ = remoteSigner.Close()
58 + return nil, nil, fmt.Errorf("create keyless TLS config: %w", err)
59 + }
60 + tlsConfig.NextProtos = []string{"http/1.1"}
61 +
62 + return tlsConfig, func() { _ = remoteSigner.Close() }, nil
63 +}
64 +
65 // ResolveMaterials prepares certificate chain and root CAs for keyless TLS mode.
66 func ResolveMaterials(
67 ctx context.Context,
portal/keyless/signer.go
+3 -3
@@ -13,7 +13,7 @@ import (
13 )
14
15 const (
16 - defaultKeyID = "relay-cert"
16 + RelayKeyID = "relay-cert"
17 defaultAllowedSkew = 30 * time.Second
18 )
19
@@ -57,7 +57,7 @@ func NewSigner(cfg Config) (*Signer, error) {
57 }
58
59 store := ksigner.NewStaticKeyStore()
60 - if err := store.Put(defaultKeyID, signingKey); err != nil {
60 + if err := store.Put(RelayKeyID, signingKey); err != nil {
61 return nil, fmt.Errorf("register keyless signing key: %w", err)
62 }
63
@@ -68,7 +68,7 @@ func NewSigner(cfg Config) (*Signer, error) {
68
69 return &Signer{
70 service: svc,
71 - keyID: defaultKeyID,
71 + keyID: RelayKeyID,
72 }, nil
73 }
74
portal/lease.go
+2 -2
@@ -23,8 +23,8 @@ type Lease struct {
23 Name string `json:"name"`
24 Metadata Metadata `json:"metadata"`
25 Expires time.Time `json:"expires"`
26 - TLSMode string `json:"tls_mode"` // no-tls, self, keyless
27 - ReverseToken string `json:"-"` // shared secret for reverse connect authentication
26 + TLS bool `json:"tls"`
27 + ReverseToken string `json:"-"` // shared secret for reverse connect authentication
28 }
29
30 // Metadata holds service metadata
portal/reverse_hub.go
+4 -1
@@ -44,6 +44,9 @@ const (
44 // ReverseIdleKeepaliveInterval sends an idle keepalive byte to reduce
45 // reverse websocket disconnections from intermediate idle timeouts.
46 ReverseIdleKeepaliveInterval = 25 * time.Second
47 +
48 + // ReverseConnectTokenHeader is the websocket handshake header carrying reverse auth token.
49 + ReverseConnectTokenHeader = "X-Portal-Reverse-Token"
50 )
51
52 // ReverseConn wraps a net.Conn with lifecycle management for the connection pool.
@@ -328,7 +331,7 @@ func parseReverseConnectCredentials(req *http.Request) (leaseID, token string) {
331 return "", ""
332 }
333 leaseID = strings.TrimSpace(req.URL.Query().Get("lease_id"))
331 - token = strings.TrimSpace(req.URL.Query().Get("token"))
334 + token = strings.TrimSpace(req.Header.Get(ReverseConnectTokenHeader))
335 return leaseID, token
336 }
337
portal/reverse_hub_test.go
+26
@@ -3,6 +3,7 @@ package portal
3 import (
4 "io"
5 "net"
6 + "net/http/httptest"
7 "testing"
8 "time"
9 )
@@ -119,3 +120,28 @@ func TestAcquireForHTTPSendsStartMarker(t *testing.T) {
120 t.Fatal("timed out waiting for start marker")
121 }
122 }
123 +
124 +func TestParseReverseConnectCredentials_HeaderTokenPreferred(t *testing.T) {
125 + req := httptest.NewRequest("GET", "/sdk/connect?lease_id=lease-1&token=query-token", nil)
126 + req.Header.Set(ReverseConnectTokenHeader, "header-token")
127 +
128 + leaseID, token := parseReverseConnectCredentials(req)
129 + if leaseID != "lease-1" {
130 + t.Fatalf("unexpected lease_id: %q", leaseID)
131 + }
132 + if token != "header-token" {
133 + t.Fatalf("expected header token, got %q", token)
134 + }
135 +}
136 +
137 +func TestParseReverseConnectCredentials_QueryTokenIgnored(t *testing.T) {
138 + req := httptest.NewRequest("GET", "/sdk/connect?lease_id=lease-2&token=query-token", nil)
139 +
140 + leaseID, token := parseReverseConnectCredentials(req)
141 + if leaseID != "lease-2" {
142 + t.Fatalf("unexpected lease_id: %q", leaseID)
143 + }
144 + if token != "" {
145 + t.Fatalf("expected empty token when header is missing, got %q", token)
146 + }
147 +}
sdk/client.go
+44 -124
@@ -2,7 +2,6 @@
2 package sdk
3
4 import (
5 - "context"
5 "crypto/rand"
6 "crypto/tls"
7 "encoding/hex"
@@ -14,7 +13,6 @@ import (
13 "sync"
14 "time"
15
17 - keylesstls "github.com/gosuda/keyless_tls/keyless"
16 "github.com/rs/zerolog/log"
17
18 "gosuda.org/portal/portal"
@@ -32,7 +30,7 @@ func NewClient(opt ...ClientOption) (*Client, error) {
30 config := &ClientConfig{
31 BootstrapServers: []string{},
32 ReverseDialTimeout: 5 * time.Second,
35 - TLSMode: TLSModeNoTLS,
33 + TLS: false,
34 }
35
36 for _, o := range opt {
@@ -118,18 +116,17 @@ func (c *Client) Listen(name string, options ...MetadataOption) (net.Listener, e
116 listener = newMultiRelayListener(lease.ID, listeners)
117 }
118
121 - if c.config.TLSMode != TLSModeNoTLS {
119 + if c.config.TLS {
120 log.Info().
121 Str("lease_id", lease.ID).
122 Str("name", name).
123 Bool("tls", true).
126 - Str("tls_mode", string(c.config.TLSMode)).
124 Msg("[SDK] Lease registered with TLS")
125 } else {
126 log.Info().
127 Str("lease_id", lease.ID).
128 Str("name", name).
132 - Str("tls_mode", string(TLSModeNoTLS)).
129 + Bool("tls", false).
130 Msg("[SDK] Lease registered")
131 }
132
@@ -142,16 +139,21 @@ func (c *Client) newLease(name string, options ...MetadataOption) (*portal.Lease
139 option(&metadata)
140 }
141
145 - reverseToken, err := generateToken(16)
146 - if err != nil {
142 + idBytes := make([]byte, 16)
143 + if _, err := rand.Read(idBytes); err != nil {
144 + return nil, fmt.Errorf("generate lease ID: %w", err)
145 + }
146 +
147 + tokenBytes := make([]byte, 16)
148 + if _, err := rand.Read(tokenBytes); err != nil {
149 return nil, fmt.Errorf("generate reverse token: %w", err)
150 }
151
152 lease := &portal.Lease{
151 - ID: generateID(),
153 + ID: hex.EncodeToString(idBytes),
154 Name: name,
153 - TLSMode: string(c.config.TLSMode),
154 - ReverseToken: reverseToken,
155 + TLS: c.config.TLS,
156 + ReverseToken: hex.EncodeToString(tokenBytes),
157 Metadata: portal.Metadata{
158 Description: metadata.Description,
159 Tags: metadata.Tags,
@@ -164,6 +166,37 @@ func (c *Client) newLease(name string, options ...MetadataOption) (*portal.Lease
166 return lease, nil
167 }
168
169 +func (c *Client) buildTLSConfig(relayAddr, leaseName string) (*tls.Config, []func(), error) {
170 + if !c.config.TLS {
171 + return nil, nil, nil
172 + }
173 +
174 + parsed, err := url.Parse(relayAddr)
175 + if err != nil {
176 + return nil, nil, fmt.Errorf("invalid relay address: %s, %w", relayAddr, err)
177 + }
178 + keylessServerName := parsed.Hostname()
179 + if keylessServerName == "" {
180 + return nil, nil, fmt.Errorf("relay hostname is required: %s", relayAddr)
181 + }
182 + baseDomain := ExtractBaseDomain(relayAddr)
183 + if baseDomain == "" {
184 + return nil, nil, fmt.Errorf("keyless base domain is required for relay %s", relayAddr)
185 + }
186 + domain := leaseName + "." + baseDomain
187 +
188 + tlsConfig, closeFn, err := keyless.BuildClientTLSConfig(relayAddr, keylessServerName, domain)
189 + if err != nil {
190 + return nil, nil, err
191 + }
192 + return tlsConfig, []func(){closeFn}, nil
193 +}
194 +
195 +// Close closes the client.
196 +func (c *Client) Close() error {
197 + return nil
198 +}
199 +
200 func ExtractBaseDomain(rawURL string) string {
201 trimmed := strings.TrimSpace(rawURL)
202 if trimmed == "" {
@@ -210,116 +243,3 @@ func normalizeRelayAPIURLs(bootstrapServers []string) ([]string, error) {
243 }
244 return out, nil
245 }
213 -
214 -func (c *Client) buildTLSConfig(relayAddr, leaseName string) (*tls.Config, []func(), error) {
215 - tlsMode := c.config.TLSMode
216 - if tlsMode == TLSModeNoTLS {
217 - return nil, nil, nil
218 - }
219 -
220 - switch tlsMode {
221 - case TLSModeSelf:
222 - if c.config.TLSSelfCertFile == "" || c.config.TLSSelfKeyFile == "" {
223 - return nil, nil, fmt.Errorf("self TLS mode requires certificate/key files (WithTLSSelfCertificateFiles)")
224 - }
225 - cert, err := tls.LoadX509KeyPair(c.config.TLSSelfCertFile, c.config.TLSSelfKeyFile)
226 - if err != nil {
227 - return nil, nil, fmt.Errorf("load self TLS certificate files: %w", err)
228 - }
229 -
230 - tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12}
231 - tlsConfig.NextProtos = []string{"http/1.1"}
232 - tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
233 - return &cert, nil
234 - }
235 - return tlsConfig, nil, nil
236 -
237 - case TLSModeKeyless:
238 - keylessEndpoint := c.config.TLSKeylessEndpoint
239 - if keylessEndpoint == "" {
240 - keylessEndpoint = relayAddr
241 - }
242 -
243 - keylessKeyID := "relay-cert"
244 -
245 - keylessServerName := ""
246 - if parsed, err := url.Parse(keylessEndpoint); err == nil {
247 - keylessServerName = parsed.Hostname()
248 - }
249 -
250 - certPEM, rootCAPEM, err := keyless.ResolveMaterials(
251 - context.Background(),
252 - keylessEndpoint,
253 - keylessServerName,
254 - nil,
255 - nil,
256 - )
257 - if err != nil {
258 - return nil, nil, fmt.Errorf("prepare keyless materials: %w", err)
259 - }
260 -
261 - baseDomain := c.config.TLSKeylessBaseDomain
262 - if baseDomain == "" {
263 - baseDomain = ExtractBaseDomain(relayAddr)
264 - }
265 - if baseDomain == "" {
266 - baseDomain = ExtractBaseDomain(keylessEndpoint)
267 - }
268 - if baseDomain == "" {
269 - return nil, nil, fmt.Errorf("keyless base domain is required for relay %s", relayAddr)
270 - }
271 - domain := leaseName + "." + baseDomain
272 - if err := keyless.VerifyCertificateHostname(certPEM, domain); err != nil {
273 - return nil, nil, fmt.Errorf("keyless certificate does not cover %s: %w", domain, err)
274 - }
275 -
276 - remoteSigner, err := keylesstls.NewRemoteSigner(keylesstls.RemoteSignerConfig{
277 - Endpoint: keylessEndpoint,
278 - ServerName: keylessServerName,
279 - KeyID: keylessKeyID,
280 - RootCAPEM: rootCAPEM,
281 - }, certPEM)
282 - if err != nil {
283 - return nil, nil, fmt.Errorf("create keyless remote signer: %w", err)
284 - }
285 -
286 - tlsConfig, err := keylesstls.NewServerTLSConfig(keylesstls.ServerTLSConfig{
287 - CertPEM: certPEM,
288 - Signer: remoteSigner,
289 - })
290 - if err != nil {
291 - _ = remoteSigner.Close()
292 - return nil, nil, fmt.Errorf("create keyless TLS config: %w", err)
293 - }
294 - tlsConfig.NextProtos = []string{"http/1.1"}
295 -
296 - return tlsConfig, []func(){
297 - func() { _ = remoteSigner.Close() },
298 - }, nil
299 - default:
300 - return nil, nil, fmt.Errorf("unsupported TLS mode: %s", tlsMode)
301 - }
302 -}
303 -
304 -// Close closes the client.
305 -func (c *Client) Close() error {
306 - return nil
307 -}
308 -
309 -// generateID generates a unique ID for the lease.
310 -func generateID() string {
311 - b := make([]byte, 16)
312 - rand.Read(b)
313 - return hex.EncodeToString(b)
314 -}
315 -
316 -func generateToken(size int) (string, error) {
317 - if size <= 0 {
318 - size = 16
319 - }
320 - b := make([]byte, size)
321 - if _, err := rand.Read(b); err != nil {
322 - return "", err
323 - }
324 - return hex.EncodeToString(b), nil
325 -}
sdk/listener.go
+7 -11
@@ -208,7 +208,7 @@ func (l *Listener) keepaliveLoop() {
208 case <-ticker.C:
209 if err := l.sendKeepalive(); err != nil {
210 if isLeaseNotFoundError(err) {
211 - if rerr := l.reRegisterLease(); rerr != nil {
211 + if rerr := l.registerWithRelay(); rerr != nil {
212 log.Warn().
213 Err(rerr).
214 Str("lease_id", l.lease.ID).
@@ -287,6 +287,7 @@ func (l *Listener) openReverseConnection() (net.Conn, error) {
287 if err != nil {
288 return nil, fmt.Errorf("new reverse websocket config: %w", err)
289 }
290 + cfg.Header.Set(portal.ReverseConnectTokenHeader, l.lease.ReverseToken)
291 cfg.Dialer = &net.Dialer{
292 Timeout: l.reverseDialTimeout,
293 }
@@ -340,18 +341,18 @@ func (l *Listener) registerWithRelay() error {
341 LeaseID: l.lease.ID,
342 Name: l.lease.Name,
343 Metadata: l.lease.Metadata,
343 - TLSMode: TLSMode(l.lease.TLSMode),
344 + TLS: l.lease.TLS,
345 ReverseToken: l.lease.ReverseToken,
346 }
347
347 - return l.postJSON("/sdk/register", reqBody)
348 + return l.postJSON(SDKPathRegister, reqBody)
349 }
350
351 func (l *Listener) unregisterFromRelay() error {
352 reqBody := UnregisterRequest{
353 LeaseID: l.lease.ID,
354 }
354 - return l.postJSON("/sdk/unregister", reqBody)
355 + return l.postJSON(SDKPathUnregister, reqBody)
356 }
357
358 func (l *Listener) sendKeepalive() error {
@@ -359,11 +360,7 @@ func (l *Listener) sendKeepalive() error {
360 LeaseID: l.lease.ID,
361 ReverseToken: l.lease.ReverseToken,
362 }
362 - return l.postJSON("/sdk/renew", reqBody)
363 -}
364 -
365 -func (l *Listener) reRegisterLease() error {
366 - return l.registerWithRelay()
363 + return l.postJSON(SDKPathRenew, reqBody)
364 }
365
366 func (l *Listener) postJSON(path string, body any) error {
@@ -477,10 +474,9 @@ func relayConnectURL(relayAddr, leaseID, token string) (string, error) {
474 default:
475 return "", fmt.Errorf("unsupported relay URL scheme: %q", u.Scheme)
476 }
480 - u.Path = "/sdk/connect"
477 + u.Path = SDKPathConnect
478 q := u.Query()
479 q.Set("lease_id", leaseID)
483 - q.Set("token", token)
480 u.RawQuery = q.Encode()
481 u.Fragment = ""
482 return u.String(), nil
sdk/listener_test.go
+2 -2
@@ -65,8 +65,8 @@ func TestRelayConnectURL(t *testing.T) {
65 if !strings.Contains(got, "lease_id=lease-1") {
66 t.Fatalf("missing lease_id in URL: %q", got)
67 }
68 - if !strings.Contains(got, "token=token-1") {
69 - t.Fatalf("missing token in URL: %q", got)
68 + if strings.Contains(got, "token=token-1") {
69 + t.Fatalf("token must not be present in URL query: %q", got)
70 }
71
72 if _, err := relayConnectURL("http://localhost:4017", "", "token-1"); err == nil {
sdk/types.go
+13 -42
@@ -18,29 +18,11 @@ var (
18 ErrInvalidMetadata = errors.New("invalid metadata")
19 )
20
21 -type TLSMode string
22 -
23 -const (
24 - TLSModeNoTLS TLSMode = "no-tls"
25 - TLSModeSelf TLSMode = "self"
26 - TLSModeKeyless TLSMode = "keyless"
27 -)
28 -
21 type ClientConfig struct {
22 BootstrapServers []string
23 ReverseDialTimeout time.Duration // Reverse websocket dial timeout (default: 5 seconds)
24
33 - TLSMode TLSMode
34 -
35 - // Self TLS mode certificate/key file paths.
36 - TLSSelfCertFile string
37 - TLSSelfKeyFile string
38 -
39 - // Optional keyless overrides.
40 - // If endpoint is empty, SDK uses the relay URL.
41 - TLSKeylessEndpoint string
42 - // If base domain is empty, SDK derives it from relay or signer endpoint.
43 - TLSKeylessBaseDomain string
25 + TLS bool
26 }
27
28 type ClientOption func(*ClientConfig)
@@ -57,29 +39,10 @@ func WithReverseDialTimeout(timeout time.Duration) ClientOption {
39 }
40 }
41
60 -// WithTLSSelfCertificateFiles enables self TLS mode using certificate/key file paths.
61 -func WithTLSSelfCertificateFiles(certFile, keyFile string) ClientOption {
42 +// WithTLS enables keyless TLS mode using relay-derived defaults.
43 +func WithTLS() ClientOption {
44 return func(c *ClientConfig) {
63 - c.TLSMode = TLSModeSelf
64 - c.TLSSelfCertFile = certFile
65 - c.TLSSelfKeyFile = keyFile
66 - }
67 -}
68 -
69 -// WithTLSKeyless enables keyless TLS mode with optional signer overrides.
70 -func WithTLSKeyless(endpoint, baseDomain string) ClientOption {
71 - return func(c *ClientConfig) {
72 - c.TLSMode = TLSModeKeyless
73 - c.TLSKeylessEndpoint = endpoint
74 - c.TLSKeylessBaseDomain = baseDomain
75 - }
76 -}
77 -
78 -// WithTLSKeylessDefaults enables keyless TLS mode with SDK-managed defaults.
79 -// Certificate chain and signer trust are auto-discovered from signer endpoint when not explicitly provided.
80 -func WithTLSKeylessDefaults() ClientOption {
81 - return func(c *ClientConfig) {
82 - c.TLSMode = TLSModeKeyless
45 + c.TLS = true
46 }
47 }
48
@@ -122,7 +85,7 @@ type RegisterRequest struct {
85 LeaseID string `json:"lease_id"`
86 Name string `json:"name"`
87 Metadata portal.Metadata `json:"metadata"`
125 - TLSMode TLSMode `json:"tls_mode"` // no-tls, self, keyless
88 + TLS bool `json:"tls"`
89 ReverseToken string `json:"reverse_token"`
90 }
91
@@ -146,3 +109,11 @@ type APIResponse struct {
109 Success bool `json:"success"`
110 Message string `json:"message,omitempty"`
111 }
112 +
113 +const (
114 + SDKPathRegister = "/sdk/register"
115 + SDKPathUnregister = "/sdk/unregister"
116 + SDKPathRenew = "/sdk/renew"
117 + SDKPathDomain = "/sdk/domain"
118 + SDKPathConnect = "/sdk/connect"
119 +)