Commit 5e0f218f authored by epriestley's avatar epriestley
Browse files

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
parent c2f0955e
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 {
......
<?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;
}
}
<?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));
}
}
<?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();
}
}
<?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';
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment