diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a33f85..f0f8cfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ Changes: * Fix connection failure when sending too many environment variables (fix [#22](https://github.com/moul/sshportal/issues/22)) * Fix panic when entering empty command (fix [#13](https://github.com/moul/sshportal/issues/13)) * Add `config backup --ignore-events` option +* Add `sshportal healthcheck` cli command +* Add [Docker Healthcheck](https://docs.docker.com/engine/reference/builder/#healthcheck) helper ## v1.6.0 (2017-12-12) diff --git a/Dockerfile b/Dockerfile index 5938ef7..494c8b8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,8 +5,9 @@ WORKDIR /go/src/github.com/moul/sshportal RUN make _docker_install # minimal runtime -FROM scratch +FROM alpine COPY --from=builder /go/bin/sshportal /bin/sshportal ENTRYPOINT ["/bin/sshportal"] CMD ["server"] EXPOSE 2222 +HEALTHCHECK --interval=10s --timeout=10s CMD /bin/sshportal healthcheck diff --git a/README.md b/README.md index fb43d50..91ff938 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Jump host/Jump server without the jump, a.k.a Transparent SSH bastion * [`scp`](https://linux.die.net/man/1/scp) support * [`rsync`](https://linux.die.net/man/1/rsync) support * Git support (can be used to easily use multiple user keys on GitHub, or access your own firewalled gitlab server) -* Do not require any SSH client modification or custom `.ssh/config`, works with every tested SSH programming libraries and every tested SSH +* Do not require any SSH client modification or custom `.ssh/config`, works with every tested SSH programming libraries and every tested SSH ## (Known) limitations @@ -54,7 +54,7 @@ Jump host/Jump server without the jump, a.k.a Transparent SSH bastion Start the server ```console -$ sshportal +$ sshportal server 2017/11/13 10:58:35 Admin user created, use the user 'invite:BpLnfgDsc2WD8F2q' to associate a public key with this account 2017/11/13 10:58:35 SSH Server accepting connections on :2222 ``` @@ -346,6 +346,15 @@ $ the `healtcheck` user can be changed using the `healthcheck-user` option. +--- + +Alternatively, you can run the built-in healthcheck helper (requiring no ssh client nor ssh key): + +```console +$ sshportal healthcheck --addr=localhost:2222; echo $? +$ 0 +``` + ## Scaling `sshportal` is stateless but relies on a database to store configuration and logs. @@ -383,4 +392,4 @@ This is totally experimental for now, so please file issues to let me know what ## License -[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fmoul%2Fsshportal.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fmoul%2Fsshportal?ref=badge_large) \ No newline at end of file +[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fmoul%2Fsshportal.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fmoul%2Fsshportal?ref=badge_large) diff --git a/examples/integration/Makefile b/examples/integration/Makefile index 7cea339..d329184 100644 --- a/examples/integration/Makefile +++ b/examples/integration/Makefile @@ -1,6 +1,11 @@ run: docker-compose down + docker-compose up -d sshportal + @echo "Waiting for sshportal to be healthy" + @sleep 3 + docker-compose build client docker-compose run client /integration/_client.sh + docker-compose down diff --git a/examples/integration/_client.sh b/examples/integration/_client.sh index 8ff513b..8d318f6 100755 --- a/examples/integration/_client.sh +++ b/examples/integration/_client.sh @@ -11,9 +11,6 @@ Host sshportal HostName sshportal EOF -#ping -c 1 sshportal -sleep 3 - set -x # login diff --git a/main.go b/main.go index f8e3030..0d6f36e 100644 --- a/main.go +++ b/main.go @@ -1,10 +1,12 @@ package main import ( + "bytes" "errors" "fmt" "log" "math/rand" + "net" "os" "path" "strings" @@ -86,6 +88,15 @@ func main() { Usage: "Encrypt sensitive data in database (length: 16, 24 or 32)", }, }, + }, { + Name: "healthcheck", + Action: healthcheck, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "addr, a", + Value: "localhost:2222", + }, + }, }, } if err := app.Run(os.Args); err != nil { @@ -122,7 +133,9 @@ func server(c *cli.Context) error { // ssh server ssh.Handle(func(s ssh.Session) { currentUser := s.Context().Value(userContextKey).(User) - log.Printf("New connection: sshUser=%q remote=%q local=%q command=%q dbUser=id:%q,email:%s", s.User(), s.RemoteAddr(), s.LocalAddr(), s.Command(), currentUser.ID, currentUser.Email) + if s.User() != "healthcheck" { + log.Printf("New connection: sshUser=%q remote=%q local=%q command=%q dbUser=id:%q,email:%s", s.User(), s.RemoteAddr(), s.LocalAddr(), s.Command(), currentUser.ID, currentUser.Email) + } if err := s.Context().Value(errorContextKey); err != nil { fmt.Fprintf(s, "error: %v\n", err) @@ -208,6 +221,11 @@ func server(c *cli.Context) error { }) opts := []ssh.Option{} + opts = append(opts, ssh.PasswordAuth(func(ctx ssh.Context, pass string) bool { + ctx.SetValue(userContextKey, User{}) + return ctx.User() == "healthcheck" + })) + opts = append(opts, ssh.PublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool { var ( userKey UserKey @@ -278,3 +296,37 @@ func server(c *cli.Context) error { log.Printf("info: SSH Server accepting connections on %s", c.String("bind-address")) return ssh.ListenAndServe(c.String("bind-address"), nil, opts...) } + +// perform a healthcheck test without requiring an ssh client or an ssh key (used for Docker's HEALTHCHECK) +func healthcheck(c *cli.Context) error { + config := gossh.ClientConfig{ + User: "healthcheck", + HostKeyCallback: func(hostname string, remote net.Addr, key gossh.PublicKey) error { return nil }, + Auth: []gossh.AuthMethod{gossh.Password("healthcheck")}, + } + client, err := gossh.Dial("tcp", c.String("addr"), &config) + if err != nil { + return err + } + + session, err := client.NewSession() + if err != nil { + return err + } + defer func() { + if err := session.Close(); err != nil { + panic(err) + } + }() + + var b bytes.Buffer + session.Stdout = &b + if err := session.Run(""); err != nil { + return err + } + stdout := strings.TrimSpace(b.String()) + if stdout != "OK" { + return fmt.Errorf("invalid stdout: %q expected 'OK'", stdout) + } + return nil +}