introduce first docker plugin
This commit is contained in:
parent
1d8dde5d02
commit
fdc6691d75
7 changed files with 394 additions and 0 deletions
40
plugin/docker/README.md
Normal file
40
plugin/docker/README.md
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# docker plugin for sshpiperd
|
||||
|
||||
This plugin queries dockerd for containers and creates pipes to them.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
sshpiperd docker
|
||||
```
|
||||
|
||||
start a container with sshpiper labels
|
||||
|
||||
```
|
||||
docker run -d -e USER_NAME=user -e USER_PASSWORD=pass -e PASSWORD_ACCESS=true -l sshpiper.username=pass -l sshpiper.container_username=user -l sshpiper.port=2222 lscr.io/linuxserver/openssh-server
|
||||
```
|
||||
|
||||
connect to piper
|
||||
|
||||
```
|
||||
ssh -l pass piper
|
||||
```
|
||||
|
||||
### Config docker connection
|
||||
|
||||
Docker connection is configured with environment variables below:
|
||||
|
||||
<https://pkg.go.dev/github.com/docker/docker/client#FromEnv>
|
||||
|
||||
* DOCKER_HOST: to set the url to the docker server, default "unix:///var/run/docker.sock"
|
||||
* DOCKER_API_VERSION: to set the version of the API to reach, leave empty for latest.
|
||||
* DOCKER_CERT_PATH: to load the TLS certificates from.
|
||||
* DOCKER_TLS_VERIFY: to enable or disable TLS verification, off by default.
|
||||
|
||||
### Container Labels for plugin
|
||||
|
||||
* sshpiper.username: username to filter containers by `downstream`'s username. left empty to auth with `authorized_keys` only.
|
||||
* sshpiper.container_username: username of container's sshd
|
||||
* sshpiper.port: port of container's sshd
|
||||
* sshpiper.authorized_keys: authorized_keys to verify against `downstream`. in base64 format
|
||||
* sshpiper.private_key: private_key to sent to container's sshd. in base64 format
|
||||
192
plugin/docker/docker.go
Normal file
192
plugin/docker/docker.go
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/patrickmn/go-cache"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/tg123/sshpiper/libplugin"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type pipe struct {
|
||||
ClientUsername string
|
||||
ContainerUsername string
|
||||
Host string
|
||||
AuthorizedKeys string
|
||||
PrivateKey string
|
||||
}
|
||||
|
||||
type plugin struct {
|
||||
dockerCli *client.Client
|
||||
cache *cache.Cache
|
||||
}
|
||||
|
||||
func newDockerPlugin() (*plugin, error) {
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &plugin{
|
||||
dockerCli: cli,
|
||||
cache: cache.New(1*time.Minute, 10*time.Minute),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *plugin) listPipes() ([]pipe, error) {
|
||||
// filter := filters.NewArgs()
|
||||
// filter.Add("label", fmt.Sprintf("sshpiper.username=%v", username))
|
||||
|
||||
containers, err := p.dockerCli.ContainerList(context.Background(), types.ContainerListOptions{
|
||||
// Filters: filter,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var pipes []pipe
|
||||
for _, c := range containers {
|
||||
// TODO: support env?
|
||||
p := pipe{}
|
||||
p.ClientUsername = c.Labels["sshpiper.username"]
|
||||
p.ContainerUsername = c.Labels["sshpiper.container_username"]
|
||||
p.AuthorizedKeys = c.Labels["sshpiper.authorized_keys"]
|
||||
p.PrivateKey = c.Labels["sshpiper.private_key"]
|
||||
|
||||
if p.ClientUsername == "" && p.AuthorizedKeys == "" {
|
||||
log.Debugf("skipping container %v without sshpiper.username or sshpiper.authorized_keys or sshpiper.private_key", c.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
if p.AuthorizedKeys != "" && p.PrivateKey == "" {
|
||||
log.Errorf("skipping container %v without sshpiper.private_key but has sshpiper.authorized_keys", c.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, network := range c.NetworkSettings.Networks {
|
||||
if network.IPAddress != "" {
|
||||
port := c.Labels["sshpiper.port"]
|
||||
if port == "" {
|
||||
p.Host = network.IPAddress // default 22
|
||||
} else {
|
||||
p.Host = net.JoinHostPort(network.IPAddress, port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pipes = append(pipes, p)
|
||||
}
|
||||
|
||||
return pipes, nil
|
||||
}
|
||||
|
||||
func (p *plugin) supportedMethods() ([]string, error) {
|
||||
pipes, err := p.listPipes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
set := make(map[string]bool)
|
||||
|
||||
for _, pipe := range pipes {
|
||||
if pipe.AuthorizedKeys != "" {
|
||||
set["publickey"] = true // found authorized_keys, so we support publickey
|
||||
} else {
|
||||
set["password"] = true // no authorized_keys, so we support password
|
||||
}
|
||||
}
|
||||
|
||||
var methods []string
|
||||
for k := range set {
|
||||
methods = append(methods, k)
|
||||
}
|
||||
return methods, nil
|
||||
}
|
||||
|
||||
func (p *plugin) createUpstream(conn libplugin.ConnMetadata, to pipe, originPassword string) (*libplugin.Upstream, error) {
|
||||
host, port, err := libplugin.SplitHostPortForSSH(to.Host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u := &libplugin.Upstream{
|
||||
Host: host,
|
||||
Port: int32(port),
|
||||
UserName: to.ContainerUsername,
|
||||
IgnoreHostKey: true,
|
||||
}
|
||||
|
||||
// password found
|
||||
if originPassword != "" {
|
||||
u.Auth = libplugin.CreatePasswordAuth([]byte(originPassword))
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// try private key
|
||||
data, err := base64.StdEncoding.DecodeString(to.PrivateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if data != nil {
|
||||
u.Auth = libplugin.CreatePrivateKeyAuth(data)
|
||||
return u, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no password or private key found")
|
||||
}
|
||||
|
||||
func (p *plugin) findAndCreateUpstream(conn libplugin.ConnMetadata, password string, publicKey []byte) (*libplugin.Upstream, error) {
|
||||
user := conn.User()
|
||||
|
||||
pipes, err := p.listPipes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, pipe := range pipes {
|
||||
|
||||
// test password
|
||||
if publicKey == nil {
|
||||
if pipe.ClientUsername != user {
|
||||
continue
|
||||
}
|
||||
|
||||
return p.createUpstream(conn, pipe, password)
|
||||
}
|
||||
|
||||
// test public key
|
||||
if pipe.ClientUsername != "" {
|
||||
if pipe.ClientUsername != user {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// ignore username and match all
|
||||
rest, err := base64.RawStdEncoding.DecodeString(pipe.AuthorizedKeys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var authedPubkey ssh.PublicKey
|
||||
for len(rest) > 0 {
|
||||
authedPubkey, _, _, rest, err = ssh.ParseAuthorizedKey(rest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if bytes.Equal(authedPubkey.Marshal(), publicKey) {
|
||||
return p.createUpstream(conn, pipe, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no matching pipe for username [%v] found", user)
|
||||
}
|
||||
35
plugin/docker/main.go
Normal file
35
plugin/docker/main.go
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/tg123/sshpiper/libplugin"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
libplugin.CreateAndRunPluginTemplate(&libplugin.PluginTemplate{
|
||||
Name: "docker",
|
||||
Usage: "sshpiperd docker plugin, see config in https://github.com/tg123/sshpiper/tree/master/plugin/docker",
|
||||
CreateConfig: func(c *cli.Context) (*libplugin.SshPiperPluginConfig, error) {
|
||||
plugin, err := newDockerPlugin()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &libplugin.SshPiperPluginConfig{
|
||||
|
||||
NextAuthMethodsCallback: func(_ libplugin.ConnMetadata) ([]string, error) {
|
||||
return plugin.supportedMethods()
|
||||
},
|
||||
|
||||
PasswordCallback: func(conn libplugin.ConnMetadata, password []byte) (*libplugin.Upstream, error) {
|
||||
return plugin.findAndCreateUpstream(conn, string(password), nil)
|
||||
},
|
||||
|
||||
PublicKeyCallback: func(conn libplugin.ConnMetadata, key []byte) (*libplugin.Upstream, error) {
|
||||
return plugin.findAndCreateUpstream(conn, "", key)
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue