Skip to content

Commit

Permalink
base/v0_5_exp: add local file embedding for SSH keys
Browse files Browse the repository at this point in the history
  • Loading branch information
Okeanos committed Dec 4, 2021
1 parent c36768a commit 8a88923
Show file tree
Hide file tree
Showing 7 changed files with 236 additions and 26 deletions.
29 changes: 15 additions & 14 deletions base/v0_5_exp/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,20 +168,21 @@ type PasswdGroup struct {
}

type PasswdUser struct {
Gecos *string `yaml:"gecos"`
Groups []Group `yaml:"groups"`
HomeDir *string `yaml:"home_dir"`
Name string `yaml:"name"`
NoCreateHome *bool `yaml:"no_create_home"`
NoLogInit *bool `yaml:"no_log_init"`
NoUserGroup *bool `yaml:"no_user_group"`
PasswordHash *string `yaml:"password_hash"`
PrimaryGroup *string `yaml:"primary_group"`
ShouldExist *bool `yaml:"should_exist"`
SSHAuthorizedKeys []SSHAuthorizedKey `yaml:"ssh_authorized_keys"`
Shell *string `yaml:"shell"`
System *bool `yaml:"system"`
UID *int `yaml:"uid"`
Gecos *string `yaml:"gecos"`
Groups []Group `yaml:"groups"`
HomeDir *string `yaml:"home_dir"`
Name string `yaml:"name"`
NoCreateHome *bool `yaml:"no_create_home"`
NoLogInit *bool `yaml:"no_log_init"`
NoUserGroup *bool `yaml:"no_user_group"`
PasswordHash *string `yaml:"password_hash"`
PrimaryGroup *string `yaml:"primary_group"`
ShouldExist *bool `yaml:"should_exist"`
SSHAuthorizedKeys []SSHAuthorizedKey `yaml:"ssh_authorized_keys"`
SSHAuthorizedKeysLocal []string `yaml:"ssh_authorized_keys_local"`
Shell *string `yaml:"shell"`
System *bool `yaml:"system"`
UID *int `yaml:"uid"`
}

type Proxy struct {
Expand Down
80 changes: 80 additions & 0 deletions base/v0_5_exp/translate.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ func (c Config) ToIgn3_4Unvalidated(options common.TranslateOptions) (types.Conf
tr.AddCustomTranslator(translateDirectory)
tr.AddCustomTranslator(translateLink)
tr.AddCustomTranslator(translateResource)
tr.AddCustomTranslator(translatePasswdUser)

tm, r := translate.Prefixed(tr, "ignition", &c.Ignition, &ret.Ignition)
tm.AddTranslation(path.New("yaml", "version"), path.New("json", "ignition", "version"))
Expand Down Expand Up @@ -217,6 +218,85 @@ func translateLink(from Link, options common.TranslateOptions) (to types.Link, t
return
}

func translatePasswdUser(from PasswdUser, options common.TranslateOptions) (to types.PasswdUser, tm translate.TranslationSet, r report.Report) {
tr := translate.NewTranslator("yaml", "json", options)
tm, r = translate.Prefixed(tr, "gecos", &from.Gecos, &to.Gecos)
translate.MergeP(tr, tm, &r, "groups", &from.Groups, &to.Groups)
translate.MergeP(tr, tm, &r, "home_dir", &from.HomeDir, &to.HomeDir)
translate.MergeP(tr, tm, &r, "name", &from.Name, &to.Name)
translate.MergeP(tr, tm, &r, "no_create_home", &from.NoCreateHome, &to.NoCreateHome)
translate.MergeP(tr, tm, &r, "no_log_init", &from.NoLogInit, &to.NoLogInit)
translate.MergeP(tr, tm, &r, "no_user_group", &from.NoUserGroup, &to.NoUserGroup)
translate.MergeP(tr, tm, &r, "password_hash", &from.PasswordHash, &to.PasswordHash)
translate.MergeP(tr, tm, &r, "primary_group", &from.PrimaryGroup, &to.PrimaryGroup)
translate.MergeP(tr, tm, &r, "should_exist", &from.ShouldExist, &to.ShouldExist)
translate.MergeP(tr, tm, &r, "shell", &from.Shell, &to.Shell)
translate.MergeP(tr, tm, &r, "system", &from.System, &to.System)
translate.MergeP(tr, tm, &r, "uid", &from.UID, &to.UID)

var c path.ContextPath
if from.SSHAuthorizedKeys != nil {
if len(from.SSHAuthorizedKeys) > 0 {
c = path.New("yaml", "ssh_authorized_keys")
tm.AddTranslation(c, path.New("json", "sshAuthorizedKeys"))
}

for i, sshKey := range from.SSHAuthorizedKeys {
sshKey = SSHAuthorizedKey(strings.TrimSpace(string(sshKey)))
if len(sshKey) == 0 {
r.AddOnError(c, common.ErrSSHKeyEmpty)
}
tm.AddTranslation(c, path.New("json", fmt.Sprintf("sshAuthorizedKeys.%d", i)))
to.SSHAuthorizedKeys = append(to.SSHAuthorizedKeys, types.SSHAuthorizedKey(sshKey))
}
}

if from.SSHAuthorizedKeysLocal != nil {
if len(from.SSHAuthorizedKeysLocal) > 0 {
c = path.New("yaml", "ssh_authorized_keys_local")
tm.AddTranslation(c, path.New("json", "sshAuthorizedKeys"))
}
for _, sshKeyFile := range from.SSHAuthorizedKeysLocal {
if options.FilesDir == "" {
r.AddOnError(c, common.ErrNoFilesDir)
return
}

// calculate file path within FilesDir and check for
// path traversal
filePath := filepath.Join(options.FilesDir, sshKeyFile)
if err := baseutil.EnsurePathWithinFilesDir(filePath, options.FilesDir); err != nil {
r.AddOnError(c, err)
continue
}
contents, err := ioutil.ReadFile(filePath)
if err != nil {
r.AddOnError(c, err)
continue
}

sshKeyStrings := strings.TrimSpace(string(contents))
if len(contents) == 0 || len(sshKeyStrings) == 0 {
r.AddOnError(c, common.ErrFileEmpty)
continue
}
// offset for TranslationSets when both ssh_authorized_keys and ssh_authorized_keys_local are available
offset := len(to.SSHAuthorizedKeys)
for i, line := range strings.Split(sshKeyStrings, "\n") {
sshKey := strings.TrimSpace(line)
if len(sshKey) == 0 {
r.AddOnError(c, common.ErrSSHKeyEmpty)
continue
}
tm.AddTranslation(c, path.New("json", fmt.Sprintf("sshAuthorizedKeys.%d", i+offset)))
to.SSHAuthorizedKeys = append(to.SSHAuthorizedKeys, types.SSHAuthorizedKey(sshKey))
}
}
}

return
}

func (c Config) processTrees(ret *types.Config, options common.TranslateOptions) (translate.TranslationSet, report.Report) {
ts := translate.NewTranslationSet("yaml", "json")
var r report.Report
Expand Down
124 changes: 124 additions & 0 deletions base/v0_5_exp/translate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package v0_5_exp

import (
"fmt"
"io/ioutil"
"net"
"os"
Expand Down Expand Up @@ -1616,6 +1617,129 @@ func TestTranslateKernelArguments(t *testing.T) {
}
}

// TestTranslateSSHAuthorizedKeyResource tests translating the butane passwd.users[i].ssh_authorized_keys_local[j] entries to ignition passwd.users[i].ssh_authorized_keys[j] entries.
func TestTranslateSSHAuthorizedKeyResource(t *testing.T) {
sshKeyDir := t.TempDir()
randomDir := t.TempDir()
var sshKey = "ssh-rsa AAAAAAAAA"
var sshKeyFileName = "id_rsa.pub"
var sshKeyMultipleKeysFileName = "multiple.pubs"
var sshKeyNonExistingFileName = "id_ed25519.pub"
var sshKeyEmptyFileName = "empty.pub"
var sshKeyBlankFileName = "blank.pub"
err := ioutil.WriteFile(filepath.Join(sshKeyDir, sshKeyFileName), []byte(sshKey), 0644)
if err != nil {
t.Error(err)
}
err = ioutil.WriteFile(filepath.Join(sshKeyDir, sshKeyMultipleKeysFileName), []byte(fmt.Sprintf("%s\n%s\n\n", sshKey, sshKey)), 0644)
if err != nil {
t.Error(err)
}
err = ioutil.WriteFile(filepath.Join(sshKeyDir, sshKeyEmptyFileName), []byte(""), 0644)
if err != nil {
t.Error(err)
}
err = ioutil.WriteFile(filepath.Join(sshKeyDir, sshKeyBlankFileName), []byte("\n\t\n\n\n\n\n\n"), 0644)
if err != nil {
t.Error(err)
}
tests := []struct {
name string
in PasswdUser
out types.PasswdUser
report string
fileDir string
}{
{
"empty user",
PasswdUser{},
types.PasswdUser{},
"",
sshKeyDir,
},
{
"valid ssh_keys_inline",
PasswdUser{SSHAuthorizedKeys: []SSHAuthorizedKey{SSHAuthorizedKey(sshKey)}},
types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{types.SSHAuthorizedKey(sshKey)}},
"",
sshKeyDir,
},
{
"valid ssh_keys_local",
PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyFileName}},
types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{types.SSHAuthorizedKey(sshKey)}},
"",
sshKeyDir,
},
{
"valid ssh_keys_local with multiple keys per file",
PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyMultipleKeysFileName}},
types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{types.SSHAuthorizedKey(sshKey), types.SSHAuthorizedKey(sshKey)}},
"",
sshKeyDir,
},
{
"valid ssh_keys_local and ssh_keys",
PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyFileName}, SSHAuthorizedKeys: []SSHAuthorizedKey{SSHAuthorizedKey(sshKey)}},
types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{types.SSHAuthorizedKey(sshKey), types.SSHAuthorizedKey(sshKey)}},
"",
sshKeyDir,
},
{
"valid ssh_keys_local with multiple keys per file and ssh_keys",
PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyMultipleKeysFileName}, SSHAuthorizedKeys: []SSHAuthorizedKey{SSHAuthorizedKey(sshKey)}},
types.PasswdUser{SSHAuthorizedKeys: []types.SSHAuthorizedKey{types.SSHAuthorizedKey(sshKey), types.SSHAuthorizedKey(sshKey), types.SSHAuthorizedKey(sshKey)}},
"",
sshKeyDir,
},
{
"non existing ssh_keys_local file name",
PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyNonExistingFileName}},
types.PasswdUser{},
"error at $.ssh_authorized_keys_local: open %FileDir%/id_ed25519.pub: no such file or directory\n",
sshKeyDir,
},
{
"empty ssh_keys_local file",
PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyEmptyFileName}},
types.PasswdUser{},
"error at $.ssh_authorized_keys_local: local file to be embedded must not be empty\n",
sshKeyDir,
},
{
"blank ssh_keys_local file",
PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyBlankFileName}},
types.PasswdUser{},
"error at $.ssh_authorized_keys_local: local file to be embedded must not be empty\n",
sshKeyDir,
},
{
"missing embed directory",
PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyFileName}},
types.PasswdUser{},
"error at $.ssh_authorized_keys_local: local file paths are relative to a files directory that must be specified with -d/--files-dir\n",
"",
},
{
"wrong embed directory",
PasswdUser{SSHAuthorizedKeysLocal: []string{sshKeyFileName}},
types.PasswdUser{},
"error at $.ssh_authorized_keys_local: open %FileDir%/id_rsa.pub: no such file or directory\n",
randomDir,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actual, translations, r := translatePasswdUser(test.in, common.TranslateOptions{FilesDir: test.fileDir})
assert.Equal(t, test.out, actual, "translation mismatch")
expectedReport := strings.ReplaceAll(test.report, "%FileDir%", test.fileDir)
assert.Equal(t, expectedReport, r.String(), "bad report")
assert.NoError(t, translations.DebugVerifyCoverage(actual), "incomplete TranslationSet coverage")
})
}
}

// TestToIgn3_4 tests the config.ToIgn3_4 function ensuring it will generate a valid config even when empty. Not much else is
// tested since it uses the Ignition translation code which has it's own set of tests.
func TestToIgn3_4(t *testing.T) {
Expand Down
2 changes: 2 additions & 0 deletions config/common/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ var (
ErrTooManyResourceSources = errors.New("only one of the following can be set: inline, local, source")
ErrFilesDirEscape = errors.New("local file path traverses outside the files directory")
ErrFileType = errors.New("trees may only contain files, directories, and symlinks")
ErrFileEmpty = errors.New("local file to be embedded must not be empty")
ErrNodeExists = errors.New("matching filesystem node has existing contents or different type")
ErrNoFilesDir = errors.New("local file paths are relative to a files directory that must be specified with -d/--files-dir")
ErrTreeNotDirectory = errors.New("root of tree must be a directory")
Expand Down Expand Up @@ -68,4 +69,5 @@ var (
ErrUserFieldSupport = errors.New("fields other than \"name\" and \"ssh_authorized_keys\" are not supported in this spec version")
ErrUserNameSupport = errors.New("users other than \"core\" are not supported in this spec version")
ErrKernelArgumentSupport = errors.New("this field cannot be used for kernel arguments in this spec version; use openshift.kernel_arguments instead")
ErrSSHKeyEmpty = errors.New("ssh key must not be empty")
)
23 changes: 12 additions & 11 deletions config/openshift/v4_10_exp/translate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -458,17 +458,18 @@ func TestValidateSupport(t *testing.T) {
Groups: []base.Group{
"z",
},
HomeDir: util.StrToPtr("/home/drum"),
NoCreateHome: util.BoolToPtr(true),
NoLogInit: util.BoolToPtr(true),
NoUserGroup: util.BoolToPtr(true),
PasswordHash: util.StrToPtr("corned beef"),
PrimaryGroup: util.StrToPtr("wheel"),
SSHAuthorizedKeys: []base.SSHAuthorizedKey{"value"},
Shell: util.StrToPtr("/bin/tcsh"),
ShouldExist: util.BoolToPtr(false),
System: util.BoolToPtr(true),
UID: util.IntToPtr(42),
HomeDir: util.StrToPtr("/home/drum"),
NoCreateHome: util.BoolToPtr(true),
NoLogInit: util.BoolToPtr(true),
NoUserGroup: util.BoolToPtr(true),
PasswordHash: util.StrToPtr("corned beef"),
PrimaryGroup: util.StrToPtr("wheel"),
SSHAuthorizedKeys: []base.SSHAuthorizedKey{"value"},
SSHAuthorizedKeysLocal: []string{},
Shell: util.StrToPtr("/bin/tcsh"),
ShouldExist: util.BoolToPtr(false),
System: util.BoolToPtr(true),
UID: util.IntToPtr(42),
},
{
Name: "bovik",
Expand Down
1 change: 1 addition & 0 deletions docs/config-fcos-v1_5-exp.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ The Fedora CoreOS configuration is a YAML document conforming to the following s
* **name** (string): the username for the account.
* **_password_hash_** (string): the hashed password for the account.
* **_ssh_authorized_keys_** (list of strings): a list of SSH keys to be added as an SSH key fragment at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique.
* **_ssh_authorized_keys_local_** (list of strings): a list of paths pointing at SSH key files to be added as an SSH key fragment at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. May contain one SSH key per line in each file.
* **_uid_** (integer): the user ID of the account.
* **_gecos_** (string): the GECOS field of the account.
* **_home_dir_** (string): the home directory of the account.
Expand Down
3 changes: 2 additions & 1 deletion docs/config-openshift-v4_10-exp.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,8 @@ The OpenShift configuration is a YAML document conforming to the following speci
* **_passwd_** (object): describes the desired additions to the passwd database.
* **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`.
* **name** (string): the username for the account. Must be `core`.
* **_ssh_authorized_keys_** (list of strings): a list of SSH keys to be added to `.ssh/authorized_keys` in the user's home directory. All SSH keys must be unique.
* **_ssh_authorized_keys_** (list of strings): a list of SSH keys to be added as an SSH key fragment at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique.
* **_ssh_authorized_keys_local_** (list of strings): a list of paths pointing at SSH key files to be added as an SSH key fragment at `.ssh/authorized_keys.d/ignition` in the user's home directory. All SSH keys must be unique. May contain one SSH key per line in each file.
* **_boot_device_** (object): describes the desired boot device configuration. At least one of `luks` or `mirror` must be specified.
* **_layout_** (string): the disk layout of the target OS image. Supported values are `aarch64`, `ppc64le`, and `x86_64`. Defaults to `x86_64`.
* **_luks_** (object): describes the clevis configuration for encrypting the root filesystem.
Expand Down

0 comments on commit 8a88923

Please sign in to comment.