diff options
author | Andrew Cady <d@jerkface.net> | 2019-07-17 00:59:13 -0400 |
---|---|---|
committer | Andrew Cady <d@jerkface.net> | 2019-07-17 00:59:13 -0400 |
commit | 0bcab35657ac1250ccc4bff714f7f4db0c5eaed5 (patch) | |
tree | b20c0327395fe3165bbe0bbbdea71d7374ddfce8 | |
parent | 89f1ccc7dcfff9b9e70cbd9c2967d90d992612c9 (diff) |
forced-ssh-command
-rwxr-xr-x | forced-ssh-command | 267 |
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 | ||
2 | authorize() | ||
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 | } | ||
17 | add_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 | |||
25 | password_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 | |||
59 | die() { echo "Error: $*" >&2; exit 1; } | ||
60 | |||
61 | dequote() | ||
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 | |||
67 | homedir_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 | |||
73 | homedir_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 | |||
90 | initialize_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 | |||
103 | is_gitdir() { git rev-parse --resolve-git-dir "$1" >/dev/null 2>&1; } | ||
104 | |||
105 | deny() { echo 'Error: permission denied.' >&2; exit 1; } | ||
106 | |||
107 | valid_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 | |||
144 | check_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 | } | ||
148 | ssh_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 | |||
157 | authorized() | ||
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 | |||
165 | maybe_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 | |||
176 | if [ "$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 | ||
195 | fi | ||
196 | |||
197 | # TODO: call password_authentication on all authorization failures | ||
198 | |||
199 | SSH_REMOTE_FINGERPRINT_TRIMMED=$(echo $SSH_REMOTE_FINGERPRINT|tr -d :) | ||
200 | # echo "SSH_ORIGINAL_COMMAND=$SSH_ORIGINAL_COMMAND" >&2 | ||
201 | case "$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 | ;; | ||
227 | esac | ||
228 | |||
229 | dequote | ||
230 | homedir_expand | ||
231 | |||
232 | if [ -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 | |||
247 | elif [ "$git_cmd" = 'git-receive-pack' ] && valid_new_public_repo "$git_dir"; then | ||
248 | |||
249 | initialize_git "$git_dir" "$SSH_REMOTE_FINGERPRINT_TRIMMED" | ||
250 | |||
251 | else | ||
252 | deny | ||
253 | fi | ||
254 | |||
255 | if 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" | ||
264 | else | ||
265 | password_authentication | ||
266 | echo 'Error: git access is unauthorized' >&2; exit 1 # unreached | ||
267 | fi | ||