1# $OpenBSD: agent-restrict.sh,v 1.6 2023/03/01 09:29:32 dtucker Exp $ 2# Placed in the Public Domain. 3 4tid="agent restrictions" 5 6SSH_AUTH_SOCK="$OBJ/agent.sock" 7export SSH_AUTH_SOCK 8rm -f $SSH_AUTH_SOCK $OBJ/agent.log $OBJ/host_[abcdex]* $OBJ/user_[abcdex]* 9rm -f $OBJ/sshd_proxy_host* $OBJ/ssh_output* $OBJ/expect_* 10rm -f $OBJ/ssh_proxy[._]* $OBJ/command 11 12verbose "generate keys" 13for h in a b c d e x ca ; do 14 $SSHKEYGEN -q -t ed25519 -C host_$h -N '' -f $OBJ/host_$h || \ 15 fatal "ssh-keygen hostkey failed" 16 $SSHKEYGEN -q -t ed25519 -C user_$h -N '' -f $OBJ/user_$h || \ 17 fatal "ssh-keygen userkey failed" 18done 19 20# Make some hostcerts 21for h in d e ; do 22 id="host_$h" 23 $SSHKEYGEN -q -s $OBJ/host_ca -I $id -n $id -h $OBJ/host_${h}.pub || \ 24 fatal "ssh-keygen certify failed" 25done 26 27verbose "prepare client config" 28egrep -vi '(identityfile|hostname|hostkeyalias|proxycommand)' \ 29 $OBJ/ssh_proxy > $OBJ/ssh_proxy.bak 30cat << _EOF > $OBJ/ssh_proxy 31IdentitiesOnly yes 32ForwardAgent yes 33ExitOnForwardFailure yes 34_EOF 35cp $OBJ/ssh_proxy $OBJ/ssh_proxy_noid 36for h in a b c d e ; do 37 cat << _EOF >> $OBJ/ssh_proxy 38Host host_$h 39 Hostname host_$h 40 HostkeyAlias host_$h 41 IdentityFile $OBJ/user_$h 42 ProxyCommand ${SUDO} env SSH_SK_HELPER=\"$SSH_SK_HELPER\" ${OBJ}/sshd-log-wrapper.sh -i -f $OBJ/sshd_proxy_host_$h 43_EOF 44 # Variant with no specified keys. 45 cat << _EOF >> $OBJ/ssh_proxy_noid 46Host host_$h 47 Hostname host_$h 48 HostkeyAlias host_$h 49 ProxyCommand ${SUDO} env SSH_SK_HELPER=\"$SSH_SK_HELPER\" ${OBJ}/sshd-log-wrapper.sh -i -f $OBJ/sshd_proxy_host_$h 50_EOF 51done 52cat $OBJ/ssh_proxy.bak >> $OBJ/ssh_proxy 53cat $OBJ/ssh_proxy.bak >> $OBJ/ssh_proxy_noid 54 55LC_ALL=C 56export LC_ALL 57echo "SetEnv LC_ALL=${LC_ALL}" >> sshd_proxy 58 59verbose "prepare known_hosts" 60rm -f $OBJ/known_hosts 61for h in a b c x ; do 62 (printf "host_$h " ; cat $OBJ/host_${h}.pub) >> $OBJ/known_hosts 63done 64(printf "@cert-authority host_* " ; cat $OBJ/host_ca.pub) >> $OBJ/known_hosts 65 66verbose "prepare server configs" 67egrep -vi '(hostkey|pidfile)' $OBJ/sshd_proxy \ 68 > $OBJ/sshd_proxy.bak 69for h in a b c d e; do 70 cp $OBJ/sshd_proxy.bak $OBJ/sshd_proxy_host_$h 71 cat << _EOF >> $OBJ/sshd_proxy_host_$h 72ExposeAuthInfo yes 73PidFile none 74Hostkey $OBJ/host_$h 75_EOF 76done 77for h in d e ; do 78 echo "HostCertificate $OBJ/host_${h}-cert.pub" \ 79 >> $OBJ/sshd_proxy_host_$h 80done 81# Create authorized_keys with canned command. 82reset_keys() { 83 _whichcmd="$1" 84 _command="" 85 case "$_whichcmd" in 86 authinfo) _command="cat \$SSH_USER_AUTH" ;; 87 keylist) _command="$SSHADD -L | cut -d' ' -f-2 | sort" ;; 88 *) fatal "unsupported command $_whichcmd" ;; 89 esac 90 trace "reset keys" 91 >$OBJ/authorized_keys_$USER 92 for h in e d c b a; do 93 (printf "%s" "restrict,agent-forwarding,command=\"$_command\" "; 94 cat $OBJ/user_$h.pub) >> $OBJ/authorized_keys_$USER 95 done 96} 97# Prepare a key for comparison with ExposeAuthInfo/$SSH_USER_AUTH. 98expect_key() { 99 _key="$OBJ/${1}.pub" 100 _file="$OBJ/$2" 101 (printf "publickey " ; cut -d' ' -f-2 $_key) > $_file 102} 103# Prepare expect_* files to compare against authinfo forced command to ensure 104# keys used for authentication match. 105reset_expect_keys() { 106 for u in a b c d e; do 107 expect_key user_$u expect_$u 108 done 109} 110# ssh to host, expecting success and that output matched expectation for 111# that host (expect_$h file). 112expect_succeed() { 113 _id="$1" 114 _case="$2" 115 shift; shift; _extra="$@" 116 _host="host_$_id" 117 trace "connect $_host expect success" 118 rm -f $OBJ/ssh_output 119 ${SSH} $_extra -F $OBJ/ssh_proxy $_host true > $OBJ/ssh_output 120 _s=$? 121 test $_s -eq 0 || fail "host $_host $_case fail, exit status $_s" 122 diff $OBJ/ssh_output $OBJ/expect_${_id} || 123 fail "unexpected ssh output" 124} 125# ssh to host using explicit key, expecting success and that the key was 126# actually used for authentication. 127expect_succeed_key() { 128 _id="$1" 129 _key="$2" 130 _case="$3" 131 shift; shift; shift; _extra="$@" 132 _host="host_$_id" 133 trace "connect $_host expect success, with key $_key" 134 _keyfile="$OBJ/$_key" 135 rm -f $OBJ/ssh_output 136 ${SSH} $_extra -F $OBJ/ssh_proxy_noid \ 137 -oIdentityFile=$_keyfile $_host true > $OBJ/ssh_output 138 _s=$? 139 test $_s -eq 0 || fail "host $_host $_key $_case fail, exit status $_s" 140 expect_key $_key expect_key 141 diff $OBJ/ssh_output $OBJ/expect_key || 142 fail "incorrect key used for authentication" 143} 144# ssh to a host, expecting it to fail. 145expect_fail() { 146 _host="$1" 147 _case="$2" 148 shift; shift; _extra="$@" 149 trace "connect $_host expect failure" 150 ${SSH} $_extra -F $OBJ/ssh_proxy $_host true >/dev/null && \ 151 fail "host $_host $_case succeeded unexpectedly" 152} 153# ssh to a host using an explicit key, expecting it to fail. 154expect_fail_key() { 155 _id="$1" 156 _key="$2" 157 _case="$3" 158 shift; shift; shift; _extra="$@" 159 _host="host_$_id" 160 trace "connect $_host expect failure, with key $_key" 161 _keyfile="$OBJ/$_key" 162 ${SSH} $_extra -F $OBJ/ssh_proxy_noid -oIdentityFile=$_keyfile \ 163 $_host true > $OBJ/ssh_output && \ 164 fail "host $_host $_key $_case succeeded unexpectedly" 165} 166# Move the private key files out of the way to force use of agent-hosted keys. 167hide_privatekeys() { 168 trace "hide private keys" 169 for u in a b c d e x; do 170 mv $OBJ/user_$u $OBJ/user_x$u || fatal "hide privkey $u" 171 done 172} 173# Put the private key files back. 174restore_privatekeys() { 175 trace "restore private keys" 176 for u in a b c d e x; do 177 mv $OBJ/user_x$u $OBJ/user_$u || fatal "restore privkey $u" 178 done 179} 180clear_agent() { 181 ${SSHADD} -D > /dev/null 2>&1 || fatal "clear agent failed" 182} 183 184reset_keys authinfo 185reset_expect_keys 186 187verbose "authentication w/o agent" 188for h in a b c d e ; do 189 expect_succeed $h "w/o agent" 190 wrongkey=user_e 191 test "$h" = "e" && wrongkey=user_a 192 expect_succeed_key $h $wrongkey "\"wrong\" key w/o agent" 193done 194hide_privatekeys 195for h in a b c d e ; do 196 expect_fail $h "w/o agent" 197done 198restore_privatekeys 199 200verbose "start agent" 201${SSHAGENT} ${EXTRA_AGENT_ARGS} -d -a $SSH_AUTH_SOCK > $OBJ/agent.log 2>&1 & 202AGENT_PID=$! 203trap "kill $AGENT_PID" EXIT 204sleep 4 # Give it a chance to start 205# Check that it's running. 206${SSHADD} -l > /dev/null 2>&1 207if [ $? -ne 1 ]; then 208 fail "ssh-add -l did not fail with exit code 1" 209fi 210 211verbose "authentication with agent (no restrict)" 212for u in a b c d e x; do 213 $SSHADD -q $OBJ/user_$u || fatal "add key $u unrestricted" 214done 215hide_privatekeys 216for h in a b c d e ; do 217 expect_succeed $h "with agent" 218 wrongkey=user_e 219 test "$h" = "e" && wrongkey=user_a 220 expect_succeed_key $h $wrongkey "\"wrong\" key with agent" 221done 222 223verbose "unrestricted keylist" 224reset_keys keylist 225rm -f $OBJ/expect_list.pre 226# List of keys from agent should contain everything. 227for u in a b c d e x; do 228 cut -d " " -f-2 $OBJ/user_${u}.pub >> $OBJ/expect_list.pre 229done 230sort $OBJ/expect_list.pre > $OBJ/expect_list 231for h in a b c d e; do 232 cp $OBJ/expect_list $OBJ/expect_$h 233 expect_succeed $h "unrestricted keylist" 234done 235restore_privatekeys 236 237verbose "authentication with agent (basic restrict)" 238reset_keys authinfo 239reset_expect_keys 240for h in a b c d e; do 241 $SSHADD -h host_$h -H $OBJ/known_hosts -q $OBJ/user_$h \ 242 || fatal "add key $u basic restrict" 243done 244# One more, unrestricted 245$SSHADD -q $OBJ/user_x || fatal "add unrestricted key" 246hide_privatekeys 247# Authentication to host with expected key should work. 248for h in a b c d e ; do 249 expect_succeed $h "with agent" 250done 251# Authentication to host with incorrect key should fail. 252verbose "authentication with agent incorrect key (basic restrict)" 253for h in a b c d e ; do 254 wrongkey=user_e 255 test "$h" = "e" && wrongkey=user_a 256 expect_fail_key $h $wrongkey "wrong key with agent (basic restrict)" 257done 258 259verbose "keylist (basic restrict)" 260reset_keys keylist 261# List from forwarded agent should contain only user_x - the unrestricted key. 262cut -d " " -f-2 $OBJ/user_x.pub > $OBJ/expect_list 263for h in a b c d e; do 264 cp $OBJ/expect_list $OBJ/expect_$h 265 expect_succeed $h "keylist (basic restrict)" 266done 267restore_privatekeys 268 269verbose "username" 270reset_keys authinfo 271reset_expect_keys 272for h in a b c d e; do 273 $SSHADD -h "${USER}@host_$h" -H $OBJ/known_hosts -q $OBJ/user_$h \ 274 || fatal "add key $u basic restrict" 275done 276hide_privatekeys 277for h in a b c d e ; do 278 expect_succeed $h "wildcard user" 279done 280restore_privatekeys 281 282verbose "username wildcard" 283reset_keys authinfo 284reset_expect_keys 285for h in a b c d e; do 286 $SSHADD -h "*@host_$h" -H $OBJ/known_hosts -q $OBJ/user_$h \ 287 || fatal "add key $u basic restrict" 288done 289hide_privatekeys 290for h in a b c d e ; do 291 expect_succeed $h "wildcard user" 292done 293restore_privatekeys 294 295verbose "username incorrect" 296reset_keys authinfo 297reset_expect_keys 298for h in a b c d e; do 299 $SSHADD -h "--BADUSER@host_$h" -H $OBJ/known_hosts -q $OBJ/user_$h \ 300 || fatal "add key $u basic restrict" 301done 302hide_privatekeys 303for h in a b c d e ; do 304 expect_fail $h "incorrect user" 305done 306restore_privatekeys 307 308 309verbose "agent restriction honours certificate principal" 310reset_keys authinfo 311reset_expect_keys 312clear_agent 313$SSHADD -h host_e -H $OBJ/known_hosts -q $OBJ/user_d || fatal "add key" 314hide_privatekeys 315expect_fail d "restricted agent w/ incorrect cert principal" 316restore_privatekeys 317 318# Prepares the script used to drive chained ssh connections for the 319# multihop tests. Believe me, this is easier than getting the escaping 320# right for 5 hops on the command-line... 321prepare_multihop_script() { 322 MULTIHOP_RUN=$OBJ/command 323 cat << _EOF > $MULTIHOP_RUN 324#!/bin/sh 325#set -x 326me="\$1" ; shift 327next="\$1" 328if test ! -z "\$me" ; then 329 rm -f $OBJ/done 330 echo "HOSTNAME host_\$me" 331 echo "AUTHINFO" 332 cat \$SSH_USER_AUTH 333fi 334echo AGENT 335$SSHADD -L | egrep "^ssh" | cut -d" " -f-2 | sort 336if test -z "\$next" ; then 337 touch $OBJ/done 338 echo "FINISH" 339 e=0 340else 341 echo NEXT 342 ${SSH} -F $OBJ/ssh_proxy_noid -oIdentityFile=$OBJ/user_a \ 343 host_\$next $MULTIHOP_RUN "\$@" 344 e=\$? 345fi 346echo "COMPLETE \"\$me\"" 347if test ! -z "\$me" ; then 348 if test ! -f $OBJ/done ; then 349 echo "DONE MARKER MISSING" 350 test \$e -eq 0 && e=63 351 fi 352fi 353exit \$e 354_EOF 355 chmod u+x $MULTIHOP_RUN 356} 357 358# Prepare expected output for multihop tests at expect_a 359prepare_multihop_expected() { 360 _keys="$1" 361 _hops="a b c d e" 362 test -z "$2" || _hops="$2" 363 _revhops=$(echo "$_hops" | rev) 364 _lasthop=$(echo "$_hops" | sed 's/.* //') 365 366 rm -f $OBJ/expect_keys 367 for h in a b c d e; do 368 cut -d" " -f-2 $OBJ/user_${h}.pub >> $OBJ/expect_keys 369 done 370 rm -f $OBJ/expect_a 371 echo "AGENT" >> $OBJ/expect_a 372 test "x$_keys" = "xnone" || sort $OBJ/expect_keys >> $OBJ/expect_a 373 echo "NEXT" >> $OBJ/expect_a 374 for h in $_hops ; do 375 echo "HOSTNAME host_$h" >> $OBJ/expect_a 376 echo "AUTHINFO" >> $OBJ/expect_a 377 (printf "publickey " ; cut -d" " -f-2 $OBJ/user_a.pub) >> $OBJ/expect_a 378 echo "AGENT" >> $OBJ/expect_a 379 if test "x$_keys" = "xall" ; then 380 sort $OBJ/expect_keys >> $OBJ/expect_a 381 fi 382 if test "x$h" != "x$_lasthop" ; then 383 if test "x$_keys" = "xfiltered" ; then 384 cut -d" " -f-2 $OBJ/user_a.pub >> $OBJ/expect_a 385 fi 386 echo "NEXT" >> $OBJ/expect_a 387 fi 388 done 389 echo "FINISH" >> $OBJ/expect_a 390 for h in $_revhops "" ; do 391 echo "COMPLETE \"$h\"" >> $OBJ/expect_a 392 done 393} 394 395prepare_multihop_script 396cp $OBJ/user_a.pub $OBJ/authorized_keys_$USER # only one key used. 397 398verbose "multihop without agent" 399clear_agent 400prepare_multihop_expected none 401$MULTIHOP_RUN "" a b c d e > $OBJ/ssh_output || fail "multihop no agent ssh failed" 402diff $OBJ/ssh_output $OBJ/expect_a || fail "unexpected ssh output" 403 404verbose "multihop agent unrestricted" 405clear_agent 406$SSHADD -q $OBJ/user_[abcde] 407prepare_multihop_expected all 408$MULTIHOP_RUN "" a b c d e > $OBJ/ssh_output || fail "multihop no agent ssh failed" 409diff $OBJ/ssh_output $OBJ/expect_a || fail "unexpected ssh output" 410 411verbose "multihop restricted" 412clear_agent 413prepare_multihop_expected filtered 414# Add user_a, with permission to connect through the whole chain. 415$SSHADD -h host_a -h "host_a>host_b" -h "host_b>host_c" \ 416 -h "host_c>host_d" -h "host_d>host_e" \ 417 -H $OBJ/known_hosts -q $OBJ/user_a \ 418 || fatal "add key user_a multihop" 419# Add the other keys, bound to a unused host. 420$SSHADD -q -h host_x -H $OBJ/known_hosts $OBJ/user_[bcde] || fail "add keys" 421hide_privatekeys 422$MULTIHOP_RUN "" a b c d e > $OBJ/ssh_output || fail "multihop ssh failed" 423diff $OBJ/ssh_output $OBJ/expect_a || fail "unexpected ssh output" 424restore_privatekeys 425 426verbose "multihop username" 427$SSHADD -h host_a -h "host_a>${USER}@host_b" -h "host_b>${USER}@host_c" \ 428 -h "host_c>${USER}@host_d" -h "host_d>${USER}@host_e" \ 429 -H $OBJ/known_hosts -q $OBJ/user_a || fatal "add key user_a multihop" 430hide_privatekeys 431$MULTIHOP_RUN "" a b c d e > $OBJ/ssh_output || fail "multihop w/ user ssh failed" 432diff $OBJ/ssh_output $OBJ/expect_a || fail "unexpected ssh output" 433restore_privatekeys 434 435verbose "multihop wildcard username" 436$SSHADD -h host_a -h "host_a>*@host_b" -h "host_b>*@host_c" \ 437 -h "host_c>*@host_d" -h "host_d>*@host_e" \ 438 -H $OBJ/known_hosts -q $OBJ/user_a || fatal "add key user_a multihop" 439hide_privatekeys 440$MULTIHOP_RUN "" a b c d e > $OBJ/ssh_output || fail "multihop w/ user ssh failed" 441diff $OBJ/ssh_output $OBJ/expect_a || fail "unexpected ssh output" 442restore_privatekeys 443 444verbose "multihop wrong username" 445$SSHADD -h host_a -h "host_a>*@host_b" -h "host_b>*@host_c" \ 446 -h "host_c>--BADUSER@host_d" -h "host_d>*@host_e" \ 447 -H $OBJ/known_hosts -q $OBJ/user_a || fatal "add key user_a multihop" 448hide_privatekeys 449$MULTIHOP_RUN "" a b c d e > $OBJ/ssh_output && \ 450 fail "multihop with wrong user succeeded unexpectedly" 451restore_privatekeys 452 453verbose "multihop cycle no agent" 454clear_agent 455prepare_multihop_expected none "a b a a c d e" 456$MULTIHOP_RUN "" a b a a c d e > $OBJ/ssh_output || \ 457 fail "multihop cycle no-agent fail" 458diff $OBJ/ssh_output $OBJ/expect_a || fail "unexpected ssh output" 459 460verbose "multihop cycle agent unrestricted" 461clear_agent 462$SSHADD -q $OBJ/user_[abcde] || fail "add keys" 463prepare_multihop_expected all "a b a a c d e" 464$MULTIHOP_RUN "" a b a a c d e > $OBJ/ssh_output || \ 465 fail "multihop cycle agent ssh failed" 466diff $OBJ/ssh_output $OBJ/expect_a || fail "unexpected ssh output" 467 468verbose "multihop cycle restricted deny" 469clear_agent 470$SSHADD -q -h host_x -H $OBJ/known_hosts $OBJ/user_[bcde] || fail "add keys" 471$SSHADD -h host_a -h "host_a>host_b" -h "host_b>host_c" \ 472 -h "host_c>host_d" -h "host_d>host_e" \ 473 -H $OBJ/known_hosts -q $OBJ/user_a \ 474 || fatal "add key user_a multihop" 475prepare_multihop_expected filtered "a b a a c d e" 476hide_privatekeys 477$MULTIHOP_RUN "" a b a a c d e > $OBJ/ssh_output && \ 478 fail "multihop cycle restricted deny succeded unexpectedly" 479restore_privatekeys 480 481verbose "multihop cycle restricted allow" 482clear_agent 483$SSHADD -q -h host_x -H $OBJ/known_hosts $OBJ/user_[bcde] || fail "add keys" 484$SSHADD -h host_a -h "host_a>host_b" -h "host_b>host_c" \ 485 -h "host_c>host_d" -h "host_d>host_e" \ 486 -h "host_b>host_a" -h "host_a>host_a" -h "host_a>host_c" \ 487 -H $OBJ/known_hosts -q $OBJ/user_a \ 488 || fatal "add key user_a multihop" 489prepare_multihop_expected filtered "a b a a c d e" 490hide_privatekeys 491$MULTIHOP_RUN "" a b a a c d e > $OBJ/ssh_output || \ 492 fail "multihop cycle restricted allow failed" 493diff $OBJ/ssh_output $OBJ/expect_a || fail "unexpected ssh output" 494restore_privatekeys 495 496