Files
securityonion/salt/manager/tools/sbin/so-client
T
Mike Reeves bbc28c88b7 Isolate the Hydra admin API on the soauth docker network
The Hydra admin API creates OAuth clients and introspects tokens without
authentication, relying on network isolation the same way Kratos does, but
so-hydra published 0.0.0.0:4445:4445 and sat on sobridge. With userland-proxy
left at its default of true, Docker ran a proxy listener on the host, so any
container reached the admin API through the manager IP or the bridge gateway
regardless of its docker network.

so-hydra now sits alone on soauth and no longer publishes 4445. 4444 is still
published, so the nginx proxy for /oauth2/token and the well-known endpoints is
unchanged. so-soc is already dual homed from the Kratos change and reaches the
admin API over soauth, so only its hostUrl moves. wait_for_hydra polls the
container address instead of the host port.

so-client reaches the admin API through docker exec, mirroring so-user. Exit
codes still propagate, so the --fail-with-body error handling is unchanged.

so-hydra is removed in up_to_3.4.0 alongside so-kratos and so-soc so the
highstate recreates it with the correct network membership. The auth network
range is already handled by set_soauth_range.
2026-09-17 16:58:49 -04:00

417 lines
10 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.
if [[ -f /usr/sbin/so-common ]]; then
source /usr/sbin/so-common
else
source $(dirname $0)/../../../common/tools/sbin/so-common
fi
function usage() {
cat <<USAGE_EOF
Usage: $0 <operation> [supporting parameters]
where <operation> is one of the following:
list: Lists all client IDs and permissions currently defined in the oauth2 system
add: Adds a new client to the oauth2 system and outputs the generated secret
Required parameters:
--name <name>
Optional parameters:
--note <note> (defaults to blank)
--json output as JSON
delete: Deletes a client from the oauth2 system
Required parameters:
--id <id>
addperm: Grants a permission to an existing client
Required parameters:
--id <id>
--permission <permission>
delperm: Removes a permission from an existing client
Required parameters:
--id <id>
--permission <permission>
update: Updates a client name and note.
Required parameters:
--id <id>
--name <name>
--note <note>
--searchusername <run-as username>
generate-secret: Regenerates a client's secret and outputs the new secret.
Required parameters:
--id <id>
Optional parameters:
--json output as JSON
USAGE_EOF
exit 1
}
if [[ $# -lt 1 || $1 == --help || $1 == -h || $1 == -? || $1 == --h ]]; then
usage
fi
operation=$1
shift
searchUsername=__MISSING__
note=__MISSING__
while [[ $# -gt 0 ]]; do
param=$1
shift
case "$param" in
--id)
id=$(echo $1 | sed 's/"/\\"/g')
[[ ${#id} -gt 55 ]] && fail "id cannot be longer than 55 characters"
shift
;;
--permission)
perm=$(echo $1 | sed 's/"/\\"/g')
[[ ${#perm} -gt 50 ]] && fail "permission cannot be longer than 50 characters"
shift
;;
--name)
name=$(echo $1 | sed 's/"/\\"/g')
[[ ${#name} -gt 50 ]] && fail "name cannot be longer than 50 characters"
shift
;;
--note)
note=$(echo $1 | sed 's/"/\\"/g')
[[ ${#note} -gt 100 ]] && fail "note cannot be longer than 100 characters"
shift
;;
--searchusername)
searchUsername=$(echo $1 | sed 's/"/\\"/g')
[[ ${#searchUsername} -gt 50 ]] && fail "search username cannot be longer than 50 characters"
shift
;;
--json)
json=1
;;
*)
echo "Encountered unexpected parameter: $param"
usage
;;
esac
done
hydraContainer=${HYDRA_CONTAINER:-so-hydra}
hydraUrl=${HYDRA_URL:-http://localhost:4445}
socRolesFile=${SOC_ROLES_FILE:-/opt/so/conf/soc/soc_clients_roles}
soUID=${SOCORE_UID:-939}
soGID=${SOCORE_GID:-939}
function lock() {
# Obtain file descriptor lock
exec 99>/var/tmp/so-client.lock || fail "Unable to create lock descriptor; if the system was not shutdown gracefully you may need to remove /var/tmp/so-client.lock manually."
flock -w 10 99 || fail "Another process is using so-client; if the system was not shutdown gracefully you may need to remove /var/tmp/so-client.lock manually."
trap 'rm -f /var/tmp/so-client.lock' EXIT
}
function fail() {
msg=$1
echo "$1"
exit 1
}
function hydraCurl() {
docker exec -i "$hydraContainer" curl "$@"
}
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 "jq"
require "docker"
response=$(hydraCurl -Ss -L ${hydraUrl}/health/alive)
[[ "$response" != '{"status":"ok"}' ]] && fail "Unable to communicate with Hydra; specify URL via HYDRA_URL environment variable"
}
function createFile() {
filename=$1
uid=$2
gid=$3
mkdir -p $(dirname "$filename")
truncate -s 0 "$filename"
chmod 600 "$filename"
chown "${uid}:${gid}" "$filename"
}
function ensureRoleFileExists() {
if [[ ! -f "$socRolesFile" ]]; then
# Generate the new roles file
rolesTmpFile="${socRolesFile}.tmp"
createFile "$rolesTmpFile" "$soUID" "$soGID"
if [[ -d "$socRolesFile" ]]; then
echo "Removing invalid roles directory created by Docker"
rm -fr "$socRolesFile"
fi
mv "${rolesTmpFile}" "${socRolesFile}"
fi
}
function listClients() {
response=$(hydraCurl -Ss -L -f ${hydraUrl}/admin/clients)
[[ $? != 0 ]] && fail "Unable to communicate with Hydra"
clientIds=$(echo "${response}" | jq -r ".[] | .client_id" | sort)
for clientId in $clientIds; do
perms=$(grep ":$clientId\$" "$socRolesFile" | cut -d: -f1 | tr '\n' ' ')
echo "$clientId: $perms"
done
}
function addClientPermission() {
id=$1
perm=$2
adjustClientPermission "$id" "$perm" "add"
}
function deleteClientPermission() {
id=$1
perm=$2
adjustClientPermission "$id" "$perm" "del"
}
function adjustClientPermission() {
identityId=$1
perm=$2
op=$3
[[ ${identityId} == "" ]] && fail "Client not found"
ensureRoleFileExists
filename="$socRolesFile"
hasPerm=0
grep "^$perm:" "$socRolesFile" | grep -q "$identityId" && hasPerm=1
if [[ "$op" == "add" ]]; then
if [[ "$hasPerm" == "1" ]]; then
echo "Client '$identityId' already has the permission: $perm"
return 1
else
echo "$perm:$identityId" >> "$filename"
fi
elif [[ "$op" == "del" ]]; then
if [[ "$hasPerm" -ne 1 ]]; then
fail "Client '$identityId' does not have the permission: $perm"
else
sed -e "\!^$perm:$identityId\$!d" "$filename" > "$filename.tmp"
cat "$filename".tmp > "$filename"
rm -f "$filename".tmp
fi
else
fail "Unsupported permission adjustment operation: $op"
fi
return 0
}
function convertNameToId() {
name=$1
name=${name//[^[:alnum:]]/_}
echo "socl_$name" | tr '[:upper:]' '[:lower:]'
}
function createClient() {
name=$1
note=$2
id=$(convertNameToId "$name")
now=$(date -u +%FT%TZ)
secret=$(get_random_value)
body=$(cat <<EOF
{
"access_token_strategy": "opaque",
"client_id": "$id",
"client_secret": "$secret",
"client_name": "$name",
"grant_types": [ "client_credentials" ],
"response_types": [ "code" ],
"metadata": {
"note": "$note",
"searchUsername": ""
}
}
EOF
)
response=$(hydraCurl -Ss -L --fail-with-body -X POST ${hydraUrl}/admin/clients -d "$body")
if [[ $? != 0 ]]; then
error=$(echo $response | jq .error)
fail "Failed to submit request to Hydra: $error"
fi
}
function update() {
clientId=$1
name=$2
note=$3
username=$4
body=$(cat <<EOF
[
{
"op": "replace",
"path": "/client_name",
"value": "$name"
},
{
"op": "replace",
"path": "/metadata",
"value": {
"note": "$note",
"searchUsername": "$username"
}
}
]
EOF
)
response=$(hydraCurl -Ss -L --fail-with-body -X PATCH ${hydraUrl}/admin/clients/$id -d "$body")
if [[ $? != 0 ]]; then
error=$(echo $response | jq .error)
fail "Failed to submit request to Hydra: $error"
fi
}
function generateSecret() {
clientId=$1
secret=$(get_random_value)
body=$(cat <<EOF
[
{
"op": "replace",
"path": "/client_secret",
"value": "$secret"
}
]
EOF
)
response=$(hydraCurl -Ss -L --fail-with-body -X PATCH ${hydraUrl}/admin/clients/$id -d "$body")
if [[ $? != 0 ]]; then
error=$(echo $response | jq .error)
fail "Failed to submit request to Hydra: $error"
fi
}
function deleteClient() {
identityId=$1
[[ ${identityId} == "" ]] && fail "Client not found"
response=$(hydraCurl -Ss -XDELETE -L --fail-with-body "${hydraUrl}/admin/clients/$identityId")
if [[ $? != 0 ]]; then
error=$(echo $response | jq .error)
fail "Failed to submit request to Hydra: $error"
fi
rolesTmpFile="${socRolesFile}.tmp"
createFile "$rolesTmpFile" "$soUID" "$soGID"
grep -v "$identityId" "$socRolesFile" > "$rolesTmpFile"
cat "$rolesTmpFile" > "$socRolesFile"
}
case "${operation}" in
"add")
verifyEnvironment
[[ "$name" == "" ]] && fail "A short client name must be provided"
lock
createClient "$name" "$note"
if [[ "$json" == "1" ]]; then
echo "{\"id\":\"$id\",\"secret\":\"$secret\"}"
else
echo "Successfully added client ID $id with generated secret: $secret"
fi
;;
"list")
verifyEnvironment
listClients
;;
"addperm")
verifyEnvironment
[[ "$id" == "" ]] && fail "Id must be provided"
[[ "$perm" == "" ]] && fail "Permission must be provided"
lock
if addClientPermission "$id" "$perm"; then
echo "Successfully added permission to client"
fi
;;
"delperm")
verifyEnvironment
[[ "$id" == "" ]] && fail "Id must be provided"
[[ "$perm" == "" ]] && fail "Permission must be provided"
lock
deleteClientPermission "$id" "$perm"
echo "Successfully removed permission from client"
;;
"update")
verifyEnvironment
[[ "$id" == "" ]] && fail "Id must be provided"
[[ "$name" == "" ]] && fail "Name must be provided"
[[ "$note" == "__MISSING__" ]] && fail "Note must be provided"
[[ "$searchUsername" == "__MISSING__" ]] && fail "Search Username must be provided"
lock
update "$id" "$name" "$note" "$searchUsername"
echo "Successfully updated client"
;;
"generate-secret")
verifyEnvironment
[[ "$id" == "" ]] && fail "Id must be provided"
lock
generateSecret "$id"
if [[ "$json" == "1" ]]; then
echo "{\"secret\":\"$secret\"}"
else
echo "Successfully generated secret: $secret"
fi
;;
"delete")
verifyEnvironment
[[ "$id" == "" ]] && fail "Id must be provided"
lock
deleteClient "$id"
echo "Successfully deleted client."
;;
*)
fail "Unsupported operation: $operation"
usage
;;
esac
exit 0