22date : 2026-04-06
33author : Onur Solmaz <onur@textcortex.com>
44title : spz Port-Forward Architecture
5- tags : [spritz, spz, cli, ssh, port-forwarding, architecture]
5+ tags : [spritz, spz, cli, ssh, websocket, https, port-forwarding, architecture]
66---
77
88## Overview
@@ -20,15 +20,22 @@ spz port-forward <instance> --local <port> --remote <port>
2020This should be the canonical Spritz CLI shape for forwarding a local loopback
2121port to a private port inside one Spritz instance.
2222
23- It should be implemented as an instance-scoped control-plane feature, not as a
24- Kubernetes workflow, not as a browser preview product, and not as a
25- deployment-specific convenience alias.
23+ It should be implemented as an instance-scoped control-plane feature. The
24+ preferred public transport should be the authenticated Spritz control plane
25+ over HTTPS/WebSocket on port ` 443 ` , not raw public SSH on port ` 22 ` .
26+
27+ It should not be framed as a Kubernetes workflow, not as a browser preview
28+ product, and not as a deployment-specific convenience alias.
2629
2730## TL;DR
2831
2932- Spritz core should add ` spz port-forward ` .
30- - The command should be explicit and transport-agnostic in meaning, while the
31- first implementation should reuse the existing SSH credential minting path.
33+ - The command should be explicit and transport-agnostic in meaning.
34+ - The preferred public transport should be authenticated HTTPS/WebSocket over
35+ the existing Spritz control plane on port ` 443 ` .
36+ - The current SSH-backed implementation may remain as a fallback, but SSH
37+ should be treated as deprecated for default public use unless somebody
38+ explicitly asks for it.
3239- The command should default to:
3340 - local bind host ` 127.0.0.1 `
3441 - remote target host ` 127.0.0.1 `
@@ -97,8 +104,12 @@ This should be the core primitive. It should mean:
97104- forward to one private remote port inside one named instance
98105- keep the tunnel alive until interrupted
99106
100- The first implementation should be built on the existing SSH certificate mint
101- flow already used by ` spz ssh ` .
107+ The public-facing design should prefer an authenticated control-plane tunnel
108+ over HTTPS/WebSocket on ` 443 ` .
109+
110+ The existing SSH certificate mint flow may still be used as an implementation
111+ fallback where raw TCP is available or explicitly desired, but Spritz should
112+ not require public raw SSH exposure as the default internet-facing transport.
102113
103114The product contract, however, should be described as "instance port
104115forwarding", not as "raw SSH with custom flags". That distinction matters:
@@ -107,6 +118,26 @@ forwarding", not as "raw SSH with custom flags". That distinction matters:
107118- the control plane remains the owner of authorization and target resolution
108119- future transports may change without renaming the user-facing intent
109120
121+ ## Preferred Public Transport
122+
123+ The ideal Spritz system should not require a public inbound instance port at
124+ all.
125+
126+ For public usage, ` spz port-forward ` should terminate on the existing Spritz
127+ control plane host over HTTPS/WebSocket on ` 443 ` , and the control plane should
128+ perform the pod-scoped forwarding inside the cluster.
129+
130+ This is the preferred architecture because it:
131+
132+ - works in environments where raw public TCP may be restricted
133+ - keeps auth, policy, rate limiting, and audit under the control plane
134+ - avoids depending on cloud-specific behavior for arbitrary inbound TCP
135+ - gives one clean public access surface instead of separate web and SSH entry
136+ points
137+
138+ SSH may still exist as a transport, but it should not be the primary public
139+ story.
140+
110141## Why ` spz port-forward ` Instead Of ` spz preview `
111142
112143` preview ` is the wrong upstream primitive because it encodes application
@@ -266,21 +297,28 @@ Press Ctrl+C to stop.
266297
267298## Relationship To ` spz ssh `
268299
269- ` spz ssh ` should remain the raw shell-access command.
300+ ` spz ssh ` should remain the raw shell-access command when explicitly needed .
270301
271302` spz port-forward ` should be a sibling command with a narrower purpose:
272303
273304- ` spz ssh ` : interactive shell access
274305- ` spz port-forward ` : local access to one private instance port
275306
276- The implementation may share most of the credential plumbing.
307+ The implementation may still share credential plumbing, but the public default
308+ for ` spz port-forward ` should not be "SSH unless proven otherwise".
277309
278- That is good:
310+ That split is still good:
279311
280312- less duplicated auth logic
281- - one consistent trust and host-verification path
282313- one clear control-plane contract for instance access
283314
315+ For now:
316+
317+ - ` spz ssh ` remains available
318+ - SSH-backed ` spz port-forward ` may remain available
319+ - SSH should be considered deprecated as the default public transport unless a
320+ deployment or operator explicitly asks for it
321+
284322## Downstream Wrappers
285323
286324Spritz core should stop at the generic primitive.
@@ -297,6 +335,8 @@ contract.
297335The holy grail shape is:
298336
299337- Spritz core provides ` spz port-forward `
338+ - Spritz routes public interactive access through one authenticated control
339+ plane on ` 443 `
300340- downstream deployments compose deployment-specific UX on top of it
301341
302342That keeps Spritz portable while still enabling polished local workflows where
@@ -307,7 +347,7 @@ needed.
307347### Phase 1: Core CLI Primitive
308348
309349- add ` spz port-forward `
310- - reuse the existing SSH credential minting path
350+ - keep the user-facing contract transport-agnostic
311351- support one local forward per command invocation
312352- keep both local and remote hosts pinned to loopback
313353
@@ -318,19 +358,43 @@ Acceptance criteria:
318358- the command requires no Kubernetes credentials
319359- the command targets one named instance, not a pod
320360
321- ### Phase 2: CLI Help And Tests
361+ ### Phase 2: Public Control-Plane Transport
362+
363+ - implement forwarding over the authenticated Spritz control plane on
364+ HTTPS/WebSocket
365+ - make that path the preferred public transport
366+ - avoid requiring any public raw TCP listener on the instance gateway
367+
368+ Acceptance criteria:
369+
370+ - the standard public path works over ` 443 `
371+ - no public per-instance or per-feature raw TCP exposure is required
372+ - authorization remains owned by the Spritz control plane
373+
374+ ### Phase 3: SSH Fallback
375+
376+ - keep the SSH-backed transport available for private networks, operators, or
377+ deployments that explicitly want it
378+ - document that SSH is a fallback transport, not the preferred public one
379+
380+ Acceptance criteria:
381+
382+ - SSH remains available when explicitly requested
383+ - SSH is no longer the default public transport assumption in docs or UX
384+
385+ ### Phase 4: CLI Help And Tests
322386
323387- document the new command in CLI help
324388- add help tests for the new usage line
325- - add command tests for printed SSH execution shape or equivalent command
389+ - add command tests for printed transport execution shape or equivalent command
326390 plumbing
327391
328392Acceptance criteria:
329393
330394- the new command is discoverable through ` spz --help `
331395- printed guidance stays generic and deployment-agnostic
332396
333- ### Phase 3 : Downstream Composition
397+ ### Phase 5 : Downstream Composition
334398
335399- allow downstreams to add wrappers without changing the core primitive
336400- document that application auth remains outside Spritz forwarding
@@ -346,10 +410,13 @@ This architecture is successful when all of the following are true:
346410
347411- the standard path for instance port access is ` spz port-forward ` , not raw
348412 ` ssh -L `
413+ - the preferred public path runs through the authenticated Spritz control
414+ plane on ` 443 `
349415- the caller does not need Kubernetes credentials
350416- the command works by instance identity rather than pod identity
351417- the default bind scope is local loopback only
352418- the application behind the forwarded port can keep its own auth model
419+ - SSH remains optional rather than mandatory for public use
353420- downstream wrappers can exist without forcing Spritz core to become
354421 app-specific
355422
0 commit comments