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

pomerium / pomerium / 19177304437

07 Nov 2025 06:16PM UTC coverage: 56.093% (-0.02%) from 56.112%
19177304437

push

github

web-flow
ssh: upstream tunnel auth stubs (#5919)

Temporary patch to set up upstream tunnel auth. The actual policy
evaluation is not yet implemented.

13 of 37 new or added lines in 4 files covered. (35.14%)

14 existing lines in 3 files now uncovered.

28519 of 50842 relevant lines covered (56.09%)

96.52 hits per line

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

81.59
/pkg/ssh/auth.go
1
package ssh
2

3
import (
4
        "bytes"
5
        "context"
6
        "crypto/sha256"
7
        "encoding/base64"
8
        "errors"
9
        "fmt"
10
        "slices"
11
        "sync/atomic"
12
        "time"
13

14
        oteltrace "go.opentelemetry.io/otel/trace"
15
        "golang.org/x/oauth2"
16
        "google.golang.org/grpc/codes"
17
        "google.golang.org/grpc/status"
18
        "google.golang.org/protobuf/types/known/timestamppb"
19

20
        extensions_ssh "github.com/pomerium/envoy-custom/api/extensions/filters/network/ssh"
21
        "github.com/pomerium/pomerium/authorize/evaluator"
22
        "github.com/pomerium/pomerium/config"
23
        "github.com/pomerium/pomerium/internal/log"
24
        "github.com/pomerium/pomerium/internal/sessions"
25
        "github.com/pomerium/pomerium/pkg/grpc/databroker"
26
        identitypb "github.com/pomerium/pomerium/pkg/grpc/identity"
27
        "github.com/pomerium/pomerium/pkg/grpc/session"
28
        "github.com/pomerium/pomerium/pkg/grpc/user"
29
        "github.com/pomerium/pomerium/pkg/identity"
30
        "github.com/pomerium/pomerium/pkg/identity/manager"
31
        "github.com/pomerium/pomerium/pkg/policy/criteria"
32
        "github.com/pomerium/pomerium/pkg/ssh/portforward"
33
)
34

35
type Evaluator interface {
36
        EvaluateSSH(ctx context.Context, streamID uint64, req *Request) (*evaluator.Result, error)
37
        GetDataBrokerServiceClient() databroker.DataBrokerServiceClient
38
        InvalidateCacheForRecords(context.Context, ...*databroker.Record)
39
}
40

41
type Request struct {
42
        Username      string
43
        Hostname      string
44
        PublicKey     []byte
45
        SessionID     string
46
        SourceAddress string
47

48
        LogOnlyIfDenied         bool
49
        UseUpstreamTunnelPolicy bool
50
}
51

52
type Auth struct {
53
        evaluator      Evaluator
54
        currentConfig  *atomic.Pointer[config.Config]
55
        tracerProvider oteltrace.TracerProvider
56
}
57

58
func NewAuth(
59
        evaluator Evaluator,
60
        currentConfig *atomic.Pointer[config.Config],
61
        tracerProvider oteltrace.TracerProvider,
62
) *Auth {
49✔
63
        auth := &Auth{
49✔
64
                evaluator:      evaluator,
49✔
65
                currentConfig:  currentConfig,
49✔
66
                tracerProvider: tracerProvider,
49✔
67
        }
49✔
68
        return auth
49✔
69
}
49✔
70

71
// GetDataBrokerServiceClient implements AuthInterface.
72
func (a *Auth) GetDataBrokerServiceClient() databroker.DataBrokerServiceClient {
89✔
73
        return a.evaluator.GetDataBrokerServiceClient()
89✔
74
}
89✔
75

76
func (a *Auth) HandlePublicKeyMethodRequest(
77
        ctx context.Context,
78
        info StreamAuthInfo,
79
        req *extensions_ssh.PublicKeyMethodRequest,
80
) (PublicKeyAuthMethodResponse, error) {
43✔
81
        resp, err := a.handlePublicKeyMethodRequest(ctx, info, req)
43✔
82
        if err != nil {
44✔
83
                log.Ctx(ctx).Error().Err(err).Msg("ssh publickey auth request error")
1✔
84
                return resp, status.Error(codes.Internal, "internal error")
1✔
85
        }
1✔
86
        return resp, err
42✔
87
}
88

89
func (a *Auth) handlePublicKeyMethodRequest(
90
        ctx context.Context,
91
        info StreamAuthInfo,
92
        req *extensions_ssh.PublicKeyMethodRequest,
93
) (PublicKeyAuthMethodResponse, error) {
45✔
94
        sessionID, err := sessionIDFromFingerprint(req.PublicKeyFingerprintSha256)
45✔
95
        if err != nil {
46✔
96
                return PublicKeyAuthMethodResponse{}, err
1✔
97
        }
1✔
98
        sshreq := &Request{
44✔
99
                Username:      *info.Username,
44✔
100
                Hostname:      *info.Hostname,
44✔
101
                PublicKey:     req.PublicKey,
44✔
102
                SessionID:     sessionID,
44✔
103
                SourceAddress: info.SourceAddress,
44✔
104
        }
44✔
105
        log.Ctx(ctx).Debug().
44✔
106
                Str("username", *info.Username).
44✔
107
                Str("hostname", *info.Hostname).
44✔
108
                Str("session-id", sessionID).
44✔
109
                Msg("ssh publickey auth request")
44✔
110

44✔
111
        // Special case: internal command (e.g. routes portal).
44✔
112
        if *info.Hostname == "" {
65✔
113
                _, err := session.Get(ctx, a.evaluator.GetDataBrokerServiceClient(), sessionID)
21✔
114
                if status.Code(err) == codes.NotFound {
40✔
115
                        // Require IdP login.
19✔
116
                        return PublicKeyAuthMethodResponse{
19✔
117
                                Allow:                    publicKeyAllowResponse(req.PublicKey),
19✔
118
                                RequireAdditionalMethods: []string{MethodKeyboardInteractive},
19✔
119
                        }, nil
19✔
120
                } else if err != nil {
22✔
121
                        return PublicKeyAuthMethodResponse{}, err
1✔
122
                }
1✔
123
        }
124

125
        res, err := a.evaluator.EvaluateSSH(ctx, info.StreamID, sshreq)
24✔
126
        if err != nil {
25✔
127
                return PublicKeyAuthMethodResponse{}, err
1✔
128
        }
1✔
129

130
        // Interpret the results of policy evaluation.
131
        if res.HasReason(criteria.ReasonSSHPublickeyUnauthorized) {
24✔
132
                // This public key is not allowed, but the client is free to try a different key.
1✔
133
                return PublicKeyAuthMethodResponse{
1✔
134
                        RequireAdditionalMethods: []string{MethodPublicKey},
1✔
135
                }, nil
1✔
136
        } else if res.HasReason(criteria.ReasonUserUnauthenticated) {
30✔
137
                // Mark public key as allowed, to initiate IdP login flow.
7✔
138
                return PublicKeyAuthMethodResponse{
7✔
139
                        Allow:                    publicKeyAllowResponse(req.PublicKey),
7✔
140
                        RequireAdditionalMethods: []string{MethodKeyboardInteractive},
7✔
141
                }, nil
7✔
142
        } else if res.Allow.Value && !res.Deny.Value {
36✔
143
                // Allowed, no login needed.
14✔
144
                return PublicKeyAuthMethodResponse{
14✔
145
                        Allow: publicKeyAllowResponse(req.PublicKey),
14✔
146
                }, nil
14✔
147
        }
14✔
148
        // Denied, no login needed.
149
        return PublicKeyAuthMethodResponse{}, nil
1✔
150
}
151

152
func publicKeyAllowResponse(publicKey []byte) *extensions_ssh.PublicKeyAllowResponse {
40✔
153
        return &extensions_ssh.PublicKeyAllowResponse{
40✔
154
                PublicKey: publicKey,
40✔
155
                Permissions: &extensions_ssh.Permissions{
40✔
156
                        PermitPortForwarding:  true,
40✔
157
                        PermitAgentForwarding: true,
40✔
158
                        PermitX11Forwarding:   true,
40✔
159
                        PermitPty:             true,
40✔
160
                        PermitUserRc:          true,
40✔
161
                        ValidStartTime:        timestamppb.New(time.Now().Add(-1 * time.Minute)),
40✔
162
                        ValidEndTime:          timestamppb.New(time.Now().Add(1 * time.Hour)),
40✔
163
                },
40✔
164
        }
40✔
165
}
40✔
166

167
func (a *Auth) HandleKeyboardInteractiveMethodRequest(
168
        ctx context.Context,
169
        info StreamAuthInfo,
170
        _ *extensions_ssh.KeyboardInteractiveMethodRequest,
171
        querier KeyboardInteractiveQuerier,
172
) (KeyboardInteractiveAuthMethodResponse, error) {
26✔
173
        resp, err := a.handleKeyboardInteractiveMethodRequest(ctx, info, querier)
26✔
174
        if err != nil {
26✔
175
                log.Ctx(ctx).Error().Err(err).Msg("ssh keyboard-interactive auth request error")
×
176
                return resp, status.Error(codes.Internal, "internal error")
×
177
        }
×
178
        return resp, err
26✔
179
}
180

181
func (a *Auth) handleKeyboardInteractiveMethodRequest(
182
        ctx context.Context,
183
        info StreamAuthInfo,
184
        querier KeyboardInteractiveQuerier,
185
) (KeyboardInteractiveAuthMethodResponse, error) {
28✔
186
        if info.PublicKeyAllow.Value == nil {
29✔
187
                // Sanity check: this method is only valid if we already accepted a public key.
1✔
188
                return KeyboardInteractiveAuthMethodResponse{}, errPublicKeyAllowNil
1✔
189
        }
1✔
190

191
        log.Ctx(ctx).Debug().
27✔
192
                Str("username", *info.Username).
27✔
193
                Str("hostname", *info.Hostname).
27✔
194
                Str("publickey-fingerprint", base64.StdEncoding.EncodeToString(info.PublicKeyFingerprintSha256)).
27✔
195
                Msg("ssh keyboard-interactive auth request")
27✔
196

27✔
197
        // Initiate the IdP login flow.
27✔
198
        err := a.handleLogin(ctx, *info.Hostname, info.PublicKeyFingerprintSha256, querier)
27✔
199
        if err != nil {
28✔
200
                return KeyboardInteractiveAuthMethodResponse{}, err
1✔
201
        }
1✔
202

203
        if err := a.EvaluateDelayed(ctx, info); err != nil {
27✔
204
                // Denied.
1✔
205
                return KeyboardInteractiveAuthMethodResponse{}, nil
1✔
206
        }
1✔
207
        // Allowed.
208
        return KeyboardInteractiveAuthMethodResponse{
25✔
209
                Allow: &extensions_ssh.KeyboardInteractiveAllowResponse{},
25✔
210
        }, nil
25✔
211
}
212

213
func (a *Auth) handleLogin(
214
        ctx context.Context,
215
        hostname string,
216
        publicKeyFingerprint []byte,
217
        querier KeyboardInteractiveQuerier,
218
) error {
27✔
219
        // Initiate the IdP login flow.
27✔
220
        idp, authenticator, err := a.getAuthenticator(ctx, hostname)
27✔
221
        if err != nil {
27✔
222
                return err
×
223
        }
×
224

225
        resp, err := authenticator.DeviceAuth(ctx)
27✔
226
        if err != nil {
27✔
227
                return err
×
228
        }
×
229

230
        // Prompt the user to sign in.
231
        if resp.VerificationURIComplete != "" {
54✔
232
                _, _ = querier.Prompt(ctx, &extensions_ssh.KeyboardInteractiveInfoPrompts{
27✔
233
                        Name:        "Please sign in with " + authenticator.Name() + " to continue",
27✔
234
                        Instruction: resp.VerificationURIComplete,
27✔
235
                        Prompts:     nil,
27✔
236
                })
27✔
237
        } else {
27✔
238
                _, _ = querier.Prompt(ctx, &extensions_ssh.KeyboardInteractiveInfoPrompts{
×
239
                        Name:        "Please sign in with " + authenticator.Name() + " and enter code " + resp.UserCode + " to continue",
×
240
                        Instruction: resp.VerificationURI,
×
241
                        Prompts:     nil,
×
242
                })
×
243
        }
×
244

245
        var sessionClaims identity.SessionClaims
27✔
246
        token, err := authenticator.DeviceAccessToken(ctx, resp, &sessionClaims)
27✔
247
        if err != nil {
27✔
248
                return err
×
249
        }
×
250
        sessionID, err := sessionIDFromFingerprint(publicKeyFingerprint)
27✔
251
        if err != nil {
28✔
252
                return err
1✔
253
        }
1✔
254
        return a.saveSession(ctx, idp.Id, sessionID, &sessionClaims, token)
26✔
255
}
256

257
var errAccessDenied = status.Error(codes.PermissionDenied, "access denied")
258

259
func (a *Auth) EvaluateDelayed(ctx context.Context, info StreamAuthInfo) error {
38✔
260
        req, err := sshRequestFromStreamAuthInfo(info)
38✔
261
        if err != nil {
38✔
262
                return err
×
263
        }
×
264
        res, err := a.evaluator.EvaluateSSH(ctx, info.StreamID, req)
38✔
265
        if err != nil {
38✔
266
                return err
×
267
        }
×
268

269
        if res.Allow.Value && !res.Deny.Value {
69✔
270
                return nil
31✔
271
        }
31✔
272
        return errAccessDenied
7✔
273
}
274

275
// EvaluatePortForward implements AuthInterface.
NEW
276
func (a *Auth) EvaluatePortForward(ctx context.Context, info StreamAuthInfo, portForwardInfo portforward.RouteInfo) error {
×
NEW
277
        // XXX: temporary stub
×
NEW
278
        _ = portForwardInfo
×
NEW
279
        req, err := sshRequestFromStreamAuthInfo(info)
×
NEW
280
        if err != nil {
×
NEW
281
                return err
×
NEW
282
        }
×
NEW
283
        req.UseUpstreamTunnelPolicy = true
×
NEW
284
        res, err := a.evaluator.EvaluateSSH(ctx, info.StreamID, req)
×
NEW
285
        if err != nil {
×
NEW
286
                return err
×
NEW
287
        }
×
288

NEW
289
        if res.Allow.Value && !res.Deny.Value {
×
NEW
290
                return nil
×
NEW
291
        }
×
NEW
292
        return errAccessDenied
×
293
}
294

295
func (a *Auth) FormatSession(ctx context.Context, info StreamAuthInfo) ([]byte, error) {
8✔
296
        sessionID, err := sessionIDFromFingerprint(info.PublicKeyFingerprintSha256)
8✔
297
        if err != nil {
9✔
298
                return nil, err
1✔
299
        }
1✔
300
        session, err := session.Get(ctx, a.evaluator.GetDataBrokerServiceClient(), sessionID)
7✔
301
        if err != nil {
7✔
302
                return nil, err
×
303
        }
×
304
        var b bytes.Buffer
7✔
305
        fmt.Fprintf(&b, "User ID:    %s\n", session.UserId)
7✔
306
        fmt.Fprintf(&b, "Session ID: %s\n", sessionID)
7✔
307
        fmt.Fprintf(&b, "Expires at: %s (in %s)\n",
7✔
308
                session.ExpiresAt.AsTime().String(),
7✔
309
                time.Until(session.ExpiresAt.AsTime()).Round(time.Second))
7✔
310
        fmt.Fprintf(&b, "Claims:\n")
7✔
311
        keys := make([]string, 0, len(session.Claims))
7✔
312
        for key := range session.Claims {
63✔
313
                keys = append(keys, key)
56✔
314
        }
56✔
315
        slices.Sort(keys)
7✔
316
        for _, key := range keys {
63✔
317
                fmt.Fprintf(&b, "  %s: ", key)
56✔
318
                vs := session.Claims[key].AsSlice()
56✔
319
                if len(vs) != 1 {
57✔
320
                        b.WriteRune('[')
1✔
321
                }
1✔
322
                if len(vs) == 1 {
111✔
323
                        switch key {
55✔
324
                        case "iat":
6✔
325
                                d, _ := vs[0].(float64)
6✔
326
                                t := time.Unix(int64(d), 0)
6✔
327
                                fmt.Fprintf(&b, "%s (%s ago)", t, time.Since(t).Round(time.Second))
6✔
328
                        case "exp":
6✔
329
                                d, _ := vs[0].(float64)
6✔
330
                                t := time.Unix(int64(d), 0)
6✔
331
                                fmt.Fprintf(&b, "%s (in %s)", t, time.Until(t).Round(time.Second))
6✔
332
                        default:
43✔
333
                                fmt.Fprintf(&b, "%#v", vs[0])
43✔
334
                        }
335
                } else if len(vs) > 1 {
2✔
336
                        for i, v := range vs {
3✔
337
                                fmt.Fprintf(&b, "%#v", v)
2✔
338
                                if i < len(vs)-1 {
3✔
339
                                        b.WriteString(", ")
1✔
340
                                }
1✔
341
                        }
342
                }
343
                if len(vs) != 1 {
57✔
344
                        b.WriteRune(']')
1✔
345
                }
1✔
346
                b.WriteRune('\n')
56✔
347
        }
348
        return b.Bytes(), nil
7✔
349
}
350

351
func (a *Auth) DeleteSession(ctx context.Context, info StreamAuthInfo) error {
8✔
352
        sessionID, err := sessionIDFromFingerprint(info.PublicKeyFingerprintSha256)
8✔
353
        if err != nil {
9✔
354
                return err
1✔
355
        }
1✔
356
        err = session.Delete(ctx, a.evaluator.GetDataBrokerServiceClient(), sessionID)
7✔
357
        a.evaluator.InvalidateCacheForRecords(ctx, &databroker.Record{
7✔
358
                Type: "type.googleapis.com/session.Session",
7✔
359
                Id:   sessionID,
7✔
360
        })
7✔
361
        return err
7✔
362
}
363

364
func (a *Auth) saveSession(
365
        ctx context.Context,
366
        idpID,
367
        id string,
368
        claims *identity.SessionClaims,
369
        token *oauth2.Token,
370
) error {
26✔
371
        now := time.Now()
26✔
372
        nowpb := timestamppb.New(now)
26✔
373
        sessionLifetime := a.currentConfig.Load().Options.CookieExpire
26✔
374

26✔
375
        h := sessions.Handle{ID: id}
26✔
376
        if err := claims.Claims.Claims(&h); err != nil {
26✔
377
                return err
×
378
        }
×
379

380
        sess := session.New(idpID, id)
26✔
381
        sess.UserId = h.UserID()
26✔
382
        sess.IssuedAt = nowpb
26✔
383
        sess.AccessedAt = nowpb
26✔
384
        sess.ExpiresAt = timestamppb.New(now.Add(sessionLifetime))
26✔
385
        sess.OauthToken = manager.ToOAuthToken(token)
26✔
386
        sess.Audience = h.Audience
26✔
387
        sess.SetRawIDToken(claims.RawIDToken)
26✔
388
        sess.AddClaims(claims.Flatten())
26✔
389

26✔
390
        u, _ := user.Get(ctx, a.evaluator.GetDataBrokerServiceClient(), sess.GetUserId())
26✔
391
        if u == nil {
26✔
392
                // if no user exists yet, create a new one
×
393
                u = &user.User{
×
394
                        Id: sess.GetUserId(),
×
395
                }
×
396
        }
×
397
        u.PopulateFromClaims(claims.Claims)
26✔
398
        resp, err := databroker.Put(ctx, a.evaluator.GetDataBrokerServiceClient(), u)
26✔
399
        if err != nil {
26✔
400
                return err
×
401
        }
×
402
        a.evaluator.InvalidateCacheForRecords(ctx, resp.GetRecord())
26✔
403

26✔
404
        resp, err = session.Put(ctx, a.evaluator.GetDataBrokerServiceClient(), sess)
26✔
405
        if err != nil {
26✔
406
                return err
×
407
        }
×
408
        a.evaluator.InvalidateCacheForRecords(ctx, resp.GetRecord())
26✔
409
        return nil
26✔
410
}
411

412
func (a *Auth) getAuthenticator(ctx context.Context, hostname string) (*identitypb.Provider, identity.Authenticator, error) {
27✔
413
        opts := a.currentConfig.Load().Options
27✔
414

27✔
415
        redirectURL, err := opts.GetAuthenticateRedirectURL()
27✔
416
        if err != nil {
27✔
417
                return nil, nil, err
×
418
        }
×
419

420
        idp, err := opts.GetIdentityProviderForPolicy(opts.GetRouteForSSHHostname(hostname))
27✔
421
        if err != nil {
27✔
422
                return nil, nil, err
×
423
        }
×
424

425
        authenticator, err := identity.GetIdentityProvider(ctx, a.tracerProvider, idp, redirectURL,
27✔
426
                opts.RuntimeFlags[config.RuntimeFlagRefreshSessionAtIDTokenExpiration])
27✔
427
        if err != nil {
27✔
428
                return nil, nil, err
×
429
        }
×
430

431
        return idp, authenticator, nil
27✔
432
}
433

434
var _ AuthInterface = (*Auth)(nil)
435

436
var errInvalidFingerprint = errors.New("invalid public key fingerprint")
437

438
func sessionIDFromFingerprint(sha256fingerprint []byte) (string, error) {
126✔
439
        if len(sha256fingerprint) != sha256.Size {
130✔
440
                return "", errInvalidFingerprint
4✔
441
        }
4✔
442
        return "sshkey-SHA256:" + base64.RawStdEncoding.EncodeToString(sha256fingerprint), nil
122✔
443
}
444

445
var errPublicKeyAllowNil = errors.New("expected PublicKeyAllow message not to be nil")
446

447
// Converts from StreamAuthInfo to an SSHRequest, assuming the PublicKeyAllow field is not nil.
448
func sshRequestFromStreamAuthInfo(info StreamAuthInfo) (*Request, error) {
38✔
449
        if info.PublicKeyAllow.Value == nil {
38✔
450
                return nil, errPublicKeyAllowNil
×
451
        }
×
452
        sessionID, err := sessionIDFromFingerprint(info.PublicKeyFingerprintSha256)
38✔
453
        if err != nil {
38✔
454
                return nil, err
×
455
        }
×
456

457
        return &Request{
38✔
458
                Username:      *info.Username,
38✔
459
                Hostname:      *info.Hostname,
38✔
460
                PublicKey:     info.PublicKeyAllow.Value.PublicKey,
38✔
461
                SessionID:     sessionID,
38✔
462
                SourceAddress: info.SourceAddress,
38✔
463

38✔
464
                LogOnlyIfDenied: info.InitialAuthComplete,
38✔
465
        }, nil
38✔
466
}
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