diff --git a/bin/ssh-auth-key b/bin/ssh-auth-key new file mode 120000 index 0000000000..7dff83c316 --- /dev/null +++ b/bin/ssh-auth-key @@ -0,0 +1 @@ +../scripts/ssh/ssh-auth-key.php \ No newline at end of file diff --git a/resources/sshd/phabricator-ssh-hook.sh b/resources/sshd/phabricator-ssh-hook.sh new file mode 100755 index 0000000000..e405729cef --- /dev/null +++ b/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" $@ diff --git a/resources/sshd/sshd_config.example b/resources/sshd/sshd_config.example new file mode 100644 index 0000000000..da3897fc8e --- /dev/null +++ b/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 diff --git a/scripts/ssh/ssh-auth-key.php b/scripts/ssh/ssh-auth-key.php new file mode 100755 index 0000000000..362e8d1be4 --- /dev/null +++ b/scripts/ssh/ssh-auth-key.php @@ -0,0 +1,61 @@ +#!/usr/bin/env php +establishConnection('r'); + +$row = queryfx_one( + $conn_r, + 'SELECT userName FROM %T u JOIN %T ssh ON u.phid = ssh.userPHID + WHERE ssh.keyType = %s AND ssh.keyBody = %s', + $user_dao->getTableName(), + $ssh_dao->getTableName(), + $type, + $body); + +if (!$row) { + exit(1); +} + +$user = idx($row, 'userName'); + +if (!$user) { + exit(1); +} + +if (!PhabricatorUser::validateUsername($user)) { + exit(1); +} + +$bin = $root.'/bin/ssh-exec'; +$cmd = csprintf('%s --phabricator-ssh-user %s', $bin, $user); +// This is additional escaping for the SSH 'command="..."' string. +$cmd = addcslashes($cmd, '"\\'); + +$options = array( + 'command="'.$cmd.'"', + 'no-port-forwarding', + 'no-X11-forwarding', + 'no-agent-forwarding', + 'no-pty', +); + +echo implode(',', $options); +exit(0); diff --git a/scripts/ssh/ssh-auth.php b/scripts/ssh/ssh-auth.php index 96e6ef389b..dc7b4d098e 100755 --- a/scripts/ssh/ssh-auth.php +++ b/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); - -if (!$row) { - exit(1); -} - -$user = idx($row, 'userName'); - -if (!$user) { - exit(1); -} - -if (!PhabricatorUser::validateUsername($user)) { - exit(1); -} + $ssh_dao->getTableName()); $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); +foreach ($rows as $row) { + $user = $row['userName']; -$options = array( - 'command="'.$cmd.'"', - 'no-port-forwarding', - 'no-X11-forwarding', - 'no-agent-forwarding', - 'no-pty', -); + $cmd = csprintf('%s --phabricator-ssh-user %s', $bin, $user); + // This is additional escaping for the SSH 'command="..."' string. + $cmd = addcslashes($cmd, '"\\'); -echo implode(',', $options); + // 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); + + + $options = array( + 'command="'.$cmd.'"', + 'no-port-forwarding', + 'no-X11-forwarding', + 'no-agent-forwarding', + 'no-pty', + ); + $options = implode(',', $options); + + $lines[] = $options.' '.$type.' '.$key."\n"; +} + +echo implode('', $lines); exit(0); diff --git a/scripts/ssh/ssh-exec.php b/scripts/ssh/ssh-exec.php index a9c639f75d..6bd5b71ef3 100755 --- a/scripts/ssh/ssh-exec.php +++ b/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 @@ try { 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 @@ try { $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) { diff --git a/src/applications/conduit/ssh/ConduitSSHWorkflow.php b/src/applications/conduit/ssh/ConduitSSHWorkflow.php index ba456efaca..74221b51dc 100644 --- a/src/applications/conduit/ssh/ConduitSSHWorkflow.php +++ b/src/applications/conduit/ssh/ConduitSSHWorkflow.php @@ -31,7 +31,7 @@ final class ConduitSSHWorkflow extends PhabricatorSSHWorkflow { 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__']);