@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Prepare to route VCS connections through SSH

Summary:
Fixes T2229. This sets the stage for a patch similar to D7417, but for SSH. In particular, SSH 6.2 introduced an `AuthorizedKeysCommand` directive, which lets us do this in a mostly-reasonable way without needing users to patch sshd (if they have a recent enough version, at least).

The way the `AuthorizedKeysCommand` works is that it gets run and produces an `authorized_keys`-style file fragment. This isn't ideal, because we have to dump every key into the result, but should be fine for most installs. The earlier patch against `sshd` passes the public key itself, which allows the script to just look up the key. We might use this eventually, since it can scale much better, so I haven't removed it.

Generally, auth is split into two scripts now which mostly do the same thing:

- `ssh-auth` is the AuthorizedKeysCommand auth, which takes nothing and dumps the whole keyfile.
- `ssh-auth-key` is the slightly cleaner and more scalable (but patch-dependent) version, which takes the public key and dumps only matching options.

I also reworked the argument parsing to be a bit more sane.

Test Plan:
This is somewhat-intentionally a bit obtuse since I don't really want anyone using it yet, but basically:

- Copy `phabricator-ssh-hook.sh` to somewhere like `/usr/libexec/openssh/`, chown it `root` and chmod it `500`.
- This script should probably also do a username check in the future.
- Create a copy of `sshd_config` and fix the paths/etc. Point the KeyScript at your copy of the hook.
- Start a copy of sshd (6.2 or newer) with `-f <your config file>` and maybe `-d -d -d` to foreground and debug.
- Run `ssh -p 2222 localhost` or similar.

Specifically, I did this setup and then ran a bunch of commands like:

- `ssh host` (denied, no command)
- `ssh host ls` (denied, not supported)
- `echo '{}' | ssh host conduit conduit.ping` (works)

Reviewers: btrahan

Reviewed By: btrahan

CC: hach-que, aran

Maniphest Tasks: T2229, T2230

Differential Revision: https://secure.phabricator.com/D7419

+152 -66
+1
bin/ssh-auth-key
··· 1 + ../scripts/ssh/ssh-auth-key.php
+8
resources/sshd/phabricator-ssh-hook.sh
··· 1 + #!/bin/sh 2 + 3 + ### 4 + ### WARNING: This feature is new and experimental. Use it at your own risk! 5 + ### 6 + 7 + ROOT=/INSECURE/devtools/phabricator 8 + exec "$ROOT/bin/ssh-auth" $@
+24
resources/sshd/sshd_config.example
··· 1 + ### 2 + ### WARNING: This feature is new and experimental. Use it at your own risk! 3 + ### 4 + 5 + # You must have OpenSSHD 6.2 or newer; support for AuthorizedKeysCommand was 6 + # added in this version. 7 + 8 + Port 2222 9 + AuthorizedKeysCommand /etc/phabricator-ssh-hook.sh 10 + AuthorizedKeysCommandUser some-unprivileged-user 11 + 12 + # You may need to tweak these options, but mostly they just turn off everything 13 + # dangerous. 14 + 15 + Protocol 2 16 + PermitRootLogin no 17 + AllowAgentForwarding no 18 + AllowTcpForwarding no 19 + PrintMotd no 20 + PrintLastLog no 21 + PasswordAuthentication no 22 + AuthorizedKeysFile none 23 + 24 + PidFile /var/run/sshd-phabricator.pid
+61
scripts/ssh/ssh-auth-key.php
··· 1 + #!/usr/bin/env php 2 + <?php 3 + 4 + $root = dirname(dirname(dirname(__FILE__))); 5 + require_once $root.'/scripts/__init_script__.php'; 6 + 7 + $cert = file_get_contents('php://stdin'); 8 + 9 + if (!$cert) { 10 + exit(1); 11 + } 12 + 13 + $parts = preg_split('/\s+/', $cert); 14 + if (count($parts) < 2) { 15 + exit(1); 16 + } 17 + 18 + list($type, $body) = $parts; 19 + 20 + $user_dao = new PhabricatorUser(); 21 + $ssh_dao = new PhabricatorUserSSHKey(); 22 + $conn_r = $user_dao->establishConnection('r'); 23 + 24 + $row = queryfx_one( 25 + $conn_r, 26 + 'SELECT userName FROM %T u JOIN %T ssh ON u.phid = ssh.userPHID 27 + WHERE ssh.keyType = %s AND ssh.keyBody = %s', 28 + $user_dao->getTableName(), 29 + $ssh_dao->getTableName(), 30 + $type, 31 + $body); 32 + 33 + if (!$row) { 34 + exit(1); 35 + } 36 + 37 + $user = idx($row, 'userName'); 38 + 39 + if (!$user) { 40 + exit(1); 41 + } 42 + 43 + if (!PhabricatorUser::validateUsername($user)) { 44 + exit(1); 45 + } 46 + 47 + $bin = $root.'/bin/ssh-exec'; 48 + $cmd = csprintf('%s --phabricator-ssh-user %s', $bin, $user); 49 + // This is additional escaping for the SSH 'command="..."' string. 50 + $cmd = addcslashes($cmd, '"\\'); 51 + 52 + $options = array( 53 + 'command="'.$cmd.'"', 54 + 'no-port-forwarding', 55 + 'no-X11-forwarding', 56 + 'no-agent-forwarding', 57 + 'no-pty', 58 + ); 59 + 60 + echo implode(',', $options); 61 + exit(0);
+28 -41
scripts/ssh/ssh-auth.php
··· 4 4 $root = dirname(dirname(dirname(__FILE__))); 5 5 require_once $root.'/scripts/__init_script__.php'; 6 6 7 - $cert = file_get_contents('php://stdin'); 8 - 9 - if (!$cert) { 10 - exit(1); 11 - } 12 - 13 - $parts = preg_split('/\s+/', $cert); 14 - if (count($parts) < 2) { 15 - exit(1); 16 - } 17 - 18 - list($type, $body) = $parts; 19 - 20 7 $user_dao = new PhabricatorUser(); 21 8 $ssh_dao = new PhabricatorUserSSHKey(); 22 9 $conn_r = $user_dao->establishConnection('r'); 23 10 24 - $row = queryfx_one( 11 + $rows = queryfx_all( 25 12 $conn_r, 26 - 'SELECT userName FROM %T u JOIN %T ssh ON u.phid = ssh.userPHID 27 - WHERE ssh.keyType = %s AND ssh.keyBody = %s', 13 + 'SELECT userName, keyBody, keyType FROM %T u JOIN %T ssh 14 + ON u.phid = ssh.userPHID', 28 15 $user_dao->getTableName(), 29 - $ssh_dao->getTableName(), 30 - $type, 31 - $body); 16 + $ssh_dao->getTableName()); 17 + 18 + $bin = $root.'/bin/ssh-exec'; 19 + foreach ($rows as $row) { 20 + $user = $row['userName']; 21 + 22 + $cmd = csprintf('%s --phabricator-ssh-user %s', $bin, $user); 23 + // This is additional escaping for the SSH 'command="..."' string. 24 + $cmd = addcslashes($cmd, '"\\'); 32 25 33 - if (!$row) { 34 - exit(1); 35 - } 26 + // Strip out newlines and other nonsense from the key type and key body. 36 27 37 - $user = idx($row, 'userName'); 28 + $type = $row['keyType']; 29 + $type = preg_replace('@[\x00-\x20]+@', '', $type); 38 30 39 - if (!$user) { 40 - exit(1); 41 - } 31 + $key = $row['keyBody']; 32 + $key = preg_replace('@[\x00-\x20]+@', '', $key); 42 33 43 - if (!PhabricatorUser::validateUsername($user)) { 44 - exit(1); 45 - } 46 34 47 - $bin = $root.'/bin/ssh-exec'; 48 - $cmd = csprintf('%s --phabricator-ssh-user %s', $bin, $user); 49 - // This is additional escaping for the SSH 'command="..."' string. 50 - $cmd = str_replace('"', '\\"', $cmd); 35 + $options = array( 36 + 'command="'.$cmd.'"', 37 + 'no-port-forwarding', 38 + 'no-X11-forwarding', 39 + 'no-agent-forwarding', 40 + 'no-pty', 41 + ); 42 + $options = implode(',', $options); 51 43 52 - $options = array( 53 - 'command="'.$cmd.'"', 54 - 'no-port-forwarding', 55 - 'no-X11-forwarding', 56 - 'no-agent-forwarding', 57 - 'no-pty', 58 - ); 44 + $lines[] = $options.' '.$type.' '.$key."\n"; 45 + } 59 46 60 - echo implode(',', $options); 47 + echo implode('', $lines); 61 48 exit(0);
+29 -24
scripts/ssh/ssh-exec.php
··· 4 4 $root = dirname(dirname(dirname(__FILE__))); 5 5 require_once $root.'/scripts/__init_script__.php'; 6 6 7 - $original_command = getenv('SSH_ORIGINAL_COMMAND'); 8 - $original_argv = id(new PhutilShellLexer())->splitArguments($original_command); 9 - $argv = array_merge($argv, $original_argv); 10 - 7 + // First, figure out the authenticated user. 11 8 $args = new PhutilArgumentParser($argv); 12 9 $args->setTagline('receive SSH requests'); 13 10 $args->setSynopsis(<<<EOSYNOPSIS 14 - **ssh-exec** --phabricator-ssh-user __user__ __commmand__ [__options__] 11 + **ssh-exec** --phabricator-ssh-user __user__ [--ssh-command __commmand__] 15 12 Receive SSH requests. 16 - 17 13 EOSYNOPSIS 18 14 ); 19 15 20 - // NOTE: Do NOT parse standard arguments. Arguments are coming from a remote 21 - // client over SSH, and they should not be able to execute "--xprofile", 22 - // "--recon", etc. 23 - 24 - $args->parsePartial( 16 + $args->parse( 25 17 array( 26 18 array( 27 19 'name' => 'phabricator-ssh-user', 28 20 'param' => 'username', 29 21 ), 22 + array( 23 + 'name' => 'ssh-command', 24 + 'param' => 'command', 25 + ), 30 26 )); 31 27 32 28 try { ··· 46 42 throw new Exception("You have been exiled."); 47 43 } 48 44 45 + if ($args->getArg('ssh-command')) { 46 + $original_command = $args->getArg('ssh-command'); 47 + } else { 48 + $original_command = getenv('SSH_ORIGINAL_COMMAND'); 49 + } 50 + 51 + // Now, rebuild the original command. 52 + $original_argv = id(new PhutilShellLexer()) 53 + ->splitArguments($original_command); 54 + if (!$original_argv) { 55 + throw new Exception("No interactive logins."); 56 + } 57 + $command = head($original_argv); 58 + array_unshift($original_argv, 'phabricator-ssh-exec'); 59 + 60 + $original_args = new PhutilArgumentParser($original_argv); 61 + 49 62 $workflows = array( 50 63 new ConduitSSHWorkflow(), 51 64 ); 52 65 53 - // This duplicates logic in parseWorkflows(), but allows us to raise more 54 - // concise/relevant exceptions when the client is a remote SSH. 55 - $remain = $args->getUnconsumedArgumentVector(); 56 - if (empty($remain)) { 57 - throw new Exception("No interactive logins."); 58 - } else { 59 - $command = head($remain); 60 - $workflow_names = mpull($workflows, 'getName', 'getName'); 61 - if (empty($workflow_names[$command])) { 62 - throw new Exception("Invalid command."); 63 - } 66 + $workflow_names = mpull($workflows, 'getName', 'getName'); 67 + if (empty($workflow_names[$command])) { 68 + throw new Exception("Invalid command."); 64 69 } 65 70 66 - $workflow = $args->parseWorkflows($workflows); 71 + $workflow = $original_args->parseWorkflows($workflows); 67 72 $workflow->setUser($user); 68 73 69 74 $sock_stdin = fopen('php://stdin', 'r'); ··· 82 87 $metrics_channel = new PhutilMetricsChannel($socket_channel); 83 88 $workflow->setIOChannel($metrics_channel); 84 89 85 - $err = $workflow->execute($args); 90 + $err = $workflow->execute($original_args); 86 91 87 92 $metrics_channel->flush(); 88 93 } catch (Exception $ex) {
+1 -1
src/applications/conduit/ssh/ConduitSSHWorkflow.php
··· 31 31 throw new Exception("Invalid JSON input."); 32 32 } 33 33 34 - $params = idx($raw_params, 'params', array()); 34 + $params = idx($raw_params, 'params', '[]'); 35 35 $params = json_decode($params, true); 36 36 $metadata = idx($params, '__conduit__', array()); 37 37 unset($params['__conduit__']);