diff --git a/salt/common/tools/sbin/so-elasticsearch-roles-load b/salt/common/tools/sbin/so-elasticsearch-roles-load
new file mode 100644
index 000000000..3b0f580fe
--- /dev/null
+++ b/salt/common/tools/sbin/so-elasticsearch-roles-load
@@ -0,0 +1,57 @@
+#!/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 .
+
+{%- set mainint = salt['pillar.get']('host:mainint') %}
+{%- set MYIP = salt['grains.get']('ip_interfaces:' ~ mainint)[0] %}
+
+default_conf_dir=/opt/so/conf
+ELASTICSEARCH_HOST="{{ MYIP }}"
+ELASTICSEARCH_PORT=9200
+
+# Define a default directory to load roles from
+ELASTICSEARCH_ROLES="$default_conf_dir/elasticsearch/roles/"
+
+# Wait for ElasticSearch to initialize
+echo -n "Waiting for ElasticSearch..."
+COUNT=0
+ELASTICSEARCH_CONNECTED="no"
+while [[ "$COUNT" -le 240 ]]; do
+ {{ ELASTICCURL }} -k --output /dev/null --silent --head --fail -L https://"$ELASTICSEARCH_HOST":"$ELASTICSEARCH_PORT"
+ if [ $? -eq 0 ]; then
+ ELASTICSEARCH_CONNECTED="yes"
+ echo "connected!"
+ break
+ else
+ ((COUNT+=1))
+ sleep 1
+ echo -n "."
+ fi
+done
+if [ "$ELASTICSEARCH_CONNECTED" == "no" ]; then
+ echo
+ echo -e "Connection attempt timed out. Unable to connect to ElasticSearch. \nPlease try: \n -checking log(s) in /var/log/elasticsearch/\n -running 'sudo docker ps' \n -running 'sudo so-elastic-restart'"
+ echo
+fi
+
+cd ${ELASTICSEARCH_ROLES}
+
+echo "Loading templates..."
+for role in *; do
+ name=$(echo "$role" | cut -d. -f1)
+ so-elasticsearch-query security/roles/$name -XPUT -d @"$role"
+done
+
+cd - >/dev/null
diff --git a/salt/common/tools/sbin/so-user b/salt/common/tools/sbin/so-user
index 742c3ca5d..7ec094efb 100755
--- a/salt/common/tools/sbin/so-user
+++ b/salt/common/tools/sbin/so-user
@@ -18,11 +18,17 @@
source $(dirname $0)/so-common
+DEFAULT_ROLE=analyst
+
if [[ $# -lt 1 || $# -gt 2 ]]; then
- echo "Usage: $0 [email]"
+ echo "Usage: $0 [email] [role]"
+ echo ""
+ echo " where is one of the following:"
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 " addrole: Grants a role to an existing user; requires 'email' and 'role' parameters"
+ echo " delrole: Removes a role from an existing user; requires 'email' and 'role' parameters"
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"
@@ -36,6 +42,7 @@ fi
operation=$1
email=$2
+role=$3
kratosUrl=${KRATOS_URL:-http://127.0.0.1:4434}
databasePath=${KRATOS_DB_PATH:-/opt/so/conf/kratos/db/db.sqlite}
@@ -138,10 +145,9 @@ function updatePassword() {
function createElasticFile() {
filename=$1
- tmpFile=${filename}
- truncate -s 0 "$tmpFile"
- chmod 600 "$tmpFile"
- chown "${esUID}:${esGID}" "$tmpFile"
+ truncate -s 0 "$filename"
+ chmod 600 "$filename"
+ chown "${esUID}:${esGID}" "$filename"
}
function syncElasticSystemUser() {
@@ -174,28 +180,15 @@ function syncElasticSystemRole() {
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
@@ -207,23 +200,12 @@ function syncElastic() {
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..."
@@ -252,11 +234,73 @@ function listUsers() {
response=$(curl -Ss -L ${kratosUrl}/identities)
[[ $? != 0 ]] && fail "Unable to communicate with Kratos"
- echo "${response}" | jq -r ".[] | .verifiable_addresses[0].value" | sort
+ users=$(echo "${response}" | jq -r ".[] | .verifiable_addresses[0].value" | sort)
+ for user in $users; do
+ roles=$(grep "$user" users_roles | cut -d: -f1 | tr '\n' ' ')
+ echo "$user: $roles"
+ done
+}
+
+function addUserRole() {
+ email=$1
+ role=$2
+
+ return adjustUserRole "$email" "$role" "add"
+}
+
+function deleteUserRole() {
+ email=$1
+ role=$2
+
+ return adjustUserRole "$email" "$role" "del"
+}
+
+function adjustUserRole() {
+ email=$1
+ role=$2
+ op=$3
+
+ identityId=$(findIdByEmail "$email")
+ [[ ${identityId} == "" ]] && fail "User not found"
+
+ if [ ! -f "$filename" ]; then
+ rolesTmpFile="${elasticRolesFile}.tmp"
+ createElasticFile "${rolesTmpFile}"
+ authPillarJson=$(lookup_salt_value "auth" "elasticsearch" "pillar" "json")
+ syncElasticSystemRole "$authPillarJson" "so_elastic_user" "superuser" "$rolesTmpFile"
+ syncElasticSystemRole "$authPillarJson" "so_kibana_user" "superuser" "$rolesTmpFile"
+ syncElasticSystemRole "$authPillarJson" "so_logstash_user" "superuser" "$rolesTmpFile"
+ syncElasticSystemRole "$authPillarJson" "so_beats_user" "superuser" "$rolesTmpFile"
+ 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"
+ mv "${rolesTmpFile}" "${elasticRolesFile}"
+ fi
+
+ filename="$elasticRolesFile"
+ grep "$role:" "$elasticRolesFile" | grep "$email" && hasRole=1
+ if [[ "$op" == "add" ]]; then
+ if [[ "$hasRole" -eq 1 ]]; then
+ fail "User '$email' already has the role: $role"
+ else
+ echo "$role:$email" >> "$filename"
+ fi
+ elif [[ "$op" == "del" ]]; then
+ if [[ "$hasRole" -ne 1 ]]; then
+ fail "User '$email' does not have the role: $role"
+ else
+ sed -i "/^$role:$email\$/d" "$filename"
+ fi
+ else
+ echo "Unsupported role adjustment operation: $op"
+ exit 1
+ fi
+ return 0
}
function createUser() {
email=$1
+ role=$1
now=$(date -u +%FT%TZ)
addUserJson=$(cat < /opt/so/conf/elasticsearch/users && chown 930:930 /opt/so/conf/elasticsearch/users && chmod 600 /opt/so/conf/elasticsearch/users
+ - name: cat /opt/so/conf/elasticsearch/users.tmp > /opt/so/conf/elasticsearch/users && chown 930:939 /opt/so/conf/elasticsearch/users && chmod 660 /opt/so/conf/elasticsearch/users
- onchanges:
- file: /opt/so/conf/elasticsearch/users.tmp
@@ -201,7 +218,7 @@ auth_users_roles_inode:
require:
- file: auth_users_roles
cmd.run:
- - name: cat /opt/so/conf/elasticsearch/users_roles.tmp > /opt/so/conf/elasticsearch/users_roles && chown 930:930 /opt/so/conf/elasticsearch/users_roles && chmod 600 /opt/so/conf/elasticsearch/users_roles
+ - name: cat /opt/so/conf/elasticsearch/users_roles.tmp > /opt/so/conf/elasticsearch/users_roles && chown 930:939 /opt/so/conf/elasticsearch/users_roles && chmod 660 /opt/so/conf/elasticsearch/users_roles
- onchanges:
- file: /opt/so/conf/elasticsearch/users_roles.tmp
@@ -283,7 +300,7 @@ so-elasticsearch-pipelines:
- file: esyml
- file: so-elasticsearch-pipelines-file
-{% if grains['role'] in ['so-manager', 'so-eval', 'so-managersearch', 'so-standalone', 'so-heavynode', 'so-node', 'so-import'] and TEMPLATES %}
+{% if TEMPLATES %}
so-elasticsearch-templates:
cmd.run:
- name: /usr/sbin/so-elasticsearch-templates-load
@@ -291,6 +308,12 @@ so-elasticsearch-templates:
- template: jinja
{% endif %}
+so-elasticsearch-roles-load:
+ cmd.run:
+ - name: /usr/sbin/so-elasticsearch-roles-load
+ - cwd: /opt/so
+ - template: jinja
+
{% endif %} {# if grains['role'] != 'so-helix' #}
{% else %}
diff --git a/salt/elasticsearch/roles/analyst.json b/salt/elasticsearch/roles/analyst.json
new file mode 100644
index 000000000..f81c5a67e
--- /dev/null
+++ b/salt/elasticsearch/roles/analyst.json
@@ -0,0 +1,45 @@
+{
+ "elasticsearch": {
+ "cluster": [
+ "cancel_task",
+ "create_snapshot",
+ "monitor",
+ "monitor_data_frame_transforms",
+ "monitor_ml",
+ "monitor_rollup",
+ "monitor_snapshot",
+ "monitor_text_structure",
+ "monitor_transform",
+ "monitor_watcher",
+ "read_ccr",
+ "read_ilm",
+ "read_pipeline",
+ "read_slm"
+ ],
+ "indices": [
+ {
+ "names": [
+ "so-*"
+ ],
+ "privileges": [
+ "read",
+ "read_cross_cluster",
+ "monitor",
+ "view_index_metadata"
+ ]
+ }
+ ],
+ "run_as": []
+ },
+ "kibana": [
+ {
+ "spaces": [
+ "*"
+ ],
+ "base": [
+ "read"
+ ],
+ "feature": {}
+ }
+ ]
+}
\ No newline at end of file
diff --git a/salt/soc/files/kratos/schema.json b/salt/soc/files/kratos/schema.json
index 986086936..19ee2197c 100644
--- a/salt/soc/files/kratos/schema.json
+++ b/salt/soc/files/kratos/schema.json
@@ -31,10 +31,6 @@
"type": "string",
"title": "Last Name"
},
- "role": {
- "type": "string",
- "title": "Role"
- },
"status": {
"type": "string",
"title": "Status"
diff --git a/salt/soc/files/soc/custom_roles b/salt/soc/files/soc/custom_roles
new file mode 100644
index 000000000..80ae7b147
--- /dev/null
+++ b/salt/soc/files/soc/custom_roles
@@ -0,0 +1,20 @@
+# Define custom business role mappings, or remove mappings that come with
+# the default SOC deployment.
+#
+# IMPORTANT: This file should be copied from the salt/default tree into
+# the salt/local tree (preserving the same directory structure).
+# Failure to do this will result in the customizations being
+# overwritten on future upgrades.
+#
+# Syntax => prebuiltRoleX: customRoleY: op
+# Explanation => roleY and roleZ are adjusted permissions of roleX, op is:
+# + add the new permissions/role mappings (default)
+# - remove existing prebuilt permissions
+#
+# In the example below, we will define a new role for junior analysts,
+# that is nearly identical to the analyst role that comes with SOC, with the
+# exception that it removes their ability to obtain details about other
+# analysts in the system.
+#
+# analyst: jr_analyst
+# user-monitor: jr_analyst:-
diff --git a/salt/soc/files/soc/soc.json b/salt/soc/files/soc/soc.json
index fc6d5f28d..6119f0e6b 100644
--- a/salt/soc/files/soc/soc.json
+++ b/salt/soc/files/soc/soc.json
@@ -85,6 +85,14 @@
"statickeyauth": {
"anonymousCidr": "{{ DNET }}/24",
"apiKey": "{{ SENSORONIKEY }}"
+ },
+ "staticrbac": {
+ "roleFiles": [
+ "rbac/permissions",
+ "rbac/roles",
+ "rbac/users_roles",
+ "rbac/custom_roles"
+ ]
}
},
"client": {
diff --git a/salt/soc/init.sls b/salt/soc/init.sls
index b8cdb09ba..c3c466849 100644
--- a/salt/soc/init.sls
+++ b/salt/soc/init.sls
@@ -62,6 +62,15 @@ soccustom:
- mode: 600
- template: jinja
+soccustomroles:
+ file.managed:
+ - name: /opt/so/conf/soc/custom_roles
+ - source: salt://soc/files/soc/custom_roles
+ - user: 939
+ - group: 939
+ - mode: 600
+ - template: jinja
+
# we dont want this added too early in setup, so we add the onlyif to verify 'startup_states: highstate'
# is in the minion config. That line is added before the final highstate during setup
sosyncusers:
@@ -81,6 +90,8 @@ so-soc:
- /opt/so/conf/soc/motd.md:/opt/sensoroni/html/motd.md:ro
- /opt/so/conf/soc/banner.md:/opt/sensoroni/html/login/banner.md:ro
- /opt/so/conf/soc/custom.js:/opt/sensoroni/html/js/custom.js:ro
+ - /opt/so/conf/soc/custom_roles:/opt/sensoroni/rbac/custom_roles:ro
+ - /opt/so/conf/elasticsearch/users_roles:/opt/sensoroni/rbac/users_roles:ro
- /opt/so/log/soc/:/opt/sensoroni/logs/:rw
{%- if salt['pillar.get']('nodestab', {}) %}
- extra_hosts:
diff --git a/setup/so-functions b/setup/so-functions
index 9a64a561e..d851c80b1 100755
--- a/setup/so-functions
+++ b/setup/so-functions
@@ -121,7 +121,7 @@ add_web_user() {
{
echo "Attempting to add administrator user for web interface...";
export SKIP_STATE_APPLY=true
- echo "$WEBPASSWD1" | /usr/sbin/so-user add "$WEBUSER";
+ echo "$WEBPASSWD1" | /usr/sbin/so-user add "$WEBUSER" "superuser";
unset SKIP_STATE_APPLY
echo "Add user result: $?";
} >> "/root/so-user-add.log" 2>&1