feat(users): Add support for creating/modifying users
This commit is contained in:
parent
202eb67ab1
commit
f48ba8fec5
@ -13,6 +13,40 @@ Provided public SSH keys will be authorized for the `core` user.
|
|||||||
The keys will be named "coreos-cloudinit" by default.
|
The keys will be named "coreos-cloudinit" by default.
|
||||||
Override this with the `--ssh-key-name` flag when calling `coreos-cloudinit`.
|
Override this with the `--ssh-key-name` flag when calling `coreos-cloudinit`.
|
||||||
|
|
||||||
|
#### users
|
||||||
|
|
||||||
|
Add or modify users with the `users` directive by providing a list of user objects, each consisting of the following fields.
|
||||||
|
Each field is optional and of type string unless otherwise noted.
|
||||||
|
All but the `passwd` and `ssh-authorized-keys` fields will be ignored if the user already exists.
|
||||||
|
|
||||||
|
- **name**: Required. Login name of user
|
||||||
|
- **gecos**: GECOS comment of user
|
||||||
|
- **passwd**: Hash of the password to use for this user
|
||||||
|
- **homedir**: User's home directory. Defaults to /home/<name>
|
||||||
|
- **no-create-home**: Boolean. Skip home directory createion.
|
||||||
|
- **primary-group**: Default group for the user. Defaults to a new group created named after the user.
|
||||||
|
- **groups**: Add user to these additional groups
|
||||||
|
- **no-user-group**: Boolean. Skip default group creation.
|
||||||
|
- **ssh-authorized-keys**: List of public SSH keys to authorize for this user
|
||||||
|
- **system**: Create the user as a system user. No home directory will be created.
|
||||||
|
- **no-log-init**: Boolean. Skip initialization of lastlog and faillog databases.
|
||||||
|
|
||||||
|
The following fields are not yet implemented:
|
||||||
|
|
||||||
|
- **inactive**: Deactivate the user upon creation
|
||||||
|
- **lock-passwd**: Boolean. Disable password login for user
|
||||||
|
- **sudo**: Entry to add to /etc/sudoers for user. By default, no sudo access is authorized.
|
||||||
|
- **selinux-user**: Corresponding SELinux user
|
||||||
|
- **ssh-import-id**: Import SSH keys by ID from Launchpad.
|
||||||
|
|
||||||
|
##### Generating a password hash
|
||||||
|
|
||||||
|
You can generate a safe hash via:
|
||||||
|
|
||||||
|
mkpasswd --method=SHA-512 --rounds=4096
|
||||||
|
|
||||||
|
Using a higher number of rounds will help create more secure passwords, but given enough time, password hashes can be reversed.
|
||||||
|
|
||||||
## Custom cloud-config Parameters
|
## Custom cloud-config Parameters
|
||||||
|
|
||||||
### coreos.etcd.discovery_url
|
### coreos.etcd.discovery_url
|
||||||
@ -80,3 +114,18 @@ coreos:
|
|||||||
[Install]
|
[Install]
|
||||||
WantedBy=local.target
|
WantedBy=local.target
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Add a user
|
||||||
|
|
||||||
|
```
|
||||||
|
#cloud-config
|
||||||
|
|
||||||
|
users:
|
||||||
|
- name: elroy
|
||||||
|
passwd: $6$5s2u6/jR$un0AvWnqilcgaNB3Mkxd5yYv6mTlWfOoCYHZmfi3LDKVltj.E8XNKEcwWm...
|
||||||
|
groups:
|
||||||
|
- staff
|
||||||
|
- docker
|
||||||
|
ssh-authorized-keys:
|
||||||
|
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC0g+ZTxC7weoIJLUafOgrm+h...
|
||||||
|
```
|
||||||
|
@ -18,6 +18,7 @@ type CloudConfig struct {
|
|||||||
}
|
}
|
||||||
WriteFiles []WriteFile `yaml:"write_files"`
|
WriteFiles []WriteFile `yaml:"write_files"`
|
||||||
Hostname string
|
Hostname string
|
||||||
|
Users []User
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCloudConfig(contents []byte) (*CloudConfig, error) {
|
func NewCloudConfig(contents []byte) (*CloudConfig, error) {
|
||||||
@ -46,8 +47,41 @@ func ApplyCloudConfig(cfg CloudConfig, sshKeyName string) error {
|
|||||||
log.Printf("Set hostname to %s", cfg.Hostname)
|
log.Printf("Set hostname to %s", cfg.Hostname)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(cfg.Users) > 0 {
|
||||||
|
for _, user := range cfg.Users {
|
||||||
|
if user.Name == "" {
|
||||||
|
log.Printf("User object has no 'name' field, skipping")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if UserExists(&user) {
|
||||||
|
log.Printf("User '%s' exists, ignoring creation-time fields", user.Name)
|
||||||
|
if user.PasswordHash != "" {
|
||||||
|
log.Printf("Setting '%s' user's password", user.Name)
|
||||||
|
if err := SetUserPassword(user.Name, user.PasswordHash); err != nil {
|
||||||
|
log.Printf("Failed setting '%s' user's password: %v", user.Name, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf("Creating user '%s'", user.Name)
|
||||||
|
if err := CreateUser(&user); err != nil {
|
||||||
|
log.Printf("Failed creating user '%s': %v", user.Name, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(user.SSHAuthorizedKeys) > 0 {
|
||||||
|
log.Printf("Authorizing %d SSH keys for user '%s'", len(user.SSHAuthorizedKeys), user.Name)
|
||||||
|
if err := AuthorizeSSHKeys(user.Name, sshKeyName, user.SSHAuthorizedKeys); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if len(cfg.SSHAuthorizedKeys) > 0 {
|
if len(cfg.SSHAuthorizedKeys) > 0 {
|
||||||
err := AuthorizeSSHKeys(sshKeyName, cfg.SSHAuthorizedKeys)
|
err := AuthorizeSSHKeys("core", sshKeyName, cfg.SSHAuthorizedKeys)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
log.Printf("Authorized SSH keys for core user")
|
log.Printf("Authorized SSH keys for core user")
|
||||||
} else {
|
} else {
|
||||||
|
@ -164,3 +164,89 @@ func TestCloudConfigSerializationHeader(t *testing.T) {
|
|||||||
t.Fatalf("Serialized config did not have expected header")
|
t.Fatalf("Serialized config did not have expected header")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCloudConfigUsers(t *testing.T) {
|
||||||
|
contents := []byte(`
|
||||||
|
users:
|
||||||
|
- name: elroy
|
||||||
|
passwd: somehash
|
||||||
|
ssh-authorized-keys:
|
||||||
|
- somekey
|
||||||
|
gecos: arbitrary comment
|
||||||
|
homedir: /home/place
|
||||||
|
no-create-home: yes
|
||||||
|
primary-group: things
|
||||||
|
groups:
|
||||||
|
- ping
|
||||||
|
- pong
|
||||||
|
no-user-group: true
|
||||||
|
system: y
|
||||||
|
no-log-init: True
|
||||||
|
`)
|
||||||
|
cfg, err := NewCloudConfig(contents)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Encountered unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cfg.Users) != 1 {
|
||||||
|
t.Fatalf("Parsed %d users, expected 1", cfg.Users)
|
||||||
|
}
|
||||||
|
|
||||||
|
user := cfg.Users[0]
|
||||||
|
|
||||||
|
if user.Name != "elroy" {
|
||||||
|
t.Errorf("User name is %q, expected 'elroy'", user.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.PasswordHash != "somehash" {
|
||||||
|
t.Errorf("User passwd is %q, expected 'somehash'", user.PasswordHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
if keys := user.SSHAuthorizedKeys; len(keys) != 1 {
|
||||||
|
t.Errorf("Parsed %d ssh keys, expected 1", len(keys))
|
||||||
|
} else {
|
||||||
|
key := user.SSHAuthorizedKeys[0]
|
||||||
|
if key != "somekey" {
|
||||||
|
t.Errorf("User SSH key is %q, expected 'somekey'", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.GECOS != "arbitrary comment" {
|
||||||
|
t.Errorf("Failed to parse gecos field, got %q", user.GECOS)
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Homedir != "/home/place" {
|
||||||
|
t.Errorf("Failed to parse homedir field, got %q", user.Homedir)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !user.NoCreateHome {
|
||||||
|
t.Errorf("Failed to parse no-create-home field")
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.PrimaryGroup != "things"{
|
||||||
|
t.Errorf("Failed to parse primary-group field, got %q", user.PrimaryGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(user.Groups) != 2 {
|
||||||
|
t.Errorf("Failed to parse 2 goups, got %d", len(user.Groups))
|
||||||
|
} else {
|
||||||
|
if user.Groups[0] != "ping" {
|
||||||
|
t.Errorf("First group was %q, not expected value 'ping'", user.Groups[0])
|
||||||
|
}
|
||||||
|
if user.Groups[1] != "pong" {
|
||||||
|
t.Errorf("First group was %q, not expected value 'pong'", user.Groups[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !user.NoUserGroup {
|
||||||
|
t.Errorf("Failed to parse no-user-group field")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !user.System {
|
||||||
|
t.Errorf("Failed to parse system field")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !user.NoLogInit {
|
||||||
|
t.Errorf("Failed to parse no-log-init field")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -10,7 +10,7 @@ import (
|
|||||||
|
|
||||||
// Add the provide SSH public key to the core user's list of
|
// Add the provide SSH public key to the core user's list of
|
||||||
// authorized keys
|
// authorized keys
|
||||||
func AuthorizeSSHKeys(name string, keys []string) error {
|
func AuthorizeSSHKeys(user string, keysName string, keys []string) error {
|
||||||
for i, key := range keys {
|
for i, key := range keys {
|
||||||
keys[i] = strings.TrimSpace(key)
|
keys[i] = strings.TrimSpace(key)
|
||||||
}
|
}
|
||||||
@ -19,7 +19,7 @@ func AuthorizeSSHKeys(name string, keys []string) error {
|
|||||||
// also ends with a newline
|
// also ends with a newline
|
||||||
joined := fmt.Sprintf("%s\n", strings.Join(keys, "\n"))
|
joined := fmt.Sprintf("%s\n", strings.Join(keys, "\n"))
|
||||||
|
|
||||||
cmd := exec.Command("update-ssh-keys", "-u", "core", "-a", name)
|
cmd := exec.Command("update-ssh-keys", "-u", user, "-a", keysName)
|
||||||
stdin, err := cmd.StdinPipe()
|
stdin, err := cmd.StdinPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
106
cloudinit/user.go
Normal file
106
cloudinit/user.go
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
package cloudinit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os/exec"
|
||||||
|
"os/user"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
PasswordHash string `yaml:"passwd"`
|
||||||
|
SSHAuthorizedKeys []string `yaml:"ssh-authorized-keys"`
|
||||||
|
GECOS string `yaml:"gecos"`
|
||||||
|
Homedir string `yaml:"homedir"`
|
||||||
|
NoCreateHome bool `yaml:"no-create-home"`
|
||||||
|
PrimaryGroup string `yaml:"primary-group"`
|
||||||
|
Groups []string `yaml:"groups"`
|
||||||
|
NoUserGroup bool `yaml:"no-user-group"`
|
||||||
|
System bool `yaml:"system"`
|
||||||
|
NoLogInit bool `yaml:"no-log-init"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func UserExists(u *User) bool {
|
||||||
|
_, err := user.Lookup(u.Name)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateUser(u *User) error {
|
||||||
|
args := []string{}
|
||||||
|
|
||||||
|
if u.PasswordHash != "" {
|
||||||
|
args = append(args, "--password", u.PasswordHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.GECOS != "" {
|
||||||
|
args = append(args, "--comment", fmt.Sprintf("%q", u.GECOS))
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.Homedir != "" {
|
||||||
|
args = append(args, "--home-dir", u.Homedir)
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.NoCreateHome {
|
||||||
|
args = append(args, "--no-create-home")
|
||||||
|
} else {
|
||||||
|
args = append(args, "--create-home")
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.PrimaryGroup != "" {
|
||||||
|
args = append(args, "--primary-group", u.PrimaryGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(u.Groups) > 0 {
|
||||||
|
args = append(args, "--groups", strings.Join(u.Groups, ","))
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.NoUserGroup {
|
||||||
|
args = append(args, "--no-user-group")
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.System {
|
||||||
|
args = append(args, "--system")
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.NoLogInit {
|
||||||
|
args = append(args, "--no-log-init")
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args, u.Name)
|
||||||
|
|
||||||
|
output, err := exec.Command("useradd", args...).CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Command 'useradd %s' failed: %v\n%s", strings.Join(args, " "), err, output)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetUserPassword(user, hash string) error {
|
||||||
|
cmd := exec.Command("/usr/sbin/chpasswd", "-e")
|
||||||
|
|
||||||
|
stdin, err := cmd.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
arg := fmt.Sprintf("%s:%s", user, hash)
|
||||||
|
_, err = stdin.Write([]byte(arg))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
stdin.Close()
|
||||||
|
|
||||||
|
err = cmd.Wait()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user