Refactor YAML schema to support multiple authorized keys and known hosts (#463)

* add coverage for go proxy

* Refactor CLI flag name for allowed proxy addresses

* Refactor YAML schema to support multiple authorized keys and known hosts

* add testcases

* Refactor YAML schema to support multiple authorized keys and known hosts

* go fmt

* go fmt
This commit is contained in:
Boshi Lian 2024-10-20 00:20:40 -07:00 • committed by GitHub
parent 4ad8e77e3f
commit 39f5eef98b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 205 additions and 29 deletions

View file

@ -27,7 +27,11 @@ pipes:
to: to:
host: host-password:2222 host: host-password:2222
username: "user" username: "user"
ignore_hostkey: true known_hosts_data:
# github.com
- fDF8RjRwTmVveUZHVEVHcEIyZ3A4RGE0WlE4TGNVPXxycVZYNU0rWTJoS0dteFphcVFBb0syRHp1TEE9IHNzaC1lZDI1NTE5IEFBQUFDM056YUMxbFpESTFOVEU1QUFBQUlPTXFxbmtWenJtMFNkRzZVT29xS0xzYWJnSDVDOW9rV2kwZGgybDlHS0psCg==
- {{ .KnownHostsKey }}
- {{ .KnownHostsPass }}
- from: - from:
- username: "publickey_simple" - username: "publickey_simple"
authorized_keys: {{ .AuthorizedKeys_Simple }} authorized_keys: {{ .AuthorizedKeys_Simple }}
@ -35,11 +39,13 @@ pipes:
host: host-publickey:2222 host: host-publickey:2222
username: "user" username: "user"
private_key: {{ .PrivateKey }} private_key: {{ .PrivateKey }}
known_hosts_data: {{ .KnownHosts }} known_hosts_data: {{ .KnownHostsKey }}
- from: - from:
- username: ".*" - username: ".*"
username_regex_match: true username_regex_match: true
authorized_keys: {{ .AuthorizedKeys_Catchall }} authorized_keys:
- {{ .AuthorizedKeys_Simple }}
- {{ .AuthorizedKeys_Catchall }}
to: to:
host: host-publickey:2222 host: host-publickey:2222
username: "user" username: "user"
@ -114,7 +120,7 @@ func TestYaml(t *testing.T) {
} }
} }
knownHostsData, err := runAndGetStdout( knownHostsKeyData, err := runAndGetStdout(
"ssh-keyscan", "ssh-keyscan",
"-p", "-p",
"2222", "2222",
@ -125,14 +131,26 @@ func TestYaml(t *testing.T) {
t.Errorf("failed to run ssh-keyscan: %v", err) t.Errorf("failed to run ssh-keyscan: %v", err)
} }
knownHostsPassData, err := runAndGetStdout(
"ssh-keyscan",
"-p",
"2222",
"host-password",
)
if err != nil {
t.Errorf("failed to run ssh-keyscan : %v", err)
}
if err := template.Must(template.New("yaml").Parse(yamlConfigTemplate)).ExecuteTemplate(yamlfile, "yaml", struct { if err := template.Must(template.New("yaml").Parse(yamlConfigTemplate)).ExecuteTemplate(yamlfile, "yaml", struct {
KnownHosts string KnownHostsKey string
KnownHostsPass string
PrivateKey string PrivateKey string
AuthorizedKeys_Simple string AuthorizedKeys_Simple string
AuthorizedKeys_Catchall string AuthorizedKeys_Catchall string
}{ }{
KnownHosts: base64.StdEncoding.EncodeToString(knownHostsData), KnownHostsKey: base64.StdEncoding.EncodeToString(knownHostsKeyData),
KnownHostsPass: base64.StdEncoding.EncodeToString(knownHostsPassData),
PrivateKey: path.Join(yamldir, "id_rsa"), PrivateKey: path.Join(yamldir, "id_rsa"),
AuthorizedKeys_Simple: path.Join(yamldir, "id_rsa_simple.pub"), AuthorizedKeys_Simple: path.Join(yamldir, "id_rsa_simple.pub"),

View file

@ -51,12 +51,32 @@
"type": "boolean" "type": "boolean"
}, },
"authorized_keys": { "authorized_keys": {
"oneOf": [
{
"type": "array",
"items": {
"type": "string" "type": "string"
}
},
{
"type": "string"
}
]
}, },
"authorized_keys_data": { "authorized_keys_data": {
"oneOf": [
{
"type": "array",
"items": {
"type": "string" "type": "string"
} }
}, },
{
"type": "string"
}
]
}
},
"required": [ "required": [
"username" "username"
] ]
@ -84,12 +104,32 @@
"type": "string" "type": "string"
}, },
"known_hosts": { "known_hosts": {
"oneOf": [
{
"type": "array",
"items": {
"type": "string" "type": "string"
}
},
{
"type": "string"
}
]
}, },
"known_hosts_data": { "known_hosts_data": {
"oneOf": [
{
"type": "array",
"items": {
"type": "string" "type": "string"
} }
}, },
{
"type": "string"
}
]
}
},
"required": [ "required": [
"host" "host"
] ]

View file

@ -20,8 +20,8 @@ import (
type pipeConfigFrom struct { type pipeConfigFrom struct {
Username string `yaml:"username"` Username string `yaml:"username"`
UsernameRegexMatch bool `yaml:"username_regex_match,omitempty"` UsernameRegexMatch bool `yaml:"username_regex_match,omitempty"`
AuthorizedKeys string `yaml:"authorized_keys,omitempty"` AuthorizedKeys listOrString `yaml:"authorized_keys,omitempty"`
AuthorizedKeysData string `yaml:"authorized_keys_data,omitempty"` AuthorizedKeysData listOrString `yaml:"authorized_keys_data,omitempty"`
} }
type pipeConfigTo struct { type pipeConfigTo struct {
@ -30,11 +30,43 @@ type pipeConfigTo struct {
Password string `yaml:"password,omitempty"` Password string `yaml:"password,omitempty"`
PrivateKey string `yaml:"private_key,omitempty"` PrivateKey string `yaml:"private_key,omitempty"`
PrivateKeyData string `yaml:"private_key_data,omitempty"` PrivateKeyData string `yaml:"private_key_data,omitempty"`
KnownHosts string `yaml:"known_hosts,omitempty"` KnownHosts listOrString `yaml:"known_hosts,omitempty"`
KnownHostsData string `yaml:"known_hosts_data,omitempty"` KnownHostsData listOrString `yaml:"known_hosts_data,omitempty"`
IgnoreHostkey bool `yaml:"ignore_hostkey,omitempty"` IgnoreHostkey bool `yaml:"ignore_hostkey,omitempty"`
} }
type listOrString struct {
List []string
Str string
}
func (l *listOrString) Any() bool {
return len(l.List) > 0 || l.Str != ""
}
func (l *listOrString) Combine() []string {
if l.Str != "" {
return append(l.List, l.Str)
}
return l.List
}
func (l *listOrString) UnmarshalYAML(value *yaml.Node) error {
// Try to unmarshal as a list
var list []string
if err := value.Decode(&list); err == nil {
l.List = list
return nil
}
// Try to unmarshal as a string
var str string
if err := value.Decode(&str); err == nil {
l.Str = str
return nil
}
return fmt.Errorf("Failed to unmarshal OneOfType")
}
type pipeConfig struct { type pipeConfig struct {
From []pipeConfigFrom `yaml:"from,flow"` From []pipeConfigFrom `yaml:"from,flow"`
To pipeConfigTo `yaml:"to,flow"` To pipeConfigTo `yaml:"to,flow"`
@ -129,6 +161,34 @@ func (p *plugin) loadFileOrDecode(file string, base64data string, vars map[strin
return nil, nil return nil, nil
} }
func (p *plugin) loadFileOrDecodeMany(files listOrString, base64data listOrString, vars map[string]string) ([]byte, error) {
var byteSlices [][]byte
for _, file := range files.Combine() {
data, err := p.loadFileOrDecode(file, "", vars)
if err != nil {
return nil, err
}
if data != nil {
byteSlices = append(byteSlices, data)
}
}
for _, data := range base64data.Combine() {
decoded, err := base64.StdEncoding.DecodeString(data)
if err != nil {
return nil, err
}
if decoded != nil {
byteSlices = append(byteSlices, decoded)
}
}
return bytes.Join(byteSlices, []byte("\n")), nil
}
func (p *plugin) supportedMethods() ([]string, error) { func (p *plugin) supportedMethods() ([]string, error) {
config, err := p.loadConfig() config, err := p.loadConfig()
if err != nil { if err != nil {
@ -139,7 +199,7 @@ func (p *plugin) supportedMethods() ([]string, error) {
for _, pipe := range config.Pipes { for _, pipe := range config.Pipes {
for _, from := range pipe.From { for _, from := range pipe.From {
if from.AuthorizedKeys != "" || from.AuthorizedKeysData != "" { if from.AuthorizedKeys.Any() || from.AuthorizedKeysData.Any() {
set["publickey"] = true // found authorized_keys, so we support publickey set["publickey"] = true // found authorized_keys, so we support publickey
} else { } else {
set["password"] = true // no authorized_keys, so we support password set["password"] = true // no authorized_keys, so we support password
@ -163,7 +223,7 @@ func (p *plugin) verifyHostKey(conn libplugin.ConnMetadata, hostname, netaddr st
to := item.(*pipeConfigTo) to := item.(*pipeConfigTo)
data, err := p.loadFileOrDecode(to.KnownHosts, to.KnownHostsData, map[string]string{ data, err := p.loadFileOrDecodeMany(to.KnownHosts, to.KnownHostsData, map[string]string{
"DOWNSTREAM_USER": conn.User(), "DOWNSTREAM_USER": conn.User(),
"UPSTREAM_USER": to.Username, "UPSTREAM_USER": to.Username,
}) })
@ -242,7 +302,7 @@ func (p *plugin) findAndCreateUpstream(conn libplugin.ConnMetadata, password str
return p.createUpstream(conn, pipe.To, password) return p.createUpstream(conn, pipe.To, password)
} }
rest, err := p.loadFileOrDecode(from.AuthorizedKeys, from.AuthorizedKeysData, map[string]string{ rest, err := p.loadFileOrDecodeMany(from.AuthorizedKeys, from.AuthorizedKeysData, map[string]string{
"DOWNSTREAM_USER": user, "DOWNSTREAM_USER": user,
}) })
if err != nil { if err != nil {

58
plugin/yaml/yaml_test.go Normal file
View file

@ -0,0 +1,58 @@
//go:build full || e2e
package main
import (
"testing"
"gopkg.in/yaml.v3"
)
const yamlConfigTemplate = `
version: "1.0"
pipes:
- from:
- username: "password_simple"
to:
host: host-password:2222
username: "user"
ignore_hostkey: true
- from:
- username: "password_.*_regex"
username_regex_match: true
to:
host: host-password:2222
username: "user"
known_hosts_data:
- fDF8RjRwTmVveUZHVEVHcEIyZ3A4RGE0WlE4TGNVPXxycVZYNU0rWTJoS0dteFphcVFBb0syRHp1TEE9IHNzaC1lZDI1NTE5IEFBQUFDM056YUMxbFpESTFOVEU1QUFBQUlPTXFxbmtWenJtMFNkRzZVT29xS0xzYWJnSDVDOW9rV2kwZGgybDlHS0psCg==
- fDF8VzRpUUd0VFVyREJwSjM3RnFuOWRwcEdVRE5jPXxEZWFna2RwVHpZZDExdDhYWXlORnlhZmROZ2c9IHNzaC1lZDI1NTE5IEFBQUFDM056YUMxbFpESTFOVEU1QUFBQUlBZnVDSEtWVGpxdXh2dDZDTTZ0ZEc0U0xwMUJ0bi9uT2VISEU1VU96UmRmCg==
- from:
- username: "publickey_simple"
authorized_keys: /tmp/auth_keys
to:
host: host-publickey:2222
username: "user"
private_key: /tmp/private_key
known_hosts_data: fDF8RjRwTmVveUZHVEVHcEIyZ3A4RGE0WlE4TGNVPXxycVZYNU0rWTJoS0dteFphcVFBb0syRHp1TEE9IHNzaC1lZDI1NTE5IEFBQUFDM056YUMxbFpESTFOVEU1QUFBQUlPTXFxbmtWenJtMFNkRzZVT29xS0xzYWJnSDVDOW9rV2kwZGgybDlHS0psCg==
- from:
- username: ".*"
username_regex_match: true
authorized_keys:
- /tmp/private_key1
- /tmp/private_key2
to:
host: host-publickey:2222
username: "user"
ignore_hostkey: true
private_key: /tmp/private_key
`
func TestYamlDecode(t *testing.T) {
var config piperConfig
err := yaml.Unmarshal([]byte(yamlConfigTemplate), &config)
if err != nil {
t.Fatalf("Failed to unmarshal yaml: %v", err)
}
}