diff --git a/CHANGELOG.md b/CHANGELOG.md index e226e41..7a146b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased -... +* Added: macOS installation now creates a user to run the service as, optionally as an administrator. -## 3.1.0 - 2020-02-26 +## 3.1.1: 2020-07-31 + +* Fixed: systemd path to buildkite agent logs; needs to be a file, not a directory. + +## 3.1.0: 2020-02-26 Add support for shims/scripts wrapping the `buildkite-agent` binary, instead of directly calling the binary. This can be useful, for example, if you need to fetch your agent registration token from a secret store before running `buildkite-agent`. @@ -18,66 +22,66 @@ If not using a shim/script, no changes are needed. To use a shim instead of directly calling the `buildkite-agent` binary, point `buildkite_agent_executable` to your shim, and `buildkite_agent_start_command` to your shim's args. -- Added `buildkite_agent_executable` option. -- Added `buildkite_agent_start_command` option. +* Added `buildkite_agent_executable` option. +* Added `buildkite_agent_start_command` option. -## 3.0.0 - 2019-11-13 +## 3.0.0: 2019-11-13 Version bump for breaking change. This is the same as 2.2.1 with corrected meta/main.yml. -## 2.2.1 - 2019-11-12 +## 2.2.1: 2019-11-12 -- fixes to ansible 2.9.0 updates. +* fixes to ansible 2.9.0 updates. -## 2.2.0 - 2019-11-07 +## 2.2.0: 2019-11-07 -- porting to support ansible 2.9.0. +* porting to support ansible 2.9.0. -## 2.1.0 - 2019-08-20 +## 2.1.0: 2019-08-20 -- `buildkite_agent_username` option for configuring the name of the user to run the service as. -- `buildkite_agent_user_description` option for configuring the description of the user to run the service as. +* `buildkite_agent_username` option for configuring the name of the user to run the service as. +* `buildkite_agent_user_description` option for configuring the description of the user to run the service as. -## 2.0.0 - 2019-08-11 +## 2.0.0: 2019-08-11 -- require ansible `2.8.x` for `win_user_profile` support. -- take care of `win_nssm` deprecations within ansible 2.8.x. +* require ansible `2.8.x` for `win_user_profile` support. +* take care of `win_nssm` deprecations within ansible 2.8.x. -## 1.2.1 - 2019-04-25 +## 1.2.1: 2019-04-25 -- `buildkite_agent_nssm_exe` option. -- `buildkite_agent_tags_including_queue` option. +* `buildkite_agent_nssm_exe` option. +* `buildkite_agent_tags_including_queue` option. -## 1.2.0 - 2019-03-16 +## 1.2.0: 2019-03-16 ### Added -- `buildkite_agent_allow_service_startup` option. -- `buildkite_agent_expose_secrets` option. -- `buildkite_agent_tags_from_gcp_labels` option. -- `buildkite_agent_start_parameters` option for Debian and Windows. -- Debian `buildkite_agent_systemd_override_template` option. - - Related - stop using systemd _template_ unit file (because `buildkite_agent_start_parameters` and v3.6.0+ allow `--spawn` for multiple job-runners). +* `buildkite_agent_allow_service_startup` option. +* `buildkite_agent_expose_secrets` option. +* `buildkite_agent_tags_from_gcp_labels` option. +* `buildkite_agent_start_parameters` option for Debian and Windows. +* Debian `buildkite_agent_systemd_override_template` option. + * Related - stop using systemd _template_ unit file (because `buildkite_agent_start_parameters` and v3.6.0+ allow `--spawn` for multiple job-runners). -## 1.1.0 - 2018-12-11 +## 1.1.0: 2018-12-11 ### Added -- macOS support - #7 -- Windows support - #6 +* macOS support - #7 +* Windows support - #6 -## 1.0.0 - 2018-04-18 +## 1.0.0: 2018-04-18 ### Added -- Support for Buildkite Agent v3. -- Support for Ubuntu 16.04. +* Support for Buildkite Agent v3. +* Support for Ubuntu 16.04. ### Removed -- Remove support for Ubuntu 14.04. -- Remove support for Buildkite Agent v2. +* Remove support for Ubuntu 14.04. +* Remove support for Buildkite Agent v2. ## 0.1.0 -- Initial release. +* Initial release. diff --git a/README.md b/README.md index bf6c57b..54627c8 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Variable names below map to [the agent configuration documentation](https://buil - `buildkite_agent_bootstrap_script` - `buildkite_agent_git_clean_flags` - `buildkite_agent_git_clone_flags` +- `buildkite_agent_grant_admin` - If `true` make the `buildkite_agent_username` user be a member of the local `Administrators` group. You must assess your own security risk tradeoff with the necessity for build tools needing privileges. - `buildkite_agent_no_color` - `buildkite_agent_no_command_eval` - `buildkite_agent_no_plugins` @@ -62,7 +63,6 @@ Variable names below map to [the agent configuration documentation](https://buil - `buildkite_agent_nssm_exe` - the full path to nssm.exe in case it's not on `PATH`. - `buildkite_agent_nssm_version` - Which version of [NSSM] to use to manage the buildkite-agent process as a service. -- `buildkite_agent_windows_grant_admin` - If `True` make the `buildkite_agent_username` user be a member of the local `Administrators` group. You must assess your own security risk tradeoff with the necessity for Windows build tools needing privileges. #### Darwin diff --git a/defaults/main.yml b/defaults/main.yml index b78120b..f6f2938 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -87,7 +87,7 @@ buildkite_agent_systemd_override_template: buildkite-agent.override.conf.j2 buildkite_agent_nssm_exe: "C:/ProgramData/chocolatey/bin/nssm.exe" buildkite_agent_nssm_version: "2.24.101.20180116" buildkite_agent_platform: "amd64" -buildkite_agent_windows_grant_admin: False +buildkite_agent_grant_admin: False # Darwin options buildkite_agent_load_bash_profile: yes diff --git a/files/create-local-user.Darwin.sh b/files/create-local-user.Darwin.sh new file mode 100755 index 0000000..750ff83 --- /dev/null +++ b/files/create-local-user.Darwin.sh @@ -0,0 +1,188 @@ +#!/usr/bin/env bash +set -o errexit -o pipefail -o nounset +[[ -n "${SCRIPT_DEBUG:-}" ]] && set -o xtrace + +function log() { + echo "${@}" >&2 +} + +function die() { + log "${@}" + exit 1 +} + +# Print traceback of call stack, starting from the call location. +# An optional argument can specify how many additional stack frames to skip. +print_traceback() { + local skip=${1:-0} + local start=$((skip + 1)) + local end=${#BASH_SOURCE[@]} + local curr=0 + log "Traceback (most recent call first):" + for ((curr = start; curr < end; curr++)); do + local prev=$((curr - 1)) + local func="${FUNCNAME[$curr]}" + local file="${BASH_SOURCE[$curr]}" + local line="${BASH_LINENO[$prev]}" + log " at ${file}:${line} in ${func}()" + done +} + +_err_trap() { + local err=$? + local cmd="${BASH_COMMAND:-}" + # Disable echoing all commands as this makes the traceback really hard to follow + set +x + log "panic: uncaught error" + print_traceback 1 + log "${cmd} exited ${err}" +} +trap _err_trap ERR + +function array_contains() { + if [[ $1 =~ (^|[[:space:]])$2($|[[:space:]]) ]]; then + return 0 + fi + return 1 +} + +function has_user() { + local user_name="${1?:"1st arg must be user name"}" + log "has_user: ${user_name}" + if /usr/bin/dscl . -list /Users | grep "^${user_name}$"; then + log "Exists" + return 0 + fi + log "Does not exist" + return 1 +} + +function random_string() { + LC_CTYPE=C tr -dc 'a-zA-Z0-9' <"/dev/urandom" | + head -c 32 || log "random_string exit'd $?" +} + +function find_new_user_id() { + local highest_id + highest_id="$(/usr/bin/dscl . -list /Users UniqueID | awk '{print $2}' | sort -ug | tail -n 1 | bc)" + + echo $(( highest_id + 1 )) +} + +function user_home_dir() { + local user_name="${1?:"1st arg must be user's username"}" + + echo "/Users/${user_name}" +} + +function create_user() { + local user_name="${1?:"1st arg must be user's username"}" + local user_pass="${2?:"2nd arg must be user's password"}" + local user_type="${3?:"3rd arg must be user's type [standard, admin]"}" + log "Creating ${user_name} ..." + + local home_dir + home_dir="$(user_home_dir "${user_name}")" + + if [[ -d "${home_dir}" ]]; then + echo "Error: ${home_dir} folder exists already." + return 1 + fi + + log "Finding new user ID..." + local user_uid + user_uid="$(find_new_user_id "${user_name}")" + log "Using user ID ${user_uid} ." + + /usr/bin/dscl "." -create "/Users/${user_name}" + /usr/bin/dscl "." -create "/Users/${user_name}" RealName "${user_name}" + /usr/bin/dscl "." -create "/Users/${user_name}" UserShell "/bin/bash" + /usr/bin/dscl "." -create "/Users/${user_name}" UniqueID "${user_uid}" + /usr/bin/dscl "." -create "/Users/${user_name}" PrimaryGroupID "20" # '20' == 'staff' + /usr/bin/dscl "." -create "/Users/${user_name}" NFSHomeDirectory "${home_dir}" + + /usr/bin/dscl "." -passwd "/Users/${user_name}" "${user_pass}" + + # https://superuser.com/questions/20420/what-is-the-difference-between-the-default-groups-on-mac-os-x + /usr/bin/dscl "." -append /Groups/staff GroupMembership "${user_name}" + # https://stackoverflow.com/questions/1837889/authorize-a-non-admin-developer-in-xcode-mac-os + /usr/bin/dscl "." -append /Groups/_developer GroupMembership "${user_name}" + if [[ "${user_type}" == "admin" ]]; then + log "Making ${user_name} an admin..." + /usr/bin/dscl "." -append /Groups/admin GroupMembership "${user_name}" + fi + + # create home directory + /bin/cp -R "/System/Library/User Template/English.lproj" "${home_dir}" + /usr/sbin/chown -R "${user_name}:staff" "${home_dir}" +} + +function write_user_defaults() { + local user_name="${1?:"1st arg must be user's username"}" + log "Writing user defaults for ${user_name} ..." + + local home_dir + home_dir="$(user_home_dir "${user_name}")" + + # disable Apple ID, iCloud, Siri, etc + local PRODUCT_VERSION + PRODUCT_VERSION="$(sw_vers -productVersion)" + local PRODUCT_BUILD + PRODUCT_BUILD="$(/usr/bin/sw_vers -buildVersion)" + local SETUP_ASSISTANT + SETUP_ASSISTANT="${home_dir}/Library/Preferences/com.apple.SetupAssistant" + /usr/bin/defaults write "${SETUP_ASSISTANT}" DidSeeApplePaySetup -bool TRUE + /usr/bin/defaults write "${SETUP_ASSISTANT}" DidSeeAvatarSetup -bool TRUE + /usr/bin/defaults write "${SETUP_ASSISTANT}" DidSeeCloudDiagnostics -bool TRUE + /usr/bin/defaults write "${SETUP_ASSISTANT}" DidSeeCloudSetup -bool TRUE + /usr/bin/defaults write "${SETUP_ASSISTANT}" DidSeeSiriSetup -bool TRUE + /usr/bin/defaults write "${SETUP_ASSISTANT}" DidSeeSyncSetup -bool TRUE + /usr/bin/defaults write "${SETUP_ASSISTANT}" GestureMovieSeen none + /usr/bin/defaults write "${SETUP_ASSISTANT}" DidSeeSyncSetup2 -bool TRUE + /usr/bin/defaults write "${SETUP_ASSISTANT}" DidSeeTouchIDSetup -bool TRUE + /usr/bin/defaults write "${SETUP_ASSISTANT}" DidSeeiCloudLoginForStorageServices -bool TRUE + /usr/bin/defaults write "${SETUP_ASSISTANT}" LastSeenCloudProductVersion "${PRODUCT_VERSION}" + /usr/bin/defaults write "${SETUP_ASSISTANT}" LastSeenBuddyBuildVersion "${PRODUCT_BUILD}" + /usr/bin/defaults write "${SETUP_ASSISTANT}" DidSeePrivacy -bool TRUE + /usr/bin/defaults write "${SETUP_ASSISTANT}" DidSeeSiriSetup -bool TRUE + /usr/bin/defaults write "${SETUP_ASSISTANT}" DidSeeAppearanceSetup -bool TRUE + /usr/bin/defaults write "${SETUP_ASSISTANT}" DidSeeTrueTonePrivacy -bool TRUE + + /usr/sbin/chown "${user_name}" "${SETUP_ASSISTANT}.plist" +} + +function configure_auto_login() { + local user_name="${1?:"1st arg must be user's username"}" + local user_pass="${2?:"2nd arg must be user's password"}" + + log "Setting up automatic login for ${user_name} ..." + + local kc_password_path + kc_password_path="$(dirname "${BASH_SOURCE[0]}")/kcpassword" + "${kc_password_path}" "${user_pass}" + /bin/chmod 600 "/private/etc/kcpassword" + /usr/bin/defaults write /Library/Preferences/com.apple.loginwindow autoLoginUser "${user_name}" + /bin/chmod 644 "/Library/Preferences/com.apple.loginwindow.plist" +} + +user_name="${1?:"1st arg must be user's name"}" +user_type="${2:-"standard"}" +auto_login="${3:-"true"}" +user_pass="${USER_PASSWORD:-"$(random_string)"}" + +if has_user "${user_name}"; then + die "Exiting: User exists ${user_name}." +fi + +home_dir="$(user_home_dir "${user_name}")" +if [[ -d "${home_dir}" ]]; then + die "Exiting: Home directory ${home_dir} already existed." +fi + +create_user "${user_name}" "${user_pass}" "${user_type}" +write_user_defaults "${user_name}" +if [[ "${auto_login}" == "true" ]]; then + configure_auto_login "${user_name}" "${user_pass}" +else + log "Skip: auto_login was ${auto_login}. If you want the user to be automatically logged in, choose 'true'." +fi diff --git a/files/kcpassword b/files/kcpassword new file mode 100755 index 0000000..2f77699 --- /dev/null +++ b/files/kcpassword @@ -0,0 +1,39 @@ +#!/usr/bin/env python + +# https://github.com/xfreebird/kcpassword/blob/master/kcpassword + +# Port of Gavin Brock's Perl kcpassword generator to Python, by Tom Taylor +# . +# Perl version: http://www.brock-family.org/gavin/perl/kcpassword.html +# https://github.com/timsutton/osx-vm-templates/blob/master/scripts/support/set_kcpassword.py + +import sys +import os + +def kcpassword(passwd): + # The magic 11 bytes - these are just repeated + # 0x7D 0x89 0x52 0x23 0xD2 0xBC 0xDD 0xEA 0xA3 0xB9 0x1F + key = [125,137,82,35,210,188,221,234,163,185,31] + key_len = len(key) + + passwd = [ord(x) for x in list(passwd)] + # pad passwd length out to an even multiple of key length + r = len(passwd) % key_len + if (r > 0): + passwd = passwd + [0] * (key_len - r) + + for n in range(0, len(passwd), len(key)): + ki = 0 + for j in range(n, min(n+len(key), len(passwd))): + passwd[j] = passwd[j] ^ key[ki] + ki += 1 + + passwd = [chr(x) for x in passwd] + return "".join(passwd) + +if __name__ == "__main__": + passwd = kcpassword(sys.argv[1]) + fd = os.open('/etc/kcpassword', os.O_WRONLY | os.O_CREAT, 0o600) + file = os.fdopen(fd, 'w') + file.write(passwd) + file.close() diff --git a/tasks/install-on-Darwin.yml b/tasks/install-on-Darwin.yml index ddc1331..4eb03dd 100644 --- a/tasks/install-on-Darwin.yml +++ b/tasks/install-on-Darwin.yml @@ -13,6 +13,33 @@ state: latest update_homebrew: yes +- name: Create user + block: + - name: make password for user + set_fact: + buildkite_agent_user_password: "{{ lookup('password', '/tmp/bk-agent-password length=32 chars=ascii_letters,digits,punctuation') }}" + no_log: "{{ buildkite_agent_hide_secrets | default(true) }}" + + - name: Write scripts needed to make user + copy: + src: "{{ item }}" + dest: "/usr/local/bin/{{ item }}" + mode: 0755 + with_items: + - "create-local-user.{{ ansible_os_family }}.sh" + - "kcpassword" + + - name: Create user via script + shell: >- + /usr/local/bin/create-local-user.{{ ansible_os_family }}.sh + '{{ agent_username }}' + '{{ buildkite_agent_grant_admin | ternary("true", "false") }}' + args: + creates: "/Users/{{ agent_username }}" + become: true + environment: + USER_PASSWORD: "{{ buildkite_agent_user_password }}" + - name: Configure Buildkite template: src: "buildkite-agent.cfg.j2" diff --git a/tasks/install-on-Windows.yml b/tasks/install-on-Windows.yml index ff0f9b0..71299a7 100644 --- a/tasks/install-on-Windows.yml +++ b/tasks/install-on-Windows.yml @@ -1,7 +1,7 @@ --- - name: make password for user set_fact: - user_password: "{{ lookup('password', '/tmp/bk-agent-password length=32 chars=ascii_letters,digits,punctuation') }}" + buildkite_agent_user_password: "{{ lookup('password', '/tmp/bk-agent-password length=32 chars=ascii_letters,digits,punctuation') }}" no_log: "{{ buildkite_agent_hide_secrets | default(true) }}" - name: make the user @@ -9,7 +9,7 @@ description: "{{ buildkite_agent_user_description }}" name: "{{ buildkite_agent_username }}" # TODO: Single static file with no isolation between runs is unsafe (but that's unlikely to occur) - password: "{{ user_password }}" + password: "{{ buildkite_agent_user_password }}" password_never_expires: yes user_cannot_change_password: yes register: buildkite_agent_user @@ -33,7 +33,7 @@ name: Administrators members: - "{{ buildkite_agent_username }}" - state: "{{ 'present' if buildkite_agent_windows_grant_admin else 'absent' }}" + state: "{{ 'present' if buildkite_agent_grant_admin else 'absent' }}" notify: - restart-windows-buildkite @@ -109,5 +109,5 @@ win_service: name: buildkite-agent username: "{{ buildkite_agent_username }}" - password: "{{ user_password }}" + password: "{{ buildkite_agent_user_password }}" state: "{{ 'started' if buildkite_agent_allow_service_startup[ansible_os_family] else 'stopped' }}"