From 9e7028416e8b6798c1b2bf04770bd165e398b5c1 Mon Sep 17 00:00:00 2001 From: Craig Anderson Date: Thu, 15 Mar 2018 06:13:56 +0000 Subject: [PATCH] [US367408] Add support for user & ssh key mgmt Change-Id: I0ef68dfd80194e6da289fbf86f5cd2ee5c7edad8 --- Makefile | 8 +- divingbell/templates/bin/_uamlite.sh.tpl | 181 ++++++++++++++++++++ divingbell/templates/configmap-uamlite.yaml | 30 ++++ divingbell/templates/daemonset-uamlite.yaml | 65 +++++++ divingbell/tools/gate/test.sh | 171 +++++++++++++++++- docs/source/index.rst | 16 +- 6 files changed, 461 insertions(+), 10 deletions(-) create mode 100644 divingbell/templates/bin/_uamlite.sh.tpl create mode 100644 divingbell/templates/configmap-uamlite.yaml create mode 100644 divingbell/templates/daemonset-uamlite.yaml diff --git a/Makefile b/Makefile index aa86e6f..be00a53 100644 --- a/Makefile +++ b/Makefile @@ -15,8 +15,9 @@ HELM := helm TASK := build -EXCLUDES := helm-toolkit doc tests tools logs +EXCLUDES := helm-toolkit docs tests tools logs CHARTS := helm-toolkit $(filter-out $(EXCLUDES), $(patsubst %/.,%,$(wildcard */.))) +CHART := divingbell all: $(CHARTS) @@ -42,3 +43,8 @@ clean: rm -rf */templates/_globals.tpl .PHONY: $(EXCLUDES) $(CHARTS) + +.PHONY: charts +charts: clean + $(HELM) dep up $(CHART) + $(HELM) package $(CHART) diff --git a/divingbell/templates/bin/_uamlite.sh.tpl b/divingbell/templates/bin/_uamlite.sh.tpl new file mode 100644 index 0000000..adbf4b6 --- /dev/null +++ b/divingbell/templates/bin/_uamlite.sh.tpl @@ -0,0 +1,181 @@ +#!/bin/bash + +{{/* +# Copyright 2018 AT&T Intellectual Property. All other rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +*/}} + +set -e + +cat <<'EOF' > {{ .Values.conf.chroot_mnt_path | quote }}/tmp/uamlite_host.sh +{{ include "divingbell.shcommon" . }} + +keyword='divingbell' +builtin_acct='ubuntu' + +add_user(){ + die_if_null "${user_name}" ", 'user_name' env var not initialized" + : ${user_sudo:=false} + + # Create user if user does not already exist + getent passwd ${user_name} && \ + log.INFO "User '${user_name}' already exists" || \ + (useradd --create-home --shell /bin/bash --comment ${keyword} ${user_name} && \ + log.INFO "User '${user_name}' successfully created") + + # Unexpire the user (if user had been previously expired) + if [ "$(chage -l ${user_name} | grep 'Account expires' | cut -d':' -f2 | + tr -d '[:space:]')" != "never" ]; then + usermod --expiredate "" ${user_name} + log.INFO "User '${user_name}' has been unexpired" + 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 + if [ -f "${user_sudo_file}" ] ; then + log.INFO "User '${user_name}' already added to sudoers: ${user_sudo_file}" + else + echo "${user_name} ALL=(ALL) NOPASSWD:ALL" > "${user_sudo_file}" + log.INFO "User '${user_name}' added to sudoers: ${user_sudo_file}" + fi + curr_sudoers="${curr_sudoers}${user_sudo_file}"$'\n' + else + log.INFO "User '${user_name}' was not requested sudo access" + fi + + curr_userlist="${curr_userlist}${user_name}"$'\n' +} + +add_sshkeys(){ + die_if_null "${user_name}" ", 'user_name' env var not initialized" + user_sshkeys="$@" + + sshkey_dir="/home/${user_name}/.ssh" + 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 + rm "${sshkey_file}" + 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' + for sshkey in "$@"; do + sshkey_file_contents="${sshkey_file_contents}${sshkey}"$'\n' + done + write_file=false + if [ -f "${sshkey_file}" ]; then + if [ "$(cat "${sshkey_file}")" = \ + "$(echo "${sshkey_file_contents}" | head -n-1)" ]; then + log.INFO "User '${user_name}' has no new SSH keys" + else + write_file=true + fi + else + write_file=true + fi + if [ "${write_file}" = "true" ]; then + mkdir -p "${sshkey_dir}" + chmod 700 "${sshkey_dir}" + echo -e "${sshkey_file_contents}" > "${sshkey_file}" + chown -R ${user_name}:${user_name} "${sshkey_dir}" || \ + (rm "${sshkey_file}" && die "Error setting ownership on ${sshkey_dir}") + log.INFO "User '${user_name}' has had SSH keys deployed: ${user_sshkeys}" + fi + custom_sshkeys_present=true + fi + +} + +{{- if hasKey .Values.conf "uamlite" }} +{{- if hasKey .Values.conf.uamlite "users" }} +{{- range $item := .Values.conf.uamlite.users }} + {{- range $key, $value := . }} + {{ $key }}={{ $value | quote }} \ + {{- end }} + add_user + + {{- range $key, $value := . }} + {{ $key }}={{ $value | quote }} \ + {{- end }} + add_sshkeys {{ range $ssh_key := .user_sshkeys }}{{ $ssh_key | quote }} {{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 +users="$(getent passwd | grep ${keyword} | cut -d':' -f1)" +echo "$users" | sort > /tmp/prev_users +echo "$curr_userlist" | sort > /tmp/curr_users +revert_list="$(comm -23 /tmp/prev_users /tmp/curr_users)" +IFS=$'\n' +for user in ${revert_list}; do + # We expire rather than delete the user to maintain local UID FS consistency + usermod --expiredate 1 ${user} + log.INFO "User '${user}' has been disabled (expired)" +done + +# Delete any previous user sudo access that is no longer defined +sudoers="$(find /etc/sudoers.d | grep ${keyword})" +echo "$sudoers" | sort > /tmp/prev_sudoers +echo "$curr_sudoers" | sort > /tmp/curr_sudoers +revert_list="$(comm -23 /tmp/prev_sudoers /tmp/curr_sudoers)" +IFS=$'\n' +for sudo_file in ${revert_list}; do + rm "${sudo_file}" + log.INFO "Sudoers file '${sudo_file}' has been deleted" +done + +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 [ "${custom_sshkeys_present}" = "true" ]; then + if [ "$(chage -l ${builtin_acct} | grep 'Account expires' | cut -d':' -f2 | + tr -d '[:space:]')" = "never" ]; then + usermod --expiredate 1 ${builtin_acct} + fi + # Re-enable built-in account as a fallback in the event that are no other + # accounts defined in this chart with a ssh key present + else + if [ "$(chage -l ${builtin_acct} | grep 'Account expires' | cut -d':' -f2 | + tr -d '[:space:]')" != "never" ]; then + usermod --expiredate "" ${builtin_acct} + fi + fi +fi + +if [ -n "${curr_userlist}" ]; then + log.INFO 'All uamlite data successfully validated on this node.' +else + log.WARN 'No uamlite overrides defined for this node.' +fi + +exit 0 +EOF + +chmod 755 {{ .Values.conf.chroot_mnt_path | quote }}/tmp/uamlite_host.sh +chroot {{ .Values.conf.chroot_mnt_path | quote }} /tmp/uamlite_host.sh + +sleep 1 +echo 'INFO Putting the daemon to sleep.' + +while [ 1 ]; do + sleep 300 +done + +exit 0 + diff --git a/divingbell/templates/configmap-uamlite.yaml b/divingbell/templates/configmap-uamlite.yaml new file mode 100644 index 0000000..3302c48 --- /dev/null +++ b/divingbell/templates/configmap-uamlite.yaml @@ -0,0 +1,30 @@ +{{/* +Copyright 2018 AT&T Intellectual Property. All other rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- define "divingbell.configmap.uamlite" }} +{{- $configMapName := index . 0 }} +{{- $envAll := index . 1 }} +{{- with $envAll }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ $configMapName }} +data: + uamlite: |+ +{{ tuple "bin/_uamlite.sh.tpl" . | include "helm-toolkit.utils.template" | indent 4 }} +{{- end }} +{{- end }} diff --git a/divingbell/templates/daemonset-uamlite.yaml b/divingbell/templates/daemonset-uamlite.yaml new file mode 100644 index 0000000..453e636 --- /dev/null +++ b/divingbell/templates/daemonset-uamlite.yaml @@ -0,0 +1,65 @@ +{{/* +# Copyright 2018 AT&T Intellectual Property. All other rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +*/}} + +{{- define "divingbell.daemonset.uamlite" }} + {{- $daemonset := index . 0 }} + {{- $configMapName := index . 1 }} + {{- $envAll := index . 2 }} + {{- with $envAll }} +--- +apiVersion: extensions/v1beta1 +kind: DaemonSet +metadata: + name: {{ $daemonset }} +spec: + template: + metadata: + labels: +{{ list $envAll .Chart.Name $daemonset | include "helm-toolkit.snippets.kubernetes_metadata_labels" | indent 8 }} + spec: + hostNetwork: true + hostPID: true + hostIPC: true + containers: + - name: {{ $daemonset }} + image: {{ .Values.images.divingbell }} + imagePullPolicy: {{ .Values.images.pull_policy }} + command: + - /tmp/{{ $daemonset }}.sh + volumeMounts: + - name: rootfs-{{ $daemonset }} + mountPath: {{ .Values.conf.chroot_mnt_path }} + - name: {{ $configMapName }} + mountPath: /tmp/{{ $daemonset }}.sh + subPath: {{ $daemonset }} + readOnly: true + securityContext: + privileged: true + volumes: + - name: rootfs-{{ $daemonset }} + hostPath: + path: / + - name: {{ $configMapName }} + configMap: + name: {{ $configMapName }} + defaultMode: 0555 + {{- end }} +{{- end }} +{{- $daemonset := "uamlite" }} +{{- $configMapName := "divingbell-uamlite" }} +{{- $daemonset_yaml := list $daemonset $configMapName . | include "divingbell.daemonset.uamlite" | toString | fromYaml }} +{{- $configmap_include := "divingbell.configmap.uamlite" }} +{{- list $daemonset $daemonset_yaml $configmap_include $configMapName . | include "helm-toolkit.utils.daemonset_overrides" }} diff --git a/divingbell/tools/gate/test.sh b/divingbell/tools/gate/test.sh index 117f4f5..10a19f0 100755 --- a/divingbell/tools/gate/test.sh +++ b/divingbell/tools/gate/test.sh @@ -33,6 +33,18 @@ ETHTOOL_KEY4=tx-nocache-copy ETHTOOL_VAL4_DEFAULT=off ETHTOOL_KEY5=tx-checksum-ip-generic ETHTOOL_VAL5_DEFAULT=on +USERNAME1=userone +USERNAME1_SUDO=true +USERNAME1_SSHKEY1="ssh-rsa abc123 comment" +USERNAME2=usertwo +USERNAME2_SUDO=false +USERNAME2_SSHKEY1="ssh-rsa xyz456 comment" +USERNAME2_SSHKEY2="ssh-rsa qwe789 comment" +USERNAME2_SSHKEY3="ssh-rsa rfv000 comment" +USERNAME3=userthree +USERNAME3_SUDO=true +USERNAME4=userfour +USERNAME4_SUDO=false nic_info="$(lshw -class network)" physical_nic='' IFS=$'\n' @@ -96,6 +108,14 @@ _write_ethtool(){ fi } +_reset_account(){ + if [ -n "$1" ]; then + sudo deluser $1 >& /dev/null || true + sudo rm -r /home/$1 >& /dev/null || true + sudo rm /etc/sudoers.d/*$1* >& /dev/null || true + fi +} + init_default_state(){ if [ "${1}" = 'make' ]; then (cd ../../../; make) @@ -112,6 +132,11 @@ init_default_state(){ _write_ethtool ${DEVICE} ${ETHTOOL_KEY3} ${ETHTOOL_VAL3_DEFAULT} _write_ethtool ${DEVICE} ${ETHTOOL_KEY4} ${ETHTOOL_VAL4_DEFAULT} _write_ethtool ${DEVICE} ${ETHTOOL_KEY5} ${ETHTOOL_VAL5_DEFAULT} + # Remove any created accounts, SSH keys + _reset_account ${USERNAME1} + _reset_account ${USERNAME2} + _reset_account ${USERNAME3} + _reset_account ${USERNAME4} } install(){ @@ -134,9 +159,9 @@ get_container_status(){ local log_connect_sleep_interval=2 local wait_time=0 while : ; do - kubectl logs "${container}" --namespace="${NAME}" > /dev/null && break || - echo "Waiting for container logs..." && - wait_time=$((${wait_time} + ${log_connect_sleep_interval})) && + kubectl logs "${container}" --namespace="${NAME}" > /dev/null && break || \ + echo "Waiting for container logs..." && \ + wait_time=$((${wait_time} + ${log_connect_sleep_interval})) && \ sleep ${log_connect_sleep_interval} if [ ${wait_time} -ge ${log_connect_timeout} ]; then echo "Hit timeout while waiting for container logs to become available." @@ -149,7 +174,8 @@ get_container_status(){ while : ; do CLOGS="$(kubectl logs --namespace="${NAME}" "${container}" 2>&1)" local status="$(echo "${CLOGS}" | tail -1)" - if [[ ${status} = *ERROR* ]] || [[ ${status} = *TRACE* ]]; then + if [[ $(echo -e ${status} | tr -d '[:cntrl:]') = *ERROR* ]] || + [[ $(echo -e ${status} | tr -d '[:cntrl:]') = *TRACE* ]]; then if [ "${2}" = 'expect_failure' ]; then echo 'Pod exited as expected' break @@ -159,8 +185,8 @@ get_container_status(){ echo "${CLOGS}" exit 1 fi - elif [ "${status}" = 'INFO Putting the daemon to sleep.' ] || - [ "${status}" = 'DEBUG + exit 0' ]; then + elif [[ $(echo -e ${status} | tr -d '[:cntrl:]') = *'INFO Putting the daemon to sleep.'* ]] || + [[ $(echo -e ${status} | tr -d '[:cntrl:]') = *'DEBUG + exit 0'* ]]; then if [ "${2}" = 'expect_failure' ]; then echo 'Expected pod to die with error, but pod completed successfully' echo 'pod logs:' @@ -475,6 +501,138 @@ test_ethtool(){ echo '[SUCCESS] ethtool test7 passed successfully' >> "${TEST_RESULTS}" } +_test_user_enabled(){ + username=$1 + user_enabled=$2 + + if [ "${user_enabled}" = "true" ]; then + # verify the user is there and not set to expire + getent passwd $username >& /dev/null + test "$(chage -l ${username} | grep 'Account expires' | cut -d':' -f2 | + tr -d '[:space:]')" = "never" + else + # If the user exists, verify it's not non-expiring + if [ -n "$(getent passwd $username)" ]; then + test "$(chage -l ${username} | grep 'Account expires' | cut -d':' -f2 | + tr -d '[:space:]')" != "never" + fi + fi +} + +_test_sudo_enabled(){ + username=$1 + sudo_enable=$2 + sudoers_file=/etc/sudoers.d/*$username* + + if [ "${sudo_enable}" = "true" ]; then + test -f $sudoers_file + else + test ! -f $sudoers_file + fi +} + +_test_ssh_keys(){ + username=$1 + sshkey=$2 + ssh_file=/home/$username/.ssh/authorized_keys + + if [ "$sshkey" = "false" ]; then + test ! -f "${ssh_file}" + else + grep "$sshkey" "${ssh_file}" + fi +} + +test_uamlite(){ + # Test the first set of values + local overrides_yaml=${LOGS_SUBDIR}/${FUNCNAME}-set1.yaml + echo "conf: + uamlite: + users: + - user_name: ${USERNAME1} + user_sudo: ${USERNAME1_SUDO} + user_sshkeys: + - ${USERNAME1_SSHKEY1} + - user_name: ${USERNAME2} + user_sudo: ${USERNAME2_SUDO} + user_sshkeys: + - ${USERNAME2_SSHKEY1} + - ${USERNAME2_SSHKEY2} + - ${USERNAME2_SSHKEY3} + - user_name: ${USERNAME3} + user_sudo: ${USERNAME3_SUDO} + - user_name: ${USERNAME4}" > "${overrides_yaml}" + install_base "--values=${overrides_yaml}" + get_container_status uamlite + _test_user_enabled ${USERNAME1} true + _test_sudo_enabled ${USERNAME1} ${USERNAME1_SUDO} + _test_ssh_keys ${USERNAME1} "${USERNAME1_SSHKEY1}" + _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_enabled ${USERNAME3} true + _test_sudo_enabled ${USERNAME3} ${USERNAME3_SUDO} + _test_ssh_keys ${USERNAME3} false + _test_user_enabled ${USERNAME4} true + _test_sudo_enabled ${USERNAME4} ${USERNAME4_SUDO} + _test_ssh_keys ${USERNAME4} false + echo '[SUCCESS] uamlite test1 passed successfully' >> "${TEST_RESULTS}" + + # Test an updated set of values + overrides_yaml=${LOGS_SUBDIR}/${FUNCNAME}-set2.yaml + uname1_sudo=false + uname2_sudo=true + uname3_sudo=false + echo "conf: + uamlite: + users: + - user_name: ${USERNAME1} + user_sudo: ${uname1_sudo} + - user_name: ${USERNAME2} + user_sudo: ${uname2_sudo} + user_sshkeys: + - ${USERNAME2_SSHKEY1} + - ${USERNAME2_SSHKEY2} + - user_name: ${USERNAME3} + user_sudo: ${uname3_sudo} + user_sshkeys: + - ${USERNAME1_SSHKEY1} + - ${USERNAME2_SSHKEY3} + - user_name: ${USERNAME4}" > "${overrides_yaml}" + install_base "--values=${overrides_yaml}" + get_container_status uamlite + _test_user_enabled ${USERNAME1} true + _test_sudo_enabled ${USERNAME1} ${uname1_sudo} + _test_ssh_keys ${USERNAME1} false + _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_enabled ${USERNAME3} true + _test_sudo_enabled ${USERNAME3} ${uname3_sudo} + _test_ssh_keys ${USERNAME3} "${USERNAME1_SSHKEY1}" + _test_ssh_keys ${USERNAME3} "${USERNAME2_SSHKEY3}" + _test_user_enabled ${USERNAME4} true + _test_sudo_enabled ${USERNAME4} ${USERNAME4_SUDO} + _test_ssh_keys ${USERNAME4} false + echo '[SUCCESS] uamlite test2 passed successfully' >> "${TEST_RESULTS}" + + # Test revert/rollback functionality + install_base + get_container_status uamlite + _test_user_enabled ${USERNAME1} false + _test_sudo_enabled ${USERNAME1} false + _test_user_enabled ${USERNAME2} false + _test_sudo_enabled ${USERNAME2} false + _test_user_enabled ${USERNAME3} false + _test_sudo_enabled ${USERNAME3} false + _test_user_enabled ${USERNAME4} false + _test_sudo_enabled ${USERNAME4} false + echo '[SUCCESS] uamlite test3 passed successfully' >> "${TEST_RESULTS}" +} + # test daemonset value overrides for hosts and labels test_overrides(){ overrides_yaml=${LOGS_SUBDIR}/${FUNCNAME}-dryrun.yaml @@ -752,6 +910,7 @@ install_base test_sysctl test_mounts test_ethtool +test_uamlite purge_containers test_overrides diff --git a/docs/source/index.rst b/docs/source/index.rst index 762d645..2af7f0e 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -112,10 +112,20 @@ packages Not implemented -users -^^^^^ +uamlite +^^^^^^^ -Not implemented +Used to manage host level local user accounts, their SSH keys, and their sudo +access. Ex:: + + conf: + uamlite: + users: + - user_name: testuser + user_sudo: True + user_sshkeys: + - ssh-rsa AAAAB3N... key1-comment + - ssh-rsa AAAAVY6... key2-comment Node specific configurations ----------------------------