diff --git a/.github/dependabot.yml b/.github/dependabot.yml index aa5064f..3dc957c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,3 +10,11 @@ updates: - nwtgck assignees: - nwtgck +- package-ecosystem: github-actions + directory: "/" + schedule: + interval: daily + timezone: Asia/Tokyo + open-pull-requests-limit: 99 + reviewers: [ nwtgck ] + assignees: [ nwtgck ] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8632df3..883a903 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,9 +6,9 @@ jobs: build_multi_platform: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Go 1.x - uses: actions/setup-go@v2 + uses: actions/setup-go@v4 with: go-version: "1.20" - name: Build for multi-platform diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 86c3c28..1b6ad7d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,15 +9,15 @@ jobs: goreleaser: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v4 with: go-version: "1.20" - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v2 + uses: goreleaser/goreleaser-action@v4 with: version: v1.3.1 args: release --rm-dist diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c0a9cc..4958a1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,16 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) ## [Unreleased] +## [0.2.0] - 2023-08-09 +### Added +* Add permissions + +### Changed +* Require one user at least + ## 0.1.0 - 2023-08-08 ### Added * Initial release -[Unreleased]: https://github.com/nwtgck/handy-sshd/compare/v0.1.0...HEAD +[Unreleased]: https://github.com/nwtgck/handy-sshd/compare/v0.2.0...HEAD +[0.2.0]: https://github.com/nwtgck/handy-sshd/compare/v0.1.0...v0.2.0 diff --git a/README.md b/README.md index be55969..871d60c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,22 @@ # handy-sshd -Portable SSH Server +[![CI](https://github.com/nwtgck/handy-sshd/actions/workflows/ci.yml/badge.svg)](https://github.com/nwtgck/handy-sshd/actions/workflows/ci.yml) + +Portable SSH Server + +## Install on Ubuntu/Debian + +```bash +wget https://github.com/nwtgck/handy-sshd/releases/download/v0.1.0/handy-sshd-0.1.0-linux-amd64.deb +sudo dpkg -i handy-sshd-0.1.0-linux-amd64.deb +``` + +## Install on Mac + +```bash +brew install nwtgck/handy-sshd/handy-sshd +``` + +Get more executables in [the releases](https://github.com/nwtgck/handy-sshd/releases). ## Examples @@ -22,3 +39,42 @@ handy-sshd -p 2222 --user "john:" --user "alice:" # Listen on unix domain socket handy-sshd --unix-socket /tmp/my-unix-socket --user "john:" ``` + +## Permissions +**All permissions are allowed when nothing is specified.** There are some permissions. +* --allow-direct-tcpip +* --allow-execute +* --allow-sftp +* --allow-tcpip-forward + +Specifying `--allow-direct-tcpip` and `--allow-execute` for example allows only them. +The log shows "allowed: " and "NOT allowed: " permissions as follows. + +```console +$ handy-sshd --user "john:" --allow-direct-tcpip --allow-execute +2023/08/09 20:49:35 INFO listening on :2222... +2023/08/09 20:49:35 INFO allowed: "direct-tcpip", "execute" +2023/08/09 20:49:35 INFO NOT allowed: "tcpip-forward", "sftp" +``` + +## --help + +``` +Portable SSH server + +Usage: + handy-sshd [flags] + +Flags: + --allow-direct-tcpip client can use local forwarding and SOCKS proxy + --allow-execute client can use shell/interactive shell + --allow-sftp client can use SFTP and SSHFS + --allow-tcpip-forward client can use remote forwarding + -h, --help help for handy-sshd + --host string SSH server host (e.g. 127.0.0.1) + -p, --port uint16 SSH server port (default 2222) + --shell string Shell + --unix-socket string Unix-domain socket + --user stringArray SSH user name (e.g. "john:mypassword") + -v, --version show version +``` diff --git a/cmd/root.go b/cmd/root.go index c964a03..9581e71 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -21,6 +21,21 @@ var flag struct { sshUnixSocket string sshShell string sshUsers []string + + allowTcpipForward bool + allowDirectTcpip bool + allowExecute bool + allowSftp bool +} + +var allPermissionFlags = []struct { + name string + flagPtr *bool +}{ + {name: "tcpip-forward", flagPtr: &flag.allowTcpipForward}, + {name: "direct-tcpip", flagPtr: &flag.allowDirectTcpip}, + {name: "execute", flagPtr: &flag.allowExecute}, + {name: "sftp", flagPtr: &flag.allowSftp}, } type sshUser struct { @@ -38,6 +53,12 @@ func init() { RootCmd.PersistentFlags().StringVarP(&flag.sshShell, "shell", "", "", "Shell") //RootCmd.PersistentFlags().StringVar(&flag.dnsServer, "dns-server", "", "DNS server (e.g. 1.1.1.1:53)") RootCmd.PersistentFlags().StringArrayVarP(&flag.sshUsers, "user", "", nil, `SSH user name (e.g. "john:mypassword")`) + + // Permission flags + RootCmd.PersistentFlags().BoolVarP(&flag.allowTcpipForward, "allow-tcpip-forward", "", false, "client can use remote forwarding") + RootCmd.PersistentFlags().BoolVarP(&flag.allowDirectTcpip, "allow-direct-tcpip", "", false, "client can use local forwarding and SOCKS proxy") + RootCmd.PersistentFlags().BoolVarP(&flag.allowExecute, "allow-execute", "", false, "client can use shell/interactive shell") + RootCmd.PersistentFlags().BoolVarP(&flag.allowSftp, "allow-sftp", "", false, "client can use SFTP and SSHFS") } var RootCmd = &cobra.Command{ @@ -51,8 +72,26 @@ var RootCmd = &cobra.Command{ return nil } logger := slog.Default() + + // Allow all permissions if all permission is not set + { + allPermissionFalse := true + for _, permissionFlag := range allPermissionFlags { + allPermissionFalse = allPermissionFalse && !*permissionFlag.flagPtr + } + if allPermissionFalse { + for _, permissionFlag := range allPermissionFlags { + *permissionFlag.flagPtr = true + } + } + } + sshServer := &handy_sshd.Server{ - Logger: logger, + Logger: logger, + AllowTcpipForward: flag.allowTcpipForward, + AllowDirectTcpip: flag.allowDirectTcpip, + AllowExecute: flag.allowExecute, + AllowSftp: flag.allowSftp, } var sshUsers []sshUser for _, u := range flag.sshUsers { @@ -62,7 +101,11 @@ var RootCmd = &cobra.Command{ } sshUsers = append(sshUsers, sshUser{name: splits[0], password: splits[1]}) } - + if len(sshUsers) == 0 { + return fmt.Errorf(`No user specified +e.g. --user "john:mypassword" +e.g. --user "john:"`) + } // (base: https://gist.github.com/jpillora/b480fde82bff51a06238) sshConfig := &ssh.ServerConfig{ //Define a function to run when a client attempts a password login @@ -109,6 +152,9 @@ var RootCmd = &cobra.Command{ logger.Info(fmt.Sprintf("listening on %s...", flag.sshUnixSocket)) } defer ln.Close() + + showPermissions(logger) + for { conn, err := ln.Accept() if err != nil { @@ -121,9 +167,29 @@ var RootCmd = &cobra.Command{ conn.Close() continue } - logger.Info("new SSH connection", "client_version", string(sshConn.ClientVersion())) + logger.Info("new SSH connection", "remote_address", sshConn.RemoteAddr(), "client_version", string(sshConn.ClientVersion())) go sshServer.HandleGlobalRequests(sshConn, reqs) go sshServer.HandleChannels(flag.sshShell, chans) } }, } + +func showPermissions(logger *slog.Logger) { + var allowedList []string + var notAllowedList []string + for _, permissionFlag := range allPermissionFlags { + if *permissionFlag.flagPtr { + allowedList = append(allowedList, `"`+permissionFlag.name+`"`) + } else { + notAllowedList = append(notAllowedList, `"`+permissionFlag.name+`"`) + } + } + showList := func(l []string) string { + if len(l) == 0 { + return "none" + } + return strings.Join(l, ", ") + } + logger.Info(fmt.Sprintf("allowed: %s", showList(allowedList))) + logger.Info(fmt.Sprintf("NOT allowed: %s", showList(notAllowedList))) +} diff --git a/go.mod b/go.mod index b005b36..7f2fccc 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/pkg/sftp v1.13.5 github.com/spf13/cobra v1.7.0 golang.org/x/crypto v0.12.0 - golang.org/x/exp v0.0.0-20230807204917-050eac23e9de + golang.org/x/exp v0.0.0-20230809094429-853ea248256d ) require ( diff --git a/go.sum b/go.sum index 7fee637..4d06c32 100644 --- a/go.sum +++ b/go.sum @@ -27,8 +27,8 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -golang.org/x/exp v0.0.0-20230807204917-050eac23e9de h1:l5Za6utMv/HsBWWqzt4S8X17j+kt1uVETUX5UFhn2rE= -golang.org/x/exp v0.0.0-20230807204917-050eac23e9de/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/exp v0.0.0-20230809094429-853ea248256d h1:wu5bD43Ana/nF1ZmaLr3lW/FQeJU8CcI+Ln7yWHViXE= +golang.org/x/exp v0.0.0-20230809094429-853ea248256d/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/main/main.go b/main/main.go index 55e5f6c..6ac23fe 100644 --- a/main/main.go +++ b/main/main.go @@ -1,14 +1,12 @@ package main import ( - "fmt" "github.com/nwtgck/handy-sshd/cmd" "os" ) func main() { if err := cmd.RootCmd.Execute(); err != nil { - _, _ = fmt.Fprintf(os.Stderr, err.Error()) os.Exit(-1) } } diff --git a/server.go b/server.go index 1ede795..f8f514b 100644 --- a/server.go +++ b/server.go @@ -28,6 +28,13 @@ import ( type Server struct { Logger *slog.Logger + + // Permissions + AllowTcpipForward bool + AllowDirectTcpip bool + AllowExecute bool // this should not be split into "allow-exec" and "allow-pty-req" for now because "pty-req" can be used not for shell execution. + AllowSftp bool + // TODO: DNS server ? } @@ -47,6 +54,10 @@ func (s *Server) handleChannel(shell string, newChannel ssh.NewChannel) { case "session": s.handleSession(shell, newChannel) case "direct-tcpip": + if !s.AllowDirectTcpip { + newChannel.Reject(ssh.Prohibited, "direct-tcpip not allowed") + break + } s.handleDirectTcpip(newChannel) default: newChannel.Reject(ssh.UnknownChannelType, fmt.Sprintf("unknown channel type: %s", newChannel.ChannelType())) @@ -67,6 +78,11 @@ func (s *Server) handleSession(shell string, newChannel ssh.NewChannel) { for req := range requests { switch req.Type { case "exec": + if !s.AllowExecute { + s.Logger.Info("execution not allowed (exec)") + req.Reply(false, nil) + break + } s.handleExecRequest(req, connection) case "shell": // We only accept the default shell @@ -75,6 +91,11 @@ func (s *Server) handleSession(shell string, newChannel ssh.NewChannel) { req.Reply(true, nil) } case "pty-req": + if !s.AllowExecute { + s.Logger.Info("execution not allowed (pty-req)") + req.Reply(false, nil) + break + } termLen := req.Payload[3] w, h := parseDims(req.Payload[termLen+4:]) shf, err = s.createPty(shell, connection) @@ -140,9 +161,17 @@ func (s *Server) handleExecRequest(req *ssh.Request, connection ssh.Channel) { func (s *Server) handleSessionSubSystem(req *ssh.Request, connection ssh.Channel) { // https://github.com/pkg/sftp/blob/42e9800606febe03f9cdf1d1283719af4a5e6456/examples/go-sftp-server/main.go#L111 - ok := string(req.Payload[4:]) == "sftp" - req.Reply(ok, nil) + if string(req.Payload[4:]) != "sftp" { + req.Reply(false, nil) + return + } + if !s.AllowSftp { + s.Logger.Info("sftp not allowed") + req.Reply(false, nil) + return + } + req.Reply(true, nil) serverOptions := []sftp.ServerOption{ sftp.WithDebug(os.Stderr), } @@ -232,6 +261,11 @@ func (s *Server) HandleGlobalRequests(sshConn *ssh.ServerConn, reqs <-chan *ssh. for req := range reqs { switch req.Type { case "tcpip-forward": + if !s.AllowTcpipForward { + s.Logger.Info("tcpip-forward not allowed") + req.Reply(false, nil) + break + } s.handleTcpipForward(sshConn, req) default: // discard diff --git a/version/version.go b/version/version.go index 31646b0..c62f7e6 100644 --- a/version/version.go +++ b/version/version.go @@ -1,3 +1,3 @@ package version -const Version = "0.1.0" +const Version = "0.2.0"