diff --git a/divingbell/templates/bin/_ethtool.sh.tpl b/divingbell/templates/bin/_ethtool.sh.tpl index 5bc6e41..0f2f497 100644 --- a/divingbell/templates/bin/_ethtool.sh.tpl +++ b/divingbell/templates/bin/_ethtool.sh.tpl @@ -152,9 +152,9 @@ WantedBy=multi-user.target" {{- range $iface, $unused := .Values.conf.ethtool }} {{- range $ethtool_key, $ethtool_val := . }} - device={{ $iface | quote }} \ - user_key={{ $ethtool_key | quote }} \ - user_val={{ $ethtool_val | quote }} \ + device={{ $iface | squote }} \ + user_key={{ $ethtool_key | squote }} \ + user_val={{ $ethtool_val | squote }} \ add_ethtool_param {{- end }} {{- end }} diff --git a/divingbell/templates/bin/_mounts.sh.tpl b/divingbell/templates/bin/_mounts.sh.tpl index f48083e..dc4f7d4 100644 --- a/divingbell/templates/bin/_mounts.sh.tpl +++ b/divingbell/templates/bin/_mounts.sh.tpl @@ -107,7 +107,7 @@ WantedBy=local-fs.target" {{- range .Values.conf.mounts }} {{- range $key, $value := . }} - {{ $key }}={{ $value | quote }} \ + {{ $key }}={{ $value | squote }} \ {{- end }} add_mounts_param {{- end }} diff --git a/divingbell/templates/bin/_sysctl.sh.tpl b/divingbell/templates/bin/_sysctl.sh.tpl index 16e3866..4c60c30 100644 --- a/divingbell/templates/bin/_sysctl.sh.tpl +++ b/divingbell/templates/bin/_sysctl.sh.tpl @@ -90,7 +90,7 @@ add_sysctl_param(){ } {{- range $key, $value := .Values.conf.sysctl }} -add_sysctl_param {{ $key | quote }} {{ $value | quote }} +add_sysctl_param {{ $key | squote }} {{ $value | squote }} {{- end }} # Revert any previously applied sysctl settings which are now absent diff --git a/divingbell/templates/bin/_uamlite.sh.tpl b/divingbell/templates/bin/_uamlite.sh.tpl index 53a8e61..8331364 100644 --- a/divingbell/templates/bin/_uamlite.sh.tpl +++ b/divingbell/templates/bin/_uamlite.sh.tpl @@ -27,6 +27,7 @@ builtin_acct='ubuntu' add_user(){ die_if_null "${user_name}" ", 'user_name' env var not initialized" : ${user_sudo:=false} + : ${user_crypt_passwd:=*} # Create user if user does not already exist getent passwd ${user_name} && \ @@ -41,10 +42,31 @@ add_user(){ log.INFO "User '${user_name}' has been unexpired" fi + # Exclude case where user should not have a password set + if [ "${user_crypt_passwd}" != '*' ]; then + local user_has_passwd=true + fi + # Set user password if current password does not match desired password + local crypt_passwd="$(getent shadow ${user_name} | cut -d':' -f2)" + if [ "${crypt_passwd}" != "${user_crypt_passwd}" ]; then + usermod -p "${user_crypt_passwd}" ${user_name} + if [ "${user_has_passwd}" = 'true' ]; then + log.INFO "User '${user_name}' password set successfully" + else + log.INFO "User '${user_name}' password removed successfully" + fi + else + if [ "${user_has_passwd}" = 'true' ]; then + log.INFO "No change required to password for user '${user_name}'" + else + log.INFO "User '${user_name}' has no password, and none was requested" + fi + fi + # Add sudoers entry if requested for user if [ "${user_sudo}" = 'true' ]; then # Add sudoers entry if it does not already exist - user_sudo_file=/etc/sudoers.d/${keyword}-${user_name}-sudo + local user_sudo_file=/etc/sudoers.d/${keyword}-${user_name}-sudo if [ -f "${user_sudo_file}" ] ; then log.INFO "User '${user_name}' already added to sudoers: ${user_sudo_file}" else @@ -56,15 +78,21 @@ add_user(){ log.INFO "User '${user_name}' was not requested sudo access" fi + if [ "${user_has_passwd}" = "true" ] && \ + [ "${user_sudo}" = "true" ] && \ + [ "${user_name}" != "${builtin_acct}" ]; then + expire_builtin_acct_passwd_vote=true + fi + curr_userlist="${curr_userlist}${user_name}"$'\n' } add_sshkeys(){ die_if_null "${user_name}" ", 'user_name' env var not initialized" - user_sshkeys="$@" + local user_sshkeys="$@" - sshkey_dir="/home/${user_name}/.ssh" - sshkey_file="${sshkey_dir}/authorized_keys" + local sshkey_dir="/home/${user_name}/.ssh" + local sshkey_file="${sshkey_dir}/authorized_keys" if [ -z "${user_sshkeys}" ]; then log.INFO "User '${user_name}' has no SSH keys defined" if [ -f "${sshkey_file}" ]; then @@ -72,11 +100,11 @@ add_sshkeys(){ log.INFO "User '${user_name}' has had its authorized_keys file wiped" fi else - sshkey_file_contents='# NOTE: This file is managed by divingbell'$'\n' + local sshkey_file_contents='# NOTE: This file is managed by divingbell'$'\n' for sshkey in "$@"; do sshkey_file_contents="${sshkey_file_contents}${sshkey}"$'\n' done - write_file=false + local write_file=false if [ -f "${sshkey_file}" ]; then if [ "$(cat "${sshkey_file}")" = \ "$(echo "${sshkey_file_contents}" | head -n-1)" ]; then @@ -98,33 +126,44 @@ add_sshkeys(){ # In the event that the user specifies ssh keys for the built-in account and # no others, do not expire the built-in account - if [ "${user_name}" != "${builtin_acct}" ]; then - expire_builtin_acct=true + if [ "${user_sudo}" = "true" ] && \ + [ "${user_name}" != "${builtin_acct}" ]; then + expire_builtin_acct_ssh_vote=true fi fi - } {{- if hasKey .Values.conf "uamlite" }} {{- if hasKey .Values.conf.uamlite "purge_expired_users" }} -purge_expired_users={{ .Values.conf.uamlite.purge_expired_users | quote }} +purge_expired_users={{ .Values.conf.uamlite.purge_expired_users | squote }} {{- end }} {{- if hasKey .Values.conf.uamlite "users" }} {{- range $item := .Values.conf.uamlite.users }} {{- range $key, $value := . }} - {{ $key }}={{ $value | quote }} \ + {{- if eq $key "user_crypt_passwd" }} + {{/* supported crypt types are 2a (blowfish), 1 (md5), 5 (sha-256), and 6 (sha-512) */}} + {{- if not (or (regexMatch "\\$2a\\$.*\\$.*" $value) (regexMatch "\\$[156]\\$.*\\$.*" $value)) }} + {{- fail (print "BAD PASSWORD FOR '" $item.user_name "': The 'user_crypt_passwd' specified for '" $item.user_name "' does not pass regex checks. Ensure that the supplied user password is encoded per divingbell documentation at https://divingbell.readthedocs.io/en/latest/#uamlite") }} + {{- end }} + {{- end }} + {{ $key }}={{ $value | squote }} \ {{- end }} add_user {{- range $key, $value := . }} - {{ $key }}={{ $value | quote }} \ + {{ $key }}={{ $value | squote }} \ {{- end }} - add_sshkeys {{ range $ssh_key := .user_sshkeys }}{{ $ssh_key | quote }} {{end}} + {{- if hasKey . "user_sshkeys" }} + {{- if not (eq (first .user_sshkeys) "Unmanaged") }} + add_sshkeys {{ range $ssh_key := .user_sshkeys }}{{ if not (or (regexMatch "ssh-dss .*" $ssh_key) (regexMatch "ecdsa-.*" $ssh_key) (regexMatch "ssh-ed25519 .*" $ssh_key) (regexMatch "ssh-rsa .*" $ssh_key)) }}{{ fail (print "BAD SSH KEY FOR '" $item.user_name "': One of the 'user_sshkeys' specified for '" $item.user_name "' does not pass regex checks: '" $ssh_key "'. Ensure that the supplied user SSH keys are supported/formatted per divingbell documentation at https://divingbell.readthedocs.io/en/latest/#uamlite") }}{{ else }}{{ $ssh_key | squote }}{{ end }} {{ end }} +{{- end }} +{{- else }} + add_sshkeys +{{- end }} {{- end }} {{- end }} {{- end }} -# TODO: This should be done before applying new settings rather than after # Expire any previously defined users that are no longer defined if [ -n "$(getent passwd | grep ${keyword} | cut -d':' -f1)" ]; then users="$(getent passwd | grep ${keyword} | cut -d':' -f1)" @@ -163,7 +202,8 @@ fi if [ -n "${builtin_acct}" ] && [ -n "$(getent passwd ${builtin_acct})" ]; then # Disable built-in account as long as there was at least one account defined # in this chart with a ssh key present - if [ "${expire_builtin_acct}" = "true" ]; then + if [ "${expire_builtin_acct_passwd_vote}" = "true" ] && \ + [ "${expire_builtin_acct_ssh_vote}" = "true" ]; then if [ "$(chage -l ${builtin_acct} | grep 'Account expires' | cut -d':' -f2 | tr -d '[:space:]')" = "never" ]; then usermod --expiredate 1 ${builtin_acct} diff --git a/divingbell/tools/gate/test.sh b/divingbell/tools/gate/test.sh index f7e73ae..876950e 100755 --- a/divingbell/tools/gate/test.sh +++ b/divingbell/tools/gate/test.sh @@ -41,6 +41,7 @@ USERNAME2_SUDO=false USERNAME2_SSHKEY1="ssh-rsa xyz456 comment" USERNAME2_SSHKEY2="ssh-rsa qwe789 comment" USERNAME2_SSHKEY3="ssh-rsa rfv000 comment" +USERNAME2_CRYPT_PASSWD='$6$AF.NLpphOJjMVTYC$GD6wyUTy9vIgatoMbtTDYcVtEJqh/Mrx3BRetVstMsNodSyn3ZFIZOMRePpRpGbFArnAxgkL1PtQxsZHCgtFn/' USERNAME3=userthree USERNAME3_SUDO=true USERNAME4=userfour @@ -556,6 +557,16 @@ _test_ssh_keys(){ fi } +_test_user_passwd(){ + username=$1 + crypt_passwd="$2" + + if [ "$crypt_passwd" != "$(getent shadow $username | cut -d':' -f2)" ]; then + echo "Error: User '$username' passwd did not match expected val '$crypt_passwd'" + return 1 + fi +} + test_uamlite(){ # Test the first set of values local overrides_yaml=${LOGS_SUBDIR}/${FUNCNAME}-set1.yaml @@ -568,6 +579,7 @@ test_uamlite(){ - ${USERNAME1_SSHKEY1} - user_name: ${USERNAME2} user_sudo: ${USERNAME2_SUDO} + user_crypt_passwd: ${USERNAME2_CRYPT_PASSWD} user_sshkeys: - ${USERNAME2_SSHKEY1} - ${USERNAME2_SSHKEY2} @@ -580,17 +592,21 @@ test_uamlite(){ _test_user_enabled ${USERNAME1} true _test_sudo_enabled ${USERNAME1} ${USERNAME1_SUDO} _test_ssh_keys ${USERNAME1} "${USERNAME1_SSHKEY1}" + _test_user_passwd ${USERNAME1} '*' _test_user_enabled ${USERNAME2} true _test_sudo_enabled ${USERNAME2} ${USERNAME2_SUDO} _test_ssh_keys ${USERNAME2} "${USERNAME2_SSHKEY1}" _test_ssh_keys ${USERNAME2} "${USERNAME2_SSHKEY2}" _test_ssh_keys ${USERNAME2} "${USERNAME2_SSHKEY3}" + _test_user_passwd ${USERNAME2} ${USERNAME2_CRYPT_PASSWD} _test_user_enabled ${USERNAME3} true _test_sudo_enabled ${USERNAME3} ${USERNAME3_SUDO} _test_ssh_keys ${USERNAME3} false + _test_user_passwd ${USERNAME3} '*' _test_user_enabled ${USERNAME4} true _test_sudo_enabled ${USERNAME4} ${USERNAME4_SUDO} _test_ssh_keys ${USERNAME4} false + _test_user_passwd ${USERNAME4} '*' echo '[SUCCESS] uamlite test1 passed successfully' >> "${TEST_RESULTS}" # Test an updated set of values @@ -619,17 +635,21 @@ test_uamlite(){ _test_user_enabled ${USERNAME1} true _test_sudo_enabled ${USERNAME1} ${uname1_sudo} _test_ssh_keys ${USERNAME1} false + _test_user_passwd ${USERNAME1} '*' _test_user_enabled ${USERNAME2} true _test_sudo_enabled ${USERNAME2} ${uname2_sudo} _test_ssh_keys ${USERNAME2} "${USERNAME2_SSHKEY1}" _test_ssh_keys ${USERNAME2} "${USERNAME2_SSHKEY2}" + _test_user_passwd ${USERNAME2} '*' _test_user_enabled ${USERNAME3} true _test_sudo_enabled ${USERNAME3} ${uname3_sudo} _test_ssh_keys ${USERNAME3} "${USERNAME1_SSHKEY1}" _test_ssh_keys ${USERNAME3} "${USERNAME2_SSHKEY3}" + _test_user_passwd ${USERNAME3} '*' _test_user_enabled ${USERNAME4} true _test_sudo_enabled ${USERNAME4} ${USERNAME4_SUDO} _test_ssh_keys ${USERNAME4} false + _test_user_passwd ${USERNAME4} '*' echo '[SUCCESS] uamlite test2 passed successfully' >> "${TEST_RESULTS}" # Test revert/rollback functionality @@ -646,7 +666,7 @@ test_uamlite(){ echo '[SUCCESS] uamlite test3 passed successfully' >> "${TEST_RESULTS}" # Test purge users flag - overrides_yaml=${LOGS_SUBDIR}/${FUNCNAME}-set3.yaml + overrides_yaml=${LOGS_SUBDIR}/${FUNCNAME}-set4.yaml echo "conf: uamlite: purge_expired_users: true" > "${overrides_yaml}" @@ -657,6 +677,33 @@ test_uamlite(){ _test_user_purged ${USERNAME3} _test_user_purged ${USERNAME4} echo '[SUCCESS] uamlite test4 passed successfully' >> "${TEST_RESULTS}" + + # Test invalid password + overrides_yaml=${LOGS_SUBDIR}/${FUNCNAME}-set5.yaml + user2_crypt_passwd_invalid='plaintextPassword' + echo "conf: + uamlite: + users: + - user_name: ${USERNAME2} + user_crypt_passwd: ${user2_crypt_passwd_invalid}" > "${overrides_yaml}" + install_base "--values=${overrides_yaml}" 2>&1 | grep 'BAD PASSWORD' || \ + (echo "[FAIL] uamlite test5 did not receive expected 'BAD PASSWORD' error" && exit 1) + echo '[SUCCESS] uamlite test5 passed successfully' >> "${TEST_RESULTS}" + + # Test invalid SSH key + overrides_yaml=${LOGS_SUBDIR}/${FUNCNAME}-set6.yaml + user2_bad_sshkey='AAAAB3NzaC1yc2EAAAABIwAAAQEAklOUpkDHrfHY17SbrmT key-comment' + echo "conf: + uamlite: + users: + - user_name: ${USERNAME2} + user_sshkeys: + - ${USERNAME2_SSHKEY1} + - ${user2_bad_sshkey} + - ${USERNAME2_SSHKEY3}" > "${overrides_yaml}" + install_base "--values=${overrides_yaml}" 2>&1 | grep 'BAD SSH KEY' || \ + (echo "[FAIL] uamlite test6 did not receive expected 'BAD SSH KEY' error" && exit 1) + echo '[SUCCESS] uamlite test6 passed successfully' >> "${TEST_RESULTS}" } # test daemonset value overrides for hosts and labels diff --git a/docs/source/index.rst b/docs/source/index.rst index 4a4e244..05f85b0 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -17,9 +17,6 @@ Divingbell ========== -What is it? ------------ - Divingbell is a lightweight solution for: 1. Bare metal configuration management for a few very targeted use cases 2. Bare metal package manager orchestration @@ -66,10 +63,6 @@ fashion: the idempotent automation for each daemonset will only re-run when Armada spawns/respawns the container, or if information relevant to the host changes in the configmap. -For upgrades, a decision was taken not to use any of the built-in Kubernetes -update strategies such as RollingUpdate. Instead, we are putting this on -Armada to handle the orchestration of how to do upgrades (e.g., rack by rack). - Daemonset configs ----------------- @@ -123,19 +116,73 @@ access. Ex:: purge_expired_users: false users: - user_name: testuser - user_sudo: True + user_crypt_passwd: $6$... + user_sudo: true user_sshkeys: - ssh-rsa AAAAB3N... key1-comment - ssh-rsa AAAAVY6... key2-comment -An update to the chart with revmoed users will result in those user's accounts -being expired, preventing those users any access through those accounts. This -does not delete their home directory or any other files, and provides UID -consistency in the event the same account gets re-added later, and they regain -access to their files again. +Setting user passwords +"""""""""""""""""""""" -However, if it is desired to purge expired and removed accounts and their home -directories, this may be done by the ``purge_expired_users`` option to ``true``. +Including ``user_crypt_passwd`` to set a user password is optional. + +If setting a password for the user, the chart expects the password to be +encrypted with SHA-512 and formatted in the way that ``crypt`` library expects. +Run the following command to generate the needed encrypted password from the +plaintext password:: + + python3 -c "from getpass import getpass; from crypt import *; p=getpass(); print('\n'+crypt(p, METHOD_SHA512)) if p==getpass('Please repeat: ') else print('\nPassword mismatch.')" + +Use the output of the above command as the ``user_crypt_passwd`` for the user. +(Credit to `unix.stackexchange.com `_.) +If the password is not formatted how crypt expects, the chart will throw an +error and fail to render. + +At least one user must be defined with a password and sudo in order for the +built-in ``ubuntu`` account to be disabled. This is because in a situation where +network access is unavailable, console username/password access will be the only +login option. + +Setting user sudo +""""""""""""""""" + +Including ``user_sudo`` to set user sudo access is optional. The default value +is ``false``. + +At least one user must be defined with sudo access in order for the built-in +``ubuntu`` account to be disabled. + +SSH keys +"""""""" + +Including ``user_sshkeys`` for defining one or more user SSH keys is optional. + +The chart will throw an error and fail to render if the SSH key is not one of +the following formats: + +- dsa (ssh-dss ...) +- ecdsa (ecdsa-...) +- ed25519 (ssh-ed25519 ...) +- rsa (ssh-rsa ...) + +Setting ``user_sshkeys`` to ``[ Unmanaged ]`` will instruct divingbell not to +manage the user's authorized_keys file. + +At least one user must be defined with an SSH key and sudo in order for the +built-in ``ubuntu`` account to be disabled. + +Purging expired users +""""""""""""""""""""" + +Including the ``purge_expired_users`` key-value pair is optional. The default +value is ``false``. + +This option must be set to ``true`` if it is desired to purge expired accounts +and remove their home directories. Otherwise, removed accounts are expired (so +users cannot login) but their home directories remain intact, in order to +maintain UID consistency (in the event the same accounts gets re-added later, +they regain access to their home directory files without UID mismatching). Node specific configurations ----------------------------