#!/bin/bash # Copyright 2014,2015,2016,2017,2018,2019,2020,2021 Security Onion Solutions, LLC # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . source $(dirname $0)/so-common if [[ $# -lt 1 || $# -gt 2 ]]; then echo "Usage: $0 [email]" echo "" echo " list: Lists all user email addresses currently defined in the identity system" echo " add: Adds a new user to the identity system; requires 'email' parameter" echo " update: Updates a user's password; requires 'email' parameter" echo " enable: Enables a user; requires 'email' parameter" echo " disable: Disables a user; requires 'email' parameter" echo " validate: Validates that the given email address and password are acceptable; requires 'email' parameter" echo " valemail: Validates that the given email address is acceptable; requires 'email' parameter" echo " valpass: Validates that a password is acceptable" echo "" echo " Note that the password can be piped into STDIN to avoid prompting for it" exit 1 fi operation=$1 email=$2 kratosUrl=${KRATOS_URL:-http://127.0.0.1:4434} databasePath=${KRATOS_DB_PATH:-/opt/so/conf/kratos/db/db.sqlite} bcryptRounds=${BCRYPT_ROUNDS:-12} elasticUsersFile=${ELASTIC_USERS_FILE:-/opt/so/saltstack/local/salt/elasticsearch/files/users} elasticRolesFile=${ELASTIC_ROLES_FILE:-/opt/so/saltstack/local/salt/elasticsearch/files/users_roles} esUID=${ELASTIC_UID:-930} esGID=${ELASTIC_GID:-930} function lock() { # Obtain file descriptor lock exec 99>/var/tmp/so-user.lock || fail "Unable to create lock descriptor; if the system was not shutdown gracefully you may need to remove /var/tmp/so-user.lock manually." flock -w 10 99 || fail "Another process is using so-user; if the system was not shutdown gracefully you may need to remove /var/tmp/so-user.lock manually." trap 'rm -f /var/tmp/so-user.lock' EXIT } function fail() { msg=$1 echo "$1" exit 1 } function require() { cmd=$1 which "$1" 2>&1 > /dev/null [[ $? != 0 ]] && fail "This script requires the following command be installed: ${cmd}" } # Verify this environment is capable of running this script function verifyEnvironment() { require "htpasswd" require "jq" require "curl" require "openssl" require "sqlite3" [[ ! -f $databasePath ]] && fail "Unable to find database file; specify path via KRATOS_DB_PATH environment variable" response=$(curl -Ss -L ${kratosUrl}/) [[ "$response" != "404 page not found" ]] && fail "Unable to communicate with Kratos; specify URL via KRATOS_URL environment variable" } function findIdByEmail() { email=$1 response=$(curl -Ss -L ${kratosUrl}/identities) identityId=$(echo "${response}" | jq ".[] | select(.verifiable_addresses[0].value == \"$email\") | .id") echo $identityId } function validatePassword() { password=$1 len=$(expr length "$password") if [[ $len -lt 6 ]]; then echo "Password does not meet the minimum requirements" exit 2 fi } function validateEmail() { email=$1 # (?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\]) if [[ ! "$email" =~ ^[[:alnum:]._%+-]+@[[:alnum:].-]+\.[[:alpha:]]{2,}$ ]]; then echo "Email address is invalid" exit 3 fi } function hashPassword() { password=$1 passwordHash=$(echo "${password}" | htpasswd -niBC $bcryptRounds SOUSER) passwordHash=$(echo "$passwordHash" | cut -c 11-) passwordHash="\$2a${passwordHash}" # still waiting for https://github.com/elastic/elasticsearch/issues/51132 echo "$passwordHash" } function updatePassword() { identityId=$1 if [ -z "$password" ]; then # Read password from stdin (show prompt only if no stdin was piped in) test -t 0 if [[ $? == 0 ]]; then echo "Enter new password:" fi read -rs password validatePassword "$password" fi if [[ -n $identityId ]]; then # Generate password hash passwordHash=$(hashPassword "$password") # Update DB with new hash echo "update identity_credentials set config=CAST('{\"hashed_password\":\"$passwordHash\"}' as BLOB) where identity_id=${identityId};" | sqlite3 "$databasePath" [[ $? != 0 ]] && fail "Unable to update password" fi } function createElasticFile() { filename=$1 tmpFile=${filename} truncate -s 0 "$tmpFile" chmod 600 "$tmpFile" chown "${esUID}:${esGID}" "$tmpFile" } function syncElasticSystemUser() { json=$1 userid=$2 usersFile=$3 user=$(echo "$json" | jq -r ".local.users.$userid.user") pass=$(echo "$json" | jq -r ".local.users.$userid.pass") [[ -z "$user" || -z "$pass" ]] && fail "Elastic auth credentials for system user '$userid' are missing" hash=$(hashPassword "$pass") echo "${user}:${hash}" >> "$usersFile" } function syncElasticSystemRole() { json=$1 userid=$2 role=$3 rolesFile=$4 user=$(echo "$json" | jq -r ".local.users.$userid.user") [[ -z "$user" ]] && fail "Elastic auth credentials for system user '$userid' are missing" echo "${role}:${user}" >> "$rolesFile" } function syncElastic() { echo "Syncing users between SOC and Elastic..." usersTmpFile="${elasticUsersFile}.tmp" rolesTmpFile="${elasticRolesFile}.tmp" createElasticFile "${usersTmpFile}" createElasticFile "${rolesTmpFile}" authPillarJson=$(lookup_salt_value "auth" "elasticsearch" "pillar" "json") syncElasticSystemUser "$authPillarJson" "so_elastic_user" "$usersTmpFile" syncElasticSystemRole "$authPillarJson" "so_elastic_user" "superuser" "$rolesTmpFile" syncElasticSystemUser "$authPillarJson" "so_kibana_user" "$usersTmpFile" syncElasticSystemRole "$authPillarJson" "so_kibana_user" "superuser" "$rolesTmpFile" syncElasticSystemUser "$authPillarJson" "so_logstash_user" "$usersTmpFile" syncElasticSystemRole "$authPillarJson" "so_logstash_user" "superuser" "$rolesTmpFile" syncElasticSystemUser "$authPillarJson" "so_beats_user" "$usersTmpFile" syncElasticSystemRole "$authPillarJson" "so_beats_user" "superuser" "$rolesTmpFile" syncElasticSystemUser "$authPillarJson" "so_monitor_user" "$usersTmpFile" syncElasticSystemRole "$authPillarJson" "so_monitor_user" "remote_monitoring_collector" "$rolesTmpFile" syncElasticSystemRole "$authPillarJson" "so_monitor_user" "remote_monitoring_agent" "$rolesTmpFile" syncElasticSystemRole "$authPillarJson" "so_monitor_user" "monitoring_user" "$rolesTmpFile" if [[ -f "$databasePath" ]]; then # Generate the new users file echo "select '{\"user\":\"' || ici.identifier || '\", \"data\":' || ic.config || '}'" \ "from identity_credential_identifiers ici, identity_credentials ic " \ "where ici.identity_credential_id=ic.id and instr(ic.config, 'hashed_password') " \ "order by ici.identifier;" | \ sqlite3 "$databasePath" | \ jq -r '.user + ":" + .data.hashed_password' \ >> "$usersTmpFile" [[ $? != 0 ]] && fail "Unable to read credential hashes from database" # Generate the new users_roles file echo "select 'superuser:' || ici.identifier " \ "from identity_credential_identifiers ici, identity_credentials ic " \ "where ici.identity_credential_id=ic.id and instr(ic.config, 'hashed_password') " \ "order by ici.identifier;" | \ sqlite3 "$databasePath" \ >> "$rolesTmpFile" [[ $? != 0 ]] && fail "Unable to read credential IDs from database" else echo "Database file does not exist yet, skipping users export" fi if [[ -s "${usersTmpFile}" ]]; then mv "${usersTmpFile}" "${elasticUsersFile}" mv "${rolesTmpFile}" "${elasticRolesFile}" if [[ -z "$SKIP_STATE_APPLY" ]]; then echo "Elastic state will be re-applied to affected minions. This may take several minutes..." echo "Applying elastic state to elastic minions at $(date)" >> /opt/so/log/soc/sync.log 2>&1 salt -C 'G@role:so-standalone or G@role:so-eval or G@role:so-import or G@role:so-manager or G@role:so-managersearch or G@role:so-node or G@role:so-heavynode' state.apply elasticsearch queue=True >> /opt/so/log/soc/sync.log 2>&1 fi else echo "Newly generated users/roles files are incomplete; aborting." fi } function syncAll() { if [[ -z "$FORCE_SYNC" && -f "$databasePath" && -f "$elasticUsersFile" ]]; then usersFileAgeSecs=$(echo $(($(date +%s) - $(date +%s -r "$elasticUsersFile")))) staleCount=$(echo "select count(*) from identity_credentials where updated_at >= Datetime('now', '-${usersFileAgeSecs} seconds');" \ | sqlite3 "$databasePath") if [[ "$staleCount" == "0" ]]; then return 1 fi fi syncElastic return 0 } function listUsers() { response=$(curl -Ss -L ${kratosUrl}/identities) [[ $? != 0 ]] && fail "Unable to communicate with Kratos" echo "${response}" | jq -r ".[] | .verifiable_addresses[0].value" | sort } function createUser() { email=$1 now=$(date -u +%FT%TZ) addUserJson=$(cat <