Skip to content

Secure Access Tunneling

The internal/tunnel package enables platform-mediated SSH access to mesh nodes through WireGuard tunnels without exposing services to the public internet. The control plane orchestrates session lifecycle via SSE events; the node agent opens a local TCP listener bound to the mesh IP, forwards connections to the target host, and reports status back to the control plane.

Data Flow

Control Plane

      │ SSE: ssh_session_setup

┌──────────────┐     ┌─────────────────┐
│ EventDispatcher│───▶│ HandleSSHSession │
│  (internal/api)│    │    Setup         │
└──────────────┘     └───────┬──────────┘


                     ┌───────────────┐
                     │ SessionManager │
                     │  CreateSession │
                     └───────┬───────┘

                     ┌───────┴───────┐
                     │    Session     │
                     │               │
                     │ ┌───────────┐ │
                     │ │  Listener  │ │  ← bound to meshIP:0
                     │ │ (TCP)     │ │
                     │ └─────┬─────┘ │
                     │       │       │
                     │ ┌─────┴─────┐ │
                     │ │ Forwarder  │ │  ← bidirectional io.Copy
                     │ └─────┬─────┘ │
                     └───────┼───────┘


                      Target Host
                      (e.g. sshd)

Event Sequence

  1. Control plane sends ssh_session_setup SSE event with session parameters
  2. HandleSSHSessionSetup parses the payload and calls SessionManager.CreateSession
  3. SessionManager creates a Session, which opens a TCP listener on meshIP:0
  4. TunnelReporter.ReportReady notifies the control plane with the listen address
  5. Client connects through the mesh to the listener; Session forwards to the target
  6. Session ends by expiry (time.AfterFunc), revocation (session_revoked SSE), or shutdown
  7. TunnelReporter.ReportClosed notifies the control plane with reason and duration

Config

Config holds secure access tunneling parameters passed to the SessionManager constructor.

FieldTypeDefaultDescription
EnabledbooltrueWhether tunneling is active
MaxSessionsint10Maximum concurrent tunnel sessions
DefaultTimeouttime.Duration30mDefault/maximum session timeout
SSHListenAddrstringSSH mesh server listen address (empty = no SSH server)
HostKeyDirstringDirectory for SSH host key (empty = transient key)
go
cfg := tunnel.Config{
    MaxSessions: 5,
}
cfg.ApplyDefaults() // Enabled=true, DefaultTimeout=30m, MaxSessions stays 5
if err := cfg.Validate(); err != nil {
    log.Fatal(err)
}

Default Heuristic

ApplyDefaults uses zero-value detection: on a fully zero-valued Config, MaxSessions == 0 triggers all defaults including Enabled = true. If MaxSessions is already set (indicating explicit construction), Enabled is left as-is. This allows Config{Enabled: false} to disable tunneling after ApplyDefaults.

Validation Rules

FieldRuleError Message
MaxSessionsMust be > 0 when enabledtunnel: config: MaxSessions must be positive when enabled
DefaultTimeoutMust be >= 1m when enabledtunnel: config: DefaultTimeout must be at least 1m when enabled

Validation is skipped entirely when Enabled is false.

Session

Represents an active tunnel session with a local TCP listener that forwards connections to a target host through the mesh.

Fields

FieldTypeDescription
SessionIDstringUnique session identifier
TargetHoststringTarget host to forward connections to
TargetPortintTarget port
MeshIPstringMesh IP to bind the listener to

Constructor

go
func NewSession(sessionID, targetHost string, targetPort int, meshIP string, expiresAt time.Time, logger *slog.Logger) *Session

Methods

MethodSignatureDescription
Start(ctx context.Context) (string, error)Opens TCP listener on meshIP:0, starts accept loop
Close() errorIdempotent shutdown: cancels context, closes listener and connection
ListenAddr() stringReturns listener address or empty string if not started

Connection Lifecycle

  1. Start binds a TCP listener to meshIP:0 (ephemeral port, mesh-only interface)
  2. acceptLoop runs in a goroutine, accepting one connection at a time
  3. Single-connection enforcement: if a connection is already active, new connections are rejected
  4. forward dials the target, sets the active connection under mutex, and runs bidirectional io.Copy with sync.Once cleanup and sync.WaitGroup for completion
  5. Close is idempotent via sync.Mutex + closed flag; cancels context, closes listener and active connection

Security

  • Listener binds to mesh IP only, never 0.0.0.0 or localhost
  • At most one active forwarded connection per session
  • Context cancellation propagates to listener and active connection

SessionManager

Central coordinator for tunnel session lifecycle.

Constructor

go
func NewSessionManager(cfg Config, meshIP string, logger *slog.Logger) *SessionManager
  • Applies config defaults via cfg.ApplyDefaults()
  • Logger is tagged with component=tunnel

Methods

MethodSignatureDescription
CreateSession(ctx context.Context, setup api.SSHSessionSetup) (string, error)Validates, creates, and starts a tunnel session
CloseSession(sessionID string, reason string) *ClosedSessionInfoCloses and removes a session by ID; returns session info
Shutdown()Closes all active sessions
ActiveCount() intReturns number of active sessions

CreateSession Validation

CheckConditionError
Tunneling disabledcfg.Enabled == falsetunnel: tunneling is disabled
Missing fieldsEmpty ID, host, or port <= 0tunnel: invalid session setup: ...
Already expiredExpiresAt in the pasttunnel: session already expired
Duplicate IDSession ID already existstunnel: duplicate session ID: {id}
Capacitylen(sessions) >= MaxSessionstunnel: max sessions reached ({n})

Expiry

  • ExpiresAt is capped at DefaultTimeout from now (never exceeds maximum)
  • time.AfterFunc schedules automatic CloseSession("expired") at the capped expiry time

Lifecycle

go
logger := slog.Default()

mgr := tunnel.NewSessionManager(tunnel.Config{}, "10.0.0.1", logger)

// Create session from SSE event payload
addr, err := mgr.CreateSession(ctx, api.SSHSessionSetup{
    SessionID:  "sess-abc",
    TargetHost: "127.0.0.1",
    TargetPort: 22,
    ExpiresAt:  time.Now().Add(10 * time.Minute),
})

// Close specific session
mgr.CloseSession("sess-abc", "revoked")

// Graceful shutdown (closes all sessions)
mgr.Shutdown()

SSE Event Handlers

Factory functions returning api.EventHandler for tunnel lifecycle events. Each parses the SignedEnvelope.Payload and calls the appropriate SessionManager method.

FactoryEvent TypePayload TypeAction
HandleSSHSessionSetupssh_session_setupapi.SSHSessionSetupCreateSession + ReportReady
HandleSessionRevokedsession_revoked{"session_id": "..."}CloseSession("revoked") + ReportClosed
  • Malformed payloads are logged at error level and return an error
  • HandleSessionRevoked is a no-op if the session ID is not found (logged at debug level)

Registration

go
mgr := tunnel.NewSessionManager(tunnel.Config{}, meshIP, logger)

dispatcher := api.NewEventDispatcher(logger)
dispatcher.Register("ssh_session_setup", tunnel.HandleSSHSessionSetup(mgr, reporter))
dispatcher.Register("session_revoked", tunnel.HandleSessionRevoked(mgr, reporter))

TunnelReporter

Interface for reporting tunnel session lifecycle events to the control plane. Abstracted for testability.

go
type TunnelReporter interface {
    ReportReady(ctx context.Context, sessionID, listenAddr string)
    ReportClosed(ctx context.Context, sessionID, reason string, duration time.Duration)
}

A production implementation would use api.ControlPlane.TunnelReady and api.ControlPlane.TunnelClosed.

API Types

Types defined in internal/api for tunnel communication with the control plane.

SSHSessionSetup

Payload of the ssh_session_setup SSE event.

go
type SSHSessionSetup struct {
    SessionID     string    `json:"session_id"`
    TargetHost    string    `json:"target_host"`
    TargetPort    int       `json:"target_port"`
    AuthorizedKey string    `json:"authorized_key"`
    ExpiresAt     time.Time `json:"expires_at"`
}

TunnelReadyRequest

Sent by the node agent when a tunnel listener is ready.

go
type TunnelReadyRequest struct {
    ListenAddr string    `json:"listen_addr"`
    Timestamp  time.Time `json:"timestamp"`
}

Endpoint: POST /v1/nodes/{node_id}/tunnels/{session_id}/ready

TunnelClosedRequest

Sent by the node agent when a tunnel session closes.

go
type TunnelClosedRequest struct {
    Reason    string    `json:"reason"`
    Duration  string    `json:"duration"`
    Timestamp time.Time `json:"timestamp"`
}

Endpoint: POST /v1/nodes/{node_id}/tunnels/{session_id}/closed

Integration Points

SSE Event Stream (internal/api)

The tunnel package consumes two SSE event types via api.EventDispatcher:

Event TypeHandlerTrigger
ssh_session_setupHandleSSHSessionSetupControl plane initiates SSH access
session_revokedHandleSessionRevokedControl plane revokes SSH session

Control Plane API (internal/api)

The node agent reports tunnel status via two endpoints on api.ControlPlane:

MethodEndpointWhen Called
TunnelReadyPOST /v1/nodes/{id}/tunnels/{sid}/readyListener is ready
TunnelClosedPOST /v1/nodes/{id}/tunnels/{sid}/closedSession closed

WireGuard Mesh (internal/wireguard)

Tunnel listeners bind to the mesh IP assigned by the WireGuard interface. Connections arrive through the encrypted mesh — no ports are exposed on the public network. The meshIP parameter in NewSessionManager comes from registration.NodeIdentity.MeshIP.

Graceful Shutdown

Call SessionManager.Shutdown() on context cancellation to close all active sessions:

go
<-ctx.Done()
mgr.Shutdown()

Access Flows

SSH Access Flow

  1. User requests SSH access through the platform UI/CLI.
  2. Control plane verifies RBAC permissions and issues a session JWT scoped to the target node and allowed actions.
  3. Control plane sends an ssh_session_setup event via SSE to the target node, including the session token.
  4. plexd opens a TCP listener on the mesh interface and tunnels the SSH connection through the encrypted mesh.
  5. The SSH session uses the node's managed host key (stored in host_key_dir). If the key file does not exist, plexd generates an Ed25519 host key on first use and reports its fingerprint to the control plane.
  6. Session environment is injected with PLEXD_SESSION_TOKEN for local action authorization.
  7. On disconnect or default_timeout, plexd tears down the session and notifies the control plane.

Kubernetes API Proxy Flow

  1. User requests kubectl access through the platform.
  2. Control plane issues a scoped kubeconfig with a short-lived token.
  3. plexd proxies the Kubernetes API request through the mesh to the target cluster's API server (auto-discovered via kubelet config or configured explicitly).
  4. The proxy terminates on default_timeout if no requests are received.

Logging

All log entries use component=tunnel. Session-scoped entries add session_id.

LevelEventKeys
InfoSession startedlisten_addr, target
InfoSession createdsession_id, listen_addr, expires_at
InfoSession closedsession_id, reason, duration
InfoAll tunnel sessions closed
DebugConnection rejected (duplicate)
DebugSession not found for closesession_id
DebugRevoked session not foundsession_id
ErrorPayload parse failedevent_id, error
ErrorFailed to dial targettarget, error