#!/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 ",no-port-forwarding,... ssh-rsa ... # # Only allows: # - scp sink mode (write): scp -t [-r] [-d] /srv/deployments//... # - mkdir: mkdir -p /srv/deployments//... 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 ' 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