diff options
-rw-r--r-- | .gitignore | 3 | ||||
-rwxr-xr-x | build-laps.sh | 36 | ||||
-rw-r--r-- | laps.spec | 47 | ||||
-rw-r--r-- | src/etc/cron.d/70_laps.cron | 4 | ||||
-rw-r--r-- | src/etc/laps/laps.conf.example | 40 | ||||
-rw-r--r-- | src/etc/laps/lapsldap.conf.example | 12 | ||||
-rw-r--r-- | src/usr/share/doc/laps/README.md | 59 | ||||
-rw-r--r-- | src/usr/share/doc/laps/version.txt | 1 | ||||
-rwxr-xr-x | src/usr/share/laps/dependencies/datetime.py | 47 | ||||
-rwxr-xr-x | src/usr/share/laps/laps.sh | 677 |
10 files changed, 926 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..971f973 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.conf +*/.*swp +*.swp diff --git a/build-laps.sh b/build-laps.sh new file mode 100755 index 0000000..a7fee94 --- /dev/null +++ b/build-laps.sh @@ -0,0 +1,36 @@ +#!/bin/sh + +test -z "${PACK_PACKAGE}" && PACK_PACKAGE=laps +test -z "${PACK_VERSION}" && PACK_VERSION=0.0.1 +test -z "${PACK_DIR}" && PACK_DIR="$( readlink -f "$( dirname "${0}" )" )" +test -z "${RPMBUILD_SOURCES_DIR}" && RPMBUILD_SOURCES_DIR="/home/${USER}/rpmbuild/SOURCES" + +test -z "${PACK_TYPE}" && PACK_TYPE="${1}" + +case "${PACK_TYPE}" in + rpm|"") + PACK_TYPE=rpm + : + ;; + deb) + echo "Gotta say unh! Not implemented yet." + exit 1 + ;; + *) + echo "Unknown type ${PACK_TYPE}" + exit 1 + ;; +esac + +case "${PACK_TYPE}" in + + rpm) + pushd "${PACK_DIR}/.." 1>/dev/null 2>&1 + /bin/rm -f "${RPMBUILD_SOURCES_DIR}/${PACK_PACKAGE}-${PACK_VERSION}.tgz" + tar -zcf "${RPMBUILD_SOURCES_DIR}/${PACK_PACKAGE}-${PACK_VERSION}.tgz" --exclude="*.conf" --exclude=".git" --exclude=".*.swp" "${PACK_PACKAGE}" + popd 1>/dev/null 2>&1 + pushd "${PACK_DIR}" 1>/dev/null 2>&1 + rpmbuild -ba "${PACK_PACKAGE}.spec" + ;; + +esac diff --git a/laps.spec b/laps.spec new file mode 100644 index 0000000..6b6417d --- /dev/null +++ b/laps.spec @@ -0,0 +1,47 @@ +%define debug_package %{nil} +Name: laps +Version: 0.0.1 +Release: 1 +Summary: local administrator password solution + +Group: system +License: CC-BY-SA 4.0 +URL: https://bgstack15.wordpress.com/ +Source0: laps-%{version}.tgz + +BuildArch: noarch +BuildRequires: coreutils +Requires: bgscripts-core +Requires: krb5-workstation +Requires: openldap-clients +Requires: passwd +Requires: util-linux +Requires: libpwquality +Requires: findutils +Requires: coreutils +Requires: sed +Requires: gawk + +%description +laps provides the GNU/Linux client for the LAPS (Local Administrator Password Solution). + +%prep +%setup -q -c %{name} + +%build +#%%configure +#make %%{?_smp_mflags} + +%install +#make install DESTDIR=%{buildroot} +cp -pr %{name}/src/* "%{buildroot}" + +%files +%doc %{_docdir}/%{name}/* +%config %attr(644, -, -) %{_sysconfdir}/%{name}/* +%config %attr(600, -, -) %{_sysconfdir}/cron.d/*.cron +%{_datadir}/%{name} + +%changelog +* Tue Oct 23 2018 B Stack <bgstack15@gmail.com> 0.0.1-1 +- initial rpm built diff --git a/src/etc/cron.d/70_laps.cron b/src/etc/cron.d/70_laps.cron new file mode 100644 index 0000000..6be219b --- /dev/null +++ b/src/etc/cron.d/70_laps.cron @@ -0,0 +1,4 @@ +# file: /etc/cron.d/70_laps.cron +# Local Administrator Password Solution + +30 1 * * * root /usr/share/laps/laps.sh 1>/dev/null 2>&1 diff --git a/src/etc/laps/laps.conf.example b/src/etc/laps/laps.conf.example new file mode 100644 index 0000000..1072451 --- /dev/null +++ b/src/etc/laps/laps.conf.example @@ -0,0 +1,40 @@ +# File: /etc/laps/laps.conf +# Config file for LAPS + +# LAPS config +LAPS_USER="toor" # packaged with a non-root user +LAPS_LDAPCONF="/etc/laps/lapsldap.conf" # a duplicate of ldap.conf(5) +LAPS_THRESHOLD="5 days" # within so much time of expiration, generate a new password +LAPS_TIMELIMIT="45 days" # how much time away to set the new expiration time +LAPS_PWGEN_FLAGS="130" # roughly, how many bits of entropy +LAPS_LOG_MSG="LAPS has updated the password for user ${LAPS_USER}" + +# Ldap configuration +LAPS_LDAPSEARCH_UNIQUE_ID="dn" +LAPS_ATTRIB_PW="ms-Mcs-AdmPwd" +LAPS_ATTRIB_TIME="ms-Mcs-AdmPwdExpirationTime" + +# Ldap interaction. You probably don't need to modify these. The script has a -h flag for selecting a different host to read, which overrides these. +LAPS_HOST="$( hostname -s )" +LAPS_LDAPSEARCH_FILTER="(cn=${LAPS_HOST}*)" + +# External commands and flags +LAPS_KINIT_BIN="/usr/bin/kinit" +LAPS_DATETIME_PY="/usr/share/laps/dependencies/datetime.py" +LAPS_KINIT_HOST_SCRIPT_DEFAULT="/usr/share/bgscripts/work/kinit-host.sh" +LAPS_KINIT_HOST_SCRIPT="/usr/share/bgscripts/work/kinit-host.sh" +LAPS_KLIST_BIN="/usr/bin/klist" +LAPS_LDAPMODIFY_BIN="/usr/bin/ldapmodify" +LAPS_LDAPMODIFY_FLAGS="-O maxssf=0 -Q -o ldif-wrap=300 -Y gssapi -f" +LAPS_LDAPSEARCH_BIN="/usr/bin/ldapsearch" +LAPS_LDAPSEARCH_FLAGS="-LLL -O maxssf=0 -o ldif-wrap=300 -Y gssapi" +LAPS_PASSWD_BIN="/bin/passwd" +LAPS_LOG_BIN="/bin/logger" +LAPS_LOG_FLAGS="-t laps -i -p authpriv.notice" +LAPS_PWGEN_SCRIPT="/usr/bin/pwmake" + +# These are designed primarily for environment variable or parameter usage +LAPS_TEST=0 # -t +LAPS_FORCE=0 # -f +LAPS_KERBEROS_USER="machine" # -u <username> +LAPS_INTERACTIVE=0 # -i diff --git a/src/etc/laps/lapsldap.conf.example b/src/etc/laps/lapsldap.conf.example new file mode 100644 index 0000000..df9e0f8 --- /dev/null +++ b/src/etc/laps/lapsldap.conf.example @@ -0,0 +1,12 @@ +# File: /etc/lapsldap.conf +# Used by laps when calling ldapsearch and ldapmodify +# Package: laps +# Documentation: see ldap.conf(5) + +TLS_CACERTDIR /etc/openldap/cacerts + +# Turning this off breaks GSSAPI used with krb5 when rdns = false +SASL_NOCANON on + +URI ldaps://ad.example.com ldaps://ds1.ad.example.com lapds://ds2.ad.example.com +BASE dc=ad,dc=example,dc=com diff --git a/src/usr/share/doc/laps/README.md b/src/usr/share/doc/laps/README.md new file mode 100644 index 0000000..204b97d --- /dev/null +++ b/src/usr/share/doc/laps/README.md @@ -0,0 +1,59 @@ +# Readme for laps +Laps is an open-source implementation of LAPS (Local Administrator Password Solution) for GNU/Linux. + +# What does this package do? +This package provides the LAPS client-side functionality. Specifically, those abilities are: +* Update machine root password saved in ldap +* Read attribute, when run as a domain admin user + +# Using laps package +The package includes an anacron file for running laps every day. The process will update the password stored in ldap and the user password based on the expiration date on the config file. +See /etc/laps/laps.conf.example for how to configure the client. + +The administrator needs to write **/etc/laps/laps.conf** and **/etc/laps/lapsldap.conf**. Copying and modifying the example config files is the recommended way to provide the configs. + +# Prepare the domain +The OU where the Linux systems are placed in the domain will need some ACLS set up, which are identical to what the LAPS documentation describes. For a brief summary: + +On the OU that contains the Linux computers, use these permissions. +* SELF, applicable to descendant computer objects + * Write ms-Mcs-AdmPwd + * Read/Write ms-Mcs-AdmPwdExpirationTime +* <Domain Admins> or other designated group, application to descendant computer objects + * Read ms-Mcs-AdmPwd + * Read/Write ms-Mcs-AdmPwdExpirationTime + +Systems should be placed in this OU which has the inheritable ACLs. + +# Building laps +## Fetch the source +There are 2 options for getting the source +* Clone the git repository + + mkdir -p ~/dev/laps ; cd ~/dev/laps + git clone https://github.com/bgstack15/laps.git + +* Download the tarball + * Save it to ~/rpmbuild/SOURCES/ + +## Building rpms +#### From the git repository +Run the build-laps.sh script which generates the tarball in ~/rpmbuild/SOURCES suitable for building the rpm. This script builds the source tarball with the correct name and runs rpmbuild using the spec file. + + ./build-laps.sh + +#### From a tarball +The source tarball includes the rpm spec file. Just run rpmbuild with the right flags. + + rpmbuild -ta laps-0.0.1.tgz + +## Building debs +// Not implemented yet + +## Installing from a downloaded tarball +The src/ directory in the tarball contains the exact directory structure. The solution consists of a few config files, a few shell and python scripts, and an anacron job file. + +# Maintaining this package + +# Reference +1. [Microsoft LAPS download page](https://www.microsoft.com/en-us/download/details.aspx?id=46899) diff --git a/src/usr/share/doc/laps/version.txt b/src/usr/share/doc/laps/version.txt new file mode 100644 index 0000000..8acdd82 --- /dev/null +++ b/src/usr/share/doc/laps/version.txt @@ -0,0 +1 @@ +0.0.1 diff --git a/src/usr/share/laps/dependencies/datetime.py b/src/usr/share/laps/dependencies/datetime.py new file mode 100755 index 0000000..c8583fb --- /dev/null +++ b/src/usr/share/laps/dependencies/datetime.py @@ -0,0 +1,47 @@ +#!/usr/bin/python2 +# File: datetime.py +# Location: /usr/share/laps4linux/dependencies +# Author: bgstack15 +# Startdate: 2018-10-17 10:32 +# Title: Script that Converts Windows FILETIME to Epoch and Vice-versa +# Purpose: convert timestamps easily +# Package: laps4linux +# Usage: see PARSE PARAMETERS block +# Reference: +# formula from https://stackoverflow.com/questions/5471379/ways-to-convert-epoch-linux-time-to-windows-time/5471380#5471380 +# python2 stderr https://stackoverflow.com/questions/5574702/how-to-print-to-stderr-in-python +# Improve: +# Dependencies: +# python2 +import sys,argparse + +datetimepyverison="2018-10-17a" + +# DEFINE FUNCTIONS +def get_epochtime(filetime): + return int(( filetime - 116444736000000000 ) / 10000000) + +def get_filetime(epochtime): + return int(( epochtime * 10000000 ) + 116444736000000000) + +# DEFINE VARIABLES +action = get_epochtime + +# example filetime +#filetime = 131859734013606415 + +# PARSE PARAMETERS +parser = argparse.ArgumentParser(description="Convert FILETIME to epoch and vice-versa") +f_or_e = parser.add_mutually_exclusive_group() +f_or_e.add_argument("-e","--epoch",action='store_true',help='convert FILETIME to epoch. Default value.') +f_or_e.add_argument("-f","--filetime",action='store_true', help='convert epoch to FILETIME') +parser.add_argument("timestamp",action='store',help='number to convert',nargs='+') +args = parser.parse_args() + +if args.filetime: + action = get_filetime + +for timestamp in args.timestamp: + # for debugging + #sys.stderr.write("python timestamp:"+str(timestamp)+"\n") + print action(float(timestamp)) diff --git a/src/usr/share/laps/laps.sh b/src/usr/share/laps/laps.sh new file mode 100755 index 0000000..ef206c8 --- /dev/null +++ b/src/usr/share/laps/laps.sh @@ -0,0 +1,677 @@ +#!/bin/sh +# Filename: laps.sh +# Location: /usr/share/ +# Author: bgstack15@gmail.com +# Startdate: 2018-10-17 11:38:00 +# Title: Local Administrator Password Solution for Linux +# Purpose: LAPS Equivalent for GNU/Linux +# Package: laps +# History: +# Usage: +# Reference: ftemplate.sh 2018-09-12a; framework.sh 2018-09-12a +# Improve: +# Dependencies: +# bundled: dependencies/datetime.py +# framework.sh (bgscripts-core) +# kinit, klist (krb5-workstation) +# ldapsearch, ldapmodify (openldap-clients) +# passwd (passwd) +# logger (util-linux) +# pwmake (libpwquality) +# xargs (find-utils) +# cp (coreutils) +# sed (sed) +# awk (gawk) +fiversion="2018-09-12a" +lapsversion="2018-10-22a" + +usage() { + ${PAGER:-/usr/bin/less -F} >&2 <<ENDUSAGE +laps is the Local Administrator Password Solution for GNU/Linux. +usage: laps.sh [-duV] [-c conffile] [-t|-a] [-f] [-r [-u <username>] [-h <hostname>]] +version ${lapsversion} + -d debug Show debugging info, including parsed variables. + -u usage Show this usage block. + -V version Show script version number. + -c conf Read in this config file. Default is /etc/laps/laps.conf + -f force Skip the time check and just update the password regardless. + --noforce Do not force. Overrides environment variable LAPS_FORCE. + -t test Dry run only. Useful with debugging on. + -a apply Turn off dry run. Default. + -r read Read password; do not set it. Can only be used by a domain admin. Can only be used with -u. + -u user Connect with kerberos ticket for this user. Default is "machine" to use host keytab. + -h <hostname> Read this hostname instead of \$( hostname -s ) +Debug levels: + 0 Silent + 9 displays sensitive info, specifically the generated password +10 function trace +Environment variables: + See documentation at /usr/share/doc/laps/ for full explanation. +Return values: + 0 Normal + 1 Help or version info displayed + 2 Parameters or environment variables are invalid + 3 Incorrect OS type + 4 Unable to find dependency + 5 Not run as root or sudo + 6 Unable to get kerberos ticket + 7 Unable to set ldap attributes + 8 Unable to change password +ENDUSAGE +} + +# DEFINE FUNCTIONS + +debuglevoutput() { + # call: commandthatgeneratesstdout | debuglevoutput 8 + # output: output to standard error prepended with "debug8: " the contents of the pipe + ___dlo_threshold="${1}" + ___dlo_silent="${2}" + if debuglev "${___dlo_threshold}" ; + then + if test -n "${___dlo_silent}" ; + then + cat + else + sed -r -e "s/^/debug${___dlo_threshold}: /;" + fi + else + cat 1>/dev/null 2>&1 + fi +} + +read_workflow() { + + # 1. get user kerberos ticket + get_user_kerberos_ticket "${LAPS_KERBEROS_USER}" "${LAPS_USER_IS_ROOT}" "${LAPS_KRB5CC_TMPFILE}" "${LAPS_INTERACTIVE}" "${LAPS_KINIT_BIN}" "${LAPS_KLIST_BIN}" + + # 2. fetch and display host password + get_attrib_from_ldap "${LAPS_LDAPSEARCH_BIN}" "${LAPS_LDAPSEARCH_FLAGS}" "${LAPS_LDAPSEARCH_FILTER}" "${LAPS_ATTRIB_PW}" "${LAPS_LDAPCONF}" "${LAPS_KRB5CC_TMPFILE}" + +} + +main_workflow() { + + # 0. fail if not root + if test "${LAPS_USER_IS_ROOT}" = "0" ; + then + ferror "${scriptfile}: 5. To set password, run this command as root. Aborted." + exit 5 + fi + + # 1. kinit-host + get_host_keytab "${LAPS_KINIT_HOST_SCRIPT}" "${LAPS_KLIST_BIN}" "${LAPS_KRB5CC_TMPFILE}" || { ferror "${0}: unable to get host kerberos ticket. Aborted." ; exit 6 ; } + + # 2. fetch timestamp from ldap + LAPS_epoch="$( wrapper_get_timestamp_from_ldap "${LAPS_LDAPSEARCH_BIN}" "${LAPS_LDAPSEARCH_FLAGS}" "${LAPS_LDAPSEARCH_FILTER}" "${LAPS_ATTRIB_TIME}" "${LAPS_LDAPCONF}" "${LAPS_DATETIME_PY}" "${LAPS_KRB5CC_TMPFILE}" )" + + # 3. check timestamp to see if close to expiration + check_ts_against_expiration_threshold "${LAPS_THRESHOLD}" "${LAPS_epoch}" "${LAPS_FORCE}" + + # 4. generate new password and timestamp + LAPS_phrase="$( wrapper_genpw "${LAPS_PWGEN_SCRIPT}" "${LAPS_PWGEN_FLAGS}" )" + LAPS_timestamp="$( get_current_filetime "${LAPS_DATETIME_PY}" "${LAPS_TIMELIMIT}" )" + + # 5. update ldap + wrapper_update_ldap "${LAPS_LDAPSEARCH_BIN}" "${LAPS_LDAPSEARCH_FLAGS}" "${LAPS_LDAPSEARCH_FILTER}" "${LAPS_LDAPSEARCH_UNIQUE_ID}" "${LAPS_LDAPCONF}" "${LAPS_KRB5CC_TMPFILE}" "${LAPS_ATTRIB_PW}" "${LAPS_phrase}" "${LAPS_ATTRIB_TIME}" "${LAPS_timestamp}" "${LAPS_LDIF_TMPFILE}" "${LAPS_LDAPMODIFY_BIN}" "${LAPS_LDAPMODIFY_FLAGS}" "${LAPS_TEST}" + + # 6. if ^ was successful, change password for configured user + wrapper_change_password "${LAPS_phrase}" "${LAPS_USER}" "${LAPS_PASSWD_BIN}" "${LAPS_TEST}" + + # 7. tell syslog password was updated + wrapper_log "${LAPS_LOG_BIN}" "${LAPS_LOG_FLAGS}" "${LAPS_LOG_MSG}" "${LAPS_TEST}" + +} + +get_host_keytab() { + # call: get_host_keytab "${LAPS_KINIT_HOST_SCRIPT}" "${LAPS_KLIST_BIN}" "${LAPS_KRB5CC_TMPFILE}" + # returns: nothing. + # action: get host kerberos ticket-granting ticket + debuglev 10 && ferror "get_host_keytab $@" + ___ghk_kinit_host_script="${1}" + ___ghk_klist_bin="${2}" + ___ghk_krb5cc_tmpfile="${3}" + + test -z "${___ghk_kinit_host_script}" && ___ghk_kinit_host_script="${LAPS_KINIT_HOST_SCRIPT_DEFAULT}" + + if test -e "${___ghk_kinit_host_script}" ; + then + KRB5CCNAME=FILE:"${___ghk_krb5cc_tmpfile}" "${___ghk_kinit_host_script}" + else + debuglev 3 && ferror "debug3: Using built-in logic to fetch host kerberos ticket because unable to find LAPS_KINIT_HOST_SCRIPT=${___ghk_kinit_host_script}" + # do internal logic here + # find kinit + ___ghk_kinit_bin="$( find "${LAPS_KINIT_BIN}" /usr/bin/kinit /bin/kinit /usr/local/bin/kinit -print -quit 2>/dev/null | head -n1 )" + if ! test -e "${___ghk_kinit_bin}" ; + then + ferror "${scriptname}: 4 fatal! Unable to find kinit. Please use variable LAPS_KINIT_BIN. Aborted." + fi + # cannot use requested server name here. root@localhost can only use its own kerberos ticket. + "${___ghk_kinit_bin}" -k -c "${___ghk_krb5cc_tmpfile}" "$( hostname -s | tr '[[:lower:]]' '[[:upper:]]' )\$" | debuglevoutput 7 + fi + + # return true if klist returns true + "${___ghk_klist_bin}" -c "${___ghk_krb5cc_tmpfile}" | debuglevoutput 7 + +} + +get_attrib_from_ldap() { + # call: get_attrib_from_ldap "${LAPS_LDAPSEARCH_BIN}" "${LAPS_LDAPSEARCH_FLAGS}" "${LAPS_LDAPSEARCH_FILTER}" "${LAPS_ATTRIB_TIME}" "${LAPS_LDAPCONF}" "${LAPS_KRB5CC_TMPFILE}" + debuglev 10 && ferror "get_attrib_from_ldap $@" + ___gtfl_ldapsearch_bin="${1}" + ___gtfl_ldapsearch_flags="${2}" + ___gtfl_ldapsearch_filter="${3}" + ___gtfl_attrib="${4}" + ___gtfl_ldapconf="${5}" + ___gtfl_krb5cc_tmpfile="${6}" + + { + debuglev 8 && set -x + KRB5CCNAME="${___gtfl_krb5cc_tmpfile}" LDAPCONF="${___gtfl_ldapconf}" "${___gtfl_ldapsearch_bin}" ${___gtfl_ldapsearch_flags} "${___gtfl_ldapsearch_filter}" "${___gtfl_attrib}" 2>&1 | debuglevoutput 8 + set +x + } 1>&2 + ___gtfl_attrib="$( KRB5CCNAME="${___gtfl_krb5cc_tmpfile}" LDAPCONF="${___gtfl_ldapconf}" "${___gtfl_ldapsearch_bin}" ${___gtfl_ldapsearch_flags} "${___gtfl_ldapsearch_filter}" "${___gtfl_attrib}" 2>/dev/null | sed -r -e 's/^#.*$//;' -e '/^\s*$/d' | grep -iE -e "^${___gtfl_attrib}:" | awk '{print $2}' )" + # no value means either the ldap connection malfunctioned or there was no attribute by that name defined. + + echo "${___gtfl_attrib}" + +} + +wrapper_get_timestamp_from_ldap() { + # call: wrapper_get_timestamp_from_ldap "${LAPS_LDAPSEARCH_BIN}" "${LAPS_LDAPSEARCH_FLAGS}" "${LAPS_LDAPSEARCH_FILTER}" "${LAPS_ATTRIB_TIME}" "${LAPS_LDAPCONF}" "${LAPS_DATETIME_PY}" "${LAPS_KRB5CC_TMPFILE}" + debuglev 10 && ferror "$wrapper_get_timestamp_from_ldap $@" + ___wgtfl_ldapsearch_bin="${1}" + ___wgtfl_ldapsearch_flags="${2}" + ___wgtfl_ldapsearch_filter="${3}" + ___wgtfl_attrib="${4}" + ___wgtfl_ldapconf="${5}" + ___wgtfl_datetime_py="${6}" + ___wgtfl_krb5cc_tmpfile="${7}" + + ts_filetime="$( get_attrib_from_ldap "${___wgtfl_ldapsearch_bin}" "${___wgtfl_ldapsearch_flags}" "${___wgtfl_ldapsearch_filter}" "${___wgtfl_attrib}" "${___wgtfl_ldapconf}" "${___wgtfl_krb5cc_tmpfile}" )" + debuglev 3 && ferror "timestamp(FILETIME): ${ts_filetime}" + ts_epoch="$( "${___wgtfl_datetime_py}" -e "${ts_filetime}" )" + debuglev 2 && ferror "timestamp(epoch): ${ts_epoch}" + debuglev 1 && ferror "timestamp(UTC): $( date -u -d "@${ts_epoch}" "+%FT%TZ" )" + + echo "${ts_epoch}" +} + +check_ts_against_expiration_threshold() { + # call: check_ts_against_expiration_threshold "${LAPS_THRESHOLD}" "${LAPS_epoch}" "${LAPS_FORCE}" + debuglev 10 && ferror "check_ts_against_expiration_threshold $@" + ___ctaeh_threshold="${1}" + ___ctaeh_epoch="${2}" + ___ctaeh_force="${3}" + + ___ctaeh_thres="$( date -u -d "now+${___ctaeh_threshold}" "+%s" )" + + # if flag --force was used, just skip the check + if fistruthy "${___ctaeh_force}" + then + return + fi + + if ! fisnum "${___ctaeh_thres}" || ! fisnum "${___ctaeh_epoch}" ; + then + ferror "${scriptfile}: 4 fatal! cannot compare ${___ctaeh_thres} to ${___ctaeh_epoch}. Aborted." + exit 4 + fi + + if ! test ${___ctaeh_thres} -ge ${___ctaeh_epoch} ; + then + debuglev 1 && echo "${scriptfile}: No changes required." + exit 0 + fi +} + +genpw() { + # call: genpw "${LAPS_PWGEN_SCRIPT}" "${LAPS_PWGEN_FLAGS}" + debuglev 10 && ferror "genpw $@" + ___genpw_pwgen_script="${1}" + ___genpw_pwgen_flags="${2}" + + "${___genpw_pwgen_script}" ${___genpw_pwgen_flags} +} + +wrapper_genpw() { + # call: wrapper_genpw "${LAPS_PWGEN_SCRIPT}" "${LAPS_PWGEN_FLAGS}" + # output: full phrase on stdout + debuglev 10 && ferror "wrapper_genpw $@" + ___wg_pwgen_script="${1}" + ___wg_pwgen_flags="${2}" + + ___wg_phrase="$( genpw "${___wg_pwgen_script}" "${___wg_pwgen_flags}" )" + ___wg_masked="$( echo "${___wg_phrase}" | head -c 8 ; echo "${___wg_phrase}" | tail -c +9 | sed -r -e 's/./*/g;' )" + #echo "___wg_phrase=\"${___wg_phrase}\"" + #echo "___wg_masked=\"${___wg_masked}\"" + if debuglev 2 ; + then + if fistruthy "${NO_MASK}" ; + then + ferror "Using ${___wg_phrase}" + else + ferror "Using ${___wg_masked}" + fi + fi + + echo "${___wg_phrase}" + +} + +get_current_filetime() { + # call: get_current_filetime "${LAPS_DATETIME_PY}" "${LAPS_TIMELIMIT}" + # returns: FILETIME format of current timestamp on stdout + debuglev 10 && ferror "get_current_filetime $@" + ___gcf_datetime_py="${1}" + ___gcf_timelimit="${2}" + + ___gcf_timestamp="$( "${___gcf_datetime_py}" -f "$( date -u -d "now+${___gcf_timelimit}" "+%s" )" )" + + if ! fisnum "${___gcf_timestamp}" ; + then + ferror "${scriptfile}: 4 fatal! Could not generate valid timestamp. Aborted." + ferror "what was generated: \"${___gcf_timestamp}\"" + exit 4 + fi + debuglev 3 && ferror "new timestamp(FILETIME): ${___gcf_timestamp}" + + echo "${___gcf_timestamp}" +} + +wrapper_update_ldap() { + # call: wrapper_update_ldap "${LAPS_LDAPSEARCH_BIN}" "${LAPS_LDAPSEARCH_FLAGS}" "${LAPS_LDAPSEARCH_FILTER}" "${LAPS_LDAPSEARCH_UNIQUE_ID}" "${LAPS_LDAPCONF}" "${LAPS_KRB5CC_TMPFILE}" "${LAPS_ATTRIB_PW}" "${LAPS_phrase}" "${LAPS_ATTRIB_TIME}" "${LAPS_timestamp}" "${LAPS_LDIF_TMPFILE}" "${LAPS_LDAPMODIFY_BIN}" "${LAPS_LDAPMODIFY_FLAGS}" "${LAPS_TEST}" + debuglev 10 && ferror "wrapper_update_ldap $@" + ___wul_ldapsearch_bin="${1}" + ___wul_ldapsearch_flags="${2}" + ___wul_ldapsearch_filter="${3}" + ___wul_ldapsearch_unique_id="${4}" + ___wul_ldapconf="${5}" + ___wul_krb5cc_tmpfile="${6}" + ___wul_attrib_pw="${7}" + ___wul_phrase="${8}" + ___wul_attrib_time="${9}" + ___wul_timestamp="${10}" + ___wul_ldif_tmpfile="${11}" + ___wul_ldapmodify_bin="${12}" + ___wul_ldapmodify_flags="${13}" + ___wul_test="${14}" + + # learn dn + ___wul_dn="$( get_attrib_from_ldap "${___wul_ldapsearch_bin}" "${___wul_ldapsearch_flags}" "${___wul_ldapsearch_filter}" "${___wul_ldapsearch_unique_id}" "${___wul_ldapconf}" "${___wul_krb5cc_tmpfile}" )" + + # generate ldif + { + echo "${___wul_ldapsearch_unique_id}: ${___wul_dn}" + echo "changetype: modify" + echo "replace: ${___wul_attrib_pw}" + echo "${___wul_attrib_pw}: ${___wul_phrase}" + printf "%s\n" "-" + echo "replace: ${___wul_attrib_time}" + echo "${___wul_attrib_time}: ${___wul_timestamp}" + } > "${___wul_ldif_tmpfile}" + unset ___wul_ldapmodify_flag_verbose ; debuglev 9 && ___wul_ldapmodify_flag_verbose="-v" + + # add -n to this command if flag --test is used. + unset ___wul_ldapmodify_flag_test ; fistruthy "${___wul_test}" && ___wul_ldapmodify_flag_test="-n" + { + KRB5CCNAME="${___wul_krb5cc_tmpfile}" LDAPCONF="${___wul_ldapconf}" "${___wul_ldapmodify_bin}" ${___wul_ldapmodify_flags} "${___wul_ldif_tmpfile}" ${___wul_ldapmodify_flag_verbose} ${___wul_ldapmodify_flag_test} 2>&1 + echo "$?" > "${LAPS_LDAPMODIFY_STATUS_TMPFILE}" + }| sed -r -e '/^\s*$/d;' | debuglevoutput 1 silent + ___wul_ldap_success="$( cat "${LAPS_LDAPMODIFY_STATUS_TMPFILE}" )" + + case "${___wul_ldap_success}" in + 0) + # continue on + : + ;; + *) + ferror "${scriptfile}: 7 fatal! ldapmodify returned ${___wul_ldap_success}. Unhandled exception. Aborted." + exit 7 + ;; + esac + + return ${___wul_ldap_success} + +} + +wrapper_change_password() { + # call: wrapper_change_password "${LAPS_phrase}" "${LAPS_USER}" "${LAPS_PASSWD_BIN}" "${LAPS_TEST}" + debuglev 10 && ferror "wrapper_change_password $@" + ___wcp_phrase="${1}" + ___wcp_user="${2}" + ___wcp_passwd_bin="${3}" + ___wcp_test="${4}" + + if fistruthy "${___wcp_test}" ; + then + echo "0" > "${LAPS_PASSWORD_STATUS_TMPFILE}" + else + ___wcp_stdout="$( echo "${___wcp_phrase}" | "${___wcp_passwd_bin}" --stdin "${___wcp_user}" ; echo "$?" > "${LAPS_PASSWORD_STATUS_TMPFILE}" )" + fi + ___wcp_passwd_result="$( cat "${LAPS_PASSWORD_STATUS_TMPFILE}" )" + + case "${___wcp_passwd_result}" in + 0) + # successful operation + debuglev 4 && ferror "${___wcp_stdout}" + ;; + *) + # successful operation + ferror "${scriptfile}: 8 fatal! Unable to change password for ${___wcp_user}:\n${___wcp_stdout}" + exit 8 + ;; + esac +} + +wrapper_log() { + # call: wrapper_log "${LAPS_LOG_BIN}" "${LAPS_LOG_FLAGS}" "${LAPS_LOG_MSG}" "${LAPS_TEST}" + debuglev 10 && ferror "wrapper_log $@" + ___wl_log_bin="${1}" + ___wl_log_flags="${2}" + ___wl_log_msg="${3}" + ___wl_test="${4}" + + if ! fistruthy "${___wl_test}" ; + then + "${___wl_log_bin}" ${___wl_log_flags} "${___wl_log_msg}" + fi + +} + +get_user_kerberos_ticket() { + # call: get_user_kerberos_ticket "${LAPS_KERBEROS_USER}" "${LAPS_USER_IS_ROOT}" "${LAPS_KRB5CC_TMPFILE}" "${LAPS_INTERACTIVE}" "${LAPS_KINIT_BIN}" "${LAPS_KLIST_BIN}" + debuglev 10 && ferror "get_user_kerberos_ticket $@" + ___gukt_kerberos_user="${1}" + ___gukt_user_is_root="${2}" + ___gukt_krb5cc_tmpfile="${3}" + ___gukt_interactive="${4}" + ___gukt_kinit_bin="${5}" + ___gukt_klist_bin="${6}" + + # LAPS on the domain side does not permit a host keytab to read the password attribute, so if user=machine, fail out + # options: + # if root, using machine ticket. ACT: fail + # if root, using user ticket. ACT: check user tgt, then prompt. + # if user, using machine ticket. ACT: check user tgt, then prompt + # if user, using user ticket ACT: check user tgt, then prompt + + if test "${___gukt_kerberos_user}" = "machine" ; + then + if test "${___gukt_user_is_root}" = "1" ; + then + ferror "${scriptfile}: 2 fatal! To read the password stored in the domain, you need LDAP_KERBEROS_USER=<username> or -u <username> or run this script as a domain admin user. Aborted." + exit 2 + else + ___gukt_kerberos_user="${USER}" + ferror "Trying with logged in user ${___gukt_kerberos_user}." + fi + fi + + # Try current user kerberos ticket to see if has a tgt for LAPS_KERBEROS_USER + ___gukt_klist_stdout="$( "${___gukt_klist_bin}" 2>/dev/null )" + echo "${___gukt_klist_stdout}" | debuglevoutput 8 + ___gukt_klist_krb5cc="$( echo "${___gukt_klist_stdout}" | grep -iE 'ticket cache:' | awk -F':' '{print $NF}' | xargs )" + ___gukt_klist_user=$( echo "${___gukt_klist_stdout}" | grep -iE 'default principal:' | awk -F':' '{print $2}' | awk -F'@' '{print $1}' | xargs ) + ___gukt_klist_krbtgt="$( echo "${___gukt_klist_stdout}" | grep -E "krbtgt\/" )" + { + echo "klist_krb5cc=${___guktk_list_krb5cc}" + echo "klist_user=${___gukt_klist_user}" + echo "klist_krbtgt=${___gukt_klist_krbtgt}" + } | debuglevoutput 7 + + # if we already have a tgt + if test -n "${___gukt_klist_krbtgt}" ; + then + case "${___gukt_klist_user}" in + # and it is for the requested user + ${___gukt_kerberos_user}) + # copy it to our temporary location + debuglev 7 && ferror "Using existing krbtgt for requested user ${___gukt_kerberos_user}" + /bin/cp -p "${___gukt_klist_krb5cc}" "${___gukt_krb5cc_tmpfile}" + ;; + *) + ferror "Using existing krb5tgt for ${___gukt_klist_user} instead of requested ${___gukt_kerberos_user}" + ___gukt_kerberos_user="${___gukt_klist_user}" + ;; + esac + else + # need to get a ticket + # are we allowed to ormpt? + if fistruthy "${___gukt_interactive}" ; + then + # prompt and save to temp kerberos location + debuglev 1 && ferror "No krbtgt found. Prompting now..." + KRB5CCNAME="${___gukt_krb5cc_tmpfile}" "${___gukt_kinit_bin}" "${___gukt_kerberos_user}" + else + ferror "${scriptfile}: 2. Need LAPS_INTERACTIVE=1 or -i flag, to allow interactive kinit prompt. Aborted." + exit 2 + fi + fi + # verify that the tgt exists now + ___gukt_klist_stdout="$( KRB5CCNAME="${___gukt_krb5cc_tmpfile}" "${___gukt_klist_bin}" 2>/dev/null )" + echo "${___gukt_klist_stdout}" | debuglevoutput 4 + ___gukt_klist_krb5cc="$( echo "${___gukt_klist_stdout}" | grep -iE 'ticket cache:' | awk -F':' '{print $NF}' | xargs )" + ___gukt_klist_user=$( echo "${___gukt_klist_stdout}" | grep -iE 'default principal:' | awk -F':' '{print $2}' | awk -F'@' '{print $1}' | xargs ) + ___gukt_klist_krbtgt="$( echo "${___gukt_klist_stdout}" | grep -E "krbtgt\/" )" + { + echo "klist_krb5cc=${___gukt_klist_krb5cc}" + echo "klist_user=${___gukt_klist_user}" + echo "klist_krbtgt=${___gukt_klist_krbtgt}" + } | debuglevoutput 3 + + if test -z "${___gukt_klist_krbtgt}" ; + then + # no krbtgt so fail out + ferror "${scriptfile}: 6 fatal! Failed to get tgt for user ${___gkt_kerberos_user}. Check password or account. Aborted." + exit 6 + fi + +} + +# DEFINE TRAPS + +clean_laps() { + # use at end of entire script if you need to clean up tmpfiles + # rm -f "${tmpfile1}" "${tmpfile2}" 2>/dev/null + + # Delayed cleanup + if test -z "${LAPS_NO_CLEAN}" ; + then + nohup /bin/bash <<EOF 1>/dev/null 2>&1 & +sleep "${LAPS_CLEANUP_SEC:-300}" ; /bin/rm -r "${LAPS_TMPDIR:-NOTHINGTODELETE}" 1>/dev/null 2>&1 ; +EOF + fi +} + +CTRLC() { + # use with: trap "CTRLC" 2 + # useful for controlling the ctrl+c keystroke + : +} + +CTRLZ() { + # use with: trap "CTRLZ" 18 + # useful for controlling the ctrl+z keystroke + : +} + +parseFlag() { + flag="$1" + hasval=0 + case ${flag} in + # INSERT FLAGS HERE + "d" | "debug" | "DEBUG" | "dd" ) setdebug; ferror "debug level ${debug}" ; __debug_set_by_param=1;; + "usage" | "help" ) usage; exit 1;; + "V" | "fcheck" | "version" ) ferror "${scriptfile} version ${lapsversion}"; exit 1;; + #"i" | "infile" | "inputfile" ) getval; infile1=${tempval};; + "c" | "conf" | "conffile" | "config" ) getval; conffile="${tempval}";; + "t" | "test" | "dryrun" | "dry-run" ) LAPS_TEST=1 ; LAPS_ACTION="update" ;; + "a" | "apply" | "nodryrun" | "no-dryrun" | "no-dry-run" ) LAPS_TEST=0 ; LAPS_ACTION="update" ;; + "f" | "force" ) LAPS_FORCE=1 ; LAPS_ACTION="update" ;; + "noforce" | "notforce" | "not-force" | "no-force" ) LAPS_FORCE=0 ; LAPS_ACTION="update" ;; + "u" | "user" ) getval; LAPS_KERBEROS_USER="${tempval}" ;; + "r" | "read" | "readonly" | "read-only" ) LAPS_ACTION="read" ;; + "i" | "interactive" ) LAPS_INTERACTIVE=1 ;; + "ni" | "nointeractive" ) LAPS_INTERACTIVE=0 ;; + "h" | "host" | "hostname" | "server" ) getval; LAPS_HOST="${tempval}" ;; + esac + + debuglev 10 && { test ${hasval} -eq 1 && ferror "flag: ${flag} = ${tempval}" || ferror "flag: ${flag}"; } +} + +# DETERMINE LOCATION OF FRAMEWORK +f_needed=20180912 +while read flocation ; do if test -e ${flocation} ; then __thisfver="$( sh ${flocation} --fcheck 2>/dev/null )" ; if test ${__thisfver} -ge ${f_needed} ; then frameworkscript="${flocation}" ; break; else printf "Obsolete: %s %s\n" "${flocation}" "${__this_fver}" 1>&2 ; fi ; fi ; done <<EOFLOCATIONS +/usr/share/bgscripts/framework.sh +/usr/share/laps/dependencies/framework.sh +/usr/share/laps/framework.sh +EOFLOCATIONS +test -z "${frameworkscript}" && echo "$0: framework not found. Aborted." 1>&2 && exit 4 + +# INITIALIZE VARIABLES +# variables set in framework: +# today server thistty scriptdir scriptfile scripttrim +# is_cronjob stdin_piped stdout_piped stderr_piped sendsh sendopts +. ${frameworkscript} || echo "$0: framework did not run properly. Continuing..." 1>&2 +infile1= +outfile1= +logfile=${scriptdir}/${scripttrim}.${today}.out +define_if_new interestedparties "bgstack15@gmail.com" +# SIMPLECONF +define_if_new default_conffile "/etc/laps/laps.conf" +#define_if_new defuser_conffile ~/.config/laps/laps.conf + +# REACT TO OPERATING SYSTEM TYPE +case $( uname -s ) in + Linux) : ;; + FreeBSD) : ;; + *) echo "${scriptfile}: 3. Indeterminate OS: $( uname -s )" 1>&2 && exit 3;; +esac + +# REACT TO ROOT STATUS +LAPS_USER_IS_ROOT=0 +case ${is_root} in + 1) # proper root + LAPS_USER_IS_ROOT=1 ;; + sudo) # sudo to root + LAPS_USER_IS_ROOT=1 ;; + "") # not root at all + #ferror "${scriptfile}: 5. Please run as root or sudo. Aborted." + #exit 5 + : + ;; +esac + +# SET CUSTOM SCRIPT AND VALUES +#setval 1 sendsh sendopts<<EOFSENDSH # if $1="1" then setvalout="critical-fail" on failure +#/usr/local/share/bgscripts/send.sh -hs # setvalout maybe be "fail" otherwise +#/usr/share/bgscripts/send.sh -hs # on success, setvalout="valid-sendsh" +#/usr/local/bin/send.sh -hs +#/usr/bin/mail -s +#EOFSENDSH +#test "${setvalout}" = "critical-fail" && ferror "${scriptfile}: 4. mailer not found. Aborted." && exit 4 + +# VALIDATE PARAMETERS +# objects before the dash are options, which get filled with the optvals +# to debug flags, use option DEBUG. Variables set in framework: fallopts +validateparams - "$@" + +# LEARN EX_DEBUG +test -z "${__debug_set_by_param}" && fisnum "${LAPS_DEBUG}" && debug="${LAPS_DEBUG}" + +# CONFIRM TOTAL NUMBER OF FLAGLESSVALS IS CORRECT +#if test ${thiscount} -lt 2; +#then +# ferror "${scriptfile}: 2. Fewer than 2 flaglessvals. Aborted." +# exit 2 +#fi + +# LOAD CONFIG FROM SIMPLECONF +# This section follows a simple hierarchy of precedence, with first being used: +# 1. parameters and flags +# 2. environment +# 3. config file +# 4. default user config: ~/.config/script/script.conf +# 5. default config: /etc/script/script.conf +if test -f "${conffile}"; +then + get_conf "${conffile}" +else + if test "${conffile}" = "${default_conffile}" || test "${conffile}" = "${defuser_conffile}"; then :; else test -n "${conffile}" && ferror "${scriptfile}: Ignoring conf file which is not found: ${conffile}."; fi +fi +test -f "${defuser_conffile}" && get_conf "${defuser_conffile}" +test -f "${default_conffile}" && get_conf "${default_conffile}" + +# CONFIGURE VARIABLES AFTER PARAMETERS +test -z "${LAPS_TMPDIR}" && LAPS_TMPDIR="$( mktemp -d /tmp/laps.XXXXXXXXXX )" +test -z "${LAPS_KRB5CC_TMPFILE}" && LAPS_KRB5CC_TMPFILE="$( TMPDIR="${LAPS_TMPDIR}" mktemp )" +test -z "${LAPS_LDIF_TMPFILE}" && LAPS_LDIF_TMPFILE="$( TMPDIR="${LAPS_TMPDIR}" mktemp )" +test -z "${LAPS_LDAPMODIFY_STATUS_TMPFILE}" && LAPS_LDAPMODIFY_STATUS_TMPFILE="$( TMPDIR="${LAPS_TMPDIR}" mktemp )" +test -z "${LAPS_PASSWORD_STATUS_TMPFILE}" && LAPS_PASSWORD_STATUS_TMPFILE="$( TMPDIR="${LAPS_TMPDIR}" mktemp )" +define_if_new LAPS_KINIT_HOST_SCRIPT "/usr/share/bgscripts/work/kinit-host.sh" +define_if_new LAPS_KINIT_HOST_SCRIPT_DEFAULT "/usr/share/bgscripts/work/kinit-host.sh" +define_if_new LAPS_KLIST_BIN "/usr/bin/klist" +define_if_new LAPS_KINIT_BIN "/usr/bin/kinit" +define_if_new LAPS_LDAPSEARCH_BIN "/usr/bin/ldapsearch" +define_if_new LAPS_LDAPSEARCH_FLAGS "-LLL -O maxssf=0 -o ldif-wrap=300 -Y gssapi" +define_if_new LAPS_HOST "$( hostname -s )" +define_if_new LAPS_LDAPSEARCH_FILTER "(cn=${LAPS_HOST}*)" +define_if_new LAPS_LDAPSEARCH_UNIQUE_ID "dn" +define_if_new LAPS_ATTRIB_PW "ms-Mcs-AdmPwd" +define_if_new LAPS_ATTRIB_TIME "ms-Mcs-AdmPwdExpirationTime" +define_if_new LAPS_LDAPMODIFY_BIN "/usr/bin/ldapmodify" +define_if_new LAPS_LDAPMODIFY_FLAGS "-O maxssf=0 -Q -o ldif-wrap=300 -Y gssapi -f" +define_if_new LAPS_LDAPCONF "/etc/laps/lapsldap.conf" +define_if_new LAPS_DATETIME_PY "/usr/share/laps/dependencies/datetime.py" +define_if_new LAPS_THRESHOLD "5 days" +define_if_new LAPS_TIMELIMIT "45 days" +define_if_new LAPS_PWGEN_SCRIPT "/usr/bin/pwmake" +define_if_new LAPS_PWGEN_FLAGS "130" +define_if_new LAPS_USER "floot" +define_if_new LAPS_PASSWD_BIN "/bin/passwd" +define_if_new LAPS_LOG_BIN "/bin/logger" +define_if_new LAPS_LOG_FLAGS "-t laps -i -p authpriv.notice" +define_if_new LAPS_LOG_MSG "LAPS has updated the password for user ${LAPS_USER}" +define_if_new LAPS_TEST 0 +define_if_new LAPS_FORCE 0 +define_if_new LAPS_KERBEROS_USER "machine" +define_if_new LAPS_ACTION "update" +define_if_new LAPS_INTERACTIVE 0 + +## REACT TO BEING A CRONJOB +#if test ${is_cronjob} -eq 1; +#then +# : +#else +# : +#fi + +# SET TRAPS +#trap "CTRLC" 2 +#trap "CTRLZ" 18 +trap '__ec=$? ; clean_laps ; trap "" 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ; exit ${__ec} ;' 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 + +# DEBUG SIMPLECONF +debuglev 5 && { + ferror "Using values" + # used values: EX_(OPT1|OPT2|VERBOSE) + set | grep -iE "^LAPS_" 1>&2 +} + +# MAIN LOOP +#{ + echo "action ${LAPS_ACTION}" | debuglevoutput 1 + case "${LAPS_ACTION}" in + read) + read_workflow + ;; + update|*) + main_workflow + ;; + esac + +#} | tee -a ${logfile} + +# EMAIL LOGFILE +#${sendsh} ${sendopts} "${server} ${scriptfile} out" ${logfile} ${interestedparties} |