diff --git a/.gitignore b/.gitignore index 874ddb5..11f3834 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ bin/ coverage/ gopath/ +/coreos-cloudinit diff --git a/Documentation/cloud-config.md b/Documentation/cloud-config.md index 9ca36ab..84e6687 100644 --- a/Documentation/cloud-config.md +++ b/Documentation/cloud-config.md @@ -346,10 +346,11 @@ coreos: ### ssh_authorized_keys -The `ssh_authorized_keys` parameter adds public SSH keys which will be authorized for the `core` user. +The `ssh_authorized_keys` parameter adds public SSH keys which will be authorized for the default user (defaults to `core`). The keys will be named "coreos-cloudinit" by default. Override this by using the `--ssh-key-name` flag when calling `coreos-cloudinit`. +Change the default user by using the `--default-user` flag. ```yaml #cloud-config diff --git a/README.md b/README.md index edaf5f9..da72e41 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,9 @@ write_files: nameserver 192.0.2.3 ``` +By default, SSH keys from `ssh_authorized_keys` and user-data are applied to the `core` user. +This can be customized using the `--default-user` flag when running `coreos-cloudinit`. + ## Executing a Script coreos-cloudinit supports executing user-data as a script instead of parsing it as a cloud-config document. diff --git a/coreos-cloudinit.go b/coreos-cloudinit.go index 3e55938..1415341 100644 --- a/coreos-cloudinit.go +++ b/coreos-cloudinit.go @@ -73,6 +73,7 @@ var ( convertNetconf string workspace string sshKeyName string + defaultUser string oem string validate bool }{} @@ -98,6 +99,7 @@ func init() { flag.StringVar(&flags.convertNetconf, "convert-netconf", "", "Read the network config provided in cloud-drive and translate it from the specified format into networkd unit files") flag.StringVar(&flags.workspace, "workspace", "/var/lib/coreos-cloudinit", "Base directory coreos-cloudinit should use to store data") flag.StringVar(&flags.sshKeyName, "ssh-key-name", initialize.DefaultSSHKeyName, "Add SSH keys to the system with the given name") + flag.StringVar(&flags.defaultUser, "default-user", initialize.DefaultUser, "Default user to receive SSH keys from cloud-config") flag.BoolVar(&flags.validate, "validate", false, "[EXPERIMENTAL] Validate the user-data but do not apply it to the system") } @@ -184,7 +186,7 @@ func main() { log.Printf("Failed fetching meta-data from datasource: %v\n", err) os.Exit(1) } - env := initialize.NewEnvironment("/", ds.ConfigRoot(), flags.workspace, flags.sshKeyName, metadata) + env := initialize.NewEnvironment("/", ds.ConfigRoot(), flags.workspace, flags.sshKeyName, flags.defaultUser, metadata) // Setup networking units if flags.convertNetconf != "" { @@ -236,7 +238,7 @@ func main() { } mergedKeys := mergeSSHKeysFromSources(metadata, udata) - if err := initialize.ApplyCoreUserSSHKeys(mergedKeys, env); err != nil { + if err := initialize.ApplyDefaultUserSSHKeys(mergedKeys, env); err != nil { log.Printf("Failed to apply SSH keys: %v", err) mustStop = true } diff --git a/initialize/config.go b/initialize/config.go index e630f91..fafd0ef 100644 --- a/initialize/config.go +++ b/initialize/config.go @@ -49,15 +49,15 @@ func ApplyHostname(hostname string) error { return nil } -func ApplyCoreUserSSHKeys(keys []string, env *Environment) error { +func ApplyDefaultUserSSHKeys(keys []string, env *Environment) error { if len(keys) == 0 { return nil } - err := system.AuthorizeSSHKeys("core", env.SSHKeyName(), keys) + err := system.AuthorizeSSHKeys(env.DefaultUser(), env.SSHKeyName(), keys) if err != nil { return err } - log.Printf("Authorized SSH keys for core user") + log.Printf("Authorized SSH keys for %s user", env.DefaultUser()) return nil } @@ -107,9 +107,9 @@ func Apply(cfg config.CloudConfig, env *Environment) error { } } - // If this is the "core" user, we skip adding the ssh keys from SSHAuthorizedKeys. Those keys will + // If this is the default user, we skip adding the ssh keys from SSHAuthorizedKeys. Those keys will // be added later along any other keys we fetch from user-data and metadata. - if len(user.SSHAuthorizedKeys) > 0 && user.Name != "core" { + if len(user.SSHAuthorizedKeys) > 0 && user.Name != env.DefaultUser() { log.Printf("Authorizing %d SSH keys for user '%s'", len(user.SSHAuthorizedKeys), user.Name) if err := system.AuthorizeSSHKeys(user.Name, env.SSHKeyName(), user.SSHAuthorizedKeys); err != nil { return err diff --git a/initialize/env.go b/initialize/env.go index a0248f9..55f92c8 100644 --- a/initialize/env.go +++ b/initialize/env.go @@ -27,17 +27,19 @@ import ( ) const DefaultSSHKeyName = "coreos-cloudinit" +const DefaultUser = "core" type Environment struct { root string configRoot string workspace string sshKeyName string + defaultUser string substitutions map[string]string } // TODO(jonboulle): this is getting unwieldy, should be able to simplify the interface somehow -func NewEnvironment(root, configRoot, workspace, sshKeyName string, metadata datasource.Metadata) *Environment { +func NewEnvironment(root, configRoot, workspace, sshKeyName, defaultUser string, metadata datasource.Metadata) *Environment { firstNonNull := func(ip net.IP, env string) string { if ip == nil { return env @@ -50,7 +52,7 @@ func NewEnvironment(root, configRoot, workspace, sshKeyName string, metadata dat "$public_ipv6": firstNonNull(metadata.PublicIPv6, os.Getenv("COREOS_PUBLIC_IPV6")), "$private_ipv6": firstNonNull(metadata.PrivateIPv6, os.Getenv("COREOS_PRIVATE_IPV6")), } - return &Environment{root, configRoot, workspace, sshKeyName, substitutions} + return &Environment{root, configRoot, workspace, sshKeyName, defaultUser, substitutions} } func (e *Environment) Workspace() string { @@ -73,6 +75,10 @@ func (e *Environment) SetSSHKeyName(name string) { e.sshKeyName = name } +func (e *Environment) DefaultUser() string { + return e.defaultUser +} + // Apply goes through the map of substitutions and replaces all instances of // the keys with their respective values. It supports escaping substitutions // with a leading '\'. diff --git a/initialize/env_test.go b/initialize/env_test.go index 99d8809..014f057 100644 --- a/initialize/env_test.go +++ b/initialize/env_test.go @@ -95,7 +95,7 @@ addr: $private_ipv4 }, } { - env := NewEnvironment("./", "./", "./", "", tt.metadata) + env := NewEnvironment("./", "./", "./", "", DefaultUser, tt.metadata) got := env.Apply(tt.input) if got != tt.out { t.Fatalf("Environment incorrectly applied.\ngot:\n%s\nwant:\n%s", got, tt.out) @@ -118,7 +118,7 @@ func TestEnvironmentFile(t *testing.T) { } defer os.RemoveAll(dir) - env := NewEnvironment("./", "./", "./", "", metadata) + env := NewEnvironment("./", "./", "./", "", DefaultUser, metadata) ef := env.DefaultEnvironmentFile() err = system.WriteEnvFile(ef, dir) if err != nil { @@ -140,9 +140,25 @@ func TestEnvironmentFileNil(t *testing.T) { os.Clearenv() metadata := datasource.Metadata{} - env := NewEnvironment("./", "./", "./", "", metadata) + env := NewEnvironment("./", "./", "./", "", DefaultUser, metadata) ef := env.DefaultEnvironmentFile() if ef != nil { t.Fatalf("Environment file not nil: %v", ef) } } + +func TestEnvironmentDefaultUser(t *testing.T) { + metadata := datasource.Metadata{} + + // Test default value + env := NewEnvironment("./", "./", "./", "", DefaultUser, metadata) + if env.DefaultUser() != "core" { + t.Fatalf("Expected default user 'core', got '%s'", env.DefaultUser()) + } + + // Test custom value + env = NewEnvironment("./", "./", "./", "", "admin", metadata) + if env.DefaultUser() != "admin" { + t.Fatalf("Expected default user 'admin', got '%s'", env.DefaultUser()) + } +} diff --git a/initialize/testdata/custom_user_cloudconfig.txt b/initialize/testdata/custom_user_cloudconfig.txt new file mode 100644 index 0000000..f77c45e --- /dev/null +++ b/initialize/testdata/custom_user_cloudconfig.txt @@ -0,0 +1,15 @@ +#cloud-config + +hostname: customhost + +ssh_authorized_keys: + - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDGdByTgSVHq global-key + +users: + - name: admin + ssh_authorized_keys: + - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCaLEE+pLP admin-key-1 + - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCf0HE7V4C admin-key-2 + - name: core + ssh_authorized_keys: + - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDd6H4rOzC core-key diff --git a/initialize/user_data.go b/initialize/user_data.go index 283b85c..5e56aef 100644 --- a/initialize/user_data.go +++ b/initialize/user_data.go @@ -356,12 +356,12 @@ func (ud *UserData) FindSSHKeys(additionalKeys []string) []string { if part.cloudConfig.Users != nil { for _, user := range part.cloudConfig.Users { - if user.Name != "core" { + if user.Name != ud.env.DefaultUser() { continue } - // The "core" user is the default user on coreos systems. Append these keys to the list. - // TODO(gabriel-samfira): make the default user configurable? + // The default user on the system receives SSH keys from cloud-config. + // The default user is configurable via the --default-user flag. for _, key := range user.SSHAuthorizedKeys { keys[key] = struct{}{} } diff --git a/initialize/user_data_test.go b/initialize/user_data_test.go index 4c96ad8..1bf33d5 100644 --- a/initialize/user_data_test.go +++ b/initialize/user_data_test.go @@ -68,7 +68,7 @@ func TestParseMultipartMime(t *testing.T) { func getTestEnv() *Environment { metadata := datasource.Metadata{} - return NewEnvironment("./", "./", "./", "", metadata) + return NewEnvironment("./", "./", "./", "", DefaultUser, metadata) } func TestNewUserDataParsesIgnition(t *testing.T) { @@ -112,3 +112,55 @@ func TestNewUserDataParsesUnknown(t *testing.T) { require.Equal(t, 1, len(udata.Parts)) require.Equal(t, udata.Parts[0].userDataType, UnknownType) } + +func TestFindSSHKeysWithDefaultUser(t *testing.T) { + data, err := os.ReadFile("testdata/custom_user_cloudconfig.txt") + require.NoError(t, err) + + // Test with default user = "core" + env := getTestEnv() + udata, err := NewUserData(string(data), env) + require.NoError(t, err) + + keys := udata.FindSSHKeys([]string{}) + require.Equal(t, 2, len(keys)) + // Should find global key and core user's key + require.Contains(t, keys, "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDGdByTgSVHq global-key") + require.Contains(t, keys, "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDd6H4rOzC core-key") +} + +func TestFindSSHKeysWithCustomDefaultUser(t *testing.T) { + data, err := os.ReadFile("testdata/custom_user_cloudconfig.txt") + require.NoError(t, err) + + // Test with default user = "admin" + metadata := datasource.Metadata{} + env := NewEnvironment("./", "./", "./", "", "admin", metadata) + udata, err := NewUserData(string(data), env) + require.NoError(t, err) + + keys := udata.FindSSHKeys([]string{}) + require.Equal(t, 3, len(keys)) + // Should find global key and admin user's two keys + require.Contains(t, keys, "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDGdByTgSVHq global-key") + require.Contains(t, keys, "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCaLEE+pLP admin-key-1") + require.Contains(t, keys, "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCf0HE7V4C admin-key-2") + // Should NOT find core user's key + for _, key := range keys { + require.NotEqual(t, "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDd6H4rOzC core-key", key) + } +} + +func TestFindSSHKeysWithAdditionalKeys(t *testing.T) { + data, err := os.ReadFile("testdata/custom_user_cloudconfig.txt") + require.NoError(t, err) + + env := getTestEnv() + udata, err := NewUserData(string(data), env) + require.NoError(t, err) + + additionalKeys := []string{"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC additional-key"} + keys := udata.FindSSHKeys(additionalKeys) + require.Equal(t, 3, len(keys)) + require.Contains(t, keys, "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC additional-key") +}