mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-26 16:52:41 +01:00
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
This commit is contained in:
parent
c7f23f522a
commit
888b3839e7
7 changed files with 155 additions and 69 deletions
1
bin/ssh-auth-key
Symbolic link
1
bin/ssh-auth-key
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../scripts/ssh/ssh-auth-key.php
|
8
resources/sshd/phabricator-ssh-hook.sh
Executable file
8
resources/sshd/phabricator-ssh-hook.sh
Executable file
|
@ -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" $@
|
24
resources/sshd/sshd_config.example
Normal file
24
resources/sshd/sshd_config.example
Normal file
|
@ -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
|
61
scripts/ssh/ssh-auth-key.php
Executable file
61
scripts/ssh/ssh-auth-key.php
Executable file
|
@ -0,0 +1,61 @@
|
||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$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(
|
||||||
|
$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);
|
|
@ -4,58 +4,45 @@
|
||||||
$root = dirname(dirname(dirname(__FILE__)));
|
$root = dirname(dirname(dirname(__FILE__)));
|
||||||
require_once $root.'/scripts/__init_script__.php';
|
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();
|
$user_dao = new PhabricatorUser();
|
||||||
$ssh_dao = new PhabricatorUserSSHKey();
|
$ssh_dao = new PhabricatorUserSSHKey();
|
||||||
$conn_r = $user_dao->establishConnection('r');
|
$conn_r = $user_dao->establishConnection('r');
|
||||||
|
|
||||||
$row = queryfx_one(
|
$rows = queryfx_all(
|
||||||
$conn_r,
|
$conn_r,
|
||||||
'SELECT userName FROM %T u JOIN %T ssh ON u.phid = ssh.userPHID
|
'SELECT userName, keyBody, keyType FROM %T u JOIN %T ssh
|
||||||
WHERE ssh.keyType = %s AND ssh.keyBody = %s',
|
ON u.phid = ssh.userPHID',
|
||||||
$user_dao->getTableName(),
|
$user_dao->getTableName(),
|
||||||
$ssh_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';
|
$bin = $root.'/bin/ssh-exec';
|
||||||
$cmd = csprintf('%s --phabricator-ssh-user %s', $bin, $user);
|
foreach ($rows as $row) {
|
||||||
// This is additional escaping for the SSH 'command="..."' string.
|
$user = $row['userName'];
|
||||||
$cmd = str_replace('"', '\\"', $cmd);
|
|
||||||
|
|
||||||
$options = array(
|
$cmd = csprintf('%s --phabricator-ssh-user %s', $bin, $user);
|
||||||
|
// This is additional escaping for the SSH 'command="..."' string.
|
||||||
|
$cmd = addcslashes($cmd, '"\\');
|
||||||
|
|
||||||
|
// 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.'"',
|
'command="'.$cmd.'"',
|
||||||
'no-port-forwarding',
|
'no-port-forwarding',
|
||||||
'no-X11-forwarding',
|
'no-X11-forwarding',
|
||||||
'no-agent-forwarding',
|
'no-agent-forwarding',
|
||||||
'no-pty',
|
'no-pty',
|
||||||
);
|
);
|
||||||
|
$options = implode(',', $options);
|
||||||
|
|
||||||
echo implode(',', $options);
|
$lines[] = $options.' '.$type.' '.$key."\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo implode('', $lines);
|
||||||
exit(0);
|
exit(0);
|
||||||
|
|
|
@ -4,29 +4,25 @@
|
||||||
$root = dirname(dirname(dirname(__FILE__)));
|
$root = dirname(dirname(dirname(__FILE__)));
|
||||||
require_once $root.'/scripts/__init_script__.php';
|
require_once $root.'/scripts/__init_script__.php';
|
||||||
|
|
||||||
$original_command = getenv('SSH_ORIGINAL_COMMAND');
|
// First, figure out the authenticated user.
|
||||||
$original_argv = id(new PhutilShellLexer())->splitArguments($original_command);
|
|
||||||
$argv = array_merge($argv, $original_argv);
|
|
||||||
|
|
||||||
$args = new PhutilArgumentParser($argv);
|
$args = new PhutilArgumentParser($argv);
|
||||||
$args->setTagline('receive SSH requests');
|
$args->setTagline('receive SSH requests');
|
||||||
$args->setSynopsis(<<<EOSYNOPSIS
|
$args->setSynopsis(<<<EOSYNOPSIS
|
||||||
**ssh-exec** --phabricator-ssh-user __user__ __commmand__ [__options__]
|
**ssh-exec** --phabricator-ssh-user __user__ [--ssh-command __commmand__]
|
||||||
Receive SSH requests.
|
Receive SSH requests.
|
||||||
|
|
||||||
EOSYNOPSIS
|
EOSYNOPSIS
|
||||||
);
|
);
|
||||||
|
|
||||||
// NOTE: Do NOT parse standard arguments. Arguments are coming from a remote
|
$args->parse(
|
||||||
// client over SSH, and they should not be able to execute "--xprofile",
|
|
||||||
// "--recon", etc.
|
|
||||||
|
|
||||||
$args->parsePartial(
|
|
||||||
array(
|
array(
|
||||||
array(
|
array(
|
||||||
'name' => 'phabricator-ssh-user',
|
'name' => 'phabricator-ssh-user',
|
||||||
'param' => 'username',
|
'param' => 'username',
|
||||||
),
|
),
|
||||||
|
array(
|
||||||
|
'name' => 'ssh-command',
|
||||||
|
'param' => 'command',
|
||||||
|
),
|
||||||
));
|
));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -46,24 +42,33 @@ try {
|
||||||
throw new Exception("You have been exiled.");
|
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(
|
$workflows = array(
|
||||||
new ConduitSSHWorkflow(),
|
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');
|
$workflow_names = mpull($workflows, 'getName', 'getName');
|
||||||
if (empty($workflow_names[$command])) {
|
if (empty($workflow_names[$command])) {
|
||||||
throw new Exception("Invalid command.");
|
throw new Exception("Invalid command.");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
$workflow = $args->parseWorkflows($workflows);
|
$workflow = $original_args->parseWorkflows($workflows);
|
||||||
$workflow->setUser($user);
|
$workflow->setUser($user);
|
||||||
|
|
||||||
$sock_stdin = fopen('php://stdin', 'r');
|
$sock_stdin = fopen('php://stdin', 'r');
|
||||||
|
@ -82,7 +87,7 @@ try {
|
||||||
$metrics_channel = new PhutilMetricsChannel($socket_channel);
|
$metrics_channel = new PhutilMetricsChannel($socket_channel);
|
||||||
$workflow->setIOChannel($metrics_channel);
|
$workflow->setIOChannel($metrics_channel);
|
||||||
|
|
||||||
$err = $workflow->execute($args);
|
$err = $workflow->execute($original_args);
|
||||||
|
|
||||||
$metrics_channel->flush();
|
$metrics_channel->flush();
|
||||||
} catch (Exception $ex) {
|
} catch (Exception $ex) {
|
||||||
|
|
|
@ -31,7 +31,7 @@ final class ConduitSSHWorkflow extends PhabricatorSSHWorkflow {
|
||||||
throw new Exception("Invalid JSON input.");
|
throw new Exception("Invalid JSON input.");
|
||||||
}
|
}
|
||||||
|
|
||||||
$params = idx($raw_params, 'params', array());
|
$params = idx($raw_params, 'params', '[]');
|
||||||
$params = json_decode($params, true);
|
$params = json_decode($params, true);
|
||||||
$metadata = idx($params, '__conduit__', array());
|
$metadata = idx($params, '__conduit__', array());
|
||||||
unset($params['__conduit__']);
|
unset($params['__conduit__']);
|
||||||
|
|
Loading…
Reference in a new issue