diff --git a/Makefile b/Makefile index 7fdcf8c66..5f6274464 100644 --- a/Makefile +++ b/Makefile @@ -40,7 +40,7 @@ all: clean build-deps test lint build ## Runs a clean, build, fmt, lint, test, a .PHONY: get-envoy get-envoy: ## Fetch envoy binaries @echo "==> $@" - @cd pkg/envoy/files && env -u GOOS go run ../get-envoy +# @cd pkg/envoy/files && env -u GOOS go run ../get-envoy .PHONY: deps-build deps-build: get-envoy ## Install build dependencies diff --git a/authenticate/handlers.go b/authenticate/handlers.go index df695f3dc..b49f1ccee 100644 --- a/authenticate/handlers.go +++ b/authenticate/handlers.go @@ -649,7 +649,7 @@ func (a *Authenticate) DeviceAuthLogin(w http.ResponseWriter, r *http.Request) e fmt.Fprintf(w, `{"token": "%s"}`, string(tokenJwt)) return nil } else { - authResp, err := authenticator.DeviceAuth(w, r) + authResp, err := authenticator.DeviceAuth(r.Context()) if err != nil { return httputil.NewError(http.StatusInternalServerError, fmt.Errorf("failed to get device code: %w", err)) diff --git a/authorize/authorize.go b/authorize/authorize.go index a25da96fe..6e9894771 100644 --- a/authorize/authorize.go +++ b/authorize/authorize.go @@ -6,6 +6,7 @@ import ( "context" "fmt" "slices" + "sync" "time" "github.com/rs/zerolog" @@ -36,6 +37,9 @@ type Authorize struct { tracerProvider oteltrace.TracerProvider tracer oteltrace.Tracer + + activeStreamsMu sync.Mutex + activeStreams []chan error } // New validates and creates a new Authorize service from a set of config options. @@ -48,6 +52,7 @@ func New(ctx context.Context, cfg *config.Config) (*Authorize, error) { globalCache: storage.NewGlobalCache(time.Minute), tracerProvider: tracerProvider, tracer: tracer, + activeStreams: []chan error{}, } a.accessTracker = NewAccessTracker(a, accessTrackerMaxSize, accessTrackerDebouncePeriod) @@ -154,6 +159,15 @@ func newPolicyEvaluator( // OnConfigChange updates internal structures based on config.Options func (a *Authorize) OnConfigChange(ctx context.Context, cfg *config.Config) { + a.activeStreamsMu.Lock() + // demo code + if cfg.Options.Routes[0].AllowAnyAuthenticatedUser == false { + for _, s := range a.activeStreams { + s <- fmt.Errorf("no longer authorized") + } + clear(a.activeStreams) + } + a.activeStreamsMu.Unlock() currentState := a.state.Load() a.currentOptions.Store(cfg.Options) if newState, err := newAuthorizeStateFromConfig(ctx, a.tracerProvider, cfg, a.store, currentState.evaluator); err != nil { diff --git a/authorize/grpc.go b/authorize/grpc.go index f7da2ab0a..1d239523f 100644 --- a/authorize/grpc.go +++ b/authorize/grpc.go @@ -9,13 +9,10 @@ import ( "strings" envoy_service_auth_v3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - "google.golang.org/protobuf/types/known/structpb" - "github.com/pomerium/pomerium/authorize/evaluator" "github.com/pomerium/pomerium/config" "github.com/pomerium/pomerium/config/envoyconfig" + "github.com/pomerium/pomerium/internal/httputil" "github.com/pomerium/pomerium/internal/log" "github.com/pomerium/pomerium/internal/sessions" @@ -24,6 +21,9 @@ import ( "github.com/pomerium/pomerium/pkg/grpc/user" "github.com/pomerium/pomerium/pkg/storage" "github.com/pomerium/pomerium/pkg/telemetry/requestid" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/structpb" ) // Check implements the envoy auth server gRPC endpoint. diff --git a/authorize/ssh_grpc.go b/authorize/ssh_grpc.go new file mode 100644 index 000000000..9769191d2 --- /dev/null +++ b/authorize/ssh_grpc.go @@ -0,0 +1,685 @@ +package authorize + +import ( + "encoding/binary" + "errors" + "fmt" + "io" + "net/url" + "slices" + "strings" + "sync/atomic" + "time" + + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + extensions_ssh "github.com/pomerium/envoy-custom/api/extensions/filters/network/ssh" + "github.com/pomerium/pomerium/config" + "github.com/pomerium/pomerium/internal/sessions" + "github.com/pomerium/pomerium/pkg/identity" + "github.com/pomerium/pomerium/pkg/identity/oauth" + gossh "golang.org/x/crypto/ssh" + "golang.org/x/sync/errgroup" + "google.golang.org/protobuf/types/known/anypb" + "google.golang.org/protobuf/types/known/timestamppb" + "google.golang.org/protobuf/types/known/wrapperspb" +) + +type StreamState struct { + Username string + Hostname string + PublicKey []byte + MethodsAuthenticated []string +} + +func (a *Authorize) ManageStream( + server extensions_ssh.StreamManagement_ManageStreamServer, +) error { + recvC := make(chan *extensions_ssh.ClientMessage, 32) + sendC := make(chan *extensions_ssh.ServerMessage, 32) + eg, ctx := errgroup.WithContext(server.Context()) + eg.Go(func() error { + defer close(recvC) + for { + req, err := server.Recv() + if err != nil { + if errors.Is(err, io.EOF) { + return nil + } + return err + } + recvC <- req + } + }) + + eg.Go(func() error { + for { + select { + case <-ctx.Done(): + return nil + case msg := <-sendC: + if err := server.Send(msg); err != nil { + if errors.Is(err, io.EOF) { + return nil + } + return err + } + } + } + }) + + var state StreamState + + deviceAuthSuccess := &atomic.Bool{} + + errC := make(chan error, 1) + a.activeStreamsMu.Lock() + a.activeStreams = append(a.activeStreams, errC) + a.activeStreamsMu.Unlock() + for { + select { + case err := <-errC: + + return err + case req, ok := <-recvC: + if !ok { + return nil + } + switch req := req.Message.(type) { + case *extensions_ssh.ClientMessage_Event: + switch event := req.Event.Event.(type) { + case *extensions_ssh.StreamEvent_DownstreamConnected: + fmt.Println("downstream connected") + _ = event + case nil: + } + case *extensions_ssh.ClientMessage_AuthRequest: + authReq := req.AuthRequest + fmt.Println("auth request") + if state.Username == "" { + state.Username = authReq.Username + } + if state.Hostname == "" { + state.Hostname = authReq.Hostname + } + switch authReq.AuthMethod { + case "publickey": + methodReq, _ := authReq.MethodRequest.UnmarshalNew() + pubkeyReq, ok := methodReq.(*extensions_ssh.PublicKeyMethodRequest) + if !ok { + return fmt.Errorf("client sent invalid auth request message") + } + + // + // validate public key here + // + + state.MethodsAuthenticated = append(state.MethodsAuthenticated, "publickey") + state.PublicKey = pubkeyReq.PublicKey + + if authReq.Username == "" && authReq.Hostname == "" { + pkData, _ := anypb.New(&extensions_ssh.PublicKeyAllowResponse{ + PublicKey: state.PublicKey, + Permissions: &extensions_ssh.Permissions{ + PermitPortForwarding: true, + PermitAgentForwarding: true, + PermitX11Forwarding: true, + PermitPty: true, + PermitUserRc: true, + ValidBefore: timestamppb.New(time.Now().Add(-1 * time.Minute)), + ValidAfter: timestamppb.New(time.Now().Add(12 * time.Hour)), + }, + }) + resp := extensions_ssh.ServerMessage{ + Message: &extensions_ssh.ServerMessage_AuthResponse{ + AuthResponse: &extensions_ssh.AuthenticationResponse{ + Response: &extensions_ssh.AuthenticationResponse_Allow{ + Allow: &extensions_ssh.AllowResponse{ + Username: state.Username, + Hostname: state.Hostname, + AllowedMethods: []*extensions_ssh.AllowedMethod{ + { + Method: "publickey", + MethodData: pkData, + }, + }, + Target: extensions_ssh.Target_Internal, + }, + }, + }, + }, + } + sendC <- &resp + continue + } + + if !slices.Contains(state.MethodsAuthenticated, "keyboard-interactive") { + resp := extensions_ssh.ServerMessage{ + Message: &extensions_ssh.ServerMessage_AuthResponse{ + AuthResponse: &extensions_ssh.AuthenticationResponse{ + Response: &extensions_ssh.AuthenticationResponse_Deny{ + Deny: &extensions_ssh.DenyResponse{ + Partial: true, + Methods: []string{"keyboard-interactive"}, + }, + }, + }, + }, + } + sendC <- &resp + } + case "keyboard-interactive": + opts := a.currentOptions.Load() + var route *config.Policy + for r := range opts.GetAllPolicies() { + if r.From == "ssh://"+strings.TrimSuffix(strings.Join([]string{state.Hostname, opts.SSHHostname}, "."), ".") { + route = r + break + } + } + if route == nil { + return fmt.Errorf("invalid route") + } + // sessionState := a.state.Load() + + idp, err := opts.GetIdentityProviderForPolicy(route) + if err != nil { + return err + } + authenticator, err := identity.NewAuthenticator(ctx, a.tracerProvider, oauth.Options{ + RedirectURL: &url.URL{}, + ProviderName: idp.GetType(), + ProviderURL: idp.GetUrl(), + ClientID: idp.GetClientId(), + ClientSecret: idp.GetClientSecret(), + Scopes: idp.GetScopes(), + AuthCodeOptions: idp.GetRequestParams(), + DeviceAuthClientType: idp.GetDeviceAuthClientType(), + }) + if err != nil { + return err + } + deviceAuthResp, err := authenticator.DeviceAuth(ctx) + if err != nil { + return err + } + infoReq := extensions_ssh.KeyboardInteractiveInfoPrompts{ + Name: "Sign in with " + idp.GetType(), + Instruction: deviceAuthResp.VerificationURIComplete, + Prompts: []*extensions_ssh.KeyboardInteractiveInfoPrompts_Prompt{ + {}, + }, + } + + infoReqAny, _ := anypb.New(&infoReq) + resp := extensions_ssh.ServerMessage{ + Message: &extensions_ssh.ServerMessage_AuthResponse{ + AuthResponse: &extensions_ssh.AuthenticationResponse{ + Response: &extensions_ssh.AuthenticationResponse_InfoRequest{ + InfoRequest: &extensions_ssh.InfoRequest{ + Method: "keyboard-interactive", + Request: infoReqAny, + }, + }, + }, + }, + } + sendC <- &resp + + go func() { + var claims identity.SessionClaims + + token, err := authenticator.DeviceAccessToken(ctx, deviceAuthResp, &claims) + if err != nil { + errC <- err + return + } + s := sessions.NewState(idp.Id) + err = claims.Claims.Claims(&s) + if err != nil { + errC <- fmt.Errorf("error unmarshaling session state: %w", err) + return + } + fmt.Println(token) + deviceAuthSuccess.Store(true) + }() + } + case *extensions_ssh.ClientMessage_InfoResponse: + resp := req.InfoResponse + if resp.Method == "keyboard-interactive" { + r, _ := resp.Response.UnmarshalNew() + respInfo, ok := r.(*extensions_ssh.KeyboardInteractiveInfoPromptResponses) + if ok { + fmt.Println(respInfo.Responses) + } + } + if deviceAuthSuccess.Load() { + state.MethodsAuthenticated = append(state.MethodsAuthenticated, "keyboard-interactive") + } else { + retryReq := extensions_ssh.KeyboardInteractiveInfoPrompts{ + Name: "", + Instruction: "Login not successful yet, try again", + Prompts: []*extensions_ssh.KeyboardInteractiveInfoPrompts_Prompt{ + {}, + }, + } + infoReqAny, _ := anypb.New(&retryReq) + + resp := extensions_ssh.ServerMessage{ + Message: &extensions_ssh.ServerMessage_AuthResponse{ + AuthResponse: &extensions_ssh.AuthenticationResponse{ + Response: &extensions_ssh.AuthenticationResponse_InfoRequest{ + InfoRequest: &extensions_ssh.InfoRequest{ + Method: "keyboard-interactive", + Request: infoReqAny, + }, + }, + }, + }, + } + sendC <- &resp + continue + } + if slices.Contains(state.MethodsAuthenticated, "publickey") { + pkData, _ := anypb.New(&extensions_ssh.PublicKeyAllowResponse{ + PublicKey: state.PublicKey, + Permissions: &extensions_ssh.Permissions{ + PermitPortForwarding: true, + PermitAgentForwarding: true, + PermitX11Forwarding: true, + PermitPty: true, + PermitUserRc: true, + ValidBefore: timestamppb.New(time.Now().Add(-1 * time.Minute)), + ValidAfter: timestamppb.New(time.Now().Add(12 * time.Hour)), + }, + }) + authResponse := extensions_ssh.ServerMessage{ + Message: &extensions_ssh.ServerMessage_AuthResponse{ + AuthResponse: &extensions_ssh.AuthenticationResponse{ + Response: &extensions_ssh.AuthenticationResponse_Allow{ + Allow: &extensions_ssh.AllowResponse{ + Username: state.Username, + Hostname: state.Hostname, + AllowedMethods: []*extensions_ssh.AllowedMethod{ + { + Method: "publickey", + MethodData: pkData, + }, + { + Method: "keyboard-interactive", + }, + }, + Target: extensions_ssh.Target_Upstream, + }, + }, + }, + }, + } + sendC <- &authResponse + } else { + resp := extensions_ssh.ServerMessage{ + Message: &extensions_ssh.ServerMessage_AuthResponse{ + AuthResponse: &extensions_ssh.AuthenticationResponse{ + Response: &extensions_ssh.AuthenticationResponse_Deny{ + Deny: &extensions_ssh.DenyResponse{ + Partial: true, + Methods: []string{"publickey"}, + }, + }, + }, + }, + } + sendC <- &resp + } + + case nil: + } + } + } + + return eg.Wait() +} + +// See RFC 4254, section 5.1. +const msgChannelOpen = 90 + +type channelOpenMsg struct { + ChanType string `sshtype:"90"` + PeersID uint32 + PeersWindow uint32 + MaxPacketSize uint32 + TypeSpecificData []byte `ssh:"rest"` +} + +const ( + msgChannelExtendedData = 95 + msgChannelData = 94 +) + +// Used for debug print outs of packets. +type channelDataMsg struct { + PeersID uint32 `sshtype:"94"` + Length uint32 + Rest []byte `ssh:"rest"` +} + +// See RFC 4254, section 5.1. +const msgChannelOpenConfirm = 91 + +type channelOpenConfirmMsg struct { + PeersID uint32 `sshtype:"91"` + MyID uint32 + MyWindow uint32 + MaxPacketSize uint32 + TypeSpecificData []byte `ssh:"rest"` +} + +const msgChannelRequest = 98 + +type channelRequestMsg struct { + PeersID uint32 `sshtype:"98"` + Request string + WantReply bool + RequestSpecificData []byte `ssh:"rest"` +} + +// See RFC 4254, section 5.4. +const msgChannelSuccess = 99 + +type channelRequestSuccessMsg struct { + PeersID uint32 `sshtype:"99"` +} + +// See RFC 4254, section 5.4. +const msgChannelFailure = 100 + +type channelRequestFailureMsg struct { + PeersID uint32 `sshtype:"100"` +} + +// See RFC 4254, section 5.3 +const msgChannelClose = 97 + +type channelCloseMsg struct { + PeersID uint32 `sshtype:"97"` +} + +// See RFC 4254, section 5.3 +const msgChannelEOF = 96 + +type channelEOFMsg struct { + PeersID uint32 `sshtype:"96"` +} + +func (a *Authorize) ServeChannel( + server extensions_ssh.StreamManagement_ServeChannelServer, +) error { + var program *tea.Program + inputR, inputW := io.Pipe() + outputR, outputW := io.Pipe() + var peerId uint32 + + var downstreamChannelInfo *extensions_ssh.SSHDownstreamChannelInfo + var downstreamPtyInfo *extensions_ssh.SSHDownstreamPTYInfo + for { + channelMsg, err := server.Recv() + if err != nil { + if errors.Is(err, io.EOF) { + return nil + } + return err + } + rawMsg := channelMsg.GetRawBytes().GetValue() + switch rawMsg[0] { + case msgChannelOpen: + var msg channelOpenMsg + gossh.Unmarshal(rawMsg, &msg) + + var confirm channelOpenConfirmMsg + peerId = msg.PeersID + confirm.PeersID = peerId + confirm.MyID = 1 + confirm.MyWindow = msg.PeersWindow + confirm.MaxPacketSize = msg.MaxPacketSize + downstreamChannelInfo = &extensions_ssh.SSHDownstreamChannelInfo{ + ChannelType: msg.ChanType, + DownstreamChannelId: confirm.PeersID, + InternalUpstreamChannelId: confirm.MyID, + InitialWindowSize: confirm.MyWindow, + MaxPacketSize: confirm.MaxPacketSize, + } + if err := server.Send(&extensions_ssh.ChannelMessage{ + Message: &extensions_ssh.ChannelMessage_RawBytes{ + RawBytes: &wrapperspb.BytesValue{ + Value: gossh.Marshal(confirm), + }, + }, + }); err != nil { + return err + } + + case msgChannelRequest: + var msg channelRequestMsg + gossh.Unmarshal(rawMsg, &msg) + + switch msg.Request { + case "pty-req": + req := parsePtyReq(msg.RequestSpecificData) + items := []list.Item{ + item("ubuntu@vm"), + item("joe@local"), + } + downstreamPtyInfo = &extensions_ssh.SSHDownstreamPTYInfo{ + TermEnv: req.TermEnv, + WidthColumns: req.Width, + HeightRows: req.Height, + WidthPx: req.WidthPx, + HeightPx: req.HeightPx, + Modes: req.Modes, + } + + const defaultWidth = 20 + + l := list.New(items, itemDelegate{}, defaultWidth, listHeight) + l.Title = "Connect to which server?" + l.SetShowStatusBar(false) + l.SetFilteringEnabled(false) + l.Styles.Title = titleStyle + l.Styles.PaginationStyle = paginationStyle + l.Styles.HelpStyle = helpStyle + + program = tea.NewProgram(model{list: l}, + tea.WithInput(inputR), + tea.WithOutput(outputW), + tea.WithAltScreen(), + tea.WithContext(server.Context()), + tea.WithEnvironment([]string{"TERM=" + req.TermEnv}), + ) + go func() { + answer, err := program.Run() + if err != nil { + return + } + username, hostname, _ := strings.Cut(answer.(model).choice, "@") + handOff, _ := anypb.New(&extensions_ssh.SSHChannelControlAction{ + Action: &extensions_ssh.SSHChannelControlAction_HandOff{ + HandOff: &extensions_ssh.SSHChannelControlAction_HandOffUpstream{ + DownstreamChannelInfo: downstreamChannelInfo, + DownstreamPtyInfo: downstreamPtyInfo, + UpstreamAuth: &extensions_ssh.AllowResponse{ + Username: username, + Hostname: hostname, + }, + }, + }, + }) + + if err := server.Send(&extensions_ssh.ChannelMessage{ + Message: &extensions_ssh.ChannelMessage_ChannelControl{ + ChannelControl: &extensions_ssh.ChannelControl{ + Protocol: "ssh", + ControlAction: handOff, + }, + }, + }); err != nil { + return + } + }() + go func() { + var buf [4096]byte + for { + n, err := outputR.Read(buf[:]) + if err != nil { + return + } + if err := server.Send(&extensions_ssh.ChannelMessage{ + Message: &extensions_ssh.ChannelMessage_RawBytes{ + RawBytes: &wrapperspb.BytesValue{ + Value: gossh.Marshal(channelDataMsg{ + PeersID: peerId, + Length: uint32(n), + Rest: buf[:n], + }), + }, + }, + }); err != nil { + return + } + } + }() + program.Send(tea.WindowSizeMsg{Width: int(req.Width), Height: int(req.Height)}) + + if err := server.Send(&extensions_ssh.ChannelMessage{ + Message: &extensions_ssh.ChannelMessage_RawBytes{ + RawBytes: &wrapperspb.BytesValue{ + Value: gossh.Marshal(channelRequestSuccessMsg{ + PeersID: peerId, + }), + }, + }, + }); err != nil { + return err + } + } + case msgChannelData: + var msg channelDataMsg + gossh.Unmarshal(rawMsg, &msg) + + if program != nil { + inputW.Write(msg.Rest) + } + case msgChannelClose: + var msg channelDataMsg + gossh.Unmarshal(rawMsg, &msg) + default: + panic("unhandled message: " + fmt.Sprint(rawMsg[1])) + } + } +} + +type ptyReq struct { + TermEnv string + Width, Height uint32 + WidthPx, HeightPx uint32 + Modes []byte +} + +func parsePtyReq(reqData []byte) ptyReq { + termEnvLen := binary.BigEndian.Uint32(reqData) + reqData = reqData[4:] + termEnv := string(reqData[:termEnvLen]) + reqData = reqData[termEnvLen:] + return ptyReq{ + TermEnv: termEnv, + Width: binary.BigEndian.Uint32(reqData), + Height: binary.BigEndian.Uint32(reqData[4:]), + WidthPx: binary.BigEndian.Uint32(reqData[8:]), + HeightPx: binary.BigEndian.Uint32(reqData[12:]), + Modes: reqData[16:], + } +} + +const listHeight = 14 + +var ( + titleStyle = lipgloss.NewStyle().MarginLeft(2) + itemStyle = lipgloss.NewStyle().PaddingLeft(4) + selectedItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("170")) + paginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(4) + helpStyle = list.DefaultStyles().HelpStyle.PaddingLeft(4).PaddingBottom(1) + quitTextStyle = lipgloss.NewStyle().Margin(1, 0, 2, 4) +) + +type item string + +func (i item) FilterValue() string { return "" } + +type itemDelegate struct{} + +func (d itemDelegate) Height() int { return 1 } +func (d itemDelegate) Spacing() int { return 0 } +func (d itemDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { return nil } +func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { + i, ok := listItem.(item) + if !ok { + return + } + + str := fmt.Sprintf("%d. %s", index+1, i) + + fn := itemStyle.Render + if index == m.Index() { + fn = func(s ...string) string { + return selectedItemStyle.Render("> " + strings.Join(s, " ")) + } + } + + fmt.Fprint(w, fn(str)) +} + +type model struct { + list list.Model + choice string + quitting bool +} + +func (m model) Init() tea.Cmd { + return nil +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.list.SetWidth(msg.Width) + return m, nil + + case tea.KeyMsg: + switch keypress := msg.String(); keypress { + case "q", "ctrl+c": + m.quitting = true + return m, tea.Quit + + case "enter": + i, ok := m.list.SelectedItem().(item) + if ok { + m.choice = string(i) + } + return m, tea.Quit + } + } + + var cmd tea.Cmd + m.list, cmd = m.list.Update(msg) + return m, cmd +} + +func (m model) View() string { + if m.choice != "" { + return quitTextStyle.Render(fmt.Sprintf("%s? Sounds good to me.", m.choice)) + } + if m.quitting { + return quitTextStyle.Render("Not hungry? That’s cool.") + } + return "\n" + m.list.View() +} diff --git a/config/envoyconfig/listeners.go b/config/envoyconfig/listeners.go index e02b39e3e..a02e12e57 100644 --- a/config/envoyconfig/listeners.go +++ b/config/envoyconfig/listeners.go @@ -65,6 +65,14 @@ func (b *Builder) BuildListeners( listeners = append(listeners, li) } + if shouldStartSSHListener(cfg.Options) { + li, err := b.buildSSHListener(ctx, cfg) + if err != nil { + return nil, err + } + listeners = append(listeners, li) + } + li, err := b.buildOutboundListener(cfg) if err != nil { return nil, err diff --git a/config/envoyconfig/listeners_grpc.go b/config/envoyconfig/listeners_grpc.go index 2f92b3de1..83442e737 100644 --- a/config/envoyconfig/listeners_grpc.go +++ b/config/envoyconfig/listeners_grpc.go @@ -121,3 +121,10 @@ func shouldStartGRPCListener(options *config.Options) bool { return config.IsAuthorize(options.Services) || config.IsDataBroker(options.Services) } + +func shouldStartSSHListener(options *config.Options) bool { + if options.SSHAddr == "" { + return false + } + return config.IsProxy(options.Services) +} diff --git a/config/envoyconfig/listeners_ssh.go b/config/envoyconfig/listeners_ssh.go new file mode 100644 index 000000000..2574692c9 --- /dev/null +++ b/config/envoyconfig/listeners_ssh.go @@ -0,0 +1,168 @@ +package envoyconfig + +import ( + "context" + "fmt" + "net/url" + "strings" + "time" + + xds_core_v3 "github.com/cncf/xds/go/xds/core/v3" + xds_matcher_v3 "github.com/cncf/xds/go/xds/type/matcher/v3" + envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + envoy_config_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" + envoy_generic_proxy_action_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/generic_proxy/action/v3" + envoy_generic_proxy_matcher_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/generic_proxy/matcher/v3" + envoy_generic_router_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/generic_proxy/router/v3" + envoy_generic_proxy_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/generic_proxy/v3" + matcherv3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3" + extensions_ssh "github.com/pomerium/envoy-custom/api/extensions/filters/network/ssh" + "github.com/pomerium/pomerium/config" + "google.golang.org/protobuf/types/known/durationpb" +) + +func (b *Builder) buildSSHListener(ctx context.Context, cfg *config.Config) (*envoy_config_listener_v3.Listener, error) { + rc, err := b.buildRouteConfig(ctx, cfg) + if err != nil { + return nil, err + } + var sshHostKeys []*extensions_ssh.HostKeyPair + for _, hk := range cfg.Options.SSHHostKeys { + sshHostKeys = append(sshHostKeys, &extensions_ssh.HostKeyPair{ + PublicKeyFile: hk.PublicKeyFile, + PrivateKeyFile: hk.PrivateKeyFile, + }) + } + var grpcClientTimeout *durationpb.Duration + if cfg.Options.GRPCClientTimeout != 0 { + grpcClientTimeout = durationpb.New(cfg.Options.GRPCClientTimeout) + } else { + grpcClientTimeout = durationpb.New(30 * time.Second) + } + li := &envoy_config_listener_v3.Listener{ + Name: "ssh", + Address: buildTCPAddress(cfg.Options.SSHAddr, 22), + FilterChains: []*envoy_config_listener_v3.FilterChain{ + { + Filters: []*envoy_config_listener_v3.Filter{ + { + Name: "generic_proxy", + ConfigType: &envoy_config_listener_v3.Filter_TypedConfig{ + TypedConfig: marshalAny(&envoy_generic_proxy_v3.GenericProxy{ + StatPrefix: "ssh", + CodecConfig: &envoy_config_core_v3.TypedExtensionConfig{ + Name: "envoy.generic_proxy.codecs.ssh", + TypedConfig: marshalAny(&extensions_ssh.CodecConfig{ + HostKeys: sshHostKeys, + UserCaKey: &extensions_ssh.HostKeyPair{ + PublicKeyFile: cfg.Options.SSHUserCAKey.PublicKeyFile, + PrivateKeyFile: cfg.Options.SSHUserCAKey.PrivateKeyFile, + }, + GrpcService: &envoy_config_core_v3.GrpcService{ + Timeout: grpcClientTimeout, + TargetSpecifier: &envoy_config_core_v3.GrpcService_EnvoyGrpc_{ + EnvoyGrpc: &envoy_config_core_v3.GrpcService_EnvoyGrpc{ + ClusterName: "pomerium-authorize", + }, + }, + }, + }), + }, + Filters: []*envoy_config_core_v3.TypedExtensionConfig{ + { + Name: "envoy.filters.generic.router", + TypedConfig: marshalAny(&envoy_generic_router_v3.Router{ + BindUpstreamConnection: true, + }), + }, + }, + RouteSpecifier: &envoy_generic_proxy_v3.GenericProxy_RouteConfig{ + RouteConfig: rc, + }, + }), + }, + }, + }, + }, + }, + } + return li, nil +} + +func (b *Builder) buildRouteConfig(_ context.Context, cfg *config.Config) (*envoy_generic_proxy_v3.RouteConfiguration, error) { + routeMatchers := []*xds_matcher_v3.Matcher_MatcherList_FieldMatcher{} + for route := range cfg.Options.GetAllPolicies() { + from, err := url.Parse(route.From) + if err != nil { + return nil, err + } + if from.Scheme != "ssh" { + continue + } + fromHost := from.Hostname() + if cfg.Options.SSHHostname != "" { + suffix := "." + cfg.Options.SSHHostname + fromHost = strings.TrimSuffix(fromHost, suffix) + } + if len(route.To) > 1 { + return nil, fmt.Errorf("only one 'to' entry allowed for ssh routes") + } + to := route.To[0].URL + if to.Scheme != "ssh" { + return nil, fmt.Errorf("'to' route url must have ssh scheme") + } + clusterId := getClusterID(route) + routeMatchers = append(routeMatchers, &xds_matcher_v3.Matcher_MatcherList_FieldMatcher{ + Predicate: &xds_matcher_v3.Matcher_MatcherList_Predicate{ + MatchType: &xds_matcher_v3.Matcher_MatcherList_Predicate_SinglePredicate_{ + SinglePredicate: &xds_matcher_v3.Matcher_MatcherList_Predicate_SinglePredicate{ + Input: &xds_core_v3.TypedExtensionConfig{ + Name: "request", + TypedConfig: marshalAny(&envoy_generic_proxy_matcher_v3.RequestMatchInput{}), + }, + Matcher: &xds_matcher_v3.Matcher_MatcherList_Predicate_SinglePredicate_CustomMatch{ + CustomMatch: &xds_core_v3.TypedExtensionConfig{ + Name: "request", + TypedConfig: marshalAny(&envoy_generic_proxy_matcher_v3.RequestMatcher{ + Host: &matcherv3.StringMatcher{ + MatchPattern: &matcherv3.StringMatcher_Exact{ + Exact: fromHost, + }, + }, + }), + }, + }, + }, + }, + }, + OnMatch: &xds_matcher_v3.Matcher_OnMatch{ + OnMatch: &xds_matcher_v3.Matcher_OnMatch_Action{ + Action: &xds_core_v3.TypedExtensionConfig{ + Name: "route", + TypedConfig: marshalAny(&envoy_generic_proxy_action_v3.RouteAction{ + ClusterSpecifier: &envoy_generic_proxy_action_v3.RouteAction_Cluster{ + Cluster: clusterId, + }, + }), + }, + }, + }, + }) + } + return &envoy_generic_proxy_v3.RouteConfiguration{ + Name: "route_config", + VirtualHosts: []*envoy_generic_proxy_v3.VirtualHost{ + { + Name: "ssh", + Hosts: []string{"*"}, + Routes: &xds_matcher_v3.Matcher{ + MatcherType: &xds_matcher_v3.Matcher_MatcherList_{ + MatcherList: &xds_matcher_v3.Matcher_MatcherList{ + Matchers: routeMatchers, + }, + }, + }, + }, + }, + }, nil +} diff --git a/config/envoyconfig/routes.go b/config/envoyconfig/routes.go index 4bbb10263..3ba942599 100644 --- a/config/envoyconfig/routes.go +++ b/config/envoyconfig/routes.go @@ -214,6 +214,11 @@ func (b *Builder) buildRoutesForPoliciesWithCatchAll( return nil, err } + // TODO + if fromURL.Scheme == "ssh" { + continue + } + if !strings.Contains(fromURL.Host, "*") { continue } diff --git a/config/options.go b/config/options.go index 06a350b89..94c15ed05 100644 --- a/config/options.go +++ b/config/options.go @@ -230,6 +230,11 @@ type Options struct { GRPCClientTimeout time.Duration `mapstructure:"grpc_client_timeout" yaml:"grpc_client_timeout,omitempty"` + SSHAddr string `mapstructure:"ssh_address" yaml:"ssh_address,omitempty"` + SSHHostname string `mapstructure:"ssh_hostname" yaml:"ssh_hostname,omitempty"` + SSHHostKeys []SSHKeyPair `mapstructure:"ssh_host_keys" yaml:"ssh_host_keys,omitempty"` + SSHUserCAKey SSHKeyPair `mapstructure:"ssh_user_ca_key" yaml:"ssh_user_ca_key,omitempty"` + // DataBrokerURLString is the routable destination of the databroker service's gRPC endpoint. DataBrokerURLString string `mapstructure:"databroker_service_url" yaml:"databroker_service_url,omitempty"` DataBrokerURLStrings []string `mapstructure:"databroker_service_urls" yaml:"databroker_service_urls,omitempty"` @@ -290,6 +295,11 @@ type certificateFilePair struct { KeyFile string `mapstructure:"key" yaml:"key,omitempty"` } +type SSHKeyPair struct { + PublicKeyFile string `mapstructure:"public_key_file" yaml:"public_key_file,omitempty"` + PrivateKeyFile string `mapstructure:"private_key_file" yaml:"private_key_file,omitempty"` +} + // DefaultOptions are the default configuration options for pomerium var defaultOptions = Options{ LogLevel: LogLevelInfo, @@ -1298,6 +1308,11 @@ func (o *Options) GetAllRouteableHTTPHosts() ([]string, error) { return nil, err } + // TODO + if fromURL.Scheme == "ssh" { + continue + } + hosts.InsertSlice(urlutil.GetDomainsForURL(fromURL, !o.IsRuntimeFlagSet(RuntimeFlagMatchAnyIncomingPort))) if policy.TLSDownstreamServerName != "" { tlsURL := fromURL.ResolveReference(&url.URL{Host: policy.TLSDownstreamServerName}) diff --git a/go.mod b/go.mod index d2ef85ecd..bba50f855 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,13 @@ require ( github.com/bits-and-blooms/bitset v1.20.0 github.com/caddyserver/certmagic v0.21.4 github.com/cenkalti/backoff/v4 v4.3.0 + github.com/charmbracelet/bubbles v0.20.0 + github.com/charmbracelet/bubbletea v1.3.3 + github.com/charmbracelet/lipgloss v1.0.0 + github.com/charmbracelet/x/ansi v0.8.0 + github.com/charmbracelet/x/term v0.2.1 github.com/cloudflare/circl v1.5.0 + github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3 github.com/coreos/go-oidc/v3 v3.11.0 github.com/docker/docker v27.4.1+incompatible github.com/envoyproxy/go-control-plane/envoy v1.32.3 @@ -88,8 +94,8 @@ require ( golang.org/x/crypto v0.31.0 golang.org/x/net v0.33.0 golang.org/x/oauth2 v0.24.0 - golang.org/x/sync v0.10.0 - golang.org/x/sys v0.28.0 + golang.org/x/sync v0.11.0 + golang.org/x/sys v0.30.0 golang.org/x/time v0.8.0 google.golang.org/api v0.214.0 google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 @@ -118,6 +124,7 @@ require ( github.com/agnivade/levenshtein v1.2.0 // indirect github.com/andybalholm/brotli v1.0.5 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/atotto/clipboard v0.1.4 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.17.48 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.22 // indirect @@ -133,10 +140,10 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.7 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.33.3 // indirect github.com/aws/smithy-go v1.22.1 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/caddyserver/zerossl v0.1.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect github.com/cpuguy83/dockercfg v0.3.2 // indirect @@ -146,6 +153,7 @@ require ( github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/fxamacker/cbor/v2 v2.6.0 // indirect @@ -179,10 +187,13 @@ require ( github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/kralicky/go-adaptive-radix-tree v0.0.0-20240624235931-330eb762e74c // indirect github.com/libdns/libdns v0.2.2 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/miekg/dns v1.1.62 // indirect github.com/minio/md5-simd v1.1.2 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect @@ -192,6 +203,9 @@ require ( github.com/moby/sys/userns v0.1.0 // indirect github.com/moby/term v0.5.0 // indirect github.com/morikuni/aec v1.0.0 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.15.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/onsi/ginkgo v1.16.5 // indirect github.com/onsi/ginkgo/v2 v2.19.1 // indirect @@ -206,9 +220,11 @@ require ( github.com/prometheus/statsd_exporter v0.22.7 // indirect github.com/quic-go/qpack v0.5.1 // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/rs/xid v1.6.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sahilm/fuzzy v0.1.1 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/sourcegraph/conc v0.3.0 // indirect @@ -246,3 +262,5 @@ require ( gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) + +replace github.com/pomerium/envoy-custom => ../envoy-custom diff --git a/go.sum b/go.sum index 029cdd535..df90c3d19 100644 --- a/go.sum +++ b/go.sum @@ -96,6 +96,8 @@ github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7D github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aws/aws-sdk-go-v2 v1.32.7 h1:ky5o35oENWi0JYWUZkB7WYvVPP+bcRF5/Iq7JWSb5Rw= github.com/aws/aws-sdk-go-v2 v1.32.7/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 h1:lL7IfaFzngfx0ZwUGOZdsFFnQ5uLvR0hWqqhyE7Q9M8= @@ -132,6 +134,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.33.3 h1:Xgv/hyNgvLda/M9l9qxXc4UFSgpp github.com/aws/aws-sdk-go-v2/service/sts v1.33.3/go.mod h1:5Gn+d+VaaRgsjewpMvGazt0WfcFO+Md4wLOuBfGR9Bc= github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro= github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -155,6 +159,16 @@ github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= +github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= +github.com/charmbracelet/bubbletea v1.3.3 h1:WpU6fCY0J2vDWM3zfS3vIDi/ULq3SYphZhkAGGvmEUY= +github.com/charmbracelet/bubbletea v1.3.3/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo= +github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= +github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= +github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= +github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -208,6 +222,8 @@ github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJP github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM= github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/exaring/otelpgx v0.8.0 h1:uqoDIW9qKkyz479z2cGrmJ8OJypydyEA+xwey4ukvNo= github.com/exaring/otelpgx v0.8.0/go.mod h1:ANkRZDfgfmN6yJS1xKMkshbnsHO8at5sYwtVEYOX8hc= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -431,6 +447,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s= github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae h1:dIZY4ULFcto4tAFlj1FYZl8ztUZ13bdq+PLY+NOfbyI= github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -443,6 +461,10 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mholt/acmez/v2 v2.0.3 h1:CgDBlEwg3QBp6s45tPQmFIBrkRIkBT4rW4orMM6p4sw= github.com/mholt/acmez/v2 v2.0.3/go.mod h1:pQ1ysaDeGrIMvJ9dfJMk5kJNkn7L2sb3UhyrX6Q91cw= @@ -475,6 +497,12 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= @@ -524,8 +552,6 @@ github.com/pomerium/csrf v1.7.0 h1:Qp4t6oyEod3svQtKfJZs589mdUTWKVf7q0PgCKYCshY= github.com/pomerium/csrf v1.7.0/go.mod h1:hAPZV47mEj2T9xFs+ysbum4l7SF1IdrryYaY6PdoIqw= github.com/pomerium/datasource v0.18.2-0.20221108160055-c6134b5ed524 h1:3YQY1sb54tEEbr0L73rjHkpLB0IB6qh3zl1+XQbMLis= github.com/pomerium/datasource v0.18.2-0.20221108160055-c6134b5ed524/go.mod h1:7fGbUYJnU8RcxZJvUvhukOIBv1G7LWDAHMfDxAf5+Y0= -github.com/pomerium/envoy-custom v1.32.4-0.20250114182541-6f6d2147bea6 h1:QLVgpx23jcbgR9qJzIicJ+uXGjQXO0GAy55SCo0Jd9o= -github.com/pomerium/envoy-custom v1.32.4-0.20250114182541-6f6d2147bea6/go.mod h1:afbaKE6YfshVUOrYc6XWUWfZcXencWmi1jTc00ki0Oo= github.com/pomerium/protoutil v0.0.0-20240813175624-47b7ac43ff46 h1:NRTg8JOXCxcIA1lAgD74iYud0rbshbWOB3Ou4+Huil8= github.com/pomerium/protoutil v0.0.0-20240813175624-47b7ac43ff46/go.mod h1:QqZmx6ZgPxz18va7kqoT4t/0yJtP7YFIDiT/W2n2fZ4= github.com/pomerium/webauthn v0.0.0-20240603205124-0428df511172 h1:TqoPqRgXSHpn+tEJq6H72iCS5pv66j3rPprThUEZg0E= @@ -573,6 +599,9 @@ github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KW github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= @@ -588,6 +617,8 @@ github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6ke github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= +github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= @@ -853,8 +884,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -900,6 +931,7 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -915,8 +947,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= diff --git a/internal/telemetry/trace/client_test.go b/internal/telemetry/trace/client_test.go index ec4771048..8958b7816 100644 --- a/internal/telemetry/trace/client_test.go +++ b/internal/telemetry/trace/client_test.go @@ -24,6 +24,7 @@ import ( "github.com/stretchr/testify/require" "go.opentelemetry.io/otel" tracev1 "go.opentelemetry.io/proto/otlp/trace/v1" + v1 "go.opentelemetry.io/proto/otlp/trace/v1" "go.uber.org/mock/gomock" ) @@ -283,6 +284,32 @@ func TestSyncClient(t *testing.T) { assert.ErrorIs(t, sc.Stop(context.Background()), trace.ErrNoClient) assert.NoError(t, sc.Update(context.Background(), nil)) }) + + t.Run("repeated updates", func(t *testing.T) { + sc := trace.NewSyncClient(nil) + for range 1000 { + sc.Update(context.Background(), &sleepClient{}) + } + }) +} + +type sleepClient struct{} + +// Start implements otlptrace.Client. +func (n sleepClient) Start(context.Context) error { + time.Sleep(10 * time.Millisecond) + return nil +} + +// Stop implements otlptrace.Client. +func (n sleepClient) Stop(context.Context) error { + time.Sleep(10 * time.Millisecond) + return nil +} + +// UploadTraces implements otlptrace.Client. +func (n sleepClient) UploadTraces(context.Context, []*v1.ResourceSpans) error { + return nil } type errHandler struct { diff --git a/internal/urlutil/url.go b/internal/urlutil/url.go index ecc853bd5..f227c3359 100644 --- a/internal/urlutil/url.go +++ b/internal/urlutil/url.go @@ -115,6 +115,7 @@ func GetDomainsForURL(u *url.URL, includeDefaultPort bool) []string { // tcp+https://ssh.example.com:22 // udp+https://ssh.example.com:22 + // ssh://ssh.example.com:22 // => ssh.example.com:22 // tcp+https://proxy.example.com/ssh.example.com:22 // udp+https://proxy.example.com/ssh.example.com:22 @@ -130,10 +131,13 @@ func GetDomainsForURL(u *url.URL, includeDefaultPort bool) []string { } var defaultPort string - if u.Scheme == "http" { + switch u.Scheme { + case "http": defaultPort = "80" - } else { + case "https": defaultPort = "443" + case "ssh": + defaultPort = "22" } // for hosts like 'example.com:1234' we only return one route diff --git a/pkg/cmd/pomerium/pomerium.go b/pkg/cmd/pomerium/pomerium.go index 01c45531b..98b079f83 100644 --- a/pkg/cmd/pomerium/pomerium.go +++ b/pkg/cmd/pomerium/pomerium.go @@ -9,6 +9,8 @@ import ( "sync" envoy_service_auth_v3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" + extensions_ssh "github.com/pomerium/envoy-custom/api/extensions/filters/network/ssh" + "go.uber.org/automaxprocs/maxprocs" "golang.org/x/sync/errgroup" @@ -267,6 +269,7 @@ func setupAuthorize(ctx context.Context, src config.Source, controlPlane *contro return nil, fmt.Errorf("error creating authorize service: %w", err) } envoy_service_auth_v3.RegisterAuthorizationServer(controlPlane.GRPCServer, svc) + extensions_ssh.RegisterStreamManagementServer(controlPlane.GRPCServer, svc) log.Ctx(ctx).Info().Msg("enabled authorize service") src.OnConfigChange(ctx, svc.OnConfigChange) diff --git a/pkg/envoy/envoy_linux.go b/pkg/envoy/envoy_linux.go index 88c3a13e5..4c3f2f644 100644 --- a/pkg/envoy/envoy_linux.go +++ b/pkg/envoy/envoy_linux.go @@ -85,6 +85,7 @@ func (srv *Server) prepareRunEnvoyCommand(ctx context.Context, sharedArgs []stri "--drain-time-s", "60", "--parent-shutdown-time-s", "120", "--drain-strategy", "immediate", + "--component-log-level", "matcher:trace,filter:debug", ) restartEpoch.value++ } else { diff --git a/pkg/envoy/extract.go b/pkg/envoy/extract.go index 0645feada..59dda160b 100644 --- a/pkg/envoy/extract.go +++ b/pkg/envoy/extract.go @@ -1,9 +1,6 @@ package envoy import ( - "bytes" - "crypto/sha256" - "encoding/hex" "fmt" "hash" "io" @@ -13,8 +10,6 @@ import ( "sync" "github.com/rs/zerolog/log" - - "github.com/pomerium/pomerium/pkg/envoy/files" ) const ( @@ -76,34 +71,6 @@ func Extract() (fullEnvoyPath string, err error) { return setupFullEnvoyPath, setupErr } -func extract(dstName string) (err error) { - checksum, err := hex.DecodeString(strings.Fields(files.Checksum())[0]) - if err != nil { - return fmt.Errorf("checksum %s: %w", files.Checksum(), err) - } - - hr := &hashReader{ - Hash: sha256.New(), - r: bytes.NewReader(files.Binary()), - } - - dst, err := os.OpenFile(dstName, os.O_CREATE|os.O_WRONLY, ownerRX) - if err != nil { - return err - } - defer func() { err = dst.Close() }() - - if _, err = io.Copy(dst, io.LimitReader(hr, maxExpandedEnvoySize)); err != nil { - return err - } - - sum := hr.Sum(nil) - if !bytes.Equal(sum, checksum) { - return fmt.Errorf("expected %x, got %x checksum", checksum, sum) - } - return nil -} - func cleanTempDir(tmpDir string) { d, err := os.Open(tmpDir) if err != nil { diff --git a/pkg/envoy/extract_debug_local.go b/pkg/envoy/extract_debug_local.go new file mode 100644 index 000000000..12ffa608e --- /dev/null +++ b/pkg/envoy/extract_debug_local.go @@ -0,0 +1,27 @@ +//go:build debug_local_envoy + +package envoy + +import ( + "os" + "path/filepath" + + "github.com/pomerium/pomerium/pkg/envoy/files" +) + +var DebugLocalEnvoyPath string + +func init() { + files.SetFiles(nil, " _", "") +} + +func extract(dstName string) (err error) { + if DebugLocalEnvoyPath == "" { + panic("DebugLocalEnvoyPath not set") + } + fullPath, err := filepath.EvalSymlinks(DebugLocalEnvoyPath) + if err != nil { + panic(err) + } + return os.Symlink(fullPath, dstName) +} diff --git a/pkg/envoy/extract_embed.go b/pkg/envoy/extract_embed.go new file mode 100644 index 000000000..99402cc73 --- /dev/null +++ b/pkg/envoy/extract_embed.go @@ -0,0 +1,43 @@ +//go:build !debug_local_envoy + +package envoy + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "os" + "strings" + + "github.com/pomerium/pomerium/pkg/envoy/files" +) + +func extract(dstName string) (err error) { + checksum, err := hex.DecodeString(strings.Fields(files.Checksum())[0]) + if err != nil { + return fmt.Errorf("checksum %s: %w", files.Checksum(), err) + } + + hr := &hashReader{ + Hash: sha256.New(), + r: bytes.NewReader(files.Binary()), + } + + dst, err := os.OpenFile(dstName, os.O_CREATE|os.O_WRONLY, ownerRX) + if err != nil { + return err + } + defer func() { err = dst.Close() }() + + if _, err = io.Copy(dst, io.LimitReader(hr, maxExpandedEnvoySize)); err != nil { + return err + } + + sum := hr.Sum(nil) + if !bytes.Equal(sum, checksum) { + return fmt.Errorf("expected %x, got %x checksum", checksum, sum) + } + return nil +} diff --git a/pkg/envoy/files/files_darwin_amd64.go b/pkg/envoy/files/files_darwin_amd64.go index faf90da6a..bad7987da 100644 --- a/pkg/envoy/files/files_darwin_amd64.go +++ b/pkg/envoy/files/files_darwin_amd64.go @@ -1,4 +1,4 @@ -//go:build darwin && amd64 && !embed_pomerium +//go:build darwin && amd64 && !embed_pomerium && !debug_local_envoy package files diff --git a/pkg/envoy/files/files_darwin_arm64.go b/pkg/envoy/files/files_darwin_arm64.go index c54c40833..9ae8eda00 100644 --- a/pkg/envoy/files/files_darwin_arm64.go +++ b/pkg/envoy/files/files_darwin_arm64.go @@ -1,5 +1,5 @@ -//go:build darwin && arm64 && !embed_pomerium -// +build darwin,arm64,!embed_pomerium +//go:build darwin && arm64 && !embed_pomerium && !debug_local_envoy +// +build darwin,arm64,!embed_pomerium,!debug_local_envoy package files diff --git a/pkg/envoy/files/files_external.go b/pkg/envoy/files/files_external.go index 3d1b29ab0..f21b58976 100644 --- a/pkg/envoy/files/files_external.go +++ b/pkg/envoy/files/files_external.go @@ -1,4 +1,4 @@ -//go:build embed_pomerium +//go:build embed_pomerium || debug_local_envoy package files diff --git a/pkg/envoy/files/files_linux_amd64.go b/pkg/envoy/files/files_linux_amd64.go index 131b51e50..b318f2dde 100644 --- a/pkg/envoy/files/files_linux_amd64.go +++ b/pkg/envoy/files/files_linux_amd64.go @@ -1,5 +1,5 @@ -//go:build linux && amd64 && !embed_pomerium -// +build linux,amd64,!embed_pomerium +//go:build linux && amd64 && !embed_pomerium && !debug_local_envoy +// +build linux,amd64,!embed_pomerium,!debug_local_envoy package files diff --git a/pkg/envoy/files/files_linux_arm64.go b/pkg/envoy/files/files_linux_arm64.go index 39e10f33c..1a4d7327c 100644 --- a/pkg/envoy/files/files_linux_arm64.go +++ b/pkg/envoy/files/files_linux_arm64.go @@ -1,5 +1,5 @@ -//go:build linux && arm64 && !embed_pomerium -// +build linux,arm64,!embed_pomerium +//go:build linux && arm64 && !embed_pomerium && !debug_local_envoy +// +build linux,arm64,!embed_pomerium,!debug_local_envoy package files diff --git a/pkg/grpc/config/config.proto b/pkg/grpc/config/config.proto index d90730f5b..d44e12f25 100644 --- a/pkg/grpc/config/config.proto +++ b/pkg/grpc/config/config.proto @@ -1,39 +1,43 @@ syntax = "proto3"; package pomerium.config; -option go_package = "github.com/pomerium/pomerium/pkg/grpc/config"; -import "google/protobuf/duration.proto"; -import "google/protobuf/struct.proto"; import "envoy/config/cluster/v3/cluster.proto"; import "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/struct.proto"; +import "google/protobuf/timestamp.proto"; + +option go_package = "github.com/pomerium/pomerium/pkg/grpc/config"; message Config { - string name = 1; - repeated Route routes = 2; - Settings settings = 3; + string name = 1; + repeated Route routes = 2; + Settings settings = 3; } message RouteRewriteHeader { string header = 1; - oneof matcher { string prefix = 3; } + oneof matcher { + string prefix = 3; + } string value = 2; } message RouteRedirect { - optional bool https_redirect = 1; + optional bool https_redirect = 1; optional string scheme_redirect = 2; - optional string host_redirect = 3; - optional uint32 port_redirect = 4; - optional string path_redirect = 5; - optional string prefix_rewrite = 6; - optional int32 response_code = 7; - optional bool strip_query = 8; + optional string host_redirect = 3; + optional uint32 port_redirect = 4; + optional string path_redirect = 5; + optional string prefix_rewrite = 6; + optional int32 response_code = 7; + optional bool strip_query = 8; } message RouteDirectResponse { uint32 status = 1; - string body = 2; + string body = 2; } enum IssuerFormat { @@ -47,13 +51,13 @@ enum IssuerFormat { // Next ID: 69. message Route { - string name = 1; + string name = 1; string description = 67; - string logo_url = 68; + string logo_url = 68; - string from = 2; - repeated string to = 3; - RouteRedirect redirect = 34; + string from = 2; + repeated string to = 3; + RouteRedirect redirect = 34; RouteDirectResponse response = 62; // https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/endpoint/v3/endpoint_components.proto#envoy-v3-api-msg-config-endpoint-v3-lbendpoint @@ -63,87 +67,88 @@ message Route { // len(load_balancing_weights) repeated uint32 load_balancing_weights = 37; - repeated string allowed_users = 4 [ deprecated = true ]; + repeated string allowed_users = 4 [deprecated = true]; // repeated string allowed_groups = 5 [ deprecated = true ]; - repeated string allowed_domains = 6 [ deprecated = true ]; - map allowed_idp_claims = 32 - [ deprecated = true ]; + repeated string allowed_domains = 6 [deprecated = true]; + map allowed_idp_claims = 32 [deprecated = true]; string prefix = 7; - string path = 8; - string regex = 9; + string path = 8; + string regex = 9; - string prefix_rewrite = 29; - string regex_rewrite_pattern = 30; - string regex_rewrite_substitution = 31; - optional int64 regex_priority_order = 61; + string prefix_rewrite = 29; + string regex_rewrite_pattern = 30; + string regex_rewrite_substitution = 31; + optional int64 regex_priority_order = 61; - bool cors_allow_preflight = 10; - bool allow_public_unauthenticated_access = 11; - bool allow_any_authenticated_user = 33; - google.protobuf.Duration timeout = 12; - google.protobuf.Duration idle_timeout = 43; - bool allow_websockets = 13; - bool allow_spdy = 44; + bool cors_allow_preflight = 10; + bool allow_public_unauthenticated_access = 11; + bool allow_any_authenticated_user = 33; + google.protobuf.Duration timeout = 12; + google.protobuf.Duration idle_timeout = 43; + bool allow_websockets = 13; + bool allow_spdy = 44; - bool tls_skip_verify = 14; - string tls_server_name = 15; - string tls_upstream_server_name = 57; + bool tls_skip_verify = 14; + string tls_server_name = 15; + string tls_upstream_server_name = 57; string tls_downstream_server_name = 58; - string tls_custom_ca = 16; - string tls_custom_ca_file = 17; + string tls_custom_ca = 16; + string tls_custom_ca_file = 17; - string tls_client_cert = 18; - string tls_client_key = 19; - string tls_client_cert_file = 20; - string tls_client_key_file = 21; - string tls_downstream_client_ca = 38; + string tls_client_cert = 18; + string tls_client_key = 19; + string tls_client_cert_file = 20; + string tls_client_key_file = 21; + string tls_downstream_client_ca = 38; string tls_downstream_client_ca_file = 39; bool tls_upstream_allow_renegotiation = 60; - map set_request_headers = 22; - repeated string remove_request_headers = 23; - map set_response_headers = 41; + map set_request_headers = 22; + repeated string remove_request_headers = 23; + map set_response_headers = 41; repeated RouteRewriteHeader rewrite_response_headers = 40; // AuthorizationHeaderMode set_authorization_header = 54; - bool preserve_host_header = 24; + bool preserve_host_header = 24; optional bool pass_identity_headers = 25; - string kubernetes_service_account_token = 26; - string kubernetes_service_account_token_file = 64; - bool enable_google_cloud_serverless_authentication = 42; - IssuerFormat jwt_issuer_format = 65; - repeated string jwt_groups_filter = 66; + string kubernetes_service_account_token = 26; + string kubernetes_service_account_token_file = 64; + bool enable_google_cloud_serverless_authentication = 42; + IssuerFormat jwt_issuer_format = 65; + repeated string jwt_groups_filter = 66; envoy.config.cluster.v3.Cluster envoy_opts = 36; - repeated Policy policies = 27; + repeated Policy policies = 27; repeated PPLPolicy ppl_policies = 63; - string id = 28; + string id = 28; - optional string host_rewrite = 50; - optional string host_rewrite_header = 51; - optional string host_path_regex_rewrite_pattern = 52; + optional string host_rewrite = 50; + optional string host_rewrite_header = 51; + optional string host_path_regex_rewrite_pattern = 52; optional string host_path_regex_rewrite_substitution = 53; - optional string idp_client_id = 55; - optional string idp_client_secret = 56; - bool show_error_details = 59; + optional string idp_client_id = 55; + optional string idp_client_secret = 56; + bool show_error_details = 59; } -message PPLPolicy { bytes raw = 1; } +message PPLPolicy { + bytes raw = 1; +} message Policy { - string id = 1; - string name = 2; + string id = 1; + string name = 2; repeated string allowed_users = 3; // repeated string allowed_groups = 4; - repeated string allowed_domains = 5; + repeated string allowed_domains = 5; map allowed_idp_claims = 7; - repeated string rego = 6; - optional string source_ppl = 10; + repeated string rego = 6; + optional string source_ppl = 10; string explanation = 8; string remediation = 9; @@ -152,150 +157,181 @@ message Policy { // Next ID: 138. message Settings { message Certificate { - bytes cert_bytes = 3; - bytes key_bytes = 4; - string id = 5; + bytes cert_bytes = 3; + bytes key_bytes = 4; + string id = 5; + } + message StringList { + repeated string values = 1; } - message StringList { repeated string values = 1; } - optional string installation_id = 71; - optional string log_level = 3; - optional StringList access_log_fields = 114; - optional StringList authorize_log_fields = 115; - optional string proxy_log_level = 4; - optional string shared_secret = 5; - optional string services = 6; - optional string address = 7; - optional bool insecure_server = 8; - optional string dns_lookup_family = 60; - repeated Certificate certificates = 9; - optional string http_redirect_addr = 10; - optional google.protobuf.Duration timeout_read = 11; - optional google.protobuf.Duration timeout_write = 12; - optional google.protobuf.Duration timeout_idle = 13; - optional string authenticate_service_url = 14; - optional string authenticate_internal_service_url = 82; - optional string signout_redirect_url = 93; - optional string authenticate_callback_path = 15; - optional string cookie_name = 16; - optional string cookie_secret = 17; - optional string cookie_domain = 18; + optional string installation_id = 71; + optional string log_level = 3; + optional StringList access_log_fields = 114; + optional StringList authorize_log_fields = 115; + optional string proxy_log_level = 4; + optional string shared_secret = 5; + optional string services = 6; + optional string address = 7; + optional bool insecure_server = 8; + optional string dns_lookup_family = 60; + repeated Certificate certificates = 9; + optional string http_redirect_addr = 10; + optional google.protobuf.Duration timeout_read = 11; + optional google.protobuf.Duration timeout_write = 12; + optional google.protobuf.Duration timeout_idle = 13; + optional string authenticate_service_url = 14; + optional string authenticate_internal_service_url = 82; + optional string signout_redirect_url = 93; + optional string authenticate_callback_path = 15; + optional string cookie_name = 16; + optional string cookie_secret = 17; + optional string cookie_domain = 18; // optional bool cookie_secure = 19; - optional bool cookie_http_only = 20; - optional google.protobuf.Duration cookie_expire = 21; - optional string cookie_same_site = 113; - optional string idp_client_id = 22; - optional string idp_client_secret = 23; - optional string idp_provider = 24; - optional string idp_provider_url = 25; - repeated string scopes = 26; + optional bool cookie_http_only = 20; + optional google.protobuf.Duration cookie_expire = 21; + optional string cookie_same_site = 113; + optional string idp_client_id = 22; + optional string idp_client_secret = 23; + optional string idp_provider = 24; + optional string idp_provider_url = 25; + repeated string scopes = 26; // optional string idp_service_account = 27; // optional google.protobuf.Duration idp_refresh_directory_timeout = 28; // optional google.protobuf.Duration idp_refresh_directory_interval = 29; - map request_params = 30; - repeated string authorize_service_urls = 32; - optional string authorize_internal_service_url = 83; - optional string override_certificate_name = 33; - optional string certificate_authority = 34; - optional string derive_tls = 96; - optional string signing_key = 36; - map set_response_headers = 69; + map request_params = 30; + repeated string authorize_service_urls = 32; + optional string authorize_internal_service_url = 83; + optional string override_certificate_name = 33; + optional string certificate_authority = 34; + optional string derive_tls = 96; + optional string signing_key = 36; + map set_response_headers = 69; // repeated string jwt_claims_headers = 37; - map jwt_claims_headers = 63; - repeated string jwt_groups_filter = 119; - optional google.protobuf.Duration default_upstream_timeout = 39; - optional string metrics_address = 40; - optional string metrics_basic_auth = 64; - optional Certificate metrics_certificate = 65; - optional string metrics_client_ca = 66; - optional string otel_traces_exporter = 121; - optional double otel_traces_sampler_arg = 122; - repeated string otel_resource_attributes = 123; - optional string otel_log_level = 124; - optional int32 otel_attribute_value_length_limit = 125; - optional string otel_exporter_otlp_endpoint = 126; - optional string otel_exporter_otlp_traces_endpoint = 127; - optional string otel_exporter_otlp_protocol = 128; - optional string otel_exporter_otlp_traces_protocol = 129; - repeated string otel_exporter_otlp_headers = 130; - repeated string otel_exporter_otlp_traces_headers = 131; - optional google.protobuf.Duration otel_exporter_otlp_timeout = 132; - optional google.protobuf.Duration otel_exporter_otlp_traces_timeout = 133; - optional google.protobuf.Duration otel_bsp_schedule_delay = 134; - optional int32 otel_bsp_max_export_batch_size = 135; + map jwt_claims_headers = 63; + repeated string jwt_groups_filter = 119; + optional google.protobuf.Duration default_upstream_timeout = 39; + optional string metrics_address = 40; + optional string metrics_basic_auth = 64; + optional Certificate metrics_certificate = 65; + optional string metrics_client_ca = 66; + optional string otel_traces_exporter = 121; + optional double otel_traces_sampler_arg = 122; + repeated string otel_resource_attributes = 123; + optional string otel_log_level = 124; + optional int32 otel_attribute_value_length_limit = 125; + optional string otel_exporter_otlp_endpoint = 126; + optional string otel_exporter_otlp_traces_endpoint = 127; + optional string otel_exporter_otlp_protocol = 128; + optional string otel_exporter_otlp_traces_protocol = 129; + repeated string otel_exporter_otlp_headers = 130; + repeated string otel_exporter_otlp_traces_headers = 131; + optional google.protobuf.Duration otel_exporter_otlp_timeout = 132; + optional google.protobuf.Duration otel_exporter_otlp_traces_timeout = 133; + optional google.protobuf.Duration otel_bsp_schedule_delay = 134; + optional int32 otel_bsp_max_export_batch_size = 135; reserved 41 to 45, 98; // legacy tracing fields - optional string grpc_address = 46; - optional bool grpc_insecure = 47; + optional string grpc_address = 46; + optional bool grpc_insecure = 47; optional google.protobuf.Duration grpc_client_timeout = 99; reserved 100; // grpc_client_dns_roundrobin // optional string forward_auth_url = 50; - repeated string databroker_service_urls = 52; - optional string databroker_internal_service_url = 84; - optional string databroker_storage_type = 101; + repeated string databroker_service_urls = 52; + optional string databroker_internal_service_url = 84; + optional string databroker_storage_type = 101; optional string databroker_storage_connection_string = 102; reserved 106; // databroker_storage_tls_skip_verify optional DownstreamMtlsSettings downstream_mtls = 116; // optional string client_ca = 53; // optional string client_crl = 74; optional string google_cloud_serverless_authentication_service_account = 55; - optional bool use_proxy_protocol = 107; - optional bool autocert = 56; - optional string autocert_ca = 76; - optional string autocert_email = 77; - optional bool autocert_use_staging = 57; - optional string autocert_eab_key_id = 78; - optional string autocert_eab_mac_key = 79; - optional bool autocert_must_staple = 58; - optional string autocert_dir = 59; - optional string autocert_trusted_ca = 80; - optional bool skip_xff_append = 61; - optional uint32 xff_num_trusted_hops = 70; - optional string envoy_admin_access_log_path = 108; - optional string envoy_admin_profile_path = 109; - optional string envoy_admin_address = 110; - optional string envoy_bind_config_source_address = 111; - optional bool envoy_bind_config_freebind = 112; - repeated string programmatic_redirect_domain_whitelist = 68; - optional envoy.extensions.filters.network.http_connection_manager.v3 - .HttpConnectionManager.CodecType codec_type = 73; + optional bool use_proxy_protocol = 107; + optional bool autocert = 56; + optional string autocert_ca = 76; + optional string autocert_email = 77; + optional bool autocert_use_staging = 57; + optional string autocert_eab_key_id = 78; + optional string autocert_eab_mac_key = 79; + optional bool autocert_must_staple = 58; + optional string autocert_dir = 59; + optional string autocert_trusted_ca = 80; + optional bool skip_xff_append = 61; + optional uint32 xff_num_trusted_hops = 70; + optional string envoy_admin_access_log_path = 108; + optional string envoy_admin_profile_path = 109; + optional string envoy_admin_address = 110; + optional string envoy_bind_config_source_address = 111; + optional bool envoy_bind_config_freebind = 112; + repeated string programmatic_redirect_domain_whitelist = 68; + optional envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager.CodecType codec_type = 73; // optional pomerium.crypt.PublicKeyEncryptionKey audit_key = 72; - optional string primary_color = 85; - optional string secondary_color = 86; - optional string darkmode_primary_color = 87; - optional string darkmode_secondary_color = 88; - optional string logo_url = 89; - optional string favicon_url = 90; - optional string error_message_first_paragraph = 91; - optional bool pass_identity_headers = 117; - map runtime_flags = 118; - optional uint32 http3_advertise_port = 136; - optional string device_auth_client_type = 137; + optional string primary_color = 85; + optional string secondary_color = 86; + optional string darkmode_primary_color = 87; + optional string darkmode_secondary_color = 88; + optional string logo_url = 89; + optional string favicon_url = 90; + optional string error_message_first_paragraph = 91; + optional bool pass_identity_headers = 117; + map runtime_flags = 118; + optional uint32 http3_advertise_port = 136; + optional string device_auth_client_type = 137; } message DownstreamMtlsSettings { - optional string ca = 1; - optional string crl = 2; - optional MtlsEnforcementMode enforcement = 3; - repeated SANMatcher match_subject_alt_names = 4; - optional uint32 max_verify_depth = 5; + optional string ca = 1; + optional string crl = 2; + optional MtlsEnforcementMode enforcement = 3; + repeated SANMatcher match_subject_alt_names = 4; + optional uint32 max_verify_depth = 5; } enum MtlsEnforcementMode { - UNKNOWN = 0; - POLICY = 1; + UNKNOWN = 0; + POLICY = 1; POLICY_WITH_DEFAULT_DENY = 2; - REJECT_CONNECTION = 3; + REJECT_CONNECTION = 3; } message SANMatcher { enum SANType { SAN_TYPE_UNSPECIFIED = 0; - EMAIL = 1; - DNS = 2; - URI = 3; - IP_ADDRESS = 4; - USER_PRINCIPAL_NAME = 5; + EMAIL = 1; + DNS = 2; + URI = 3; + IP_ADDRESS = 4; + USER_PRINCIPAL_NAME = 5; } SANType san_type = 1; - string pattern = 2; + string pattern = 2; +} + +message SSHCheckRequest { + string user_name = 1; + string service_name = 2; + string auth_method = 3; + + string hostname = 4; + string route_id = 5; + bytes public_key = 6; + string public_key_alg = 7; +} + +message SSHCheckResponse { + bool permit_port_forwarding = 1; + bool permit_agent_forwarding = 2; + bool permit_x11_forwarding = 3; + bool permit_pty = 4; + bool permit_user_rc = 5; + + google.protobuf.Timestamp valid_before = 6; + google.protobuf.Duration valid_after = 7; + + repeated string permit_open = 8; + repeated string permit_listen = 9; + + optional string force_command = 10; + map force_env = 11; + optional bool require_user_presence = 12; + optional bool require_verify = 13; } diff --git a/pkg/identity/mock_provider.go b/pkg/identity/mock_provider.go index 9dda2aee1..5ae49dc54 100644 --- a/pkg/identity/mock_provider.go +++ b/pkg/identity/mock_provider.go @@ -68,6 +68,6 @@ func (mp MockProvider) DeviceAccessToken(ctx context.Context, r *oauth2.DeviceAu } // DeviceAuth implements Authenticator. -func (mp MockProvider) DeviceAuth(w http.ResponseWriter, r *http.Request) (*oauth2.DeviceAuthResponse, error) { +func (mp MockProvider) DeviceAuth(_ context.Context) (*oauth2.DeviceAuthResponse, error) { return &mp.DeviceAuthResponse, mp.DeviceAuthError } diff --git a/pkg/identity/oauth/apple/apple.go b/pkg/identity/oauth/apple/apple.go index a02cd8620..bd1b235ac 100644 --- a/pkg/identity/oauth/apple/apple.go +++ b/pkg/identity/oauth/apple/apple.go @@ -183,7 +183,7 @@ func (p *Provider) SignOut(_ http.ResponseWriter, _ *http.Request, _, _, _ strin return oidc.ErrSignoutNotImplemented } -func (p *Provider) DeviceAuth(_ http.ResponseWriter, _ *http.Request) (*oauth2.DeviceAuthResponse, error) { +func (p *Provider) DeviceAuth(_ context.Context) (*oauth2.DeviceAuthResponse, error) { return nil, oidc.ErrDeviceAuthNotImplemented } diff --git a/pkg/identity/oauth/github/github.go b/pkg/identity/oauth/github/github.go index f44d530b8..9358c355f 100644 --- a/pkg/identity/oauth/github/github.go +++ b/pkg/identity/oauth/github/github.go @@ -257,7 +257,7 @@ func (p *Provider) SignOut(_ http.ResponseWriter, _ *http.Request, _, _, _ strin return oidc.ErrSignoutNotImplemented } -func (p *Provider) DeviceAuth(_ http.ResponseWriter, _ *http.Request) (*oauth2.DeviceAuthResponse, error) { +func (p *Provider) DeviceAuth(_ context.Context) (*oauth2.DeviceAuthResponse, error) { return nil, oidc.ErrDeviceAuthNotImplemented } diff --git a/pkg/identity/oidc/oidc.go b/pkg/identity/oidc/oidc.go index 9c4ac7ade..65c41aa12 100644 --- a/pkg/identity/oidc/oidc.go +++ b/pkg/identity/oidc/oidc.go @@ -128,7 +128,7 @@ func (p *Provider) SignIn(w http.ResponseWriter, r *http.Request, state string) return nil } -func (p *Provider) DeviceAuth(w http.ResponseWriter, r *http.Request) (*oauth2.DeviceAuthResponse, error) { +func (p *Provider) DeviceAuth(ctx context.Context) (*oauth2.DeviceAuthResponse, error) { oa, err := p.GetOauthConfig() if err != nil { return nil, err @@ -144,7 +144,7 @@ func (p *Provider) DeviceAuth(w http.ResponseWriter, r *http.Request) (*oauth2.D opts = append(opts, oauth2.SetAuthURLParam("client_secret", oa.ClientSecret)) } - resp, err := oa.DeviceAuth(r.Context(), opts...) + resp, err := oa.DeviceAuth(ctx, opts...) if err != nil { return nil, err } diff --git a/pkg/identity/providers.go b/pkg/identity/providers.go index 6a888ba83..34ec55b7c 100644 --- a/pkg/identity/providers.go +++ b/pkg/identity/providers.go @@ -41,7 +41,7 @@ type Authenticator interface { SignOut(w http.ResponseWriter, r *http.Request, idTokenHint, authenticateSignedOutURL, redirectToURL string) error // alternatives for these methods? - DeviceAuth(w http.ResponseWriter, r *http.Request) (*oauth2.DeviceAuthResponse, error) + DeviceAuth(ctx context.Context) (*oauth2.DeviceAuthResponse, error) DeviceAccessToken(ctx context.Context, r *oauth2.DeviceAuthResponse, state State) (*oauth2.Token, error) }