Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support of API hooks for password and public key authentication #83

Merged
merged 5 commits into from
Jul 15, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
31 changes: 16 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,21 +45,21 @@ COMMANDS:
help, h Shows a list of commands or help for one command

GLOBAL OPTIONS:
--verbose, -V Enable verbose mode
--syslog-server Configure a syslog server, i.e: udp://localhost:514
--bind, -b ":2222" Listen to address
--host-key, -k "built-in" Path or complete SSH host key to use, use 'system' for keys in /etc/ssh
--allowed-images List of allowed images, i.e: alpine,ubuntu:trusty,1cf3e6c
--shell "/bin/sh" Default shell
--docker-run-args "-it --rm" 'docker run' arguments
--no-join Do not join existing containers, always create new ones
--clean-on-startup Cleanup Docker containers created by ssh2docker on start
--password-auth-script Password auth hook file
--publickey-auth-script Public-key auth hook file
--local-user If setted, you can spawn a local shell (not withing docker) by SSHing to this user
--banner Display a banner on connection
--help, -h show help
--version, -v print the version
--verbose, -V Enable verbose mode
--syslog-server Configure a syslog server, i.e: udp://localhost:514
--bind, -b ":2222" Listen to address
--host-key, -k "built-in" Path or complete SSH host key to use, use 'system' for keys in /etc/ssh
--allowed-images List of allowed images, i.e: alpine,ubuntu:trusty,1cf3e6c
--shell "/bin/sh" DEFAULT shell
--docker-run-args "-it --rm" 'docker run' arguments
--no-join Do not join existing containers, always create new ones
--clean-on-startup Cleanup Docker containers created by ssh2docker on start
--password-auth-script Password auth hook file
--publickey-auth-script Public-key auth hook file
--local-user If setted, you can spawn a local shell (not withing docker) by SSHing to this user
--banner Display a banner on connection
--help, -h show help
--version, -v print the version
```

## Example
Expand Down Expand Up @@ -159,6 +159,7 @@ $ docker run --privileged -v /var/lib/docker:/var/lib/docker -it --rm -p 2222:22

### master (unreleased)

* Support of API hooks for password and public key authentication ([#80](https://github.com/moul/ssh2docker/issues/80))
* Support of exec requests ([#51](https://github.com/moul/ssh2docker/issues/51))
* Support of `docker-run-args` in hook scripts ([#30](https://github.com/moul/ssh2docker/issues/30))
* Support of `--syslog-server` + refactored logs ([#71](https://github.com/moul/ssh2docker/issues/71))
Expand Down
113 changes: 78 additions & 35 deletions auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/apex/log"
"github.com/mitchellh/go-homedir"
"github.com/moul/ssh2docker/pkg/envhelper"
"github.com/parnurzeal/gorequest"
"golang.org/x/crypto/ssh"
)

Expand Down Expand Up @@ -87,41 +88,61 @@ func (s *Server) KeyboardInteractiveCallback(conn ssh.ConnMetadata, challenge ss
return nil, s.CheckConfig(config)
}

if s.PublicKeyAuthScript != "" {
config.AuthenticationAttempts++
log.Debugf("%d keys received, trying to authenticate using hook script", len(config.Keys))
if s.PublicKeyAuthScript == "" {
log.Debugf("%d keys received, but no hook script, continuing", len(config.Keys))
return nil, s.CheckConfig(config)
}

config.AuthenticationAttempts++
log.Debugf("%d keys received, trying to authenticate using publickey hook", len(config.Keys))

var output []byte
switch {
case strings.HasPrefix(s.PublicKeyAuthScript, "http://"),
strings.HasPrefix(s.PublicKeyAuthScript, "https://"):
input := struct {
Username string `json:"username"`
Publickeys []string `json:"publickeys"`
}{
Username: username,
Publickeys: config.Keys,
}
resp, body, errs := gorequest.New().Type("json").Post(s.PublicKeyAuthScript).Send(input).End()
if len(errs) > 0 {
return nil, fmt.Errorf("gorequest errs: %v", errs)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("invalid status code: %d", resp.StatusCode)
}
output = []byte(body)
default:
script, err := homedir.Expand(s.PublicKeyAuthScript)
if err != nil {
log.Warnf("Failed to expandUser: %v", err)
return nil, err
}
args := append([]string{username}, config.Keys...)
cmd := exec.Command(script, args...)
cmd := exec.Command(script, append([]string{username}, config.Keys...)...)
// FIXME: redirect stderr to log
cmd.Stderr = os.Stderr
output, err := cmd.Output()
output, err = cmd.Output()
if err != nil {
log.Warnf("Failed to execute publickey-auth-script: %v", err)
return nil, err
}
}

if err = json.Unmarshal(output, &config); err != nil {
log.Warnf("Failed to unmarshal json %q: %v", string(output), err)
return nil, err
}

if err = s.CheckConfig(config); err != nil {
return nil, err
}
if err := json.Unmarshal(output, &config); err != nil {
log.Warnf("Failed to unmarshal json %q: %v", string(output), err)
return nil, err
}

// success
config.AuthenticationMethod = "publickey"
return nil, nil
} else {
log.Debugf("%d keys received, but no hook script, continuing", len(config.Keys))
if err := s.CheckConfig(config); err != nil {
return nil, err
}

return nil, s.CheckConfig(config)
// success
config.AuthenticationMethod = "publickey"
return nil, nil
}

// PasswordCallback is called when the user tries to authenticate using a password
Expand All @@ -148,8 +169,32 @@ func (s *Server) PasswordCallback(conn ssh.ConnMetadata, password []byte) (*ssh.
}

// if there is a password callback
if s.PasswordAuthScript != "" {
config.AuthenticationAttempts++
if s.PasswordAuthScript == "" {
return nil, s.CheckConfig(config)
}

config.AuthenticationAttempts++

var output []byte
switch {
case strings.HasPrefix(s.PasswordAuthScript, "http://"),
strings.HasPrefix(s.PasswordAuthScript, "https://"):
input := struct {
Username string `json:"username"`
Password string `json:"password"`
}{
Username: username,
Password: string(password),
}
resp, body, errs := gorequest.New().Type("json").Post(s.PasswordAuthScript).Send(input).End()
if len(errs) > 0 {
return nil, fmt.Errorf("gorequest errs: %v", errs)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("invalid status code: %d", resp.StatusCode)
}
output = []byte(body)
default:
script, err := homedir.Expand(s.PasswordAuthScript)
if err != nil {
log.Warnf("Failed to expandUser: %v", err)
Expand All @@ -158,25 +203,23 @@ func (s *Server) PasswordCallback(conn ssh.ConnMetadata, password []byte) (*ssh.
cmd := exec.Command(script, username, string(password))
// FIXME: redirect stderr to log
cmd.Stderr = os.Stderr
output, err := cmd.Output()
output, err = cmd.Output()
if err != nil {
log.Warnf("Failed to execute password-auth-script: %v", err)
return nil, err
}
}

if err = json.Unmarshal(output, &config); err != nil {
log.Warnf("Failed to unmarshal json %q: %v", string(output), err)
return nil, err
}

if err = s.CheckConfig(config); err != nil {
return nil, err
}
if err := json.Unmarshal(output, &config); err != nil {
log.Warnf("Failed to unmarshal json %q: %v", string(output), err)
return nil, err
}

// success
config.AuthenticationMethod = "password"
return nil, nil
if err := s.CheckConfig(config); err != nil {
return nil, err
}

return nil, s.CheckConfig(config)
// success
config.AuthenticationMethod = "password"
return nil, nil
}
24 changes: 24 additions & 0 deletions examples/always-no-password-auth-api
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/bin/bash

set -e

PORT=${PORT:-8080}

handle() {
echo 'HTTP/1.0 200 OK'
echo 'Content-Type: application/json'
echo 'Connection: closed'
echo
echo '{"allowed":false}'

input=$(cat)
echo "password-auth-api: $input" >&2
}


case $1 in
"bind")
(sleep 1; echo "Listening on port ${PORT}" >&2)
socat -T0.1 -v "tcp-l:${PORT},reuseaddr,fork,crlf" "system:'. $0 && handle',stderr"
;;
esac
23 changes: 23 additions & 0 deletions examples/always-no-publickey-auth-api
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/bash

set -e

PORT=${PORT:-8080}

handle() {
echo 'HTTP/1.0 200 OK'
echo 'Content-Type: application/json'
echo
echo '{"allowed":false}'

input=$(cat)
echo "publickey-auth-api: $input" >&2
}


case $1 in
"bind")
(sleep 1; echo "Listening on port ${PORT}" >&2)
socat -T0.1 -v "tcp-l:${PORT},reuseaddr,fork,crlf" "system:'. $0 && handle',stderr"
;;
esac
23 changes: 23 additions & 0 deletions examples/always-yes-password-auth-api
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/bash

set -e

PORT=${PORT:-8080}

handle() {
echo 'HTTP/1.0 200 OK'
echo 'Content-Type: application/json'
echo
echo '{"allowed":true}'

input=$(cat)
echo "password-auth-api: $input" >&2
}


case $1 in
"bind")
(sleep 1; echo "Listening on port ${PORT}" >&2)
socat -T0.1 -v "tcp-l:${PORT},reuseaddr,fork,crlf" "system:'. $0 && handle',stderr"
;;
esac
23 changes: 23 additions & 0 deletions examples/always-yes-publickey-auth-api
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/bash

set -e

PORT=${PORT:-8080}

handle() {
echo 'HTTP/1.0 200 OK'
echo 'Content-Type: application/json'
echo
echo '{"allowed":true}'

input=$(cat)
echo "publickey-auth-api: $input" >&2
}


case $1 in
"bind")
(sleep 1; echo "Listening on port ${PORT}" >&2)
socat -T0.1 -v "tcp-l:${PORT},reuseaddr,fork,crlf" "system:'. $0 && handle',stderr"
;;
esac
12 changes: 10 additions & 2 deletions glide.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions glide.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ import:
- ed25519
- ed25519/internal/edwards25519
- ssh
- package: github.com/parnurzeal/gorequest
24 changes: 24 additions & 0 deletions vendor/github.com/moul/http2curl/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions vendor/github.com/moul/http2curl/.travis.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading