From c84b9d408cb5ce8cf1108cfe06065d2178125f6f Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 2 Jan 2015 15:13:30 -0800 Subject: [PATCH] Add `bin/almanac register` to associate a host with an Almanac device and trust it Summary: Ref T2783. This is basically a more refined version of D10400, which churned a bit on things like SSH key storage, the actual way the signing protocol shook out, etc. - When Phabricator tries to make an intra-cluster service call as the omnipotent user, sign it with the host's device key. - Add `bin/almanac register` to say "this host is X device, identified by private key Y". This stores the keypair locally, adds the public key to Almanac, and trusts it. Net effect is that once a host has been registered, the daemons can make calls to other nodes as the omnipotent user. This is primarily necessary so they can access repository API methods on remote hosts. Test Plan: - Ran `bin/almanac register` with various valid and invalid inputs. - Verified keys get generated/added/stored properly. - Made a device-signed cluster Conduit call. - Made a normal old user-signed cluster Conduit call. Reviewers: btrahan Reviewed By: btrahan Subscribers: epriestley Maniphest Tasks: T2783 Differential Revision: https://secure.phabricator.com/D11158 --- .gitignore | 4 +- conf/keys/.keep | 0 src/__phutil_library_map__.php | 4 + .../AlmanacManagementRegisterWorkflow.php | 190 ++++++++++++++++++ src/applications/almanac/util/AlmanacKeys.php | 12 ++ .../diffusion/query/DiffusionQuery.php | 55 ++++- 6 files changed, 260 insertions(+), 5 deletions(-) create mode 100644 conf/keys/.keep create mode 100644 src/applications/almanac/management/AlmanacManagementRegisterWorkflow.php create mode 100644 src/applications/almanac/util/AlmanacKeys.php diff --git a/.gitignore b/.gitignore index 3ab8325a96..50a993f7e3 100644 --- a/.gitignore +++ b/.gitignore @@ -13,8 +13,8 @@ /conf/local/local.json /conf/local/ENVIRONMENT /conf/local/VERSION -/conf/local/HOSTKEY -/conf/local/HOSTID +/conf/keys/device.pub +/conf/keys/device.key # Impact Font /resources/font/impact.ttf diff --git a/conf/keys/.keep b/conf/keys/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index c7511aa2d2..51af032337 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -49,7 +49,9 @@ phutil_register_library_map(array( 'AlmanacInterfacePHIDType' => 'applications/almanac/phid/AlmanacInterfacePHIDType.php', 'AlmanacInterfaceQuery' => 'applications/almanac/query/AlmanacInterfaceQuery.php', 'AlmanacInterfaceTableView' => 'applications/almanac/view/AlmanacInterfaceTableView.php', + 'AlmanacKeys' => 'applications/almanac/util/AlmanacKeys.php', 'AlmanacManagementLockWorkflow' => 'applications/almanac/management/AlmanacManagementLockWorkflow.php', + 'AlmanacManagementRegisterWorkflow' => 'applications/almanac/management/AlmanacManagementRegisterWorkflow.php', 'AlmanacManagementTrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementTrustKeyWorkflow.php', 'AlmanacManagementUnlockWorkflow' => 'applications/almanac/management/AlmanacManagementUnlockWorkflow.php', 'AlmanacManagementUntrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementUntrustKeyWorkflow.php', @@ -3091,7 +3093,9 @@ phutil_register_library_map(array( 'AlmanacInterfacePHIDType' => 'PhabricatorPHIDType', 'AlmanacInterfaceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'AlmanacInterfaceTableView' => 'AphrontView', + 'AlmanacKeys' => 'Phobject', 'AlmanacManagementLockWorkflow' => 'AlmanacManagementWorkflow', + 'AlmanacManagementRegisterWorkflow' => 'AlmanacManagementWorkflow', 'AlmanacManagementTrustKeyWorkflow' => 'AlmanacManagementWorkflow', 'AlmanacManagementUnlockWorkflow' => 'AlmanacManagementWorkflow', 'AlmanacManagementUntrustKeyWorkflow' => 'AlmanacManagementWorkflow', diff --git a/src/applications/almanac/management/AlmanacManagementRegisterWorkflow.php b/src/applications/almanac/management/AlmanacManagementRegisterWorkflow.php new file mode 100644 index 0000000000..e0dd214462 --- /dev/null +++ b/src/applications/almanac/management/AlmanacManagementRegisterWorkflow.php @@ -0,0 +1,190 @@ +setName('register') + ->setSynopsis(pht('Register this host as an Almanac device.')) + ->setArguments( + array( + array( + 'name' => 'device', + 'param' => 'name', + 'help' => pht('Almanac device name to register.'), + ), + array( + 'name' => 'private-key', + 'param' => 'key', + 'help' => pht('Path to a private key for the host.'), + ), + array( + 'name' => 'allow-key-reuse', + 'help' => pht( + 'Register even if another host is already registered with this '. + 'keypair.'), + ), + array( + 'name' => 'force', + 'help' => pht( + 'Register this host even if keys already exist.'), + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $console = PhutilConsole::getConsole(); + + $device_name = $args->getArg('device'); + if (!strlen($device_name)) { + throw new PhutilArgumentUsageException( + pht('Specify a device with --device.')); + } + + $device = id(new AlmanacDeviceQuery()) + ->setViewer($this->getViewer()) + ->withNames(array($device_name)) + ->executeOne(); + if (!$device) { + throw new PhutilArgumentUsageException( + pht('No such device "%s" exists!', $device_name)); + } + + $private_key_path = $args->getArg('private-key'); + if (!strlen($private_key_path)) { + throw new PhutilArgumentUsageException( + pht('Specify a private key with --private-key.')); + } + + if (!Filesystem::pathExists($private_key_path)) { + throw new PhutilArgumentUsageException( + pht('Private key "%s" does not exist!', $private_key_path)); + } + + $raw_private_key = Filesystem::readFile($private_key_path); + + $phd_user = PhabricatorEnv::getEnvConfig('phd.user'); + if (!$phd_user) { + throw new PhutilArgumentUsageException( + pht( + 'Config option "phd.user" is not set. You must set this option '. + 'so the private key can be stored with the correct permissions.')); + } + + $tmp = new TempFile(); + list($err) = exec_manual('chown %s %s', $phd_user, $tmp); + if ($err) { + throw new PhutilArgumentUsageException( + pht( + 'Unable to change ownership of a file to daemon user "%s". Run '. + 'this command as %s or root.', + $phd_user, + $phd_user)); + } + + $stored_public_path = AlmanacKeys::getKeyPath('device.pub'); + $stored_private_path = AlmanacKeys::getKeyPath('device.key'); + + if (!$args->getArg('force')) { + if (Filesystem::pathExists($stored_public_path)) { + throw new PhutilArgumentUsageException( + pht( + 'This host already has a registered public key ("%s"). '. + 'Remove this key before registering the host, or use '. + '--force to overwrite it.', + Filesystem::readablePath($stored_public_path))); + } + + if (Filesystem::pathExists($stored_private_path)) { + throw new PhutilArgumentUsageException( + pht( + 'This host already has a registered private key ("%s"). '. + 'Remove this key before registering the host, or use '. + '--force to overwrite it.', + Filesystem::readablePath($stored_private_path))); + } + } + + list($raw_public_key) = execx('ssh-keygen -y -f %s', $private_key_path); + + $key_object = PhabricatorAuthSSHPublicKey::newFromRawKey($raw_public_key); + + $public_key = id(new PhabricatorAuthSSHKeyQuery()) + ->setViewer($this->getViewer()) + ->withKeys(array($key_object)) + ->executeOne(); + + if ($public_key) { + if ($public_key->getObjectPHID() !== $device->getPHID()) { + throw new PhutilArgumentUsageException( + pht( + 'The public key corresponding to the given private key is '. + 'already associated with an object other than the specified '. + 'device. You can not use a single private key to identify '. + 'multiple devices or users.')); + } else if (!$public_key->getIsTrusted()) { + throw new PhutilArgumentUsageException( + pht( + 'The public key corresponding to the given private key is '. + 'already associated with the device, but is not trusted. '. + 'Registering this key would trust the other entities which '. + 'hold it. Use a unique key, or explicitly enable trust for the '. + 'current key.')); + } else if (!$args->getArg('allow-key-reuse')) { + throw new PhutilArgumentUsageException( + pht( + 'The public key corresponding to the given private key is '. + 'already associated with the device. If you do not want to '. + 'use a unique key, use --allow-key-reuse to permit '. + 'reassociation.')); + } + } else { + $public_key = id(new PhabricatorAuthSSHKey()) + ->setObjectPHID($device->getPHID()) + ->attachObject($device) + ->setName($device->getSSHKeyDefaultName()) + ->setKeyType($key_object->getType()) + ->setKeyBody($key_object->getBody()) + ->setKeyComment(pht('Registered')) + ->setIsTrusted(1); + } + + + $console->writeOut( + "%s\n", + pht('Installing public key...')); + + $tmp_public = new TempFile(); + Filesystem::changePermissions($tmp_public, 0600); + execx('chown %s %s', $phd_user, $tmp_public); + Filesystem::writeFile($tmp_public, $raw_public_key); + execx('mv -f %s %s', $tmp_public, $stored_public_path); + + $console->writeOut( + "%s\n", + pht('Installing private key...')); + + $tmp_private = new TempFile(); + Filesystem::changePermissions($tmp_private, 0600); + execx('chown %s %s', $phd_user, $tmp_private); + Filesystem::writeFile($tmp_private, $raw_private_key); + execx('mv -f %s %s', $tmp_private, $stored_private_path); + + if (!$public_key->getID()) { + $console->writeOut( + "%s\n", + pht('Registering device key...')); + $public_key->save(); + } + + $console->writeOut( + "** %s ** %s\n", + pht('HOST REGISTERED'), + pht( + 'This host has been registered as "%s" and a trusted keypair '. + 'has been installed.', + $device_name)); + } + +} diff --git a/src/applications/almanac/util/AlmanacKeys.php b/src/applications/almanac/util/AlmanacKeys.php new file mode 100644 index 0000000000..b63cf0a98d --- /dev/null +++ b/src/applications/almanac/util/AlmanacKeys.php @@ -0,0 +1,12 @@ +getBranch(); } + // If the method we're calling doesn't actually take some of the implicit + // parameters we derive from the DiffusionRequest, omit them. + $method_object = ConduitAPIMethod::getConduitMethod($method); + $method_params = $method_object->defineParamTypes(); + foreach ($core_params as $key => $value) { + if (empty($method_params[$key])) { + unset($core_params[$key]); + } + } + $params = $params + $core_params; $service_phid = $repository->getAlmanacServicePHID(); @@ -123,9 +133,48 @@ abstract class DiffusionQuery extends PhabricatorQuery { $client = id(new ConduitClient($uri)) ->setHost($domain); - $token = PhabricatorConduitToken::loadClusterTokenForUser($user); - if ($token) { - $client->setConduitToken($token->getToken()); + if ($user->isOmnipotent()) { + // If the caller is the omnipotent user (normally, a daemon), we will + // sign the request with this host's asymmetric keypair. + + $public_path = AlmanacKeys::getKeyPath('device.pub'); + try { + $public_key = Filesystem::readFile($public_path); + } catch (Exception $ex) { + throw new PhutilAggregateException( + pht( + 'Unable to read device public key while attempting to make '. + 'authenticated method call within the Phabricator cluster. '. + 'Use `bin/almanac register` to register keys for this device. '. + 'Exception: %s', + $ex->getMessage()), + array($ex)); + } + + $private_path = AlmanacKeys::getKeyPath('device.key'); + try { + $private_key = Filesystem::readFile($private_path); + $private_key = new PhutilOpaqueEnvelope($private_key); + } catch (Exception $ex) { + throw new PhutilAggregateException( + pht( + 'Unable to read device private key while attempting to make '. + 'authenticated method call within the Phabricator cluster. '. + 'Use `bin/almanac register` to register keys for this device. '. + 'Exception: %s', + $ex->getMessage()), + array($ex)); + } + + $client->setSigningKeys($public_key, $private_key); + } else { + // If the caller is a normal user, we generate or retrieve a cluster + // API token. + + $token = PhabricatorConduitToken::loadClusterTokenForUser($user); + if ($token) { + $client->setConduitToken($token->getToken()); + } } return $client->callMethodSynchronous($method, $params);