From 0bcab35657ac1250ccc4bff714f7f4db0c5eaed5 Mon Sep 17 00:00:00 2001 From: Andrew Cady Date: Wed, 17 Jul 2019 00:59:13 -0400 Subject: forced-ssh-command --- forced-ssh-command | 267 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100755 forced-ssh-command diff --git a/forced-ssh-command b/forced-ssh-command new file mode 100755 index 0000000..9f494cf --- /dev/null +++ b/forced-ssh-command @@ -0,0 +1,267 @@ +#!/bin/dash +authorize() +{ + local pkey line authorized_keys_line cmd + [ "$SSH_USER_AUTH" -a -f "$SSH_USER_AUTH" ] || return + read pkey line < "$SSH_USER_AUTH" || return + [ "$pkey" = publickey ] || return + + authorized_keys_line="$line samizdat: password-authenticated from ${SSH_CONNECTION%% *}" + sentinel='Samizdat - YES WE CAN' + + su - "$USER" -c 'mkdir -p "$HOME"/.ssh; touch "$HOME"/.ssh/authorized_keys' + add_before_sentinel "$sentinel" \ + "$authorized_keys_line" \ + "$HOME"/.ssh/authorized_keys +} +add_before_sentinel() +{ + local sentinel="$1" add_me="$2" target="$3" + sed -i.samizdat~ \ + -e "/$sentinel/i $add_me" \ + "$target" +} + +password_authentication() +{ + [ "$USER" ] || { echo 'Error: no $USER' >&2; exit 1; } + [ "$SSH_REMOTE_KEY" ] || { echo 'Error: no $SSH_REMOTE_KEY' >&2; exit 1; } + + tty=$(tty) && [ "$tty" != 'not a tty' ] || tty= + + if [ "$SSH_ORIGINAL_COMMAND" ]; then + msg='You are not authorized to execute the command: %s\n' + if ! [ "$tty" ]; then + msg="$msg"'To authorize your public key via password, reconnect without a command, or with a terminal attached.\n' + msg="$msg"'To attach a terminal, use the "-t" option to ssh.\n' + else + msg="$msg"'To authorize your public key and execute the command, enter your password.\n' + fi + printf "\n$msg\n" "$SSH_ORIGINAL_COMMAND" >&2 + if ! [ "$tty" ]; then + exit 1 + fi + else + msg='You are not authorized to log in.\n' + msg="$msg"'To authorize your public key and log in, enter your password.\n' + printf "\n$msg\n" >&2 + fi + authorize || exit $? + # TODO: blacklist after too many authentication failures + if [ "$SSH_ORIGINAL_COMMAND" ]; then + exec "$SSH_ORIGINAL_COMMAND" + else + exec $(getent passwd "$USER"|cut -d: -f7) -i + fi + exit $? # exec failed +} + +die() { echo "Error: $*" >&2; exit 1; } + +dequote() +{ + # Sorry about the slashes. The perl would be: s{ \\(.) | '([^']+)' }{ $1$2 }gx + git_dir=$(echo -n "$git_dir" | sed -e "s/\\\\\\(.\\)\\|'\\([^']\\+\\)'/\\1\\2/g") +} + +homedir_expand() +{ + git_dir=$(homedir_expand_arg "$git_dir") && + [ "$git_dir" ] || die "Could not expand home directory. HOME=$HOME, USER=$USER, id=$(id)" +} + +homedir_expand_arg() +{ + [ "$HOME" ] || die '$HOME is not set.' + case "$1" in + \~) echo "$HOME";; + \~/*) echo "${HOME}${1#\~}";; + \~*) + local u + u=${1#\~} + u=${u%%/*} + u=$(getent passwd "$u"|cut -f6 -d:) && [ "$u" ] || return 1 + echo "$u/${1#*/}";; + /*) echo "$1";; + *) echo "$HOME/$1";; + esac +} + +initialize_git() +{ + local git_dir="$1" anonymous="$2" + if ! [ -e "$git_dir" ]; then + mkdir -p "$(dirname "$git_dir")" + git init --bare "$git_dir" >&2 + if [ "$anonymous" ]; then + git --git-dir "$git_dir" config samizdat.allow-anonymous-access true + git --git-dir "$git_dir" config samizdat.anonymous-ssh-owner "$anonymous" + fi + fi +} + +is_gitdir() { git rev-parse --resolve-git-dir "$1" >/dev/null 2>&1; } + +deny() { echo 'Error: permission denied.' >&2; exit 1; } + +valid_new_public_repo() +{ + local git_dir="$1" + [ ! -e "$git_dir" ] || return + [ "$HOME" -a -d "$HOME"/public_git ] || return + local dirname="$(dirname "$git_dir")" + + case "$git_dir" in + *.git) ;; + *) + echo 'Error: public repos must be named *.git' >&2 + return 1 ;; + esac + + case "$dirname" in + $HOME/public_git) return 0 ;; + $HOME/public_git/*) + # Ensure that no parent directory is named *.git + # Also enforce a maximum depth of 4. + # Valid: public_git/a/b/c/d.git + # Invalid: public_git/a/b/c/d/e.git + local n relative="${git_dir#$HOME/public_git/}" + for n in 1 2 3 4; do + local topmost="${relative%%/*}" + case "$topmost" in + "$relative") return 0;; + *.git) return 1;; + esac + relative=${relative#$topmost/} + done + echo 'Error: directories nest too deep' >&2 + return 1 + ;; + *) return 1 ;; + esac +} + +check_if_ssh_user_owns_repository() +{ + git --git-dir "$git_dir" config --get-all samizdat.anonymous-ssh-owner | grep -xqF "$SSH_REMOTE_FINGERPRINT_TRIMMED" +} +ssh_user_owns_repository() +{ + if [ -z "$SSH_USER_OWNS_REPOSITORY" ]; then + check_if_ssh_user_owns_repository + SSH_USER_OWNS_REPOSITORY=$? + fi + return $SSH_USER_OWNS_REPOSITORY +} + +authorized() +{ + # TODO: check SSH_REMOTE_FINGERPRINT against a blacklist + ssh_user_owns_repository && return + test "$(git --git-dir "$1" config --bool --get samizdat.allow-anonymous-access)" = true 2>/dev/null && return 0 + # TODO: check SSH_REMOTE_FINGERPRINT against a whitelist +} + +maybe_initialize_heads() +{ + [ "$GIT_NAMESPACE" ] || die 'Programmer error' + heads=$git_dir/refs/namespaces/$GIT_NAMESPACE/refs/heads + mkdir -p "$heads" + found_file=$(find "$heads" -type f -print -quit) + [ "$found_file" ] && return + [ -e "$git_dir/refs/heads/master" ] && cp "$git_dir/refs/heads/master" "$heads" +} + + +if [ "$1" = "authorize-full-access" ]; then + case "$SSH_ORIGINAL_COMMAND" in + git-receive-pack\ *) + git_cmd=git-receive-pack + git_dir="${SSH_ORIGINAL_COMMAND#git-receive-pack }" + dequote + homedir_expand + initialize_git "$git_dir" + exec "$git_cmd" "$git_dir" + ;; + "") + shell=$(getent passwd d|cut -d: -f7) + argv0=-${shell##*/} + exec chpst -b "$argv0" "$shell" + ;; + *) + exec /bin/sh -c "$SSH_ORIGINAL_COMMAND" + ;; + esac +fi + +# TODO: call password_authentication on all authorization failures + +SSH_REMOTE_FINGERPRINT_TRIMMED=$(echo $SSH_REMOTE_FINGERPRINT|tr -d :) +# echo "SSH_ORIGINAL_COMMAND=$SSH_ORIGINAL_COMMAND" >&2 +case "$SSH_ORIGINAL_COMMAND" in + git-receive-pack\ *) + git_cmd=git-receive-pack + git_dir="${SSH_ORIGINAL_COMMAND#git-receive-pack }" + export GIT_NAMESPACE="$SSH_REMOTE_FINGERPRINT_TRIMMED" + ;; + git-upload-pack\ *) + git_cmd=git-upload-pack + git_dir="${SSH_ORIGINAL_COMMAND#git-upload-pack }" + ;; + rsync\ --server\ --sender\ *) + [ -d "$HOME"/public_rsync ] || { password_authentication; exit 1; } + exec rrsync -ro "$HOME"/public_rsync + exit 1 + ;; + rsync\ --server\ *) + [ -d "$HOME"/incoming_rsync ] || { password_authentication; exit 1; } + fp=$(echo $SSH_REMOTE_FINGERPRINT|tr -d :) + destdir=$HOME/incoming_rsync/$fp/ + mkdir -p "$destdir" && exec rrsync "$destdir" + exit 1 + ;; + *) + password_authentication + exit 1 # unreached + ;; +esac + +dequote +homedir_expand + +if [ -d "$git_dir" ]; then + + is_gitdir "$git_dir" || git_dir="$git_dir/.git" + + if ! is_gitdir "$git_dir"; then +# git rev-parse --resolve-git-dir "${git_dir%/.git}" # show git's error message + deny + fi + + if [ "$git_cmd" = 'git-upload-pack' ]; then + case "$git_dir" in + $HOME/public_git/*|public_git/*) exec "$git_cmd" "$git_dir";; + esac + fi + +elif [ "$git_cmd" = 'git-receive-pack' ] && valid_new_public_repo "$git_dir"; then + + initialize_git "$git_dir" "$SSH_REMOTE_FINGERPRINT_TRIMMED" + +else + deny +fi + +if authorized "$git_dir"; then + if [ "$git_cmd" = 'git-receive-pack' ]; then + if ssh_user_owns_repository; then + unset GIT_NAMESPACE + else + maybe_initialize_heads + fi + fi + exec "$git_cmd" "$git_dir" +else + password_authentication + echo 'Error: git access is unauthorized' >&2; exit 1 # unreached +fi -- cgit v1.2.3