introduce Yaml upstream (#49)

YAML upstream provides a more flexible way to config mappings
This commit is contained in:
Boshi Lian 2020-05-18 08:16:15 -07:00 committed by GitHub
parent 4317868ccc
commit 0f58c3b449
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 820 additions and 49 deletions

3
go.mod
View file

@ -4,7 +4,7 @@ go 1.12
replace (
github.com/jessevdk/go-flags => github.com/tg123/go-flags v1.4.0-globalref
golang.org/x/crypto => github.com/tg123/sshpiper.crypto v0.0.0-sshpiper-20200515
golang.org/x/crypto => github.com/tg123/sshpiper.crypto v0.0.0-sshpiper-20200515-v2
)
require (
@ -29,4 +29,5 @@ require (
github.com/pkg/errors v0.9.1 // indirect
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37
golang.org/x/sys v0.0.0-20200513112337-417ce2331b5c // indirect
gopkg.in/yaml.v3 v3.0.0-20200506231410-2ff61e1afc86
)

8
go.sum
View file

@ -77,8 +77,8 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/tg123/go-flags v1.4.0-globalref h1:pfUF3Mdnw5gZ5izA7s95Yrp+AetZPlnO9x39RByCi98=
github.com/tg123/go-flags v1.4.0-globalref/go.mod h1:G60U6XrJAj49cFQ8MY2Wr+SEjylerbSqj0I8FZy2tFE=
github.com/tg123/sshpiper.crypto v0.0.0-sshpiper-20200515 h1:ag4BBUO+Ixmiz5vpSxBVlmvGqQfOlY+0NGa2QLR1yyo=
github.com/tg123/sshpiper.crypto v0.0.0-sshpiper-20200515/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
github.com/tg123/sshpiper.crypto v0.0.0-sshpiper-20200515-v2 h1:vGv7a9lvys1S0eng/lz3p56uQbAES4H1D0OU1rp8Bh4=
github.com/tg123/sshpiper.crypto v0.0.0-sshpiper-20200515-v2/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
@ -88,3 +88,7 @@ golang.org/x/sys v0.0.0-20200513112337-417ce2331b5c/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200506231410-2ff61e1afc86 h1:OfFoIUYv/me30yv7XlMy4F9RJw8DEm8WQ6QG1Ph4bH0=
gopkg.in/yaml.v3 v3.0.0-20200506231410-2ff61e1afc86/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -19,28 +19,22 @@ services:
- names:/names
- ./test.sh:/test.sh
- ./wait.sh:/wait.sh
links:
- "piper:piper"
- "piper_sqlite:piper_sqlite"
- "piper_mysql:piper_mysql"
- "piper_pg:piper_pg"
command:
- "/bin/sh"
- "/test.sh"
depends_on:
- piper
- piper_yaml
- piper_mysql
- piper_pg
- piper_mssql
- piper_sqlite
piper:
environment:
- "SSHPIPERD_UPSTREAM_WORKINGDIR_NOCHECKPERM=true"
- "SSHPIPERD_AUDITOR=typescript-logger"
- "SSHPIPERD_BANNERTEXT=hellopiper"
build: ../..
links:
- "host1:host1"
- "host2:host2"
volumes:
- workingdir:/var/sshpiper
- ./piper.sh:/piper.sh
@ -51,15 +45,27 @@ services:
depends_on:
- host1
- host2
piper_yaml:
environment:
- "SSHPIPERD_UPSTREAM_DRIVER=yaml"
- "SSHPIPERD_UPSTREAM_YAML_NOCHECKPERM=true"
- "SSHPIPERD_UPSTREAM_YAML_FILE=/sshpiperd.yaml"
- "SSHPIPERD_AUDITOR=typescript-logger"
- "SSHPIPERD_BANNERTEXT=hellopiper"
build: ../..
volumes:
- localkey:/local
- workingdir:/var/sshpiper
- ./sshpiperd.yaml:/sshpiperd.yaml
depends_on:
- host1
- host2
piper_sqlite:
environment:
- "SSHPIPERD_UPSTREAM_DRIVER=sqlite"
- "SSHPIPERD_AUDITOR=typescript-logger"
- "SSHPIPERD_BANNERTEXT=hellopiper"
build: ../..
links:
- "host1:host1"
- "host2:host2"
volumes:
- workingdir:/var/sshpiper
- ./piper.sh:/piper.sh
@ -79,10 +85,6 @@ services:
- "SSHPIPERD_AUDITOR=typescript-logger"
- "SSHPIPERD_BANNERTEXT=hellopiper"
build: ../..
links:
- "host1:host1"
- "host2:host2"
- "mysql:mysql"
volumes:
- ./piper.sh:/piper.sh
- workingdir:/var/sshpiper
@ -111,10 +113,6 @@ services:
- "SSHPIPERD_AUDITOR=typescript-logger"
- "SSHPIPERD_BANNERTEXT=hellopiper"
build: ../..
links:
- "host1:host1"
- "host2:host2"
- "postgres:postgres"
volumes:
- ./piper.sh:/piper.sh
- workingdir:/var/sshpiper
@ -145,10 +143,6 @@ services:
- "SSHPIPERD_AUDITOR=typescript-logger"
- "SSHPIPERD_BANNERTEXT=hellopiper"
build: ../..
links:
- "host1:host1"
- "host2:host2"
- "mssql:mssql"
volumes:
- ./piper.sh:/piper.sh
- workingdir:/var/sshpiper

View file

@ -0,0 +1,81 @@
version: 1
pipes:
- username: passthrough
upstream_host: host2
ignore_hostkey: true
authmap:
mapped_username: root
- username: mappasspass
upstream_host: host2
ignore_hostkey: true
authmap:
mapped_username: root
from:
- type: password
password: pass
to:
type: password
password: root
- username: mappasskey
upstream_host: host1
ignore_hostkey: true
authmap:
mapped_username: root
from:
- type: password
password: pass
to:
type: privatekey
private_key: /var/sshpiper/host1/id_rsa
- username: mapkeypass
upstream_host: host2
ignore_hostkey: true
authmap:
mapped_username: root
from:
- type: publickey
authorized_keys: /local/id_rsa2.pub
to:
type: password
password: root
- username: mapkeykey
upstream_host: host1
ignore_hostkey: true
authmap:
mapped_username: root
from:
- type: publickey
authorized_keys: /local/id_rsa2.pub
to:
type: privatekey
private_key: /var/sshpiper/host1/id_rsa
- username: mapkeykey2
upstream_host: host1
ignore_hostkey: true
authmap:
mapped_username: root
from:
- type: publickey
allow_any_public_key: true
to:
type: privatekey
key_map:
- authorized_keys: /local/id_rsa2.pub
private_key: /var/sshpiper/host1/id_rsa
- username: ^regex.*$
username_regex_match: true
upstream_host: host2
ignore_hostkey: true
authmap:
mapped_username: root
- username: ^host.*$
username_regex_match: true
upstream_host: host1
ignore_hostkey: true
authmap:
mapped_username: root
from:
- type: any
to:
type: privatekey
private_key: /var/sshpiper/$USER/id_rsa

View file

@ -1,6 +1,9 @@
#!/bin/sh
/wait.sh piper 2222
/wait.sh piper_yaml 2222
/wait.sh piper_sqlite 2222
/wait.sh piper_mysql 2222
/wait.sh piper_pg 2222
/wait.sh piper_mssql 2222
@ -14,6 +17,9 @@ mkdir -p /workingdir/host{1,2}
ssh-keygen -N '' -f /local/id_rsa
ssh-keygen -N '' -f /workingdir/host1/id_rsa #TODO pipe cmd
ssh-keygen -N '' -f /local/id_rsa2
/bin/cp /local/id_rsa.pub /workingdir/host1/authorized_keys
/bin/cp /workingdir/host1/id_rsa.pub /host1/authorized_keys
@ -23,11 +29,13 @@ succ="\033[0;32mSUCC\033[0m"
runtest(){
casename=$1
host=$2
user=$3
cmd=$4
rnd=`head -c 20 /dev/urandom | base64`
echo $rnd > /names/$host
rm -f /tmp/$host.stderr
t=$($3 2>/tmp/$host.stderr)
t=$($cmd 2>/tmp/$host.stderr)
if [ "$t" != "$rnd" ];then
echo -e $casename $fail
@ -36,7 +44,7 @@ runtest(){
echo -e $casename $succ
fi
grep $rnd /workingdir/$host/*
grep $rnd /workingdir/$user/*
if [ $? -ne 0 ];then
echo -e "grep typescript logger" $fail
@ -51,11 +59,20 @@ runtest(){
fi
}
runtest "host1 with public key:" "host1" "ssh host1@piper -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /local/id_rsa cat /names/host1"
runtest "host2 with password:" "host2" "sshpass -p root ssh host2@piper -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null cat /names/host2"
runtest "host1 with public key:" "host1" "host1" "ssh host1@piper -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /local/id_rsa cat /names/host1"
runtest "host2 with password:" "host2" "host2" "sshpass -p root ssh host2@piper -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null cat /names/host2"
runtest "sqlite host2 with password:" "host2" "sshpass -p root ssh host2@piper_sqlite -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null cat /names/host2"
runtest "mysql host2 with password:" "host2" "sshpass -p root ssh host2@piper_mysql -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null cat /names/host2"
runtest "pg host2 with password:" "host2" "sshpass -p root ssh host2@piper_pg -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null cat /names/host2"
runtest "msql host2 with password:" "host2" "sshpass -p root ssh host2@piper_mssql -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null cat /names/host2"
runtest "sqlite host2 with password:" "host2" "host2" "sshpass -p root ssh host2@piper_sqlite -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null cat /names/host2"
runtest "mysql host2 with password:" "host2" "host2" "sshpass -p root ssh host2@piper_mysql -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null cat /names/host2"
runtest "pg host2 with password:" "host2" "host2" "sshpass -p root ssh host2@piper_pg -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null cat /names/host2"
runtest "msql host2 with password:" "host2" "host2" "sshpass -p root ssh host2@piper_mssql -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null cat /names/host2"
runtest "yaml host2 with password passthrough:" "host2" "passthrough" "sshpass -p root ssh passthrough@piper_yaml -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null cat /names/host2"
runtest "yaml host2 with password mappasspass:" "host2" "mappasspass" "sshpass -p pass ssh mappasspass@piper_yaml -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null cat /names/host2"
runtest "yaml host1 with password mappasskey:" "host1" "mappasskey" "sshpass -p pass ssh mappasskey@piper_yaml -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null cat /names/host1"
runtest "yaml host2 with password mapkeypass:" "host2" "mapkeypass" "ssh mapkeypass@piper_yaml -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /local/id_rsa2 cat /names/host2"
runtest "yaml host2 with key mapkeykey:" "host1" "mapkeykey" "ssh mapkeykey@piper_yaml -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /local/id_rsa2 cat /names/host1"
runtest "yaml host2 with key mapkeykey2:" "host1" "mapkeykey2" "ssh mapkeykey2@piper_yaml -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /local/id_rsa2 cat /names/host1"
runtest "yaml host2 with password regex:" "host2" "regex000" "sshpass -p root ssh regex000@piper_yaml -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null cat /names/host2"
runtest "yaml host1 with none host1:" "host1" "host1" "ssh host1@piper_yaml -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null cat /names/host1"

View file

@ -3,6 +3,7 @@ package main
import (
_ "github.com/tg123/sshpiper/sshpiperd/upstream/database"
_ "github.com/tg123/sshpiper/sshpiperd/upstream/workingdir"
_ "github.com/tg123/sshpiper/sshpiperd/upstream/yaml"
_ "github.com/tg123/sshpiper/sshpiperd/challenger/authy"
_ "github.com/tg123/sshpiper/sshpiperd/challenger/azdevicecode"

View file

@ -1,5 +1,5 @@
name: sshpiperd
version: 0.4.5
version: 0.5.0
summary: The missing reverse proxy for ssh scp
description: |
SSH Piper works as a proxy-like ware, and route connections by username, src ip , etc.

View file

@ -6,6 +6,8 @@ import (
"net"
"golang.org/x/crypto/ssh"
upstreamprovider "github.com/tg123/sshpiper/sshpiperd/upstream"
)
func (p *plugin) findUpstream(conn ssh.ConnMetadata, challengeContext ssh.AdditionalChallengeContext) (net.Conn, *ssh.AuthPipe, error) {
@ -26,7 +28,7 @@ func (p *plugin) findUpstream(conn ssh.ConnMetadata, challengeContext ssh.Additi
logger.Printf("mapping user [%v] to [%v@%v]", user, upuser, addr)
c, err := dial(addr)
c, err := upstreamprovider.DialForSSH(addr)
if err != nil {
return nil, nil, err
@ -115,15 +117,3 @@ func lookupConfigValue(db *gorm.DB, entry string) (string, error) {
return c.Value, nil
}
func dial(addr string) (net.Conn, error) {
if _, _, err := net.SplitHostPort(addr); err != nil && addr != "" {
// test valid after concat :22
if _, _, err := net.SplitHostPort(addr + ":22"); err == nil {
addr += ":22"
}
}
return net.Dial("tcp", addr)
}

View file

@ -102,3 +102,16 @@ func SplitHostPortForSSH(addr string) (host string, port int, err error) {
return
}
// DialForSSH is the modified version of net.Dial, would add ":22" automaticlly
func DialForSSH(addr string) (net.Conn, error) {
if _, _, err := net.SplitHostPort(addr); err != nil && addr != "" {
// test valid after concat :22
if _, _, err := net.SplitHostPort(addr + ":22"); err == nil {
addr += ":22"
}
}
return net.Dial("tcp", addr)
}

View file

@ -0,0 +1,250 @@
package yaml
import (
"bytes"
"fmt"
"io/ioutil"
"github.com/tg123/sshpiper/sshpiperd/upstream"
"gopkg.in/yaml.v3"
)
// Return All pipes inside upstream
func (p *plugin) ListPipe() ([]upstream.Pipe, error) {
config, err := p.loadConfig()
if err != nil {
return nil, err
}
out, err := yaml.Marshal(config)
if err != nil {
return nil, err
}
fmt.Println(string(out))
var pipes []upstream.Pipe
for _, pipe := range config.Pipes {
host, port, err := upstream.SplitHostPortForSSH(pipe.UpstreamHost)
if err != nil {
return nil, err
}
mappeduser := pipe.Authmap.MappedUsername
if mappeduser == "" {
mappeduser = pipe.Username
}
pipes = append(pipes, upstream.Pipe{
Host: host,
Port: port,
UpstreamUsername: mappeduser,
Username: pipe.Username,
})
}
return pipes, nil
}
func findnode(root *yaml.Node, test func(*yaml.Node) bool) *yaml.Node {
var q []*yaml.Node
q = append(q, root)
for len(q) > 0 {
e := q[0]
q = q[1:]
if test(e) {
return e
}
for _, n := range e.Content {
q = append(q, n)
}
}
return nil
}
func findByMapKey(m *yaml.Node, k string) (*yaml.Node, int) {
for i := 1; i < len(m.Content); i += 2 {
if m.Content[i-1].Value == k {
return m.Content[i], i
}
}
return nil, -1
}
func toYamlNode(s interface{}) (*yaml.Node, error) {
t := yaml.Node{}
out, err := yaml.Marshal(s)
if err != nil {
return nil, err
}
err = yaml.Unmarshal(out, &t)
if err != nil {
return nil, err
}
return t.Content[0], nil
}
func (p *plugin) loadConfigRaw() ([]byte, *yaml.Node, error) {
var config yaml.Node
configbyte, err := ioutil.ReadFile(p.Config.File)
if err != nil {
return nil, nil, err
}
err = yaml.Unmarshal(configbyte, &config)
if err != nil {
return nil, nil, err
}
return configbyte, &config, nil
}
func (p *plugin) writeConfig(config []byte) error {
return ioutil.WriteFile(p.Config.File, config, 0600)
}
func toPipeConfig(opt upstream.CreatePipeOption) pipeConfig {
p := pipeConfig{
Username: opt.Username,
UpstreamHost: fmt.Sprintf("%v:%v", opt.Host, opt.Port),
}
if len(opt.UpstreamUsername) > 0 {
p.Authmap.MappedUsername = opt.UpstreamUsername
}
return p
}
// Create a pipe inside upstream
func (p *plugin) CreatePipe(opt upstream.CreatePipeOption) error {
configbyte, config, err := p.loadConfigRaw()
if err != nil {
return err
}
if len(config.Content) == 0 {
var buf bytes.Buffer
buf.Write(configbyte)
fmt.Fprintln(&buf)
out, err := yaml.Marshal(piperConfig{
Version: 1,
Pipes: []pipeConfig{toPipeConfig(opt)},
})
if err != nil {
return err
}
buf.Write(out)
return p.writeConfig(buf.Bytes())
}
pipes, idx := findByMapKey(config.Content[0], "pipes")
if idx > 0 && pipes.Tag == "!!null" {
// replace null
t, err := toYamlNode([]pipeConfig{toPipeConfig(opt)})
if err != nil {
return err
}
config.Content[0].Content[idx] = t
out, err := yaml.Marshal(config)
if err != nil {
return err
}
return p.writeConfig(out)
}
if pipes.Kind != yaml.SequenceNode {
return fmt.Errorf("pipes should be !!seq")
}
for _, pnode := range pipes.Content {
pipe, _ := findByMapKey(pnode, "username")
if pipe != nil && pipe.Value == opt.Username {
return fmt.Errorf("username [%v] already exists", opt.Username)
}
}
// append
{
t, err := toYamlNode(toPipeConfig(opt))
if err != nil {
return err
}
pipes.Content = append(pipes.Content, t)
out, err := yaml.Marshal(config)
if err != nil {
return err
}
return p.writeConfig(out)
}
}
// Remove a pipe from upstream
func (p *plugin) RemovePipe(name string) error {
_, config, err := p.loadConfigRaw()
if err != nil {
return err
}
if len(config.Content) == 0 {
return nil
}
pipes, idx := findByMapKey(config.Content[0], "pipes")
if idx > 0 && pipes.Tag == "!!null" {
return nil
}
if pipes.Kind != yaml.SequenceNode {
return fmt.Errorf("pipes should be !!seq")
}
for i, pnode := range pipes.Content {
pipe, _ := findByMapKey(pnode, "username")
if pipe != nil && pipe.Value == name {
rest := pipes.Content[i+1:]
pipes.Content = append(pipes.Content[:i], rest...)
out, err := yaml.Marshal(config)
if err != nil {
return err
}
return p.writeConfig(out)
}
}
return nil
}

View file

@ -0,0 +1,41 @@
package yaml
import (
"log"
"github.com/tg123/sshpiper/sshpiperd/upstream"
)
type plugin struct {
Config struct {
File string `long:"upstream-yaml-file" default:"/var/sshpiper/sshpiperd.yaml" description:"Yaml config file path" env:"SSHPIPERD_UPSTREAM_YAML_FILE" ini-name:"upstream-yaml-file"`
NoCheckPerm bool `long:"upstream-yaml-nocheckperm" description:"Disable 0400 checking when using config file" env:"SSHPIPERD_UPSTREAM_YAML_NOCHECKPERM" ini-name:"upstream-yaml-nocheckperm"`
}
logger *log.Logger
}
// The name of the Plugin
func (p *plugin) GetName() string {
return "yaml"
}
// A ref to a struct which holds the options for the plugins
// will be populated by cmd or other plugin runners
func (p *plugin) GetOpts() interface{} {
return &p.Config
}
// Will be called before the Plugin is used to ensure the Plugin is ready
func (p *plugin) Init(logger *log.Logger) error {
p.logger = logger
return nil
}
func (p *plugin) GetHandler() upstream.Handler {
return p.findUpstream
}
func init() {
upstream.Register("yaml", &plugin{})
}

View file

View file

@ -0,0 +1,379 @@
package yaml
import (
"bytes"
"encoding/base64"
"fmt"
"io/ioutil"
"net"
"os"
"path/filepath"
"regexp"
"github.com/tg123/sshpiper/sshpiperd/upstream"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/knownhosts"
"gopkg.in/yaml.v3"
)
type pipeConfig struct {
Username string `yaml:"username"`
UsernameRegexMatch bool `yaml:"username_regex_match,omitempty"`
UpstreamHost string `yaml:"upstream_host"`
Authmap struct {
MappedUsername string `yaml:"mapped_username,omitempty"`
From []struct {
Type string `yaml:"type"`
Password string `yaml:"password,omitempty"`
AuthorizedKeys string `yaml:"authorized_keys,omitempty"`
AuthorizedKeysData string `yaml:"authorized_keys_data,omitempty"`
AllowAnyPublicKey bool `yaml:"allow_any_public_key,omitempty"`
} `yaml:"from,flow"`
To struct {
Type string `yaml:"type"`
Password string `yaml:"password,omitempty"`
PrivateKey string `yaml:"private_key,omitempty"`
PrivateKeyData string `yaml:"private_key_data,omitempty"`
KeyMap []struct {
AuthorizedKeys string `yaml:"authorized_keys,omitempty"`
AuthorizedKeysData string `yaml:"authorized_keys_data,omitempty"`
PrivateKey string `yaml:"private_key,omitempty"`
PrivateKeyData string `yaml:"private_key_data,omitempty"`
} `yaml:"key_map,flow"`
} `yaml:"to,flow"`
NoPassthrough bool `yaml:"no_passthrough,omitempty"`
} `yaml:"authmap,omitempty,flow"`
KnownHosts string `yaml:"known_hosts,omitempty"`
KnownHostsData string `yaml:"known_hosts_data,omitempty"`
IgnoreHostkey bool `yaml:"ignore_hostkey,omitempty"`
}
type piperConfig struct {
Version int `yaml:"version"`
Pipes []pipeConfig `yaml:"pipes,flow"`
}
func (p *plugin) checkPerm() error {
filename := p.Config.File
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
return err
}
if p.Config.NoCheckPerm {
return nil
}
if fi.Mode().Perm()&0077 != 0 {
return fmt.Errorf("%v's perm is too open", filename)
}
return nil
}
func (p *plugin) loadConfig() (piperConfig, error) {
var config piperConfig
err := p.checkPerm()
if err != nil {
return config, err
}
configbyte, err := ioutil.ReadFile(p.Config.File)
if err != nil {
return config, err
}
err = yaml.Unmarshal(configbyte, &config)
if err != nil {
return config, err
}
return config, nil
}
type createPipeCtx struct {
pipe pipeConfig
conn ssh.ConnMetadata
challengeContext ssh.AdditionalChallengeContext
}
func (p *plugin) loadFileOrDecode(file string, base64data string, ctx createPipeCtx) ([]byte, error) {
if file != "" {
file = os.Expand(file, func(placeholderName string) string {
switch placeholderName {
case "USER":
return ctx.conn.User()
case "MAPPED_USER":
return ctx.pipe.Authmap.MappedUsername
}
return os.Getenv(placeholderName)
})
if !filepath.IsAbs(file) {
file = filepath.Join(filepath.Dir(p.Config.File), file)
}
return ioutil.ReadFile(file)
}
if base64data != "" {
return base64.StdEncoding.DecodeString(base64data)
}
return nil, nil
}
func (p *plugin) createAuthPipe(pipe pipeConfig, conn ssh.ConnMetadata, challengeContext ssh.AdditionalChallengeContext) (*ssh.AuthPipe, error) {
ctx := createPipeCtx{pipe, conn, challengeContext}
hostKeyCallback := ssh.InsecureIgnoreHostKey()
if !pipe.IgnoreHostkey {
data, err := p.loadFileOrDecode(pipe.KnownHosts, pipe.KnownHostsData, ctx)
if err != nil {
return nil, err
}
if len(data) == 0 {
return nil, fmt.Errorf("no known hosts spicified")
}
hostKeyCallback, err = knownhosts.NewFromReader(bytes.NewReader(data))
if err != nil {
return nil, err
}
}
to := func(key ssh.PublicKey) (ssh.AuthPipeType, ssh.AuthMethod, error) {
switch pipe.Authmap.To.Type {
case "none":
return ssh.AuthPipeTypeNone, nil, nil
case "password":
return ssh.AuthPipeTypeMap, ssh.Password(pipe.Authmap.To.Password), nil
case "privatekey":
privateBytes, err := p.loadFileOrDecode(pipe.Authmap.To.PrivateKey, pipe.Authmap.To.PrivateKeyData, ctx)
if err != nil {
return ssh.AuthPipeTypeDiscard, nil, err
}
// did not find to 1 private key try key map
if len(privateBytes) == 0 && key != nil {
for _, privkey := range pipe.Authmap.To.KeyMap {
rest, err := p.loadFileOrDecode(privkey.AuthorizedKeys, privkey.AuthorizedKeysData, ctx)
if err != nil {
return ssh.AuthPipeTypeDiscard, nil, err
}
var authedPubkey ssh.PublicKey
for len(rest) > 0 {
authedPubkey, _, _, rest, err = ssh.ParseAuthorizedKey(rest)
if err != nil {
return ssh.AuthPipeTypeDiscard, nil, err
}
keydata := key.Marshal()
if bytes.Equal(authedPubkey.Marshal(), keydata) {
privateBytes, err = p.loadFileOrDecode(privkey.PrivateKey, privkey.PrivateKeyData, ctx)
if err != nil {
return ssh.AuthPipeTypeDiscard, nil, err
}
if len(privateBytes) > 0 {
// found mapped
break
}
}
}
}
}
if len(privateBytes) == 0 {
return ssh.AuthPipeTypeDiscard, nil, fmt.Errorf("no private key found")
}
private, err := ssh.ParsePrivateKey(privateBytes)
if err != nil {
return ssh.AuthPipeTypeDiscard, nil, err
}
return ssh.AuthPipeTypeMap, ssh.PublicKeys(private), nil
default:
p.logger.Printf("unsupport type [%v] fallback to passthrough", pipe.Authmap.To.Type)
}
if pipe.Authmap.NoPassthrough {
return ssh.AuthPipeTypeDiscard, nil, nil
}
return ssh.AuthPipeTypePassThrough, nil, nil
}
allowPasswords := make(map[string]bool)
var allowPubKeys []ssh.PublicKey
allowAnyPubKey := false
a := &ssh.AuthPipe{
User: pipe.Authmap.MappedUsername,
UpstreamHostKeyCallback: hostKeyCallback,
}
for _, from := range pipe.Authmap.From {
switch from.Type {
case "none":
if a.NoneAuthCallback == nil {
a.NoneAuthCallback = func(conn ssh.ConnMetadata) (ssh.AuthPipeType, ssh.AuthMethod, error) {
return to(nil)
}
}
case "password":
allowPasswords[from.Password] = true
if a.PasswordCallback == nil {
a.PasswordCallback = func(conn ssh.ConnMetadata, password []byte) (ssh.AuthPipeType, ssh.AuthMethod, error) {
_, ok := allowPasswords[string(password)]
if ok {
return to(nil)
}
if pipe.Authmap.NoPassthrough {
return ssh.AuthPipeTypeDiscard, nil, nil
}
return ssh.AuthPipeTypePassThrough, nil, nil
}
}
case "publickey":
allowAnyPubKey = allowAnyPubKey || from.AllowAnyPublicKey
if !allowAnyPubKey {
rest, err := p.loadFileOrDecode(from.AuthorizedKeys, from.AuthorizedKeysData, ctx)
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
}
allowPubKeys = append(allowPubKeys, authedPubkey)
}
}
if a.PublicKeyCallback == nil {
a.PublicKeyCallback = func(conn ssh.ConnMetadata, key ssh.PublicKey) (ssh.AuthPipeType, ssh.AuthMethod, error) {
if allowAnyPubKey {
return to(key)
}
keydata := key.Marshal()
for _, authedPubkey := range allowPubKeys {
if bytes.Equal(authedPubkey.Marshal(), keydata) {
return to(key)
}
}
if pipe.Authmap.NoPassthrough {
return ssh.AuthPipeTypeDiscard, nil, nil
}
// will fail but discard will lead a timeout
return ssh.AuthPipeTypePassThrough, nil, nil
}
}
case "any":
a.NoneAuthCallback = func(conn ssh.ConnMetadata) (ssh.AuthPipeType, ssh.AuthMethod, error) {
return to(nil)
}
a.PasswordCallback = func(conn ssh.ConnMetadata, password []byte) (ssh.AuthPipeType, ssh.AuthMethod, error) {
return to(nil)
}
a.PublicKeyCallback = func(conn ssh.ConnMetadata, key ssh.PublicKey) (ssh.AuthPipeType, ssh.AuthMethod, error) {
return to(key)
}
return a, nil
default:
p.logger.Printf("unsupport type [%v], ignore section", from.Type)
}
}
return a, nil
}
func (p *plugin) findUpstream(conn ssh.ConnMetadata, challengeContext ssh.AdditionalChallengeContext) (net.Conn, *ssh.AuthPipe, error) {
user := conn.User()
config, err := p.loadConfig()
if err != nil {
return nil, nil, err
}
for _, pipe := range config.Pipes {
matched := pipe.Username == user
if pipe.UsernameRegexMatch {
matched, _ = regexp.MatchString(pipe.Username, user)
}
if matched {
p.logger.Printf("mapping [%v] to [%v]", user, pipe.Username)
c, err := upstream.DialForSSH(pipe.UpstreamHost)
if err != nil {
return nil, nil, err
}
a, err := p.createAuthPipe(pipe, conn, challengeContext)
if err != nil {
return nil, nil, err
}
return c, a, nil
}
}
return nil, nil, fmt.Errorf("username not [%v] found", user)
}

2
ver
View file

@ -1 +1 @@
v0.4.5dev
v0.5.0