Files
securityonion/salt/manager/tools/sbin/so-minion
T
Josh Patterson a9f7ffc3fe FIX: validate MAINIP without so-common during setup
setup runs so-minion -o=setup before /usr/sbin/so-common is installed, so
valid_ip4 was undefined, every MAINIP was rejected, and no minion pillar
was written. Pillar compile then failed for the new manager and setup gave
up waiting for the salt master. Use an inline IPv4 regex instead.
2026-09-15 13:10:33 -04:00

1258 lines
36 KiB
Bash
Executable File

#!/bin/bash
# Copyright Security Onion Solutions LLC and/or licensed to Security Onion Solutions LLC under one
# or more contributor license agreements. Licensed under the Elastic License 2.0 as shown at
# https://securityonion.net/license; you may not use this file except in compliance with the
# Elastic License 2.0.
#
# Note: Per the Elastic License 2.0, the second limitation states:
#
# "You may not move, change, disable, or circumvent the license key functionality
# in the software, and you may not remove or obscure any functionality in the
# software that is protected by the license key."
# Setup logging
LOG_FILE="/opt/so/log/so-minion.log"
LOG_DIR=$(dirname "$LOG_FILE")
# Create log directory if it doesn't exist
if [ ! -d "$LOG_DIR" ]; then
mkdir -p "$LOG_DIR"
fi
# Logging function
log() {
local level=$1
shift
local message="$*"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] [$level] $message" >> "$LOG_FILE"
}
if [ -f /usr/sbin/so-common ]; then
. /usr/sbin/so-common
fi
if [ -f /usr/sbin/so-elastic-fleet-common ]; then
. /usr/sbin/so-elastic-fleet-common
fi
function usage() {
local usage_text="Usage: $0 -o=<operation> -m=[id]
where <operation> is one of the following:
add: Accepts a new key and adds the minion files
delete: Removes the key and deletes the minion files
list: Lists all keys with hashes
reject: Rejects a key
restart: Restart a minion (reboot)
test: Perform minion test
"
echo "$usage_text"
log "ERROR" "Invalid usage"
return 1
}
if [[ $# -lt 1 ]]; then
usage
exit 1
fi
for i in "$@"; do
case $i in
-o=*|--operation=*)
OPERATION="${i#*=}"
shift
;;
-m=*|--minionid=*)
MINION_ID="${i#*=}"
shift
;;
# The following args are used internally during setup, not to be specified manually.
-e=*|--esheap=*)
ES_HEAP_SIZE="${i#*=}"
shift
;;
-L=*|--lshostname=*)
LSHOSTNAME="${i#*=}"
shift
;;
-l=*|--lsheap=*)
LSHEAP="${i#*=}"
shift
;;
-n=*|--mgmtnic=*)
MNIC="${i#*=}"
shift
;;
-d=*|--description=*)
NODE_DESCRIPTION="${i#*=}"
shift
;;
-a=*|--monitor=*)
INTERFACE="${i#*=}"
shift
;;
-i=*|--ip=*)
MAINIP="${i#*=}"
shift
;;
# Usable / Load Balance Cores for Zeek / Suricata
-C=*|--lbc=*)
CORECOUNT="${i#*=}"
shift
;;
# Total number of CPU Cores
-c=*|--cpu=*)
CPUCORES="${i#*=}"
shift
;;
-*|--*)
echo "Unknown option $i"
log "ERROR" "Unknown option $i"
exit 1
;;
*)
usage
exit 1
;;
esac
done
if [[ -n "$MINION_ID" && ! "$MINION_ID" =~ ^[A-Za-z0-9._-]{1,253}$ ]]; then
echo "Invalid minion id: $MINION_ID"
log "ERROR" "Invalid minion id: $MINION_ID"
exit 1
fi
readonly PILLARFILE=/opt/so/saltstack/local/pillar/minions/$MINION_ID.sls
readonly ADVPILLARFILE=/opt/so/saltstack/local/pillar/minions/adv_$MINION_ID.sls
function getinstallinfo() {
log "INFO" "Getting install info for minion $MINION_ID"
# Pull from file
INSTALLVARS=$(sudo salt "$MINION_ID" cp.get_file_str /opt/so/install.txt --out=newline_values_only)
if [ $? -ne 0 ]; then
log "ERROR" "Failed to get install info from $MINION_ID"
return 1
fi
# install.txt is controlled by the minion; only accept known keys and never eval or export them
local line key
while IFS= read -r line; do
[[ "$line" == *=* ]] || continue
key=${line%%=*}
case "$key" in
MAINIP|MNIC|NODE_DESCRIPTION|ES_HEAP_SIZE|PATCHSCHEDULENAME|INTERFACE|NODETYPE|CORECOUNT|LSHOSTNAME|LSHEAP|CPUCORES|IDH_MGTRESTRICT|IDH_SERVICES)
printf -v "$key" '%s' "${line#*=}"
;;
*)
log "WARN" "Ignoring unexpected install var from $MINION_ID: ${key:0:64}"
;;
esac
done <<< "$INSTALLVARS"
if [[ "$NODE_DESCRIPTION" == \'*\' ]]; then
NODE_DESCRIPTION=${NODE_DESCRIPTION:1:-1}
fi
log "INFO" "Fetched install info for $MINION_ID (node type: ${NODETYPE:-unset})"
}
function pcapspace() {
log "INFO" "Calculating PCAP space for minion $MINION_ID"
if [[ "$OPERATION" == "setup" ]]; then
# Use 25% for PCAP
PCAP_PERCENTAGE=1
DFREEPERCENT=21
local SPACESIZE=$(df -k /nsm | tail -1 | awk '{print $2}' | tr -d \n)
if [ $? -ne 0 ]; then
log "ERROR" "Failed to get NSM partition size"
return 1
fi
else
local NSMSIZE=$(salt "$MINION_ID" disk.usage --out=json | jq -r '.[]."/nsm"."1K-blocks" ')
if [ $? -ne 0 ]; then
log "ERROR" "Failed to get NSM disk usage from minion"
return 1
fi
local ROOTSIZE=$(salt "$MINION_ID" disk.usage --out=json | jq -r '.[]."/"."1K-blocks" ')
if [ $? -ne 0 ]; then
log "ERROR" "Failed to get root disk usage from minion"
return 1
fi
if [[ "$NSMSIZE" == "null" ]]; then
# Looks like there is no dedicated nsm partition. Using root
local SPACESIZE=$ROOTSIZE
log "INFO" "No dedicated NSM partition found, using root partition"
else
local SPACESIZE=$NSMSIZE
fi
fi
# Must be checked before arithmetic expansion, which evaluates array subscripts
if [[ ! "$SPACESIZE" =~ ^[0-9]+$ ]]; then
log "ERROR" "Invalid disk size for $MINION_ID: ${SPACESIZE:0:64}"
return 1
fi
local s=$(( $SPACESIZE / 1000000 ))
local s1=$(( $s / 4 * $PCAP_PERCENTAGE ))
MAX_PCAP_SPACE=$s1
}
function testMinion() {
log "INFO" "Testing minion $MINION_ID"
so-test
result=$?
if [ $result -ne 0 ]; then
log "ERROR" "Local so-test failed with exit code $result"
return 1
fi
local_id=$(lookup_grain id)
if [[ ! "$local_id" =~ "${MINION_ID}_" && "$local_id" != "${MINION_ID}" ]]; then
salt "$MINION_ID" cmd.run 'so-test'
result=$?
if [ $result -ne 0 ]; then
log "ERROR" "Remote so-test failed on $MINION_ID with exit code $result"
return 1
fi
fi
}
function restartMinion() {
log "INFO" "Restarting minion $MINION_ID"
salt "$MINION_ID" system.reboot --async
result=$?
if [ $result -ne 0 ]; then
log "ERROR" "Failed to restart minion $MINION_ID (exit code: $result)"
return 1
fi
}
function listMinions() {
salt-key list -F --out=json
result=$?
if [ $result -ne 0 ]; then
log "ERROR" "Failed to list minion keys (exit code: $result)"
return 1
fi
}
function rejectMinion() {
log "INFO" "Rejecting minion $MINION_ID"
salt-key -y -r $MINION_ID
result=$?
if [ $result -ne 0 ]; then
log "ERROR" "Failed to reject minion $MINION_ID (exit code: $result)"
return 1
fi
}
function acceptminion() {
log "INFO" "Accepting minion $MINION_ID"
if [[ "$MINION_ID" == *"_hypervisor" ]]; then
FEATURES=$(/usr/sbin/so-yaml.py get /opt/so/saltstack/local/pillar/soc/license.sls features)
if [[ $? -ne 0 || ! "$FEATURES" =~ "vrt" ]]; then
error_msg="Hypervisor nodes are a feature supported only for customers with a valid license.\n Contact Security Onion Solutions, LLC via our website at https://securityonionsolutions.com\n for more information about purchasing a license to enable this feature."
log "ERROR" "$error_msg"
echo -e "Error: $error_msg"
return 1
fi
fi
salt-key -y -a $MINION_ID
result=$?
if [ $result -ne 0 ]; then
log "ERROR" "Failed to accept minion $MINION_ID (exit code: $result)"
return 1
fi
}
function deleteMinion() {
log "INFO" "Deleting minion $MINION_ID"
salt-key -y -d $MINION_ID
result=$?
if [ $result -ne 0 ]; then
log "ERROR" "Failed to delete minion $MINION_ID (exit code: $result)"
return 1
fi
}
function deleteMinionFiles () {
rm -f $PILLARFILE
if [ $? -ne 0 ]; then
log "ERROR" "Failed to delete $PILLARFILE"
return 1
fi
rm -f $ADVPILLARFILE
if [ $? -ne 0 ]; then
log "ERROR" "Failed to delete $ADVPILLARFILE"
return 1
fi
}
# Remove this minion's postgres Telegraf credential from the shared creds
# pillar and drop the matching role in Postgres. Always returns 0 so a dead
# or unreachable so-postgres doesn't block minion deletion — in that case we
# log a warning and leave the role behind for manual cleanup.
function remove_postgres_telegraf_from_minion() {
local MINION_SAFE
MINION_SAFE=$(echo "$MINION_ID" | tr '.-' '__' | tr '[:upper:]' '[:lower:]')
local PG_USER="so_telegraf_${MINION_SAFE}"
log "INFO" "Removing postgres telegraf cred for $MINION_ID"
so-telegraf-cred remove "$MINION_ID" >/dev/null 2>&1 || true
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^so-postgres$'; then
if ! docker exec -i so-postgres psql -v ON_ERROR_STOP=1 -U postgres -d so_telegraf >/dev/null 2>&1 <<EOSQL
DO \$\$
BEGIN
IF EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = '$PG_USER') THEN
EXECUTE format('REASSIGN OWNED BY %I TO so_telegraf', '$PG_USER');
EXECUTE format('DROP OWNED BY %I', '$PG_USER');
EXECUTE format('DROP ROLE %I', '$PG_USER');
END IF;
END
\$\$;
EOSQL
then
log "WARN" "Failed to drop postgres role $PG_USER; pillar entry was removed — drop manually if the role persists"
fi
else
log "WARN" "so-postgres container is not running; skipping DB role cleanup for $PG_USER"
fi
}
# Create the minion file
function ensure_socore_ownership() {
log "INFO" "Setting socore ownership on minion files"
chown -R socore:socore /opt/so/saltstack/local/pillar/minions/
if [ $? -ne 0 ]; then
log "ERROR" "Failed to set socore ownership on minion files"
return 1
fi
}
function create_minion_files() {
mkdir -p /opt/so/saltstack/local/pillar/minions
if [ $? -ne 0 ]; then
log "ERROR" "Failed to create minions directory"
return 1
fi
touch $ADVPILLARFILE
if [ $? -ne 0 ]; then
log "ERROR" "Failed to create advanced pillar file"
return 1
fi
if [ -f "$PILLARFILE" ]; then
rm $PILLARFILE
if [ $? -ne 0 ]; then
log "ERROR" "Failed to remove existing pillar file"
return 1
fi
fi
# Set proper ownership after file creation
ensure_socore_ownership || return 1
}
# Add Elastic settings to the minion file
function add_elasticsearch_to_minion() {
printf '%s\n'\
"elasticsearch:"\
" enabled: True"\
" esheap: '$ES_HEAP_SIZE'"\
" " >> $PILLARFILE
if [ $? -ne 0 ]; then
log "ERROR" "Failed to add elasticsearch configuration to $PILLARFILE"
return 1
fi
}
# Add Elastic Agent settings to the minion file
function add_elastic_agent_to_minion() {
printf '%s\n'\
"elasticagent:"\
" enabled: True"\
" " >> $PILLARFILE
if [ $? -ne 0 ]; then
log "ERROR" "Failed to add elastic agent configuration to $PILLARFILE"
return 1
fi
}
# Add Elastic Fleet Server settings to the minion file
function add_fleet_to_minion() {
# Create ES Token for Fleet server (Curl to Kibana API)
ESTOKEN=$(curl -K /opt/so/conf/elasticsearch/curl.config -L -X POST "localhost:5601/api/fleet/service_tokens" -H 'kbn-xsrf: true' -H 'Content-Type: application/json' | jq -r .value)
if [ $? -ne 0 ]; then
log "ERROR" "Failed to obtain ES token for fleet server"
return 1
fi
# Write out settings to minion file
printf '%s\n'\
"elasticfleet:"\
" enabled: True"\
" config:"\
" server:"\
" es_token: '$ESTOKEN'"\
" " >> $PILLARFILE
if [ $? -ne 0 ]; then
log "ERROR" "Failed to add fleet configuration to $PILLARFILE"
return 1
fi
}
# Add IDH Services info to the minion file
function add_idh_to_minion() {
printf '%s\n'\
"idh:"\
" enabled: True"\
" restrict_management_ip: $IDH_MGTRESTRICT"\
" " >> $PILLARFILE
if [ $? -ne 0 ]; then
log "ERROR" "Failed to add IDH configuration to $PILLARFILE"
return 1
fi
}
function add_logstash_to_minion() {
# Create the logstash advanced pillar
printf '%s\n'\
"logstash:"\
" enabled: True"\
" config:"\
" pipeline_x_workers: $CPUCORES"\
" settings:"\
" lsheap: $LSHEAP"\
" " >> $PILLARFILE
if [ $? -ne 0 ]; then
log "ERROR" "Failed to add logstash configuration to $PILLARFILE"
return 1
fi
}
# Security Onion Desktop
function add_desktop_to_minion() {
printf '%s\n'\
"desktop:"\
" gui:"\
" enabled: true"\ >> $PILLARFILE
if [ $? -ne 0 ]; then
log "ERROR" "Failed to add desktop configuration to $PILLARFILE"
return 1
fi
}
# Add basic host info to the minion file
function add_host_to_minion() {
printf '%s\n'\
"host:"\
" mainip: '$MAINIP'"\
" mainint: '$MNIC'" >> $PILLARFILE
if [ $? -ne 0 ]; then
log "ERROR" "Failed to add host configuration to $PILLARFILE"
return 1
fi
}
# Add sensoroni specific information - Can we pull node_adrees from the host pillar?
function add_sensoroni_to_minion() {
printf '%s\n'\
"sensoroni:"\
" enabled: True"\
" config:"\
" node_description: '${NODE_DESCRIPTION//\'/''}'"\
" " >> $PILLARFILE
if [ $? -ne 0 ]; then
log "ERROR" "Failed to add sensoroni configuration to $PILLARFILE"
return 1
fi
}
# Add sensoroni specific information - Can we pull node_adrees from the host pillar?
function add_sensoroni_with_analyze_to_minion() {
printf '%s\n'\
"sensoroni:"\
" enabled: True"\
" config:"\
" analyze:"\
" enabled: True"\
" node_description: '${NODE_DESCRIPTION//\'/''}'"\
" " >> $PILLARFILE
if [ $? -ne 0 ]; then
log "ERROR" "Failed to add sensoroni analyze configuration to $PILLARFILE"
return 1
fi
}
# Sensor settings for the minion pillar
function add_sensor_to_minion() {
log "INFO" "Writing sensor configuration for $MINION_ID (interface: ${INTERFACE:-unset})"
{
echo "sensor:"
echo " interface: '$INTERFACE'"
echo " mtu: 9000"
echo " channels: 1"
echo "zeek:"
echo " enabled: True"
echo " config:"
echo " node:"
echo " lb_procs: '$CORECOUNT'"
echo "suricata:"
echo " enabled: True "
echo " pcap:"
echo " enabled: True"
if [[ $is_pcaplimit ]]; then
echo " maxsize: $MAX_PCAP_SPACE"
fi
echo " config:"
echo " af-packet:"
echo " threads: '$CORECOUNT'"
echo " "
} >> $PILLARFILE
if [ $? -ne 0 ]; then
log "ERROR" "Failed to add sensor configuration to $PILLARFILE"
return 1
fi
log "INFO" "Wrote sensor configuration for $MINION_ID"
}
function add_elastalert_to_minion() {
printf '%s\n'\
"elastalert:"\
" enabled: True"\
" " >> $PILLARFILE
if [ $? -ne 0 ]; then
log "ERROR" "Failed to add elastalert configuration to $PILLARFILE"
return 1
fi
}
function add_kibana_to_minion() {
printf '%s\n'\
"kibana:"\
" enabled: True"\
" " >> $PILLARFILE
if [ $? -ne 0 ]; then
log "ERROR" "Failed to add kibana configuration to $PILLARFILE"
return 1
fi
}
function add_redis_to_minion() {
printf '%s\n'\
"redis:"\
" enabled: True"\
" " >> $PILLARFILE
if [ $? -ne 0 ]; then
log "ERROR" "Failed to add redis configuration to $PILLARFILE"
return 1
fi
}
function add_strelka_to_minion() {
printf '%s\n'\
"strelka:"\
" backend:"\
" enabled: True"\
" filestream:"\
" enabled: True"\
" frontend:"\
" enabled: True"\
" manager:"\
" enabled: True"\
" coordinator:"\
" enabled: True"\
" gatekeeper:"\
" enabled: True"\
" " >> $PILLARFILE
if [ $? -ne 0 ]; then
log "ERROR" "Failed to add strelka configuration to $PILLARFILE"
return 1
fi
}
function add_telegraf_to_minion() {
printf '%s\n'\
"telegraf:"\
" enabled: True"\
" " >> $PILLARFILE
if [ $? -ne 0 ]; then
log "ERROR" "Failed to add telegraf configuration to $PILLARFILE"
return 1
fi
# Provision the per-minion postgres Telegraf credential in the shared
# telegraf/creds.sls pillar. so-telegraf-cred is the only writer; it
# generates a password on first add and is a no-op on re-add so the cred
# is stable across repeated so-minion runs. postgres.telegraf_users on the
# manager creates/updates the DB role from the same pillar.
log "INFO" "Provisioning postgres telegraf credential for $MINION_ID"
so-telegraf-cred add "$MINION_ID"
local result=$?
if [ $result -ne 0 ]; then
log "ERROR" "Failed to provision postgres telegraf cred for $MINION_ID (exit code: $result)"
return 1
fi
log "INFO" "Provisioned postgres telegraf credential for $MINION_ID"
}
function add_influxdb_to_minion() {
printf '%s\n'\
"influxdb:"\
" enabled: True"\
" " >> $PILLARFILE
if [ $? -ne 0 ]; then
log "ERROR" "Failed to add influxdb configuration to $PILLARFILE"
return 1
fi
}
function add_nginx_to_minion() {
printf '%s\n'\
"nginx:"\
" enabled: True"\
" " >> $PILLARFILE
if [ $? -ne 0 ]; then
log "ERROR" "Failed to add nginx configuration to $PILLARFILE"
return 1
fi
}
function add_soc_to_minion() {
printf '%s\n'\
"soc:"\
" enabled: True"\
" " >> $PILLARFILE
if [ $? -ne 0 ]; then
log "ERROR" "Failed to add soc configuration to $PILLARFILE"
return 1
fi
}
function add_registry_to_minion() {
printf '%s\n'\
"registry:"\
" enabled: True"\
" " >> $PILLARFILE
if [ $? -ne 0 ]; then
log "ERROR" "Failed to add registry configuration to $PILLARFILE"
return 1
fi
}
function add_kratos_to_minion() {
printf '%s\n'\
"kratos:"\
" enabled: True"\
" " >> $PILLARFILE
if [ $? -ne 0 ]; then
log "ERROR" "Failed to add kratos configuration to $PILLARFILE"
return 1
fi
}
function add_elastic_fleet_package_registry_to_minion() {
printf '%s\n'\
"elastic_fleet_package_registry:"\
" enabled: True"\
" " >> $PILLARFILE
if [ $? -ne 0 ]; then
log "ERROR" "Failed to add elastic fleet package registry configuration to $PILLARFILE"
return 1
fi
}
function create_fleet_policy() {
log "INFO" "Creating fleet policy for $MINION_ID"
# First, set the default output to Elasticsearch
# This is required because of the license output bug
JSON_STRING=$(jq -n \
'{
"name": "so-manager_elasticsearch",
"type": "elasticsearch",
"is_default": true,
"is_default_monitoring": false
}')
if [ $? -ne 0 ]; then
log "ERROR" "Failed to create elasticsearch output JSON"
return 1
fi
curl -K /opt/so/conf/elasticsearch/curl.config -L -X PUT "localhost:5601/api/fleet/outputs/so-manager_elasticsearch" -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -d "$JSON_STRING"
if [ $? -ne 0 ]; then
log "ERROR" "Failed to set elasticsearch as default output"
return 1
fi
# Create the Fleet Server Policy
elastic_fleet_policy_create "FleetServer_$LSHOSTNAME" "Fleet Server - $LSHOSTNAME" "false" "120"
if [ $? -ne 0 ]; then
log "ERROR" "Failed to create fleet server policy"
return 1
fi
# Modify the default integration policy to update the policy_id with the correct naming
UPDATED_INTEGRATION_POLICY=$(jq --arg policy_id "FleetServer_$LSHOSTNAME" --arg name "fleet_server-$LSHOSTNAME" '
.policy_id = $policy_id |
.name = $name' /opt/so/conf/elastic-fleet/integrations/fleet-server/fleet-server.json)
if [ $? -ne 0 ]; then
log "ERROR" "Failed to update integration policy"
return 1
fi
# Add the Fleet Server Integration to the new Fleet Policy
elastic_fleet_integration_create "$UPDATED_INTEGRATION_POLICY"
if [ $? -ne 0 ]; then
log "ERROR" "Failed to create fleet server integration"
return 1
fi
# Set the default output back to the default
/sbin/so-elastic-fleet-outputs-update
if [ $? -ne 0 ]; then
log "ERROR" "Failed to update fleet outputs"
return 1
fi
}
function update_fleet_host_urls() {
log "INFO" "Updating fleet host URLs for $MINION_ID"
# Query for current Fleet Host URLs & append New Fleet Node Hostname & IP
JSON_STRING=$(curl -K /opt/so/conf/elasticsearch/curl.config 'http://localhost:5601/api/fleet/fleet_server_hosts/grid-default' | jq --arg HOSTNAME "https://$LSHOSTNAME:8220" --arg IP "https://$MAINIP:8220" '.item.host_urls += [ $HOSTNAME, $IP ] | {"name":"grid-default","is_default":true,"host_urls": .item.host_urls}')
if [ $? -ne 0 ]; then
log "ERROR" "Failed to get current fleet host URLs"
return 1
fi
# Update Fleet Host URLs
curl -K /opt/so/conf/elasticsearch/curl.config -L -X PUT "localhost:5601/api/fleet/fleet_server_hosts/grid-default" -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -d "$JSON_STRING"
if [ $? -ne 0 ]; then
log "ERROR" "Failed to update fleet host URLs"
return 1
fi
}
function update_logstash_outputs() {
log "INFO" "Updating logstash outputs for $MINION_ID"
# Query for current Logstash outputs & append New Fleet Node Hostname & IP
JSON_STRING=$(curl -K /opt/so/conf/elasticsearch/curl.config 'http://localhost:5601/api/fleet/outputs/so-manager_logstash' | jq --arg HOSTNAME "$LSHOSTNAME:5055" --arg IP "$MAINIP:5055" -r '.item.hosts += [ $HOSTNAME, $IP ] | {"name":"grid-logstash","type":"logstash","hosts": .item.hosts,"is_default":true,"is_default_monitoring":true,"config_yaml":""}')
if [ $? -ne 0 ]; then
log "ERROR" "Failed to get current logstash outputs"
return 1
fi
# Update Logstash Outputs
curl -K /opt/so/conf/elasticsearch/curl.config -L -X PUT "localhost:5601/api/fleet/outputs/so-manager_logstash" -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -d "$JSON_STRING"
if [ $? -ne 0 ]; then
log "ERROR" "Failed to update logstash outputs"
return 1
fi
}
function checkMine() {
local func=$1
log "INFO" "Checking mine for function $func on minion $MINION_ID"
# make sure the minion sees itself in the mine since it needs to see itself for states as opposed to using salt-run
retry 20 1 "salt '$MINION_ID' mine.get '\*' '$func'" "$MINION_ID" || {
log "ERROR" "Failed to verify mine function $func for $MINION_ID"
return 1
}
}
function create_ca_pillar() {
local capillar=/opt/so/saltstack/local/pillar/ca/init.sls
printf '%s\n'\
"ca:"\
" server: $MINION_ID"\
" " > $capillar
if [ $? -ne 0 ]; then
log "ERROR" "Failed to add $MINION_ID to $capillar"
return 1
fi
}
function createEVAL() {
log "INFO" "Creating EVAL configuration for minion $MINION_ID"
is_pcaplimit=true
pcapspace || return 1
add_elasticsearch_to_minion || return 1
add_sensor_to_minion || return 1
add_strelka_to_minion || return 1
add_elastalert_to_minion || return 1
add_kibana_to_minion || return 1
add_telegraf_to_minion || return 1
add_influxdb_to_minion || return 1
add_nginx_to_minion || return 1
add_soc_to_minion || return 1
add_registry_to_minion || return 1
add_kratos_to_minion || return 1
add_elastic_fleet_package_registry_to_minion || return 1
}
function createSTANDALONE() {
log "INFO" "Creating STANDALONE configuration for minion $MINION_ID"
is_pcaplimit=true
pcapspace || return 1
add_elasticsearch_to_minion || return 1
add_logstash_to_minion || return 1
add_sensor_to_minion || return 1
add_strelka_to_minion || return 1
add_elastalert_to_minion || return 1
add_kibana_to_minion || return 1
add_redis_to_minion || return 1
add_telegraf_to_minion || return 1
add_influxdb_to_minion || return 1
add_nginx_to_minion || return 1
add_soc_to_minion || return 1
add_registry_to_minion || return 1
add_kratos_to_minion || return 1
add_elastic_fleet_package_registry_to_minion || return 1
}
function createMANAGER() {
log "INFO" "Creating MANAGER configuration for minion $MINION_ID"
add_elasticsearch_to_minion || return 1
add_logstash_to_minion || return 1
add_elastalert_to_minion || return 1
add_kibana_to_minion || return 1
add_redis_to_minion || return 1
add_telegraf_to_minion || return 1
add_influxdb_to_minion || return 1
add_nginx_to_minion || return 1
add_soc_to_minion || return 1
add_registry_to_minion || return 1
add_kratos_to_minion || return 1
add_elastic_fleet_package_registry_to_minion || return 1
}
function createMANAGERSEARCH() {
log "INFO" "Creating MANAGERSEARCH configuration for minion $MINION_ID"
add_elasticsearch_to_minion || return 1
add_logstash_to_minion || return 1
add_elastalert_to_minion || return 1
add_kibana_to_minion || return 1
add_redis_to_minion || return 1
add_telegraf_to_minion || return 1
add_influxdb_to_minion || return 1
add_nginx_to_minion || return 1
add_soc_to_minion || return 1
add_registry_to_minion || return 1
add_kratos_to_minion || return 1
add_elastic_fleet_package_registry_to_minion || return 1
}
function createIMPORT() {
log "INFO" "Creating IMPORT configuration for minion $MINION_ID"
add_elasticsearch_to_minion || return 1
add_sensor_to_minion || return 1
add_kibana_to_minion || return 1
add_telegraf_to_minion || return 1
add_influxdb_to_minion || return 1
add_nginx_to_minion || return 1
add_soc_to_minion || return 1
add_registry_to_minion || return 1
add_kratos_to_minion || return 1
add_elastic_fleet_package_registry_to_minion || return 1
}
function createFLEET() {
log "INFO" "Creating FLEET configuration for minion $MINION_ID"
add_fleet_to_minion || return 1
add_logstash_to_minion || return 1
create_fleet_policy || return 1
update_fleet_host_urls || return 1
#update_logstash_outputs
add_telegraf_to_minion || return 1
add_nginx_to_minion || return 1
}
function createIDH() {
log "INFO" "Creating IDH configuration for minion $MINION_ID"
add_idh_to_minion || return 1
add_telegraf_to_minion || return 1
}
function createHEAVYNODE() {
log "INFO" "Creating HEAVYNODE configuration for minion $MINION_ID"
is_pcaplimit=true
PCAP_PERCENTAGE=1
DFREEPERCENT=21
pcapspace || return 1
add_elasticsearch_to_minion || return 1
add_elastic_agent_to_minion || return 1
add_sensor_to_minion || return 1
add_strelka_to_minion || return 1
add_telegraf_to_minion || return 1
}
function createSENSOR() {
log "INFO" "Creating SENSOR configuration for minion $MINION_ID"
is_pcaplimit=true
DFREEPERCENT=10
PCAP_PERCENTAGE=3
pcapspace || return 1
add_sensor_to_minion || return 1
add_strelka_to_minion || return 1
add_telegraf_to_minion || return 1
}
function createSEARCHNODE() {
log "INFO" "Creating SEARCHNODE configuration for minion $MINION_ID"
add_elasticsearch_to_minion || return 1
add_logstash_to_minion || return 1
add_telegraf_to_minion || return 1
}
function createRECEIVER() {
log "INFO" "Creating RECEIVER configuration for minion $MINION_ID"
add_logstash_to_minion || return 1
add_redis_to_minion || return 1
add_telegraf_to_minion || return 1
}
function createHYPERVISOR() {
log "INFO" "Creating HYPERVISOR configuration for minion $MINION_ID"
FEATURES=$(/usr/sbin/so-yaml.py get /opt/so/saltstack/local/pillar/soc/license.sls features)
if [[ $? -ne 0 || ! "$FEATURES" =~ "vrt" ]]; then
error_msg="Hypervisor nodes are a feature supported only for customers with a valid license.\n Contact Security Onion Solutions, LLC via our website at https://securityonionsolutions.com\n for more information about purchasing a license to enable this feature."
log "ERROR" "$error_msg"
echo -e "Error: $error_msg"
return 1
fi
add_telegraf_to_minion || return 1
}
function createMANAGERHYPE() {
log "INFO" "Creating MANAGERHYPE configuration for minion $MINION_ID"
add_elasticsearch_to_minion || return 1
add_logstash_to_minion || return 1
add_elastalert_to_minion || return 1
add_kibana_to_minion || return 1
add_redis_to_minion || return 1
add_telegraf_to_minion || return 1
add_influxdb_to_minion || return 1
add_nginx_to_minion || return 1
add_soc_to_minion || return 1
add_registry_to_minion || return 1
add_kratos_to_minion || return 1
add_elastic_fleet_package_registry_to_minion || return 1
}
function createDESKTOP() {
log "INFO" "Creating DESKTOP configuration for minion $MINION_ID"
add_desktop_to_minion || return 1
add_telegraf_to_minion || return 1
}
function testConnection() {
log "INFO" "Testing connection to minion $MINION_ID"
retry 15 1 "grep 'Authentication accepted from $MINION_ID' /opt/so/log/salt/master"
local retauth=$?
if [[ $retauth != 0 ]]; then
error_msg="The Minion did not authenticate with the Salt master in the allotted time"
log "ERROR" "$error_msg"
echo "$error_msg"
echo "Deleting the key"
deleteminion
return 1
fi
retry 15 3 "salt '$MINION_ID' test.ping" True
local ret=$?
if [[ $ret != 0 ]]; then
error_msg="The Minion has been accepted but is not online"
log "ERROR" "$error_msg"
echo "$error_msg"
echo "Deleting the key"
deleteminion
return 1
fi
}
function addMinion() {
log "INFO" "Starting minion addition process for $MINION_ID"
# Accept the salt key
acceptminion || {
log "ERROR" "Failed to accept minion key"
return 1
}
# Test to see if the minion was accepted
testConnection || {
log "ERROR" "Failed to establish connection with minion"
return 1
}
# Pull the info from the file to build what is needed
getinstallinfo || {
log "ERROR" "Failed to get installation information"
return 1
}
log "INFO" "Successfully added minion $MINION_ID"
}
function updateMineAndApplyStates() {
log "INFO" "Updating mine and applying states for minion $MINION_ID"
# calls so-common and set_minionid sets MINIONID to local minion id
set_minionid
if [ $? -ne 0 ]; then
log "ERROR" "Failed to set minion ID"
return 1
fi
# We don't want a hypervisor node to highstate until the image is downloaded and built. This will be triggered from the setup_hypervisor runner
if [[ "$NODETYPE" == "HYPERVISOR" ]]; then
FEATURES=$(/usr/sbin/so-yaml.py get /opt/so/saltstack/local/pillar/soc/license.sls features)
if [[ $? -ne 0 || ! "$FEATURES" =~ "vrt" ]]; then
error_msg="Hypervisor nodes are a feature supported only for customers with a valid license.\n Contact Security Onion Solutions, LLC via our website at https://securityonionsolutions.com\n for more information about purchasing a license to enable this feature."
log "ERROR" "$error_msg"
echo -e "Error: $error_msg"
return 1
fi
log "INFO" "Skipping state application for hypervisor node"
return 0
fi
# if this is a searchnode or heavynode, start downloading logstash and elasticsearch containers while the manager prepares for the new node
if [[ "$NODETYPE" == "SEARCHNODE" || "$NODETYPE" == "HEAVYNODE" ]]; then
log "INFO" "Starting container download for $NODETYPE"
salt-run state.orch orch.container_download pillar="{'setup': {'newnode': $MINION_ID }}" > /dev/null 2>&1 &
if [ $? -ne 0 ]; then
log "ERROR" "Failed to start container download"
return 1
fi
fi
if [[ "$NODETYPE" == "RECEIVER" ]]; then
# Setup nodeid for Kafka
log "INFO" "Setting up Kafka node ID"
salt-call state.apply kafka.nodes queue=True
if [ $? -ne 0 ]; then
log "ERROR" "Failed to setup Kafka node ID"
return 1
fi
fi
# $MINIONID is the minion id of the manager and $MINION_ID is the target node or the node being configured
log "INFO" "Deploying new node configuration"
salt-run state.orch orch.deploy_newnode pillar="{'setup': {'manager': $MINIONID, 'newnode': $MINION_ID }}" > /dev/null 2>&1 &
if [ $? -ne 0 ]; then
log "ERROR" "Failed to start node deployment"
return 1
fi
}
# Values end up in a Jinja-rendered pillar and in bash, and may come from the minion
function validate_minion_vars() {
local error_msg=""
# Inline rather than valid_ip4: so-common is not installed yet when setup runs -o=setup
local octet='(25[0-5]|2[0-4][0-9]|1?[0-9]?[0-9])'
local ip4_re="^($octet\.){3}$octet$"
case "$NODETYPE" in
EVAL|STANDALONE|MANAGER|MANAGERSEARCH|MANAGERHYPE|IMPORT)
# Manager pillars also rewrite the CA pillar, so never accept them from a remote node
[[ "$OPERATION" == "setup" ]] || error_msg="Node type $NODETYPE can only be configured during setup"
;;
FLEET|IDH|HEAVYNODE|SENSOR|SEARCHNODE|RECEIVER|HYPERVISOR|DESKTOP)
;;
*)
error_msg="Invalid node type: ${NODETYPE:0:64}"
;;
esac
if [[ -z "$error_msg" ]]; then
if [[ ! "$MAINIP" =~ $ip4_re ]]; then
error_msg="Invalid MAINIP: ${MAINIP:0:64}"
elif [[ ! "$MNIC" =~ ^[A-Za-z0-9._-]*$ ]]; then
error_msg="Invalid MNIC: ${MNIC:0:64}"
elif [[ ! "$INTERFACE" =~ ^[A-Za-z0-9._-]*$ ]]; then
error_msg="Invalid INTERFACE: ${INTERFACE:0:64}"
elif [[ ! "$LSHOSTNAME" =~ ^[A-Za-z0-9._-]*$ ]]; then
error_msg="Invalid LSHOSTNAME: ${LSHOSTNAME:0:64}"
elif [[ ! "$ES_HEAP_SIZE" =~ ^([0-9]+[kKmMgG]?)?$ ]]; then
error_msg="Invalid ES_HEAP_SIZE: ${ES_HEAP_SIZE:0:64}"
elif [[ ! "$LSHEAP" =~ ^([0-9]+[kKmMgG]?)?$ ]]; then
error_msg="Invalid LSHEAP: ${LSHEAP:0:64}"
elif [[ ! "$CORECOUNT" =~ ^[0-9]*$ ]]; then
error_msg="Invalid CORECOUNT: ${CORECOUNT:0:64}"
elif [[ ! "$CPUCORES" =~ ^[0-9]*$ ]]; then
error_msg="Invalid CPUCORES: ${CPUCORES:0:64}"
elif [[ ! "$IDH_MGTRESTRICT" =~ ^(True|False)?$ ]]; then
error_msg="Invalid IDH_MGTRESTRICT: ${IDH_MGTRESTRICT:0:64}"
fi
fi
if [[ -n "$error_msg" ]]; then
log "ERROR" "$error_msg"
echo "$error_msg"
return 1
fi
# Free text; removing braces is enough to prevent any Jinja delimiter
NODE_DESCRIPTION=${NODE_DESCRIPTION//[\{\}[:cntrl:]]/}
}
function setupMinionFiles() {
log "INFO" "Setting up minion files for $MINION_ID (pillar: $PILLARFILE)"
# Check to see if nodetype is set
if [ -z $NODETYPE ]; then
error_msg="No node type specified"
log "ERROR" "$error_msg"
echo "$error_msg"
return 1
fi
validate_minion_vars || return 1
# Create the base minion files
create_minion_files || return 1
# Add host configuration
add_host_to_minion || return 1
# Add sensoroni configuration based on node type
managers=("EVAL" "STANDALONE" "IMPORT" "MANAGER" "MANAGERSEARCH")
if echo "${managers[@]}" | grep -qw "$NODETYPE"; then
add_sensoroni_with_analyze_to_minion || return 1
create_ca_pillar || return 1
else
add_sensoroni_to_minion || return 1
fi
# Create node-specific configuration
create$NODETYPE || {
log "ERROR" "Failed to create $NODETYPE configuration for $MINION_ID"
return 1
}
# Ensure proper ownership after all content is written
ensure_socore_ownership || return 1
log "INFO" "Successfully created minion files for $MINION_ID"
}
case "$OPERATION" in
"add")
log "INFO" "Adding minion $MINION_ID"
addMinion || {
log "ERROR" "Failed to add minion $MINION_ID"
exit 1
}
setupMinionFiles || {
log "ERROR" "Failed to setup minion files for $MINION_ID"
exit 1
}
updateMineAndApplyStates || {
log "ERROR" "Failed to update mine and apply states for $MINION_ID"
exit 1
}
log "INFO" "Successfully completed all operations for minion $MINION_ID"
;;
"addVM")
log "INFO" "Adding VM minion $MINION_ID"
FEATURES=$(/usr/sbin/so-yaml.py get /opt/so/saltstack/local/pillar/soc/license.sls features)
if [[ $? -ne 0 || ! "$FEATURES" =~ "vrt" ]]; then
error_msg="Hypervisor nodes are a feature supported only for customers with a valid license.\n Contact Security Onion Solutions, LLC via our website at https://securityonionsolutions.com\n for more information about purchasing a license to enable this feature."
log "ERROR" "$error_msg"
echo -e "Error: $error_msg"
exit 1
fi
setupMinionFiles || {
log "ERROR" "Failed to setup VM minion files for $MINION_ID"
exit 1
}
log "INFO" "Successfully added VM minion $MINION_ID"
;;
"delete")
log "INFO" "Removing minion $MINION_ID"
remove_postgres_telegraf_from_minion
deleteMinionFiles || {
log "ERROR" "Failed to delete minion files for $MINION_ID"
exit 1
}
deleteMinion || {
log "ERROR" "Failed to delete minion key for $MINION_ID"
exit 1
}
log "INFO" "Successfully removed minion $MINION_ID"
;;
"list")
listMinions || {
log "ERROR" "Failed to list minion keys"
exit 1
}
;;
"reject")
rejectMinion || {
log "ERROR" "Failed to reject minion $MINION_ID"
exit 1
}
;;
"restart")
restartMinion || {
log "ERROR" "Failed to restart minion $MINION_ID"
exit 1
}
;;
"setup")
# only should be invoked directly during setup, never manually
setupMinionFiles || {
log "ERROR" "Failed to setup minion files during setup for $MINION_ID"
exit 1
}
log "INFO" "Successfully completed setup for minion $MINION_ID"
;;
"test")
testMinion || {
log "ERROR" "Failed to test minion $MINION_ID"
exit 1
}
;;
*)
log "ERROR" "Invalid operation: $OPERATION"
usage
;;
esac