From dba8915d4f600173d8646a01815c3010d1f5c6ca Mon Sep 17 00:00:00 2001 From: Trent Mick Date: Mon, 20 Feb 2017 19:39:16 -0800 Subject: [PATCH] joyent/node-triton#150 fallback to ~/.ssh during docker profile creation Reviewed by: Todd Whiteman Approved by: Todd Whiteman --- lib/do_profile/do_create.js | 2 +- lib/do_profile/profilecommon.js | 89 +++++++++++++++++++++++++-------- 2 files changed, 70 insertions(+), 21 deletions(-) diff --git a/lib/do_profile/do_create.js b/lib/do_profile/do_create.js index 6a43a38..428ff35 100644 --- a/lib/do_profile/do_create.js +++ b/lib/do_profile/do_create.js @@ -359,7 +359,7 @@ function _createProfile(opts, cb) { console.log(wrap80('This section will setup authentication to ' + 'Triton DataCenter\'s Docker endpoint using your account ' + 'and key information specified above. This is only required ' + - 'if you intend to use `docker` with this profile.')); + 'if you intend to use `docker` with this profile.\n')); profilecommon.profileDockerSetup({ cli: cli, diff --git a/lib/do_profile/profilecommon.js b/lib/do_profile/profilecommon.js index 62b8c55..22cb75d 100644 --- a/lib/do_profile/profilecommon.js +++ b/lib/do_profile/profilecommon.js @@ -141,15 +141,11 @@ function setCurrentProfile(opts, cb) { * - {Boolean} implicit: Optional. Boolean indicating if the Docker setup * is implicit (e.g. as a default part of `triton profile create`). If * implicit, we silently skip if ListServices shows no Docker service. - * - {Object} keyPaths: Optional. An object with `private` and/or `public` - * properties pointing to a full path to an SSH private and/or public - * key to use for cert signing. */ function profileDockerSetup(opts, cb) { assert.object(opts.cli, 'opts.cli'); assert.string(opts.name, 'opts.name'); assert.optionalBool(opts.implicit, 'opts.implicit'); - assert.optionalObject(opts.keyPaths, 'opts.keyPaths'); assert.func(cb, 'cb'); var cli = opts.cli; @@ -163,7 +159,7 @@ function profileDockerSetup(opts, cb) { vasync.pipeline({arg: {tritonapi: tritonapi}, funcs: [ function dockerKeyWarning(arg, next) { - console.log(wordwrap('\nWARNING: Docker uses authentication via ' + + console.log(wordwrap('WARNING: Docker uses authentication via ' + 'client TLS certificates that do not support encrypted ' + '(passphrase protected) keys or SSH agents. If you continue,' + 'this profile setup will attempt to write a copy of your ' + @@ -254,18 +250,6 @@ function profileDockerSetup(opts, cb) { next(); }, - function checkSshPrivKey(arg, next) { - try { - tritonapi.keyPair.getPrivateKey(); - } catch (e) { - next(new errors.SetupError(format('could not obtain SSH ' + - 'private key for keypair with fingerprint "%s" ' + - 'to create Docker certificate.', profile.keyId))); - return; - } - next(); - }, - /* * Find the `docker` version, if we can. This can be used later to * control some envvars that depend on the docker version. @@ -319,6 +303,72 @@ function profileDockerSetup(opts, cb) { }); }, + /* + * We need the private key to format as a client cert. If this profile's + * key was found in the SSH agent (and by default it prefers to take + * it from there), then we can't use `tritonapi.keyPair`, because + * the SSH agent protocol will not allow us access to the private key + * data (by design). + * + * As a fallback we'll look (via KeyRing) for a local copy of the + * private key to use, and then unlock it if necessary. + */ + function getPrivKey(arg, next) { + // If the key pair already works, then use that... + try { + arg.privKey = tritonapi.keyPair.getPrivateKey(); + next(); + return; + } catch (_) { + // ... else fall through. + } + + var kr = new auth.KeyRing(); + kr.list(function (listErr, keyMap) { + if (listErr) { + next(listErr); + return; + } + + /* + * If our keyId was found, and with the 'homedir' plugin, then + * we should have access to the private key (modulo unlocking). + */ + var keyPairs = keyMap[tritonapi.profile.keyId] || []; + var homedirKeyPair; + for (var i = 0; i < keyPairs.length; i++) { + if (keyPairs[i].plugin === 'homedir') { + homedirKeyPair = keyPairs[i]; + break; + } + } + if (homedirKeyPair) { + common.promptPassphraseUnlockKey({ + // Fake the `tritonapi` object, only `.keyPair` is used. + tritonapi: {keyPair: homedirKeyPair} + }, function (unlockErr) { + if (unlockErr) { + next(unlockErr); + return; + } + try { + arg.privKey = homedirKeyPair.getPrivateKey(); + } catch (homedirErr) { + next(new errors.SetupError(homedirErr, format( + 'could not obtain SSH private key for keyId ' + + '"%s" to create Docker certificate', + profile.keyId))); + return; + } + next(); + }); + } else { + next(new errors.SetupError(format('could not obtain SSH ' + + 'private key for keyId "%s" to create Docker ' + + 'certificate', profile.keyId))); + } + }); + }, function genClientCert_dir(arg, next) { arg.dockerCertPath = path.resolve(cli.configDir, @@ -327,7 +377,7 @@ function profileDockerSetup(opts, cb) { }, function genClientCert_key(arg, next) { arg.keyPath = path.resolve(arg.dockerCertPath, 'key.pem'); - var data = tritonapi.keyPair.getPrivateKey().toBuffer('pkcs1'); + var data = arg.privKey.toBuffer('pkcs1'); fs.writeFile(arg.keyPath, data, function (err) { if (err) { next(new errors.SetupError(err, format( @@ -340,9 +390,8 @@ function profileDockerSetup(opts, cb) { function genClientCert_cert(arg, next) { arg.certPath = path.resolve(arg.dockerCertPath, 'cert.pem'); - var privKey = tritonapi.keyPair.getPrivateKey(); var id = sshpk.identityFromDN('CN=' + profile.account); - var cert = sshpk.createSelfSignedCertificate(id, privKey); + var cert = sshpk.createSelfSignedCertificate(id, arg.privKey); var data = cert.toBuffer('pem'); fs.writeFile(arg.certPath, data, function (err) {