• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pomerium / pomerium / 19238204824

10 Nov 2025 04:14PM UTC coverage: 54.846% (-0.6%) from 55.476%
19238204824

push

github

web-flow
fix (ui): fixed content layout shift for sign-in verify countdown (#5924)

## Summary

Fixes a small ui issue where the content shifts for the countdown on the
sign-in verify page.

## Related issues

Relates to #5873

## User Explanation

Now there is no more content layout shift (CLS) when the countdown
counts down.

## Screenshots and Recordings

**Before**

![CleanShot 2025-11-10 at 10 12
38](https://github.com/user-attachments/assets/20684e9d-8120-468e-882b-28d6a53670db)

**After**

![CleanShot 2025-11-10 at 10 11
19](https://github.com/user-attachments/assets/45010d97-ad6f-45a2-9720-7457ef39a8dd)

## Checklist

- [x] reference any related issues
- [ ] updated unit tests
- [x] add appropriate label (`enhancement`, `bug`, `breaking`,
`dependencies`, `ci`)
- [x] ready for review

28702 of 52332 relevant lines covered (54.85%)

93.83 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.25
/pkg/ssh/stream.go
1
package ssh
2

3
import (
4
        "context"
5
        "iter"
6
        "sync"
7
        "sync/atomic"
8
        "time"
9

10
        corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
11
        envoy_config_endpoint_v3 "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
12
        gossh "golang.org/x/crypto/ssh"
13
        "google.golang.org/grpc/codes"
14
        "google.golang.org/grpc/status"
15
        "google.golang.org/protobuf/types/known/anypb"
16

17
        extensions_ssh "github.com/pomerium/envoy-custom/api/extensions/filters/network/ssh"
18
        "github.com/pomerium/pomerium/config"
19
        "github.com/pomerium/pomerium/internal/log"
20
        "github.com/pomerium/pomerium/pkg/grpc/databroker"
21
        "github.com/pomerium/pomerium/pkg/protoutil"
22
        "github.com/pomerium/pomerium/pkg/slices"
23
        "github.com/pomerium/pomerium/pkg/ssh/portforward"
24
)
25

26
const (
27
        MethodPublicKey           = "publickey"
28
        MethodKeyboardInteractive = "keyboard-interactive"
29

30
        ChannelTypeSession     = "session"
31
        ChannelTypeDirectTcpip = "direct-tcpip"
32

33
        ServiceConnection = "ssh-connection"
34
)
35

36
type KeyboardInteractiveQuerier interface {
37
        // Prompts the client and returns their responses to the given prompts.
38
        Prompt(ctx context.Context, prompts *extensions_ssh.KeyboardInteractiveInfoPrompts) (*extensions_ssh.KeyboardInteractiveInfoPromptResponses, error)
39
}
40

41
type AuthMethodResponse[T any] struct {
42
        Allow                    *T
43
        RequireAdditionalMethods []string
44
}
45

46
type (
47
        PublicKeyAuthMethodResponse           = AuthMethodResponse[extensions_ssh.PublicKeyAllowResponse]
48
        KeyboardInteractiveAuthMethodResponse = AuthMethodResponse[extensions_ssh.KeyboardInteractiveAllowResponse]
49
)
50

51
//go:generate go run go.uber.org/mock/mockgen -typed -destination ./mock/mock_auth_interface.go . AuthInterface
52

53
type AuthInterface interface {
54
        HandlePublicKeyMethodRequest(ctx context.Context, info StreamAuthInfo, req *extensions_ssh.PublicKeyMethodRequest) (PublicKeyAuthMethodResponse, error)
55
        HandleKeyboardInteractiveMethodRequest(ctx context.Context, info StreamAuthInfo, req *extensions_ssh.KeyboardInteractiveMethodRequest, querier KeyboardInteractiveQuerier) (KeyboardInteractiveAuthMethodResponse, error)
56
        EvaluateDelayed(ctx context.Context, info StreamAuthInfo) error
57
        EvaluatePortForward(ctx context.Context, info StreamAuthInfo, portForwardInfo portforward.RouteInfo) error
58
        FormatSession(ctx context.Context, info StreamAuthInfo) ([]byte, error)
59
        DeleteSession(ctx context.Context, info StreamAuthInfo) error
60
        GetDataBrokerServiceClient() databroker.DataBrokerServiceClient
61
}
62

63
type ClusterStatsListener interface {
64
        HandleClusterStatsUpdate(*envoy_config_endpoint_v3.ClusterStats)
65
}
66

67
type EndpointDiscoveryInterface interface {
68
        UpdateClusterEndpoints(added map[string]portforward.RoutePortForwardInfo, removed map[string]struct{})
69
}
70

71
type AuthMethodValue[T any] struct {
72
        attempted bool
73
        Value     *T
74
}
75

76
func (v *AuthMethodValue[T]) Update(value *T) {
188✔
77
        v.attempted = true
188✔
78
        v.Value = value
188✔
79
}
188✔
80

81
func (v *AuthMethodValue[T]) IsValid() bool {
228✔
82
        if v.attempted {
380✔
83
                // method was attempted - valid iff there is a value
152✔
84
                return v.Value != nil
152✔
85
        }
152✔
86
        return true // method was not attempted - valid
76✔
87
}
88

89
type StreamAuthInfo struct {
90
        Username                   *string
91
        Hostname                   *string
92
        StreamID                   uint64
93
        SourceAddress              string
94
        ChannelType                string
95
        PublicKeyFingerprintSha256 []byte
96
        PublicKeyAllow             AuthMethodValue[extensions_ssh.PublicKeyAllowResponse]
97
        KeyboardInteractiveAllow   AuthMethodValue[extensions_ssh.KeyboardInteractiveAllowResponse]
98
        InitialAuthComplete        bool
99
}
100

101
func (i *StreamAuthInfo) allMethodsValid() bool {
114✔
102
        return i.PublicKeyAllow.IsValid() && i.KeyboardInteractiveAllow.IsValid()
114✔
103
}
114✔
104

105
type StreamState struct {
106
        StreamAuthInfo
107
        RemainingUnauthenticatedMethods []string
108
        DownstreamChannelInfo           *extensions_ssh.SSHDownstreamChannelInfo
109
}
110

111
type TUIDefaultMode int
112

113
const (
114
        TUIModeInternalCLI TUIDefaultMode = iota
115
        TUIModeTunnelStatus
116
)
117

118
// StreamHandler handles a single SSH stream
119
type StreamHandler struct {
120
        auth       AuthInterface
121
        discovery  EndpointDiscoveryInterface
122
        config     *config.Config
123
        downstream *extensions_ssh.DownstreamConnectEvent
124
        writeC     chan *extensions_ssh.ServerMessage
125
        readC      chan *extensions_ssh.ClientMessage
126
        reauthC    chan struct{}
127
        terminateC chan error
128

129
        state        *StreamState
130
        close        func()
131
        portForwards *portforward.Manager
132

133
        expectingInternalChannel bool
134
        internalSession          atomic.Pointer[ChannelHandler]
135

136
        tuiDefaultModeLock sync.Mutex
137
        tuiDefaultMode     TUIDefaultMode
138
}
139

140
var _ StreamHandlerInterface = (*StreamHandler)(nil)
141

142
func NewStreamHandler(
143
        auth AuthInterface,
144
        discovery EndpointDiscoveryInterface,
145
        cfg *config.Config,
146
        downstream *extensions_ssh.DownstreamConnectEvent,
147
        onClosed func(),
148
) *StreamHandler {
178✔
149
        writeC := make(chan *extensions_ssh.ServerMessage, 32)
178✔
150
        sh := &StreamHandler{
178✔
151
                auth:       auth,
178✔
152
                discovery:  discovery,
178✔
153
                config:     cfg,
178✔
154
                downstream: downstream,
178✔
155
                writeC:     make(chan *extensions_ssh.ServerMessage, 32),
178✔
156
                readC:      make(chan *extensions_ssh.ClientMessage, 32),
178✔
157
                reauthC:    make(chan struct{}),
178✔
158
                terminateC: make(chan error, 1),
178✔
159
                close: func() {
343✔
160
                        onClosed()
165✔
161
                        close(writeC)
165✔
162
                },
165✔
163
        }
164
        return sh
178✔
165
}
166

167
// EvaluateRoute implements portforward.RouteEvaluator.
168
func (sh *StreamHandler) EvaluateRoute(ctx context.Context, info portforward.RouteInfo) error {
100✔
169
        return sh.auth.EvaluatePortForward(ctx, sh.state.StreamAuthInfo, info)
100✔
170
}
100✔
171

172
// OnClusterEndpointsUpdated implements portforward.UpdateListener.
173
func (sh *StreamHandler) OnClusterEndpointsUpdated(added map[string]portforward.RoutePortForwardInfo, removed map[string]struct{}) {
220✔
174
        sh.discovery.UpdateClusterEndpoints(added, removed)
220✔
175
}
220✔
176

177
// OnPermissionsUpdated implements portforward.UpdateListener.
178
func (sh *StreamHandler) OnPermissionsUpdated(_ []portforward.Permission) {
174✔
179
}
174✔
180

181
// OnRoutesUpdated implements portforward.UpdateListener.
182
func (sh *StreamHandler) OnRoutesUpdated(_ []portforward.RouteInfo) {
160✔
183
}
160✔
184

185
func (sh *StreamHandler) Terminate(err error) {
11✔
186
        sh.terminateC <- err
11✔
187
}
11✔
188

189
func (sh *StreamHandler) Close() {
165✔
190
        sh.close()
165✔
191
}
165✔
192

193
func (sh *StreamHandler) IsExpectingInternalChannel() bool {
98✔
194
        return sh.expectingInternalChannel
98✔
195
}
98✔
196

197
func (sh *StreamHandler) ReadC() chan<- *extensions_ssh.ClientMessage {
590✔
198
        return sh.readC
590✔
199
}
590✔
200

201
func (sh *StreamHandler) WriteC() <-chan *extensions_ssh.ServerMessage {
338✔
202
        return sh.writeC
338✔
203
}
338✔
204

205
// Reauth blocks until authorization policy is reevaluated.
206
func (sh *StreamHandler) Reauth() {
46✔
207
        sh.reauthC <- struct{}{}
46✔
208
}
46✔
209

210
func (sh *StreamHandler) periodicReauth() (cancel func()) {
176✔
211
        t := time.NewTicker(1 * time.Minute)
176✔
212
        go func() {
352✔
213
                for range t.C {
176✔
214
                        sh.Reauth()
×
215
                }
×
216
        }()
217
        return t.Stop
176✔
218
}
219

220
// Prompt implements KeyboardInteractiveQuerier.
221
func (sh *StreamHandler) Prompt(ctx context.Context, prompts *extensions_ssh.KeyboardInteractiveInfoPrompts) (*extensions_ssh.KeyboardInteractiveInfoPromptResponses, error) {
46✔
222
        sh.sendInfoPrompts(prompts)
46✔
223
        select {
46✔
224
        case <-ctx.Done():
2✔
225
                return nil, context.Cause(ctx)
2✔
226
        case err := <-sh.terminateC:
×
227
                return nil, err
×
228
        case req := <-sh.readC:
44✔
229
                switch msg := req.Message.(type) {
44✔
230
                case *extensions_ssh.ClientMessage_InfoResponse:
42✔
231
                        if msg.InfoResponse.Method != MethodKeyboardInteractive {
44✔
232
                                return nil, status.Errorf(codes.Internal, "received invalid info response")
2✔
233
                        }
2✔
234
                        r, _ := msg.InfoResponse.Response.UnmarshalNew()
40✔
235
                        respInfo, ok := r.(*extensions_ssh.KeyboardInteractiveInfoPromptResponses)
40✔
236
                        if !ok {
42✔
237
                                return nil, status.Errorf(codes.InvalidArgument, "received invalid prompt response")
2✔
238
                        }
2✔
239
                        return respInfo, nil
38✔
240
                default:
2✔
241
                        return nil, status.Errorf(codes.InvalidArgument, "received invalid message, expecting info response")
2✔
242
                }
243
        }
244
}
245

246
func (sh *StreamHandler) Run(ctx context.Context) error {
178✔
247
        if sh.state != nil {
180✔
248
                panic("Run called twice")
2✔
249
        }
250
        sh.state = &StreamState{
176✔
251
                RemainingUnauthenticatedMethods: []string{MethodPublicKey},
176✔
252
                StreamAuthInfo: StreamAuthInfo{
176✔
253
                        StreamID:      sh.downstream.StreamId,
176✔
254
                        SourceAddress: sh.downstream.SourceAddress.GetSocketAddress().GetAddress(),
176✔
255
                },
176✔
256
        }
176✔
257
        cancelReauth := sh.periodicReauth()
176✔
258
        defer cancelReauth()
176✔
259
        for {
668✔
260
                select {
492✔
261
                case <-ctx.Done():
119✔
262
                        return context.Cause(ctx)
119✔
263
                case <-sh.reauthC:
46✔
264
                        if err := sh.reauth(ctx); err != nil {
52✔
265
                                return err
6✔
266
                        }
6✔
267
                case err := <-sh.terminateC:
11✔
268
                        return err
11✔
269
                case req := <-sh.readC:
316✔
270
                        switch req := req.Message.(type) {
316✔
271
                        case *extensions_ssh.ClientMessage_Event:
32✔
272
                                switch event := req.Event.Event.(type) {
32✔
273
                                case *extensions_ssh.StreamEvent_DownstreamConnected:
2✔
274
                                        // this was already received as the first message in the stream
2✔
275
                                        return status.Errorf(codes.Internal, "received duplicate downstream connected event")
2✔
276
                                case *extensions_ssh.StreamEvent_UpstreamConnected:
26✔
277
                                        log.Ctx(ctx).Debug().
26✔
278
                                                Msg("ssh: upstream connected")
26✔
279
                                case *extensions_ssh.StreamEvent_DownstreamDisconnected:
2✔
280
                                        log.Ctx(ctx).Debug().
2✔
281
                                                Uint64("stream-id", sh.downstream.StreamId).
2✔
282
                                                Str("reason", event.DownstreamDisconnected.Reason).
2✔
283
                                                Msg("ssh: downstream disconnected")
2✔
284
                                case *extensions_ssh.StreamEvent_ChannelEvent:
×
285
                                        if ch := sh.internalSession.Load(); ch != nil {
×
286
                                                ch.HandleEvent(event.ChannelEvent)
×
287
                                        }
×
288
                                        // if there is no internal session, this is a no-op
289
                                case nil:
2✔
290
                                        return status.Errorf(codes.Internal, "received invalid event")
2✔
291
                                }
292
                        case *extensions_ssh.ClientMessage_AuthRequest:
222✔
293
                                if err := sh.handleAuthRequest(ctx, req.AuthRequest); err != nil {
256✔
294
                                        return err
34✔
295
                                }
34✔
296
                        case *extensions_ssh.ClientMessage_GlobalRequest:
60✔
297
                                if err := sh.handleGlobalRequest(ctx, req.GlobalRequest); err != nil {
60✔
298
                                        return err
×
299
                                }
×
300
                        default:
2✔
301
                                return status.Errorf(codes.Internal, "received invalid client message type %#T", req)
2✔
302
                        }
303
                }
304
        }
305
}
306

307
func (sh *StreamHandler) handleGlobalRequest(ctx context.Context, globalRequest *extensions_ssh.GlobalRequest) error {
60✔
308
        sh.tuiDefaultModeLock.Lock()
60✔
309
        defer sh.tuiDefaultModeLock.Unlock()
60✔
310
        switch request := globalRequest.Request.(type) {
60✔
311
        case *extensions_ssh.GlobalRequest_TcpipForwardRequest:
40✔
312
                if sh.portForwards == nil {
40✔
313
                        return status.Errorf(codes.InvalidArgument, "cannot request port-forward before auth is complete")
×
314
                }
×
315
                reqHost := request.TcpipForwardRequest.RemoteAddress
40✔
316
                reqPort := request.TcpipForwardRequest.RemotePort
40✔
317
                log.Ctx(ctx).Debug().
40✔
318
                        Uint64("stream-id", sh.state.StreamID).
40✔
319
                        Str("host", reqHost).
40✔
320
                        Msg("got tcpip-forward request")
40✔
321

40✔
322
                serverPort, err := sh.portForwards.AddPermission(reqHost, reqPort)
40✔
323
                if err != nil {
40✔
324
                        sh.sendGlobalRequestResponse(&extensions_ssh.GlobalRequestResponse{
×
325
                                Success:      false,
×
326
                                DebugMessage: err.Error(),
×
327
                        })
×
328
                        return nil
×
329
                }
×
330

331
                log.Ctx(ctx).Debug().
40✔
332
                        Uint64("stream-id", sh.state.StreamID).
40✔
333
                        Msg("sending global request success")
40✔
334

40✔
335
                // https://datatracker.ietf.org/doc/html/rfc4254#section-7.1
40✔
336
                if globalRequest.WantReply && reqPort == 0 {
40✔
337
                        sh.sendGlobalRequestResponse(&extensions_ssh.GlobalRequestResponse{
×
338
                                Success: true,
×
339
                                Response: &extensions_ssh.GlobalRequestResponse_TcpipForwardResponse{
×
340
                                        TcpipForwardResponse: &extensions_ssh.TcpipForwardResponse{
×
341
                                                ServerPort: serverPort.Value,
×
342
                                        },
×
343
                                },
×
344
                        })
×
345
                }
×
346

347
                sh.tuiDefaultMode = TUIModeTunnelStatus
40✔
348
                return nil
40✔
349
        case *extensions_ssh.GlobalRequest_CancelTcpipForwardRequest:
20✔
350
                if sh.portForwards == nil {
20✔
351
                        return status.Errorf(codes.InvalidArgument, "cannot request port-forward before auth is complete")
×
352
                }
×
353
                err := sh.portForwards.RemovePermission(
20✔
354
                        request.CancelTcpipForwardRequest.RemoteAddress,
20✔
355
                        request.CancelTcpipForwardRequest.RemotePort)
20✔
356
                if err != nil {
20✔
357
                        sh.sendGlobalRequestResponse(&extensions_ssh.GlobalRequestResponse{
×
358
                                Success:      false,
×
359
                                DebugMessage: err.Error(),
×
360
                        })
×
361
                } else if globalRequest.WantReply {
20✔
362
                        sh.sendGlobalRequestResponse(&extensions_ssh.GlobalRequestResponse{
×
363
                                Success: true,
×
364
                        })
×
365
                }
×
366
                return nil
20✔
367
        default:
×
368
                return status.Errorf(codes.Unimplemented, "received unknown global request")
×
369
        }
370
}
371

372
func (sh *StreamHandler) sendGlobalRequestResponse(response *extensions_ssh.GlobalRequestResponse) {
×
373
        sh.writeC <- &extensions_ssh.ServerMessage{
×
374
                Message: &extensions_ssh.ServerMessage_GlobalRequestResponse{
×
375
                        GlobalRequestResponse: response,
×
376
                },
×
377
        }
×
378
}
×
379

380
func (sh *StreamHandler) ServeChannel(
381
        stream extensions_ssh.StreamManagement_ServeChannelServer,
382
        metadata *extensions_ssh.FilterMetadata,
383
) error {
64✔
384
        // The first channel message on this stream should be a ChannelOpen
64✔
385
        channelOpen, err := stream.Recv()
64✔
386
        if err != nil {
67✔
387
                return err
3✔
388
        }
3✔
389
        rawMsg, ok := channelOpen.GetMessage().(*extensions_ssh.ChannelMessage_RawBytes)
61✔
390
        if !ok {
63✔
391
                return status.Errorf(codes.InvalidArgument, "first channel message was not ChannelOpen")
2✔
392
        }
2✔
393
        var msg ChannelOpenMsg
59✔
394
        if err := gossh.Unmarshal(rawMsg.RawBytes.GetValue(), &msg); err != nil {
61✔
395
                return status.Errorf(codes.InvalidArgument, "first channel message was not ChannelOpen")
2✔
396
        }
2✔
397

398
        sh.state.DownstreamChannelInfo = &extensions_ssh.SSHDownstreamChannelInfo{
57✔
399
                ChannelType:               msg.ChanType,
57✔
400
                DownstreamChannelId:       msg.PeersID,
57✔
401
                InternalUpstreamChannelId: metadata.ChannelId,
57✔
402
                InitialWindowSize:         msg.PeersWindow,
57✔
403
                MaxPacketSize:             msg.MaxPacketSize,
57✔
404
        }
57✔
405
        sh.state.ChannelType = msg.ChanType
57✔
406
        channel := NewChannelImpl(sh, stream, sh.state.DownstreamChannelInfo)
57✔
407
        switch msg.ChanType {
57✔
408
        case ChannelTypeSession:
41✔
409
                ch := NewChannelHandler(channel, sh.config)
41✔
410
                if !sh.internalSession.CompareAndSwap(nil, ch) {
41✔
411
                        return channel.SendMessage(ChannelOpenFailureMsg{
×
412
                                PeersID: sh.state.DownstreamChannelInfo.DownstreamChannelId,
×
413
                                Reason:  Prohibited,
×
414
                                Message: "multiple concurrent internal session channels not supported",
×
415
                        })
×
416
                }
×
417
                if err := channel.SendMessage(ChannelOpenConfirmMsg{
41✔
418
                        PeersID:       sh.state.DownstreamChannelInfo.DownstreamChannelId,
41✔
419
                        MyID:          sh.state.DownstreamChannelInfo.InternalUpstreamChannelId,
41✔
420
                        MyWindow:      ChannelWindowSize,
41✔
421
                        MaxPacketSize: ChannelMaxPacket,
41✔
422
                }); err != nil {
41✔
423
                        return err
×
424
                }
×
425
                var mode TUIDefaultMode
41✔
426
                sh.tuiDefaultModeLock.Lock()
41✔
427
                mode = sh.tuiDefaultMode
41✔
428
                sh.tuiDefaultModeLock.Unlock()
41✔
429

41✔
430
                err := ch.Run(stream.Context(), mode)
41✔
431
                sh.internalSession.Store(nil)
41✔
432
                return err
41✔
433
        case ChannelTypeDirectTcpip:
14✔
434
                if !sh.config.Options.IsRuntimeFlagSet(config.RuntimeFlagSSHAllowDirectTcpip) {
18✔
435
                        return status.Errorf(codes.Unavailable, "direct-tcpip channels are not enabled")
4✔
436
                }
4✔
437
                var subMsg ChannelOpenDirectMsg
10✔
438
                if err := gossh.Unmarshal(msg.TypeSpecificData, &subMsg); err != nil {
11✔
439
                        return err
1✔
440
                }
1✔
441
                action, err := sh.PrepareHandoff(stream.Context(), subMsg.DestAddr, nil)
9✔
442
                if err != nil {
11✔
443
                        return err
2✔
444
                }
2✔
445
                return channel.SendControlAction(action)
7✔
446
        default:
2✔
447
                return status.Errorf(codes.InvalidArgument, "unexpected channel type in ChannelOpen message: %s", msg.ChanType)
2✔
448
        }
449
}
450

451
func (sh *StreamHandler) handleAuthRequest(ctx context.Context, req *extensions_ssh.AuthenticationRequest) error {
222✔
452
        if req.Protocol != "ssh" {
224✔
453
                return status.Errorf(codes.InvalidArgument, "invalid protocol: %s", req.Protocol)
2✔
454
        }
2✔
455
        if req.Service != ServiceConnection {
222✔
456
                return status.Errorf(codes.InvalidArgument, "invalid service: %s", req.Service)
2✔
457
        }
2✔
458
        if !slices.Contains(sh.state.RemainingUnauthenticatedMethods, req.AuthMethod) {
224✔
459
                return status.Errorf(codes.InvalidArgument, "unexpected auth method: %s", req.AuthMethod)
6✔
460
        }
6✔
461

462
        if sh.state.Username == nil {
352✔
463
                if req.Username == "" {
142✔
464
                        return status.Errorf(codes.InvalidArgument, "username missing")
2✔
465
                }
2✔
466
                sh.state.Username = &req.Username
138✔
467
        } else if *sh.state.Username != req.Username {
74✔
468
                return status.Errorf(codes.InvalidArgument, "inconsistent username")
2✔
469
        }
2✔
470
        if sh.state.Hostname == nil {
346✔
471
                sh.state.Hostname = &req.Hostname
138✔
472
        } else if *sh.state.Hostname != req.Hostname {
212✔
473
                return status.Errorf(codes.InvalidArgument, "inconsistent hostname")
4✔
474
        }
4✔
475

476
        updateMethods := func(add []string) {
392✔
477
                sh.state.RemainingUnauthenticatedMethods = slices.Remove(sh.state.RemainingUnauthenticatedMethods, req.AuthMethod)
188✔
478
                sh.state.RemainingUnauthenticatedMethods = append(sh.state.RemainingUnauthenticatedMethods, add...)
188✔
479
        }
188✔
480
        log.Ctx(ctx).Debug().
204✔
481
                Str("method", req.AuthMethod).
204✔
482
                Str("username", *sh.state.Username).
204✔
483
                Str("hostname", *sh.state.Hostname).
204✔
484
                Msg("ssh: handling auth request")
204✔
485

204✔
486
        var partial bool
204✔
487
        switch req.AuthMethod {
204✔
488
        case MethodPublicKey:
154✔
489
                methodReq, _ := req.MethodRequest.UnmarshalNew()
154✔
490
                pubkeyReq, ok := methodReq.(*extensions_ssh.PublicKeyMethodRequest)
154✔
491
                if !ok {
156✔
492
                        return status.Errorf(codes.InvalidArgument, "invalid public key method request type")
2✔
493
                }
2✔
494
                response, err := sh.auth.HandlePublicKeyMethodRequest(ctx, sh.state.StreamAuthInfo, pubkeyReq)
152✔
495
                if err != nil {
154✔
496
                        return err
2✔
497
                } else if response.Allow != nil {
280✔
498
                        partial = true
128✔
499
                        sh.state.PublicKeyFingerprintSha256 = pubkeyReq.PublicKeyFingerprintSha256
128✔
500
                }
128✔
501
                sh.state.PublicKeyAllow.Update(response.Allow)
150✔
502
                updateMethods(response.RequireAdditionalMethods)
150✔
503
        case MethodKeyboardInteractive:
48✔
504
                methodReq, _ := req.MethodRequest.UnmarshalNew()
48✔
505
                kbiReq, ok := methodReq.(*extensions_ssh.KeyboardInteractiveMethodRequest)
48✔
506
                if !ok {
50✔
507
                        return status.Errorf(codes.InvalidArgument, "invalid keyboard-interactive method request type")
2✔
508
                }
2✔
509
                response, err := sh.auth.HandleKeyboardInteractiveMethodRequest(ctx, sh.state.StreamAuthInfo, kbiReq, sh)
46✔
510
                if err != nil {
54✔
511
                        return err
8✔
512
                }
8✔
513
                partial = response.Allow != nil
38✔
514
                sh.state.KeyboardInteractiveAllow.Update(response.Allow)
38✔
515
                updateMethods(response.RequireAdditionalMethods)
38✔
516
        default:
2✔
517
                return status.Errorf(codes.Internal, "bug: server requested an unsupported auth method %q", req.AuthMethod)
2✔
518
        }
519
        log.Ctx(ctx).Debug().
188✔
520
                Str("method", req.AuthMethod).
188✔
521
                Bool("partial", partial).
188✔
522
                Strs("methods-remaining", sh.state.RemainingUnauthenticatedMethods).
188✔
523
                Msg("ssh: auth request complete")
188✔
524

188✔
525
        if len(sh.state.RemainingUnauthenticatedMethods) == 0 && sh.state.allMethodsValid() {
302✔
526
                // If there are no methods remaining, the user is allowed if all attempted
114✔
527
                // methods have a valid response in the state
114✔
528
                sh.state.InitialAuthComplete = true
114✔
529
                // Initialize the port forward manager
114✔
530
                sh.portForwards = portforward.NewManager(ctx, sh)
114✔
531
                sh.portForwards.OnConfigUpdate(sh.config)
114✔
532
                sh.portForwards.AddUpdateListener(sh)
114✔
533

114✔
534
                log.Ctx(ctx).Debug().Msg("ssh: all methods valid, sending allow response")
114✔
535
                sh.sendAllowResponse()
114✔
536
        } else {
188✔
537
                log.Ctx(ctx).Debug().Msg("ssh: unauthenticated methods remain, sending deny response")
74✔
538
                sh.sendDenyResponseWithRemainingMethods(partial)
74✔
539
        }
74✔
540
        return nil
188✔
541
}
542

543
func (sh *StreamHandler) reauth(ctx context.Context) error {
46✔
544
        if !sh.state.InitialAuthComplete {
46✔
545
                return nil
×
546
        }
×
547
        return sh.auth.EvaluateDelayed(ctx, sh.state.StreamAuthInfo)
46✔
548
}
549

550
func (sh *StreamHandler) PrepareHandoff(ctx context.Context, hostname string, ptyInfo *extensions_ssh.SSHDownstreamPTYInfo) (*extensions_ssh.SSHChannelControlAction, error) {
10✔
551
        if hostname == "" {
11✔
552
                return nil, status.Errorf(codes.PermissionDenied, "invalid hostname")
1✔
553
        }
1✔
554
        if sh.state.Hostname == nil {
9✔
555
                panic("bug: PrepareHandoff called but state is missing a hostname")
×
556
        }
557
        if *sh.state.Hostname != "" {
9✔
558
                panic("bug: PrepareHandoff called but previous hostname is not empty")
×
559
        }
560
        *sh.state.Hostname = hostname
9✔
561
        err := sh.auth.EvaluateDelayed(ctx, sh.state.StreamAuthInfo)
9✔
562
        if err != nil {
10✔
563
                return nil, status.Error(codes.PermissionDenied, err.Error())
1✔
564
        }
1✔
565
        log.Ctx(ctx).Debug().
8✔
566
                Str("hostname", *sh.state.Hostname).
8✔
567
                Str("username", *sh.state.Username).
8✔
568
                Msg("ssh: initiating handoff to upstream")
8✔
569
        upstreamAllow := sh.buildUpstreamAllowResponse()
8✔
570
        action := &extensions_ssh.SSHChannelControlAction{
8✔
571
                Action: &extensions_ssh.SSHChannelControlAction_HandOff{
8✔
572
                        HandOff: &extensions_ssh.SSHChannelControlAction_HandOffUpstream{
8✔
573
                                DownstreamChannelInfo: sh.state.DownstreamChannelInfo,
8✔
574
                                DownstreamPtyInfo:     ptyInfo,
8✔
575
                                UpstreamAuth:          upstreamAllow,
8✔
576
                        },
8✔
577
                },
8✔
578
        }
8✔
579
        return action, nil
8✔
580
}
581

582
func (sh *StreamHandler) FormatSession(ctx context.Context) ([]byte, error) {
14✔
583
        return sh.auth.FormatSession(ctx, sh.state.StreamAuthInfo)
14✔
584
}
14✔
585

586
func (sh *StreamHandler) DeleteSession(ctx context.Context) error {
12✔
587
        return sh.auth.DeleteSession(ctx, sh.state.StreamAuthInfo)
12✔
588
}
12✔
589

590
func (sh *StreamHandler) AllSSHRoutes() iter.Seq[*config.Policy] {
6✔
591
        return func(yield func(*config.Policy) bool) {
12✔
592
                for route := range sh.config.Options.GetAllPolicies() {
30✔
593
                        if route.IsSSH() {
34✔
594
                                if !yield(route) {
12✔
595
                                        return
2✔
596
                                }
2✔
597
                        }
598
                }
599
        }
600
}
601

602
// DownstreamChannelID implements StreamHandlerInterface.
603
func (sh *StreamHandler) DownstreamChannelID() uint32 {
212✔
604
        return sh.state.DownstreamChannelInfo.DownstreamChannelId
212✔
605
}
212✔
606

607
// Hostname implements StreamHandlerInterface.
608
func (sh *StreamHandler) Hostname() *string {
42✔
609
        return sh.state.Hostname
42✔
610
}
42✔
611

612
// Username implements StreamHandlerInterface.
613
func (sh *StreamHandler) Username() *string {
79✔
614
        return sh.state.Username
79✔
615
}
79✔
616

617
// AddPortForwardUpdateListener implements StreamHandlerInterface.
618
func (sh *StreamHandler) AddPortForwardUpdateListener(listener portforward.UpdateListener) {
×
619
        sh.portForwards.AddUpdateListener(listener)
×
620
}
×
621

622
// RemovePortForwardUpdateListener implements StreamHandlerInterface.
623
func (sh *StreamHandler) RemovePortForwardUpdateListener(listener portforward.UpdateListener) {
×
624
        sh.portForwards.RemoveUpdateListener(listener)
×
625
}
×
626

627
func (sh *StreamHandler) sendDenyResponseWithRemainingMethods(partial bool) {
74✔
628
        sh.writeC <- &extensions_ssh.ServerMessage{
74✔
629
                Message: &extensions_ssh.ServerMessage_AuthResponse{
74✔
630
                        AuthResponse: &extensions_ssh.AuthenticationResponse{
74✔
631
                                Response: &extensions_ssh.AuthenticationResponse_Deny{
74✔
632
                                        Deny: &extensions_ssh.DenyResponse{
74✔
633
                                                Partial: partial,
74✔
634
                                                Methods: sh.state.RemainingUnauthenticatedMethods,
74✔
635
                                        },
74✔
636
                                },
74✔
637
                        },
74✔
638
                },
74✔
639
        }
74✔
640
}
74✔
641

642
func (sh *StreamHandler) sendAllowResponse() {
114✔
643
        var allow *extensions_ssh.AllowResponse
114✔
644
        if *sh.state.Hostname == "" {
204✔
645
                sh.expectingInternalChannel = true
90✔
646
                allow = sh.buildInternalAllowResponse()
90✔
647
        } else {
114✔
648
                allow = sh.buildUpstreamAllowResponse()
24✔
649
        }
24✔
650

651
        sh.writeC <- &extensions_ssh.ServerMessage{
114✔
652
                Message: &extensions_ssh.ServerMessage_AuthResponse{
114✔
653
                        AuthResponse: &extensions_ssh.AuthenticationResponse{
114✔
654
                                Response: &extensions_ssh.AuthenticationResponse_Allow{
114✔
655
                                        Allow: allow,
114✔
656
                                },
114✔
657
                        },
114✔
658
                },
114✔
659
        }
114✔
660
}
661

662
func (sh *StreamHandler) sendInfoPrompts(prompts *extensions_ssh.KeyboardInteractiveInfoPrompts) {
46✔
663
        sh.writeC <- &extensions_ssh.ServerMessage{
46✔
664
                Message: &extensions_ssh.ServerMessage_AuthResponse{
46✔
665
                        AuthResponse: &extensions_ssh.AuthenticationResponse{
46✔
666
                                Response: &extensions_ssh.AuthenticationResponse_InfoRequest{
46✔
667
                                        InfoRequest: &extensions_ssh.InfoRequest{
46✔
668
                                                Method:  MethodKeyboardInteractive,
46✔
669
                                                Request: protoutil.NewAny(prompts),
46✔
670
                                        },
46✔
671
                                },
46✔
672
                        },
46✔
673
                },
46✔
674
        }
46✔
675
}
46✔
676

677
func (sh *StreamHandler) buildUpstreamAllowResponse() *extensions_ssh.AllowResponse {
32✔
678
        var allowedMethods []*extensions_ssh.AllowedMethod
32✔
679
        if value := sh.state.PublicKeyAllow.Value; value != nil {
64✔
680
                allowedMethods = append(allowedMethods, &extensions_ssh.AllowedMethod{
32✔
681
                        Method:     MethodPublicKey,
32✔
682
                        MethodData: protoutil.NewAny(value),
32✔
683
                })
32✔
684
        }
32✔
685
        if value := sh.state.KeyboardInteractiveAllow.Value; value != nil {
58✔
686
                allowedMethods = append(allowedMethods, &extensions_ssh.AllowedMethod{
26✔
687
                        Method:     MethodKeyboardInteractive,
26✔
688
                        MethodData: protoutil.NewAny(value),
26✔
689
                })
26✔
690
        }
26✔
691
        return &extensions_ssh.AllowResponse{
32✔
692
                Username: *sh.state.Username,
32✔
693
                Target: &extensions_ssh.AllowResponse_Upstream{
32✔
694
                        Upstream: &extensions_ssh.UpstreamTarget{
32✔
695
                                Hostname:       *sh.state.Hostname,
32✔
696
                                DirectTcpip:    sh.state.ChannelType == ChannelTypeDirectTcpip,
32✔
697
                                AllowedMethods: allowedMethods,
32✔
698
                        },
32✔
699
                },
32✔
700
        }
32✔
701
}
702

703
func (sh *StreamHandler) buildInternalAllowResponse() *extensions_ssh.AllowResponse {
90✔
704
        return &extensions_ssh.AllowResponse{
90✔
705
                Username: *sh.state.Username,
90✔
706
                Target: &extensions_ssh.AllowResponse_Internal{
90✔
707
                        Internal: &extensions_ssh.InternalTarget{
90✔
708
                                SetMetadata: &corev3.Metadata{
90✔
709
                                        TypedFilterMetadata: map[string]*anypb.Any{
90✔
710
                                                "com.pomerium.ssh": protoutil.NewAny(&extensions_ssh.FilterMetadata{
90✔
711
                                                        StreamId: sh.downstream.StreamId,
90✔
712
                                                }),
90✔
713
                                        },
90✔
714
                                },
90✔
715
                        },
90✔
716
                },
90✔
717
        }
90✔
718
}
90✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc