Dobrev.EU Blog

Things I want to share

Managing Users and SSH Keys With Puppet and Hiera

| Comments

A while back I stumbled upon a very untidy way of managing keys with Hiera and Puppet.

Legacy SSH Key management
1
2
3
4
---
sshkeys:
   "johndoe@john.doe.dev": //1024 bit key hash
   "johndoe@johnny.other.doe.dev": //1024 bit key hash

The only possible option it accepts is the key. No type, no additional info, nothing at all besides the key. What this meant to me: obviously here the idea is to be as straight-minded as possible limiting the variations of SSH key types to just one hard-coded value. Fine with this concept but how do I find out what type of key I’m allowed to use then? The more I questioned myself the stronger the feeling I need to dive in Puppet for an answer. I won’t ever bother describing you how “well-organized” in Puppet it all was but eventually I was lucky enough to just realize that this type of SSH key management is the perfect example of how NOT to do things.

The solution? A full rewrite from scratch.

Desired solution

  • Ability to add User in Linux with the most widely used parameters like username, uid, gid, description, password etc.
  • Every user can have at least one SSH key which needs a type and description and eventually a flag denoting if the key needs to be copied to other users as-well
New Hiera structure
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
---
users:
  jdoe:
    options:
      uid: 1000
      password: "jdoepassword"
    advoptions:
      salt: SomeRandomString
      type: des|md5|sha256|sha512
    sshkeys:
      "home.jdoe.domain": //Description
         type: ssh-rsa
         key: AAAAB3NzaC1yc2EAAAADAQABAAACAQD...0sw2irltAMQptXDcaUOoWuimEgVy/blVoL16w==
         enabled: true
         also_add_to: [ "root" ]

What this means you might wonder.

  • options is a hash of all parameters that user accepts.
  • advoptions can be used to set the type of hashing and the salt for it if I want to provide plain text password in options.
  • sshkeys is a hash with all the keys the user has and their options. If set also_add_to is going to instruct Puppet to copy the key for the user(s) in question as-well

How this translates to Puppet?

The solution needs the be split in few definitions – management of the users and eventually management of their key(s). In order to use the advanced options I’m adding four custom parser functions in Puppet for hashing of the password. All together it looks like this

User management (init.pp) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# ---
# users:
#     jdoe:
#         options:
#             uid: 1000
#             password: "jdoepassword"
#         advoptions:
#             salt: SomeRandomString
#             type: des|md5|sha256|sha512
#         sshkeys:
#             "home.jdoe.domain": //Description
#                 type: ssh-rsa
#                 key: AAAAB3NzaC1yc2EAAAADAQABAAACAQDjpmdrzLQ4zOcXU3W...0sw2irltAMQptXDcaUOoWuimEgVy/blVoL16w==
#                 enabled: true
#                 also_add_to: [ "root" ]


class advusermgmt(
    $hiera_key            = "users",
    $default_user_options = {},
    $adv_options          = {}
) {
    define also_add_to (
        $sshkey = {}
    ) {
        $split_title = split($title, "@")
        $user        = $split_title[1]
        $description = $split_title[0]

        # Realize the key folder
        File <| alias == "${user}dotssh" |>

        ssh_authorized_key { "${user}--${description}":
            ensure => bool2num($sshkey["enabled"]) ? {
                0 => "absent",
                1 => "present"
            },
            type => $sshkey["type"],
            key  => $sshkey["key"],
            user => $user,
        }
    }

    define add_user_key (
        $sshkeys = {},
    ) {
        $split_title = split($title, "@")
        $user = $split_title[0]
        $key_title = $split_title[1]
        $key = $sshkeys[$key_title]

        # Realize the home folder
        File <| alias == "${user}dotssh" |>

        # Add the key 
        ssh_authorized_key { "${user}_${key_title}":
            ensure => bool2num($key["enabled"]) ? {
                0 => "absent",
                1 => "present"
            },
            type => $key["type"],
            key  => $key["key"],
            user => $user,
            require => File["${user}dotssh"]
        }

        # Add it to the other users as-well
        if ! empty($key["also_add_to"]) {
            $also_add_to_prefixed = prefix($key["also_add_to"], "${user}_${key_title}@")
            also_add_to { $also_add_to_prefixed:
                sshkey => $key
            }
        }
    }

    define add_user_keys (
        $home_folder = undef,
        $sshkeys = {}
    ) {
        # Realize the user just in case
        User <| name == $title |>

        # Create a virtual folder for the SSH key
        @file { "$home_folder/.ssh":
            ensure => "directory",
            owner  => $title,
            mode   => 0600,
            alias  => "${title}dotssh"
        }

        $keys_prefixed = prefix(keys($sshkeys), "$title@")

        add_user_key { $keys_prefixed:
            sshkeys => $sshkeys
        }
    }


    # Main method
    define add_user(
        $options = {},
        $advoptions = {},
        $sshkeys = {},
    ) {
        # Static settings
        $overwrites = { managehome => true }

        # User requires options
        if empty($options) {
            fail("Can't add user without any options")
        }

        # Do we have any advanced options?
        if ! empty($advoptions) {

            # where to put user's home folder
            if has_key($advoptions, "home_prefix") {
                $home_prefix = $advoptions["home_prefix"]
            } else {
                $home_prefix = "/home"
            }

            # If our password option needs to be encrypted
            if has_key($advoptions, "salt") and has_key($advoptions, "type") {
                $overwrites["password"] = upcase($advoptions["type"]) ? {
                    "SHA512" => advusermgmt_sha512($options["password"], $advoptions["salt"]),
                    "SHA256" => advusermgmt_sha256($options["password"], $advoptions["salt"]),
                    "MD5"    => advusermgmt_md5($options["password"], $advoptions["salt"]),
                    "DES"    => advusermgmt_des($options["password"], $advoptions["salt"])
                }
            }
        }

        if ( $title == "root") {
            $home = "/root"
        } else {
            $home = "$home_prefix/$title"
        }

        if ! has_key($options, "home") {
            $overwrites["home"] = $home
        }

        if ! has_key($options, "shell") {
            $overwrites["shell"] = "/bin/bash"
        }

        if ! has_key($options, "name") {
            $overwrites["name"] = $title
        }

        $final_options = merge($options, $overwrites)

        # Create virtual user resources
        create_resources("@user", { "$title" => $final_options })

        # User got SSH key(s)?
        if ! empty($sshkeys) {
            create_resources("::advusermgmt::add_user_keys", { "$title" => { home_folder => $home, sshkeys => $sshkeys }})
        }

        # Realize the user
        User <| name == $title |>
    }


    # Hiera query
    $users = hiera_hash($hiera_key,{})

    if ! empty($users) {
        create_resources("::advusermgmt::add_user", $users, { options => $default_user_options, advoptions => $adv_options, sshkeys => {} })
        }
    else {
        fail("Hiera returned an empty hash when quering for: ${hiera_key}")
        }

}

Known issues

  • using “@” in key names might lead to unexpected behaviour as I use it to do some prefix/split magic
  • also_add_to doesn’t check if the receiver user exists

Additional info

At the time of writing of this article Puppet 3.6.2 is the latest version this module was successfully tested on. The additional Puppet parser functions can be replaced with generate_* equivalents from puppetlabs-stdlib if they’re released.

Licensing

This article is for educational purposes only. It can not be used in full or partly for any type of commercial activities without my written consent. Commercial licenses can be issued free of charge depending on the use case. If you need a license please contact me at martin [at] dobrev.eu describing the desired use.

Comments