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