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:
host: host-password:2222
username: "user"
ignore_hostkey: true
known_hosts_data:
# github.com
- fDF8RjRwTmVveUZHVEVHcEIyZ3A4RGE0WlE4TGNVPXxycVZYNU0rWTJoS0dteFphcVFBb0syRHp1TEE9IHNzaC1lZDI1NTE5IEFBQUFDM056YUMxbFpESTFOVEU1QUFBQUlPTXFxbmtWenJtMFNkRzZVT29xS0xzYWJnSDVDOW9rV2kwZGgybDlHS0psCg==
- {{ .KnownHostsKey }}
- {{ .KnownHostsPass }}
- from:
- username: "publickey_simple"
authorized_keys: {{ .AuthorizedKeys_Simple }}
@ -35,11 +39,13 @@ pipes:
host: host-publickey:2222
username: "user"
private_key: {{ .PrivateKey }}
known_hosts_data: {{ .KnownHosts }}
known_hosts_data: {{ .KnownHostsKey }}
- from:
- username: ".*"
username_regex_match: true
authorized_keys: {{ .AuthorizedKeys_Catchall }}
authorized_keys:
- {{ .AuthorizedKeys_Simple }}
- {{ .AuthorizedKeys_Catchall }}
to:
host: host-publickey:2222
username: "user"
@ -114,7 +120,7 @@ func TestYaml(t *testing.T) {
}
}
knownHostsData, err := runAndGetStdout(
knownHostsKeyData, err := runAndGetStdout(
"ssh-keyscan",
"-p",
"2222",
@ -125,15 +131,27 @@ func TestYaml(t *testing.T) {
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 {
KnownHosts string
PrivateKey string
KnownHostsKey string
KnownHostsPass string
PrivateKey string
AuthorizedKeys_Simple string
AuthorizedKeys_Catchall string
}{
KnownHosts: base64.StdEncoding.EncodeToString(knownHostsData),
PrivateKey: path.Join(yamldir, "id_rsa"),
KnownHostsKey: base64.StdEncoding.EncodeToString(knownHostsKeyData),
KnownHostsPass: base64.StdEncoding.EncodeToString(knownHostsPassData),
PrivateKey: path.Join(yamldir, "id_rsa"),
AuthorizedKeys_Simple: path.Join(yamldir, "id_rsa_simple.pub"),
AuthorizedKeys_Catchall: path.Join(yamldir, "id_rsa_catchall.pub"),

View file

@ -51,10 +51,30 @@
"type": "boolean"
},
"authorized_keys": {
"type": "string"
"oneOf": [
{
"type": "array",
"items": {
"type": "string"
}
},
{
"type": "string"
}
]
},
"authorized_keys_data": {
"type": "string"
"oneOf": [
{
"type": "array",
"items": {
"type": "string"
}
},
{
"type": "string"
}
]
}
},
"required": [
@ -76,7 +96,7 @@
},
"password": {
"type": "string"
},
},
"private_key": {
"type": "string"
},
@ -84,10 +104,30 @@
"type": "string"
},
"known_hosts": {
"type": "string"
"oneOf": [
{
"type": "array",
"items": {
"type": "string"
}
},
{
"type": "string"
}
]
},
"known_hosts_data": {
"type": "string"
"oneOf": [
{
"type": "array",
"items": {
"type": "string"
}
},
{
"type": "string"
}
]
}
},
"required": [
@ -95,4 +135,4 @@
]
}
}
}
}

View file

@ -18,21 +18,53 @@ import (
)
type pipeConfigFrom struct {
Username string `yaml:"username"`
UsernameRegexMatch bool `yaml:"username_regex_match,omitempty"`
AuthorizedKeys string `yaml:"authorized_keys,omitempty"`
AuthorizedKeysData string `yaml:"authorized_keys_data,omitempty"`
Username string `yaml:"username"`
UsernameRegexMatch bool `yaml:"username_regex_match,omitempty"`
AuthorizedKeys listOrString `yaml:"authorized_keys,omitempty"`
AuthorizedKeysData listOrString `yaml:"authorized_keys_data,omitempty"`
}
type pipeConfigTo struct {
Username string `yaml:"username,omitempty"`
Host string `yaml:"host"`
Password string `yaml:"password,omitempty"`
PrivateKey string `yaml:"private_key,omitempty"`
PrivateKeyData string `yaml:"private_key_data,omitempty"`
KnownHosts string `yaml:"known_hosts,omitempty"`
KnownHostsData string `yaml:"known_hosts_data,omitempty"`
IgnoreHostkey bool `yaml:"ignore_hostkey,omitempty"`
Username string `yaml:"username,omitempty"`
Host string `yaml:"host"`
Password string `yaml:"password,omitempty"`
PrivateKey string `yaml:"private_key,omitempty"`
PrivateKeyData string `yaml:"private_key_data,omitempty"`
KnownHosts listOrString `yaml:"known_hosts,omitempty"`
KnownHostsData listOrString `yaml:"known_hosts_data,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 {
@ -129,6 +161,34 @@ func (p *plugin) loadFileOrDecode(file string, base64data string, vars map[strin
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) {
config, err := p.loadConfig()
if err != nil {
@ -139,7 +199,7 @@ func (p *plugin) supportedMethods() ([]string, error) {
for _, pipe := range config.Pipes {
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
} else {
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)
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(),
"UPSTREAM_USER": to.Username,
})
@ -242,7 +302,7 @@ func (p *plugin) findAndCreateUpstream(conn libplugin.ConnMetadata, password str
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,
})
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)
}
}