Index: bin/ssh-auth-key =================================================================== --- /dev/null +++ bin/ssh-auth-key @@ -0,0 +1 @@ +../scripts/ssh/ssh-auth-key.php \ No newline at end of file Index: resources/sshd/phabricator-ssh-hook.sh =================================================================== --- /dev/null +++ resources/sshd/phabricator-ssh-hook.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +### +### WARNING: This feature is new and experimental. Use it at your own risk! +### + +ROOT=/INSECURE/devtools/phabricator +exec "$ROOT/bin/ssh-auth" $@ Index: resources/sshd/sshd_config.example =================================================================== --- /dev/null +++ resources/sshd/sshd_config.example @@ -0,0 +1,24 @@ +### +### WARNING: This feature is new and experimental. Use it at your own risk! +### + +# You must have OpenSSHD 6.2 or newer; support for AuthorizedKeysCommand was +# added in this version. + +Port 2222 +AuthorizedKeysCommand /etc/phabricator-ssh-hook.sh +AuthorizedKeysCommandUser some-unprivileged-user + +# You may need to tweak these options, but mostly they just turn off everything +# dangerous. + +Protocol 2 +PermitRootLogin no +AllowAgentForwarding no +AllowTcpForwarding no +PrintMotd no +PrintLastLog no +PasswordAuthentication no +AuthorizedKeysFile none + +PidFile /var/run/sshd-phabricator.pid Index: scripts/ssh/ssh-auth-key.php =================================================================== --- scripts/ssh/ssh-auth-key.php +++ scripts/ssh/ssh-auth-key.php @@ -47,7 +47,7 @@ $bin = $root.'/bin/ssh-exec'; $cmd = csprintf('%s --phabricator-ssh-user %s', $bin, $user); // This is additional escaping for the SSH 'command="..."' string. -$cmd = str_replace('"', '\\"', $cmd); +$cmd = addcslashes($cmd, '"\\'); $options = array( 'command="'.$cmd.'"', Index: scripts/ssh/ssh-auth.php =================================================================== --- scripts/ssh/ssh-auth.php +++ scripts/ssh/ssh-auth.php @@ -4,58 +4,45 @@ $root = dirname(dirname(dirname(__FILE__))); require_once $root.'/scripts/__init_script__.php'; -$cert = file_get_contents('php://stdin'); - -if (!$cert) { - exit(1); -} - -$parts = preg_split('/\s+/', $cert); -if (count($parts) < 2) { - exit(1); -} - -list($type, $body) = $parts; - $user_dao = new PhabricatorUser(); $ssh_dao = new PhabricatorUserSSHKey(); $conn_r = $user_dao->establishConnection('r'); -$row = queryfx_one( +$rows = queryfx_all( $conn_r, - 'SELECT userName FROM %T u JOIN %T ssh ON u.phid = ssh.userPHID - WHERE ssh.keyType = %s AND ssh.keyBody = %s', + 'SELECT userName, keyBody, keyType FROM %T u JOIN %T ssh + ON u.phid = ssh.userPHID', $user_dao->getTableName(), - $ssh_dao->getTableName(), - $type, - $body); + $ssh_dao->getTableName()); -if (!$row) { - exit(1); -} +$bin = $root.'/bin/ssh-exec'; +foreach ($rows as $row) { + $user = $row['userName']; -$user = idx($row, 'userName'); + $cmd = csprintf('%s --phabricator-ssh-user %s', $bin, $user); + // This is additional escaping for the SSH 'command="..."' string. + $cmd = addcslashes($cmd, '"\\'); -if (!$user) { - exit(1); -} + // Strip out newlines and other nonsense from the key type and key body. + + $type = $row['keyType']; + $type = preg_replace('@[\x00-\x20]+@', '', $type); + + $key = $row['keyBody']; + $key = preg_replace('@[\x00-\x20]+@', '', $key); -if (!PhabricatorUser::validateUsername($user)) { - exit(1); + + $options = array( + 'command="'.$cmd.'"', + 'no-port-forwarding', + 'no-X11-forwarding', + 'no-agent-forwarding', + 'no-pty', + ); + $options = implode(',', $options); + + $lines[] = $options.' '.$type.' '.$key."\n"; } -$bin = $root.'/bin/ssh-exec'; -$cmd = csprintf('%s --phabricator-ssh-user %s', $bin, $user); -// This is additional escaping for the SSH 'command="..."' string. -$cmd = str_replace('"', '\\"', $cmd); - -$options = array( - 'command="'.$cmd.'"', - 'no-port-forwarding', - 'no-X11-forwarding', - 'no-agent-forwarding', - 'no-pty', -); - -echo implode(',', $options); +echo implode('', $lines); exit(0); Index: scripts/ssh/ssh-exec.php =================================================================== --- scripts/ssh/ssh-exec.php +++ scripts/ssh/ssh-exec.php @@ -4,29 +4,25 @@ $root = dirname(dirname(dirname(__FILE__))); require_once $root.'/scripts/__init_script__.php'; -$original_command = getenv('SSH_ORIGINAL_COMMAND'); -$original_argv = id(new PhutilShellLexer())->splitArguments($original_command); -$argv = array_merge($argv, $original_argv); - +// First, figure out the authenticated user. $args = new PhutilArgumentParser($argv); $args->setTagline('receive SSH requests'); $args->setSynopsis(<<parsePartial( +$args->parse( array( array( 'name' => 'phabricator-ssh-user', 'param' => 'username', ), + array( + 'name' => 'ssh-command', + 'param' => 'command', + ), )); try { @@ -46,24 +42,33 @@ throw new Exception("You have been exiled."); } + if ($args->getArg('ssh-command')) { + $original_command = $args->getArg('ssh-command'); + } else { + $original_command = getenv('SSH_ORIGINAL_COMMAND'); + } + + // Now, rebuild the original command. + $original_argv = id(new PhutilShellLexer()) + ->splitArguments($original_command); + if (!$original_argv) { + throw new Exception("No interactive logins."); + } + $command = head($original_argv); + array_unshift($original_argv, 'phabricator-ssh-exec'); + + $original_args = new PhutilArgumentParser($original_argv); + $workflows = array( new ConduitSSHWorkflow(), ); - // This duplicates logic in parseWorkflows(), but allows us to raise more - // concise/relevant exceptions when the client is a remote SSH. - $remain = $args->getUnconsumedArgumentVector(); - if (empty($remain)) { - throw new Exception("No interactive logins."); - } else { - $command = head($remain); - $workflow_names = mpull($workflows, 'getName', 'getName'); - if (empty($workflow_names[$command])) { - throw new Exception("Invalid command."); - } + $workflow_names = mpull($workflows, 'getName', 'getName'); + if (empty($workflow_names[$command])) { + throw new Exception("Invalid command."); } - $workflow = $args->parseWorkflows($workflows); + $workflow = $original_args->parseWorkflows($workflows); $workflow->setUser($user); $sock_stdin = fopen('php://stdin', 'r'); @@ -82,7 +87,7 @@ $metrics_channel = new PhutilMetricsChannel($socket_channel); $workflow->setIOChannel($metrics_channel); - $err = $workflow->execute($args); + $err = $workflow->execute($original_args); $metrics_channel->flush(); } catch (Exception $ex) { Index: src/applications/conduit/ssh/ConduitSSHWorkflow.php =================================================================== --- src/applications/conduit/ssh/ConduitSSHWorkflow.php +++ src/applications/conduit/ssh/ConduitSSHWorkflow.php @@ -31,7 +31,7 @@ throw new Exception("Invalid JSON input."); } - $params = idx($raw_params, 'params', array()); + $params = idx($raw_params, 'params', '[]'); $params = json_decode($params, true); $metadata = idx($params, '__conduit__', array()); unset($params['__conduit__']);