mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-22 14:52:41 +01:00
Allow device SSH keys to be trusted
Summary: Ref T6240. Some discussion in that task. In instance/cluster environments, daemons need to make Conduit calls that bypass policy checks. We can't just let anyone add SSH keys with this capability to the web directly, because then an adminstrator could just add a key they own and start signing requests with it, bypassing policy checks. Add a `bin/almanac trust-key --id <x>` workflow for trusting keys. Only trusted keys can sign requests. Test Plan: - Generated a user key. - Generated a device key. - Trusted a device key. - Untrusted a device key. - Hit the various errors on trust/untrust. - Tried to edit a trusted key. {F236010} Reviewers: btrahan Reviewed By: btrahan Subscribers: epriestley Maniphest Tasks: T6240 Differential Revision: https://secure.phabricator.com/D10878
This commit is contained in:
parent
c2f0955e9b
commit
5e0f218fe4
15 changed files with 219 additions and 87 deletions
2
resources/sql/autopatches/20141119.sshtrust.sql
Normal file
2
resources/sql/autopatches/20141119.sshtrust.sql
Normal file
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE {$NAMESPACE}_auth.auth_sshkey
|
||||
ADD isTrusted BOOL NOT NULL;
|
|
@ -20,7 +20,6 @@ phutil_register_library_map(array(
|
|||
'AlmanacBindingTransactionQuery' => 'applications/almanac/query/AlmanacBindingTransactionQuery.php',
|
||||
'AlmanacBindingViewController' => 'applications/almanac/controller/AlmanacBindingViewController.php',
|
||||
'AlmanacConduitAPIMethod' => 'applications/almanac/conduit/AlmanacConduitAPIMethod.php',
|
||||
'AlmanacConduitUtil' => 'applications/almanac/util/AlmanacConduitUtil.php',
|
||||
'AlmanacConsoleController' => 'applications/almanac/controller/AlmanacConsoleController.php',
|
||||
'AlmanacController' => 'applications/almanac/controller/AlmanacController.php',
|
||||
'AlmanacCoreCustomField' => 'applications/almanac/customfield/AlmanacCoreCustomField.php',
|
||||
|
@ -46,7 +45,8 @@ phutil_register_library_map(array(
|
|||
'AlmanacInterfacePHIDType' => 'applications/almanac/phid/AlmanacInterfacePHIDType.php',
|
||||
'AlmanacInterfaceQuery' => 'applications/almanac/query/AlmanacInterfaceQuery.php',
|
||||
'AlmanacInterfaceTableView' => 'applications/almanac/view/AlmanacInterfaceTableView.php',
|
||||
'AlmanacManagementRegisterWorkflow' => 'applications/almanac/management/AlmanacManagementRegisterWorkflow.php',
|
||||
'AlmanacManagementTrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementTrustKeyWorkflow.php',
|
||||
'AlmanacManagementUntrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementUntrustKeyWorkflow.php',
|
||||
'AlmanacManagementWorkflow' => 'applications/almanac/management/AlmanacManagementWorkflow.php',
|
||||
'AlmanacNames' => 'applications/almanac/util/AlmanacNames.php',
|
||||
'AlmanacNamesTestCase' => 'applications/almanac/util/__tests__/AlmanacNamesTestCase.php',
|
||||
|
@ -3000,7 +3000,6 @@ phutil_register_library_map(array(
|
|||
'AlmanacBindingTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
|
||||
'AlmanacBindingViewController' => 'AlmanacServiceController',
|
||||
'AlmanacConduitAPIMethod' => 'ConduitAPIMethod',
|
||||
'AlmanacConduitUtil' => 'Phobject',
|
||||
'AlmanacConsoleController' => 'AlmanacController',
|
||||
'AlmanacController' => 'PhabricatorController',
|
||||
'AlmanacCoreCustomField' => array(
|
||||
|
@ -3040,7 +3039,8 @@ phutil_register_library_map(array(
|
|||
'AlmanacInterfacePHIDType' => 'PhabricatorPHIDType',
|
||||
'AlmanacInterfaceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
|
||||
'AlmanacInterfaceTableView' => 'AphrontView',
|
||||
'AlmanacManagementRegisterWorkflow' => 'AlmanacManagementWorkflow',
|
||||
'AlmanacManagementTrustKeyWorkflow' => 'AlmanacManagementWorkflow',
|
||||
'AlmanacManagementUntrustKeyWorkflow' => 'AlmanacManagementWorkflow',
|
||||
'AlmanacManagementWorkflow' => 'PhabricatorManagementWorkflow',
|
||||
'AlmanacNames' => 'Phobject',
|
||||
'AlmanacNamesTestCase' => 'PhabricatorTestCase',
|
||||
|
|
|
@ -161,6 +161,8 @@ final class AlmanacDeviceViewController
|
|||
->setUser($viewer)
|
||||
->setKeys($keys)
|
||||
->setCanEdit($can_edit)
|
||||
->setShowID(true)
|
||||
->setShowTrusted(true)
|
||||
->setNoDataString(pht('This device has no associated SSH public keys.'));
|
||||
|
||||
try {
|
||||
|
|
|
@ -1,64 +0,0 @@
|
|||
<?php
|
||||
|
||||
final class AlmanacManagementRegisterWorkflow
|
||||
extends AlmanacManagementWorkflow {
|
||||
|
||||
public function didConstruct() {
|
||||
$this
|
||||
->setName('register')
|
||||
->setSynopsis(pht('Register this host for authorized Conduit access.'))
|
||||
->setArguments(array());
|
||||
}
|
||||
|
||||
public function execute(PhutilArgumentParser $args) {
|
||||
$console = PhutilConsole::getConsole();
|
||||
|
||||
if (Filesystem::pathExists(AlmanacConduitUtil::getHostPrivateKeyPath())) {
|
||||
throw new Exception(
|
||||
'This host already has a private key for Conduit access.');
|
||||
}
|
||||
|
||||
$pair = PhabricatorSSHKeyGenerator::generateKeypair();
|
||||
list($public_key, $private_key) = $pair;
|
||||
|
||||
$host = id(new AlmanacDevice())
|
||||
->setName(php_uname('n'))
|
||||
->save();
|
||||
|
||||
id(new AlmanacProperty())
|
||||
->setObjectPHID($host->getPHID())
|
||||
->setName('conduitPublicOpenSSHKey')
|
||||
->setValue($public_key)
|
||||
->save();
|
||||
|
||||
id(new AlmanacProperty())
|
||||
->setObjectPHID($host->getPHID())
|
||||
->setName('conduitPublicOpenSSLKey')
|
||||
->setValue($this->convertToOpenSSLPublicKey($public_key))
|
||||
->save();
|
||||
|
||||
Filesystem::writeFile(
|
||||
AlmanacConduitUtil::getHostPrivateKeyPath(),
|
||||
$private_key);
|
||||
|
||||
Filesystem::writeFile(
|
||||
AlmanacConduitUtil::getHostIDPath(),
|
||||
$host->getID());
|
||||
|
||||
$console->writeOut("Registered as device %d.\n", $host->getID());
|
||||
}
|
||||
|
||||
private function convertToOpenSSLPublicKey($openssh_public_key) {
|
||||
$ssh_public_key_file = new TempFile();
|
||||
Filesystem::writeFile($ssh_public_key_file, $openssh_public_key);
|
||||
|
||||
list($public_key, $stderr) = id(new ExecFuture(
|
||||
'ssh-keygen -e -f %s -m pkcs8',
|
||||
$ssh_public_key_file))->resolvex();
|
||||
|
||||
unset($ssh_public_key_file);
|
||||
|
||||
return $public_key;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
|
||||
final class AlmanacManagementTrustKeyWorkflow
|
||||
extends AlmanacManagementWorkflow {
|
||||
|
||||
public function didConstruct() {
|
||||
$this
|
||||
->setName('trust-key')
|
||||
->setSynopsis(pht('Mark a public key as trusted.'))
|
||||
->setArguments(
|
||||
array(
|
||||
array(
|
||||
'name' => 'id',
|
||||
'param' => 'id',
|
||||
'help' => pht('ID of the key to trust.'),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
public function execute(PhutilArgumentParser $args) {
|
||||
$console = PhutilConsole::getConsole();
|
||||
|
||||
$id = $args->getArg('id');
|
||||
if (!$id) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht('Specify a public key to trust with --id.'));
|
||||
}
|
||||
|
||||
$key = id(new PhabricatorAuthSSHKeyQuery())
|
||||
->setViewer($this->getViewer())
|
||||
->withIDs(array($id))
|
||||
->executeOne();
|
||||
if (!$key) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht('No public key exists with ID "%s".', $id));
|
||||
}
|
||||
|
||||
if ($key->getIsTrusted()) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht('Public key with ID %s is already trusted.', $id));
|
||||
}
|
||||
|
||||
if (!($key->getObject() instanceof AlmanacDevice)) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht('You can only trust keys associated with Almanac devices.'));
|
||||
}
|
||||
|
||||
$handle = id(new PhabricatorHandleQuery())
|
||||
->setViewer($this->getViewer())
|
||||
->withPHIDs(array($key->getObject()->getPHID()))
|
||||
->executeOne();
|
||||
|
||||
$console->writeOut(
|
||||
"**<bg:red> %s </bg>**\n\n%s\n\n%s\n\n%s",
|
||||
pht('IMPORTANT!'),
|
||||
phutil_console_wrap(
|
||||
pht(
|
||||
'Trusting a public key gives anyone holding the corresponding '.
|
||||
'private key complete, unrestricted access to all data in '.
|
||||
'Phabricator. The private key will be able to sign requests that '.
|
||||
'skip policy and security checks.')),
|
||||
phutil_console_wrap(
|
||||
pht(
|
||||
'This is an advanced feature which should normally be used only '.
|
||||
'when building a Phabricator cluster. This feature is very '.
|
||||
'dangerous if misused.')),
|
||||
pht('This key is associated with device "%s".', $handle->getName()));
|
||||
|
||||
$prompt = pht(
|
||||
'Really trust this key?');
|
||||
if (!phutil_console_confirm($prompt)) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht('User aborted workflow.'));
|
||||
}
|
||||
|
||||
$key->setIsTrusted(1);
|
||||
$key->save();
|
||||
|
||||
$console->writeOut(
|
||||
"**<bg:green> %s </bg>** %s\n",
|
||||
pht('TRUSTED'),
|
||||
pht('Key %s has been marked as trusted.', $id));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
final class AlmanacManagementUntrustKeyWorkflow
|
||||
extends AlmanacManagementWorkflow {
|
||||
|
||||
public function didConstruct() {
|
||||
$this
|
||||
->setName('untrust-key')
|
||||
->setSynopsis(pht('Revoke trust of a public key.'))
|
||||
->setArguments(
|
||||
array(
|
||||
array(
|
||||
'name' => 'id',
|
||||
'param' => 'id',
|
||||
'help' => pht('ID of the key to revoke trust for.'),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
public function execute(PhutilArgumentParser $args) {
|
||||
$console = PhutilConsole::getConsole();
|
||||
|
||||
$id = $args->getArg('id');
|
||||
if (!$id) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht('Specify a public key to revoke trust for with --id.'));
|
||||
}
|
||||
|
||||
$key = id(new PhabricatorAuthSSHKeyQuery())
|
||||
->setViewer($this->getViewer())
|
||||
->withIDs(array($id))
|
||||
->executeOne();
|
||||
if (!$key) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht('No public key exists with ID "%s".', $id));
|
||||
}
|
||||
|
||||
if (!$key->getIsTrusted()) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht('Public key with ID %s is not trusted.', $id));
|
||||
}
|
||||
|
||||
$key->setIsTrusted(0);
|
||||
$key->save();
|
||||
|
||||
$console->writeOut(
|
||||
"**<bg:green> %s </bg>** %s\n",
|
||||
pht('TRUST REVOKED'),
|
||||
pht('Trust has been revoked for public key %s.', $id));
|
||||
}
|
||||
|
||||
}
|
|
@ -170,5 +170,9 @@ final class AlmanacDevice
|
|||
return $this->getURI();
|
||||
}
|
||||
|
||||
public function getSSHKeyDefaultName() {
|
||||
return $this->getName();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
<?php
|
||||
|
||||
final class AlmanacConduitUtil extends Phobject {
|
||||
|
||||
public static function getHostPrivateKeyPath() {
|
||||
$root = dirname(phutil_get_library_root('phabricator'));
|
||||
$path = $root.'/conf/local/HOSTKEY';
|
||||
return $path;
|
||||
}
|
||||
|
||||
public static function getHostIDPath() {
|
||||
$root = dirname(phutil_get_library_root('phabricator'));
|
||||
$path = $root.'/conf/local/HOSTID';
|
||||
return $path;
|
||||
}
|
||||
|
||||
}
|
|
@ -32,6 +32,22 @@ final class PhabricatorAuthSSHKeyEditController
|
|||
|
||||
$cancel_uri = $key->getObject()->getSSHPublicKeyManagementURI($viewer);
|
||||
|
||||
if ($key->getIsTrusted()) {
|
||||
$id = $key->getID();
|
||||
|
||||
return $this->newDialog()
|
||||
->setTitle(pht('Can Not Edit Trusted Key'))
|
||||
->appendParagraph(
|
||||
pht(
|
||||
'This key is trusted. Trusted keys can not be edited. '.
|
||||
'Use %s to revoke trust before editing the key.',
|
||||
phutil_tag(
|
||||
'tt',
|
||||
array(),
|
||||
"bin/almanac untrust-key --id {$id}")))
|
||||
->addCancelButton($cancel_uri, pht('Okay'));
|
||||
}
|
||||
|
||||
$token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
|
||||
$viewer,
|
||||
$request,
|
||||
|
|
|
@ -19,13 +19,15 @@ final class PhabricatorAuthSSHKeyGenerateController
|
|||
$cancel_uri);
|
||||
|
||||
if ($request->isFormPost()) {
|
||||
$default_name = $key->getObject()->getSSHKeyDefaultName();
|
||||
|
||||
$keys = PhabricatorSSHKeyGenerator::generateKeypair();
|
||||
list($public_key, $private_key) = $keys;
|
||||
|
||||
$file = PhabricatorFile::buildFromFileDataOrHash(
|
||||
$private_key,
|
||||
array(
|
||||
'name' => 'id_rsa_phabricator.key',
|
||||
'name' => $default_name.'.key',
|
||||
'ttl' => time() + (60 * 10),
|
||||
'viewPolicy' => $viewer->getPHID(),
|
||||
));
|
||||
|
@ -36,7 +38,7 @@ final class PhabricatorAuthSSHKeyGenerateController
|
|||
$body = $public_key->getBody();
|
||||
|
||||
$key
|
||||
->setName('id_rsa_phabricator')
|
||||
->setName($default_name)
|
||||
->setKeyType($type)
|
||||
->setKeyBody($body)
|
||||
->setKeyComment(pht('Generated'))
|
||||
|
|
|
@ -11,4 +11,10 @@ interface PhabricatorSSHPublicKeyInterface {
|
|||
*/
|
||||
public function getSSHPublicKeyManagementURI(PhabricatorUser $viewer);
|
||||
|
||||
|
||||
/**
|
||||
* Provide a default name for generated SSH keys.
|
||||
*/
|
||||
public function getSSHKeyDefaultName();
|
||||
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ final class PhabricatorAuthSSHKey
|
|||
protected $keyIndex;
|
||||
protected $keyBody;
|
||||
protected $keyComment = '';
|
||||
protected $isTrusted = 0;
|
||||
|
||||
private $object = self::ATTACHABLE;
|
||||
|
||||
|
@ -21,6 +22,7 @@ final class PhabricatorAuthSSHKey
|
|||
'keyIndex' => 'bytes12',
|
||||
'keyBody' => 'text',
|
||||
'keyComment' => 'text255',
|
||||
'isTrusted' => 'bool',
|
||||
),
|
||||
self::CONFIG_KEY_SCHEMA => array(
|
||||
'key_object' => array(
|
||||
|
|
|
@ -5,6 +5,8 @@ final class PhabricatorAuthSSHKeyTableView extends AphrontView {
|
|||
private $keys;
|
||||
private $canEdit;
|
||||
private $noDataString;
|
||||
private $showTrusted;
|
||||
private $showID;
|
||||
|
||||
public function setNoDataString($no_data_string) {
|
||||
$this->noDataString = $no_data_string;
|
||||
|
@ -16,6 +18,16 @@ final class PhabricatorAuthSSHKeyTableView extends AphrontView {
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function setShowTrusted($show_trusted) {
|
||||
$this->showTrusted = $show_trusted;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setShowID($show_id) {
|
||||
$this->showID = $show_id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setKeys(array $keys) {
|
||||
assert_instances_of($keys, 'PhabricatorAuthSSHKey');
|
||||
$this->keys = $keys;
|
||||
|
@ -32,9 +44,15 @@ final class PhabricatorAuthSSHKeyTableView extends AphrontView {
|
|||
$delete_class = 'small grey button disabled';
|
||||
}
|
||||
|
||||
$trusted_icon = id(new PHUIIconView())
|
||||
->setIconFont('fa-star blue');
|
||||
$untrusted_icon = id(new PHUIIconView())
|
||||
->setIconFont('fa-times grey');
|
||||
|
||||
$rows = array();
|
||||
foreach ($keys as $key) {
|
||||
$rows[] = array(
|
||||
$key->getID(),
|
||||
javelin_tag(
|
||||
'a',
|
||||
array(
|
||||
|
@ -42,6 +60,7 @@ final class PhabricatorAuthSSHKeyTableView extends AphrontView {
|
|||
'sigil' => 'workflow',
|
||||
),
|
||||
$key->getName()),
|
||||
$key->getIsTrusted() ? $trusted_icon : $untrusted_icon,
|
||||
$key->getKeyComment(),
|
||||
$key->getKeyType(),
|
||||
phabricator_datetime($key->getDateCreated(), $viewer),
|
||||
|
@ -60,15 +79,25 @@ final class PhabricatorAuthSSHKeyTableView extends AphrontView {
|
|||
->setNoDataString($this->noDataString)
|
||||
->setHeaders(
|
||||
array(
|
||||
pht('ID'),
|
||||
pht('Name'),
|
||||
pht('Trusted'),
|
||||
pht('Comment'),
|
||||
pht('Type'),
|
||||
pht('Added'),
|
||||
null,
|
||||
))
|
||||
->setColumnVisibility(
|
||||
array(
|
||||
$this->showID,
|
||||
true,
|
||||
$this->showTrusted,
|
||||
))
|
||||
->setColumnClasses(
|
||||
array(
|
||||
'',
|
||||
'wide pri',
|
||||
'center',
|
||||
'',
|
||||
'',
|
||||
'right',
|
||||
|
|
|
@ -268,6 +268,15 @@ final class PhabricatorConduitAPIController
|
|||
if ($object instanceof PhabricatorUser) {
|
||||
$user = $object;
|
||||
} else {
|
||||
if (!$stored_key->getIsTrusted()) {
|
||||
return array(
|
||||
'ERR-INVALID-AUTH',
|
||||
pht(
|
||||
'The key which signed this request is not trusted. Only '.
|
||||
'trusted keys can be used to sign API calls.'),
|
||||
);
|
||||
}
|
||||
|
||||
throw new Exception(
|
||||
pht('Not Implemented: Would authenticate Almanac device.'));
|
||||
}
|
||||
|
|
|
@ -943,4 +943,8 @@ EOBODY;
|
|||
}
|
||||
}
|
||||
|
||||
public function getSSHKeyDefaultName() {
|
||||
return 'id_rsa_phabricator';
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue