summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Cady <d@jerkface.net>2019-07-17 00:59:13 -0400
committerAndrew Cady <d@jerkface.net>2019-07-17 00:59:13 -0400
commit0bcab35657ac1250ccc4bff714f7f4db0c5eaed5 (patch)
treeb20c0327395fe3165bbe0bbbdea71d7374ddfce8
parent89f1ccc7dcfff9b9e70cbd9c2967d90d992612c9 (diff)
forced-ssh-command
-rwxr-xr-xforced-ssh-command267
1 files changed, 267 insertions, 0 deletions
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 @@
1#!/bin/dash
2authorize()
3{
4 local pkey line authorized_keys_line cmd
5 [ "$SSH_USER_AUTH" -a -f "$SSH_USER_AUTH" ] || return
6 read pkey line < "$SSH_USER_AUTH" || return
7 [ "$pkey" = publickey ] || return
8
9 authorized_keys_line="$line samizdat: password-authenticated from ${SSH_CONNECTION%% *}"
10 sentinel='Samizdat - YES WE CAN'
11
12 su - "$USER" -c 'mkdir -p "$HOME"/.ssh; touch "$HOME"/.ssh/authorized_keys'
13 add_before_sentinel "$sentinel" \
14 "$authorized_keys_line" \
15 "$HOME"/.ssh/authorized_keys
16}
17add_before_sentinel()
18{
19 local sentinel="$1" add_me="$2" target="$3"
20 sed -i.samizdat~ \
21 -e "/$sentinel/i $add_me" \
22 "$target"
23}
24
25password_authentication()
26{
27 [ "$USER" ] || { echo 'Error: no $USER' >&2; exit 1; }
28 [ "$SSH_REMOTE_KEY" ] || { echo 'Error: no $SSH_REMOTE_KEY' >&2; exit 1; }
29
30 tty=$(tty) && [ "$tty" != 'not a tty' ] || tty=
31
32 if [ "$SSH_ORIGINAL_COMMAND" ]; then
33 msg='You are not authorized to execute the command: %s\n'
34 if ! [ "$tty" ]; then
35 msg="$msg"'To authorize your public key via password, reconnect without a command, or with a terminal attached.\n'
36 msg="$msg"'To attach a terminal, use the "-t" option to ssh.\n'
37 else
38 msg="$msg"'To authorize your public key and execute the command, enter your password.\n'
39 fi
40 printf "\n$msg\n" "$SSH_ORIGINAL_COMMAND" >&2
41 if ! [ "$tty" ]; then
42 exit 1
43 fi
44 else
45 msg='You are not authorized to log in.\n'
46 msg="$msg"'To authorize your public key and log in, enter your password.\n'
47 printf "\n$msg\n" >&2
48 fi
49 authorize || exit $?
50 # TODO: blacklist after too many authentication failures
51 if [ "$SSH_ORIGINAL_COMMAND" ]; then
52 exec "$SSH_ORIGINAL_COMMAND"
53 else
54 exec $(getent passwd "$USER"|cut -d: -f7) -i
55 fi
56 exit $? # exec failed
57}
58
59die() { echo "Error: $*" >&2; exit 1; }
60
61dequote()
62{
63 # Sorry about the slashes. The perl would be: s{ \\(.) | '([^']+)' }{ $1$2 }gx
64 git_dir=$(echo -n "$git_dir" | sed -e "s/\\\\\\(.\\)\\|'\\([^']\\+\\)'/\\1\\2/g")
65}
66
67homedir_expand()
68{
69 git_dir=$(homedir_expand_arg "$git_dir") &&
70 [ "$git_dir" ] || die "Could not expand home directory. HOME=$HOME, USER=$USER, id=$(id)"
71}
72
73homedir_expand_arg()
74{
75 [ "$HOME" ] || die '$HOME is not set.'
76 case "$1" in
77 \~) echo "$HOME";;
78 \~/*) echo "${HOME}${1#\~}";;
79 \~*)
80 local u
81 u=${1#\~}
82 u=${u%%/*}
83 u=$(getent passwd "$u"|cut -f6 -d:) && [ "$u" ] || return 1
84 echo "$u/${1#*/}";;
85 /*) echo "$1";;
86 *) echo "$HOME/$1";;
87 esac
88}
89
90initialize_git()
91{
92 local git_dir="$1" anonymous="$2"
93 if ! [ -e "$git_dir" ]; then
94 mkdir -p "$(dirname "$git_dir")"
95 git init --bare "$git_dir" >&2
96 if [ "$anonymous" ]; then
97 git --git-dir "$git_dir" config samizdat.allow-anonymous-access true
98 git --git-dir "$git_dir" config samizdat.anonymous-ssh-owner "$anonymous"
99 fi
100 fi
101}
102
103is_gitdir() { git rev-parse --resolve-git-dir "$1" >/dev/null 2>&1; }
104
105deny() { echo 'Error: permission denied.' >&2; exit 1; }
106
107valid_new_public_repo()
108{
109 local git_dir="$1"
110 [ ! -e "$git_dir" ] || return
111 [ "$HOME" -a -d "$HOME"/public_git ] || return
112 local dirname="$(dirname "$git_dir")"
113
114 case "$git_dir" in
115 *.git) ;;
116 *)
117 echo 'Error: public repos must be named *.git' >&2
118 return 1 ;;
119 esac
120
121 case "$dirname" in
122 $HOME/public_git) return 0 ;;
123 $HOME/public_git/*)
124 # Ensure that no parent directory is named *.git
125 # Also enforce a maximum depth of 4.
126 # Valid: public_git/a/b/c/d.git
127 # Invalid: public_git/a/b/c/d/e.git
128 local n relative="${git_dir#$HOME/public_git/}"
129 for n in 1 2 3 4; do
130 local topmost="${relative%%/*}"
131 case "$topmost" in
132 "$relative") return 0;;
133 *.git) return 1;;
134 esac
135 relative=${relative#$topmost/}
136 done
137 echo 'Error: directories nest too deep' >&2
138 return 1
139 ;;
140 *) return 1 ;;
141 esac
142}
143
144check_if_ssh_user_owns_repository()
145{
146 git --git-dir "$git_dir" config --get-all samizdat.anonymous-ssh-owner | grep -xqF "$SSH_REMOTE_FINGERPRINT_TRIMMED"
147}
148ssh_user_owns_repository()
149{
150 if [ -z "$SSH_USER_OWNS_REPOSITORY" ]; then
151 check_if_ssh_user_owns_repository
152 SSH_USER_OWNS_REPOSITORY=$?
153 fi
154 return $SSH_USER_OWNS_REPOSITORY
155}
156
157authorized()
158{
159 # TODO: check SSH_REMOTE_FINGERPRINT against a blacklist
160 ssh_user_owns_repository && return
161 test "$(git --git-dir "$1" config --bool --get samizdat.allow-anonymous-access)" = true 2>/dev/null && return 0
162 # TODO: check SSH_REMOTE_FINGERPRINT against a whitelist
163}
164
165maybe_initialize_heads()
166{
167 [ "$GIT_NAMESPACE" ] || die 'Programmer error'
168 heads=$git_dir/refs/namespaces/$GIT_NAMESPACE/refs/heads
169 mkdir -p "$heads"
170 found_file=$(find "$heads" -type f -print -quit)
171 [ "$found_file" ] && return
172 [ -e "$git_dir/refs/heads/master" ] && cp "$git_dir/refs/heads/master" "$heads"
173}
174
175
176if [ "$1" = "authorize-full-access" ]; then
177 case "$SSH_ORIGINAL_COMMAND" in
178 git-receive-pack\ *)
179 git_cmd=git-receive-pack
180 git_dir="${SSH_ORIGINAL_COMMAND#git-receive-pack }"
181 dequote
182 homedir_expand
183 initialize_git "$git_dir"
184 exec "$git_cmd" "$git_dir"
185 ;;
186 "")
187 shell=$(getent passwd d|cut -d: -f7)
188 argv0=-${shell##*/}
189 exec chpst -b "$argv0" "$shell"
190 ;;
191 *)
192 exec /bin/sh -c "$SSH_ORIGINAL_COMMAND"
193 ;;
194 esac
195fi
196
197# TODO: call password_authentication on all authorization failures
198
199SSH_REMOTE_FINGERPRINT_TRIMMED=$(echo $SSH_REMOTE_FINGERPRINT|tr -d :)
200# echo "SSH_ORIGINAL_COMMAND=$SSH_ORIGINAL_COMMAND" >&2
201case "$SSH_ORIGINAL_COMMAND" in
202 git-receive-pack\ *)
203 git_cmd=git-receive-pack
204 git_dir="${SSH_ORIGINAL_COMMAND#git-receive-pack }"
205 export GIT_NAMESPACE="$SSH_REMOTE_FINGERPRINT_TRIMMED"
206 ;;
207 git-upload-pack\ *)
208 git_cmd=git-upload-pack
209 git_dir="${SSH_ORIGINAL_COMMAND#git-upload-pack }"
210 ;;
211 rsync\ --server\ --sender\ *)
212 [ -d "$HOME"/public_rsync ] || { password_authentication; exit 1; }
213 exec rrsync -ro "$HOME"/public_rsync
214 exit 1
215 ;;
216 rsync\ --server\ *)
217 [ -d "$HOME"/incoming_rsync ] || { password_authentication; exit 1; }
218 fp=$(echo $SSH_REMOTE_FINGERPRINT|tr -d :)
219 destdir=$HOME/incoming_rsync/$fp/
220 mkdir -p "$destdir" && exec rrsync "$destdir"
221 exit 1
222 ;;
223 *)
224 password_authentication
225 exit 1 # unreached
226 ;;
227esac
228
229dequote
230homedir_expand
231
232if [ -d "$git_dir" ]; then
233
234 is_gitdir "$git_dir" || git_dir="$git_dir/.git"
235
236 if ! is_gitdir "$git_dir"; then
237# git rev-parse --resolve-git-dir "${git_dir%/.git}" # show git's error message
238 deny
239 fi
240
241 if [ "$git_cmd" = 'git-upload-pack' ]; then
242 case "$git_dir" in
243 $HOME/public_git/*|public_git/*) exec "$git_cmd" "$git_dir";;
244 esac
245 fi
246
247elif [ "$git_cmd" = 'git-receive-pack' ] && valid_new_public_repo "$git_dir"; then
248
249 initialize_git "$git_dir" "$SSH_REMOTE_FINGERPRINT_TRIMMED"
250
251else
252 deny
253fi
254
255if authorized "$git_dir"; then
256 if [ "$git_cmd" = 'git-receive-pack' ]; then
257 if ssh_user_owns_repository; then
258 unset GIT_NAMESPACE
259 else
260 maybe_initialize_heads
261 fi
262 fi
263 exec "$git_cmd" "$git_dir"
264else
265 password_authentication
266 echo 'Error: git access is unauthorized' >&2; exit 1 # unreached
267fi