1
0
Fork 0
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:
epriestley 2014-11-20 17:33:30 -08:00
parent c2f0955e9b
commit 5e0f218fe4
15 changed files with 219 additions and 87 deletions

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_auth.auth_sshkey
ADD isTrusted BOOL NOT NULL;

View file

@ -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',

View file

@ -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 {

View file

@ -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;
}
}

View file

@ -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));
}
}

View file

@ -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));
}
}

View file

@ -170,5 +170,9 @@ final class AlmanacDevice
return $this->getURI();
}
public function getSSHKeyDefaultName() {
return $this->getName();
}
}

View file

@ -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;
}
}

View file

@ -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,

View file

@ -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'))

View file

@ -11,4 +11,10 @@ interface PhabricatorSSHPublicKeyInterface {
*/
public function getSSHPublicKeyManagementURI(PhabricatorUser $viewer);
/**
* Provide a default name for generated SSH keys.
*/
public function getSSHKeyDefaultName();
}

View file

@ -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(

View file

@ -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',

View file

@ -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.'));
}

View file

@ -943,4 +943,8 @@ EOBODY;
}
}
public function getSSHKeyDefaultName() {
return 'id_rsa_phabricator';
}
}