gtat-tech-career-kickstarte.../setup_utils/restricted_deploy.sh

116 lines
3.2 KiB
Bash

#!/bin/bash
# restricted_deploy.sh — Forced SSH command for per-team deployment isolation.
#
# Installed on UAT at /usr/local/bin/restricted_deploy.sh.
# Referenced in deployer's authorized_keys as:
# command="/usr/local/bin/restricted_deploy.sh <team>",no-port-forwarding,... ssh-rsa ...
#
# Only allows:
# - scp sink mode (write): scp -t [-r] [-d] /srv/deployments/<team>/...
# - mkdir: mkdir -p /srv/deployments/<team>/...
set -euo pipefail
TEAM_NAME="$1"
BASE_DIR="/srv/deployments"
ALLOWED_DIR="${BASE_DIR}/${TEAM_NAME}"
if [ ! -d "$ALLOWED_DIR" ]; then
echo "ERROR: team directory does not exist" >&2
exit 1
fi
ALLOWED_REAL=$(readlink -f "$ALLOWED_DIR")
ORIG_CMD="${SSH_ORIGINAL_COMMAND:-}"
if [ -z "$ORIG_CMD" ]; then
echo "ERROR: interactive shell access denied" >&2
exit 1
fi
CMD_VERB=$(echo "$ORIG_CMD" | awk '{print $1}')
case "$CMD_VERB" in
scp)
# Extract the target path (always the last argument)
TARGET_PATH=$(echo "$ORIG_CMD" | awk '{print $NF}')
# Validate flags: only -t, -r, -d are allowed
FLAGS=$(echo "$ORIG_CMD" | awk '{$1=""; $NF=""; print}' | tr -s ' ')
for flag in $FLAGS; do
case "$flag" in
-t|-r|-d) ;;
*) echo "ERROR: disallowed scp flag: $flag" >&2; exit 1 ;;
esac
done
# Must contain -t (sink/write mode)
if ! echo "$ORIG_CMD" | grep -q -- '-t'; then
echo "ERROR: only scp sink mode (-t) is allowed" >&2
exit 1
fi
;;
mkdir)
if ! echo "$ORIG_CMD" | grep -q '^mkdir -p '; then
echo "ERROR: only 'mkdir -p <path>' is allowed" >&2
exit 1
fi
TARGET_PATH=$(echo "$ORIG_CMD" | awk '{print $NF}')
;;
*)
echo "ERROR: command not allowed: $CMD_VERB" >&2
exit 1
;;
esac
# ---- Path validation (anti-traversal) ----
# Reject paths containing ".."
if echo "$TARGET_PATH" | grep -q '\.\.'; then
echo "ERROR: path traversal detected" >&2
exit 1
fi
# Textual prefix check
case "$TARGET_PATH" in
"${ALLOWED_DIR}/"*|"${ALLOWED_DIR}")
;;
*)
echo "ERROR: target path is outside allowed directory" >&2
exit 1
;;
esac
# Canonical check for existing paths (defeats symlink escapes)
if [ -e "$TARGET_PATH" ]; then
REAL_TARGET=$(readlink -f "$TARGET_PATH")
case "$REAL_TARGET" in
"${ALLOWED_REAL}/"*|"${ALLOWED_REAL}")
;;
*)
echo "ERROR: resolved path is outside allowed directory" >&2
exit 1
;;
esac
else
# For non-existent paths (mkdir case), resolve the nearest existing parent
CHECK_PATH="$TARGET_PATH"
while [ ! -e "$CHECK_PATH" ] && [ "$CHECK_PATH" != "/" ]; do
CHECK_PATH=$(dirname "$CHECK_PATH")
done
if [ -e "$CHECK_PATH" ]; then
REAL_CHECK=$(readlink -f "$CHECK_PATH")
case "$REAL_CHECK" in
"${ALLOWED_REAL}/"*|"${ALLOWED_REAL}")
;;
*)
echo "ERROR: resolved parent path is outside allowed directory" >&2
exit 1
;;
esac
fi
fi
# All checks passed — execute the original command
exec $ORIG_CMD