joyent/node-triton#259 "triton ssh" could support proxying through a bastion host

Reviewed by: Tim Foster <tim.foster@joyent.com>
Approved by: Brian Bennett <brian.bennett@joyent.com>
This commit is contained in:
Joshua M. Clulow 2018-12-18 00:40:30 +00:00
parent 05f1bae869
commit 4921fd2e36
3 changed files with 218 additions and 7 deletions

View File

@ -6,6 +6,15 @@ Known issues:
## not yet released ## not yet released
## 6.3.0
- [joyent/node-triton#259] Added basic support for use of SSH bastion hosts
to access zones on private fabrics. If the `tritoncli.ssh.proxy` tag is set
on an instance, `triton ssh` will look up the name or UUID of the proxy
instance and use `ssh -o ProxyJump` to tunnel the connection to the target.
If the `tritoncli.ssh.ip` tag is set on an instance, `triton ssh` will use
that IP address instead of the `primaryIp` when making its connection.
## 6.2.0 ## 6.2.0
- [joyent/node-triton#255, joyent/node-triton#257] Improved the interface - [joyent/node-triton#255, joyent/node-triton#257] Improved the interface

View File

@ -5,11 +5,12 @@
*/ */
/* /*
* Copyright 2017 Joyent, Inc. * Copyright (c) 2018, Joyent, Inc.
* *
* `triton instance ssh ...` * `triton instance ssh ...`
*/ */
var assert = require('assert-plus');
var path = require('path'); var path = require('path');
var spawn = require('child_process').spawn; var spawn = require('child_process').spawn;
var vasync = require('vasync'); var vasync = require('vasync');
@ -17,6 +18,30 @@ var vasync = require('vasync');
var common = require('../common'); var common = require('../common');
var errors = require('../errors'); var errors = require('../errors');
/*
* The tag "tritoncli.ssh.ip" may be set to an IP address that belongs to the
* instance but which is not the primary IP. If set, we will use that IP
* address for the SSH connection instead of the primary IP.
*/
var TAG_SSH_IP = 'tritoncli.ssh.ip';
/*
* The tag "tritoncli.ssh.proxy" may be set to either the name or the UUID of
* another instance in this account. If set, we will use the "ProxyJump"
* feature of SSH to tunnel through the SSH server on that host. This is
* useful when exposing a single zone to the Internet while keeping the rest of
* your infrastructure on a private fabric.
*/
var TAG_SSH_PROXY = 'tritoncli.ssh.proxy';
/*
* The tag "tritoncli.ssh.proxyuser" may be set on the instance used as an SSH
* proxy. If set, we will use this value when making the proxy connection
* (i.e., it will be passed via the "ProxyJump" option). If not set, the
* default user selection behaviour applies.
*/
var TAG_SSH_PROXY_USER = 'tritoncli.ssh.proxyuser';
function do_ssh(subcmd, opts, args, callback) { function do_ssh(subcmd, opts, args, callback) {
if (opts.help) { if (opts.help) {
@ -30,10 +55,12 @@ function do_ssh(subcmd, opts, args, callback) {
var id = args.shift(); var id = args.shift();
var user; var user;
var overrideUser = false;
var i = id.indexOf('@'); var i = id.indexOf('@');
if (i >= 0) { if (i >= 0) {
user = id.substr(0, i); user = id.substr(0, i);
id = id.substr(i + 1); id = id.substr(i + 1);
overrideUser = true;
} }
vasync.pipeline({arg: {cli: this.top}, funcs: [ vasync.pipeline({arg: {cli: this.top}, funcs: [
@ -48,17 +75,112 @@ function do_ssh(subcmd, opts, args, callback) {
ctx.inst = inst; ctx.inst = inst;
if (inst.tags && inst.tags[TAG_SSH_IP]) {
ctx.ip = inst.tags[TAG_SSH_IP];
if (!inst.ips || inst.ips.indexOf(ctx.ip) === -1) {
next(new Error('IP address ' + ctx.ip + ' not ' +
'attached to the instance'));
return;
}
} else {
ctx.ip = inst.primaryIp; ctx.ip = inst.primaryIp;
}
if (!ctx.ip) { if (!ctx.ip) {
next(new Error('primaryIp not found for instance')); next(new Error('IP address not found for instance'));
return; return;
} }
next(); next();
}); });
}, },
function getInstanceBastionIp(ctx, next) {
if (opts.no_proxy) {
setImmediate(next);
return;
}
if (!ctx.inst.tags || !ctx.inst.tags[TAG_SSH_PROXY]) {
setImmediate(next);
return;
}
ctx.cli.tritonapi.getInstance(ctx.inst.tags[TAG_SSH_PROXY],
function (err, proxy) {
if (err) {
next(err);
return;
}
if (proxy.tags && proxy.tags[TAG_SSH_IP]) {
ctx.proxyIp = proxy.tags[TAG_SSH_IP];
if (!proxy.ips || proxy.ips.indexOf(ctx.proxyIp) === -1) {
next(new Error('IP address ' + ctx.proxyIp + ' not ' +
'attached to the instance'));
return;
}
} else {
ctx.proxyIp = proxy.primaryIp;
}
ctx.proxyImage = proxy.image;
/*
* Selecting the right user to use for the proxy connection is
* somewhat nuanced, in order to allow for various useful
* configurations. We wish to enable the following cases:
*
* 1. The least sophisticated configuration; i.e., using two
* instances (the target instance and the proxy instnace)
* with the default "root" (or, e.g., "ubuntu") account
* and smartlogin or authorized_keys metadata for SSH key
* management.
*
* 2. The user has set up their own accounts (e.g., "roberta")
* in all of their instances and does their own SSH key
* management. They connect with:
*
* triton inst ssh roberta@instance
*
* In this case we will use "roberta" for both the proxy
* and the target instance. This means a user provided on
* the command line will override the per-image default
* user (e.g., "root" or "ubuntu") -- if the user wants to
* retain the default account for the proxy, they should
* use case 3 below.
*
* 3. The user has set up their own accounts in the target
* instance (e.g., "felicity"), but the proxy instance is
* using a single specific account that should be used by
* all users in the organisation (e.g., "partyline"). In
* this case, we want the user to be able to specify the
* global proxy account setting as a tag on the proxy
* instance, so that for:
*
* triton inst ssh felicity@instance
*
* ... we will use "-o ProxyJump partyline@proxy" but
* still use "felicity" for the target connection. This
* last case requires the proxy user tag (if set) to
* override a user provided on the command line.
*/
if (proxy.tags && proxy.tags[TAG_SSH_PROXY_USER]) {
ctx.proxyUser = proxy.tags[TAG_SSH_PROXY_USER];
}
if (!ctx.proxyIp) {
next(new Error('IP address not found for proxy instance'));
return;
}
next();
});
},
function getUser(ctx, next) { function getUser(ctx, next) {
if (user) { if (overrideUser) {
assert.string(user, 'user');
next(); next();
return; return;
} }
@ -73,8 +195,8 @@ function do_ssh(subcmd, opts, args, callback) {
} }
/* /*
* This is a convention as seen on Joyent's * This is a convention as seen on Joyent's "ubuntu-certified"
* "ubuntu-certified" KVM images. * KVM images.
*/ */
if (image.tags && image.tags.default_user) { if (image.tags && image.tags.default_user) {
user = image.tags.default_user; user = image.tags.default_user;
@ -86,9 +208,64 @@ function do_ssh(subcmd, opts, args, callback) {
}); });
}, },
function getBastionUser(ctx, next) {
if (!ctx.proxyImage || ctx.proxyUser) {
/*
* If there is no image for the proxy host, or an override user
* was already provided in the tags of the proxy instance
* itself, we don't need to look up the default user.
*/
next();
return;
}
if (overrideUser) {
/*
* A user was provided on the command line, but no user
* override tag was present on the proxy instance. To enable
* use case 2 (see comments above) we'll prefer this user over
* the image default.
*/
assert.string(user, 'user');
ctx.proxyUser = user;
next();
return;
}
ctx.cli.tritonapi.getImage({
name: ctx.proxyImage,
useCache: true
}, function (getImageErr, image) {
if (getImageErr) {
next(getImageErr);
return;
}
/*
* This is a convention as seen on Joyent's "ubuntu-certified"
* KVM images.
*/
assert.ok(!ctx.proxyUser, 'proxy user set twice');
if (image.tags && image.tags.default_user) {
ctx.proxyUser = image.tags.default_user;
} else {
ctx.proxyUser = 'root';
}
next();
});
},
function doSsh(ctx, next) { function doSsh(ctx, next) {
args = ['-l', user, ctx.ip].concat(args); args = ['-l', user, ctx.ip].concat(args);
if (ctx.proxyIp) {
assert.string(ctx.proxyUser, 'ctx.proxyUser');
args = [
'-o', 'ProxyJump=' + ctx.proxyUser + '@' + ctx.proxyIp
].concat(args);
}
/* /*
* By default we disable ControlMaster (aka mux, aka SSH * By default we disable ControlMaster (aka mux, aka SSH
* connection multiplexing) because of * connection multiplexing) because of
@ -133,6 +310,11 @@ do_ssh.options = [
names: ['help', 'h'], names: ['help', 'h'],
type: 'bool', type: 'bool',
help: 'Show this help.' help: 'Show this help.'
},
{
names: ['no-proxy'],
type: 'bool',
help: 'Disable SSH proxy support (ignore "tritoncli.ssh.proxy" tag)'
} }
]; ];
do_ssh.synopses = ['{{name}} ssh [-h] [USER@]INST [SSH-ARGUMENTS]']; do_ssh.synopses = ['{{name}} ssh [-h] [USER@]INST [SSH-ARGUMENTS]'];
@ -150,6 +332,26 @@ do_ssh.help = [
'If USER is not specified and the default_user tag is not set, the user', 'If USER is not specified and the default_user tag is not set, the user',
'is assumed to be \"root\".', 'is assumed to be \"root\".',
'', '',
'The "tritoncli.ssh.proxy" tag on the target instance may be set to',
'the name or the UUID of another instance through which to proxy this',
'SSH connection. If set, the primary IP of the proxy instance will be',
'loaded and passed to SSH via the ProxyJump option. The --no-proxy',
'flag can be used to ignore the tag and force a direct connection.',
'',
'For example, to proxy connections to zone "narnia" through "wardrobe":',
' triton instance tag set narnia tritoncli.ssh.proxy=wardrobe',
'',
'The "tritoncli.ssh.ip" tag on the target instance may be set to the',
'IP address to use for SSH connections. This may be useful if the',
'primary IP address is not available for SSH connections. This address',
'must be set to one of the IP addresses attached to the instance.',
'',
'The "tritoncli.ssh.proxyuser" tag on the proxy instance may be set to',
'the user account that should be used for the proxy connection (i.e., via',
'the SSH ProxyJump option). This is useful when all users of the proxy',
'instance should use a special common account, and will override the USER',
'value (if one is provided) for the SSH connection to the target instance.',
'',
'There is a known issue with SSH connection multiplexing (a.k.a. ', 'There is a known issue with SSH connection multiplexing (a.k.a. ',
'ControlMaster, mux) where stdout/stderr is lost. As a workaround, `ssh`', 'ControlMaster, mux) where stdout/stderr is lost. As a workaround, `ssh`',
'is spawned with options disabling ControlMaster. See ', 'is spawned with options disabling ControlMaster. See ',

View File

@ -1,7 +1,7 @@
{ {
"name": "triton", "name": "triton",
"description": "Joyent Triton CLI and client (https://www.joyent.com/triton)", "description": "Joyent Triton CLI and client (https://www.joyent.com/triton)",
"version": "6.2.0", "version": "6.3.0",
"author": "Joyent (joyent.com)", "author": "Joyent (joyent.com)",
"homepage": "https://github.com/joyent/node-triton", "homepage": "https://github.com/joyent/node-triton",
"dependencies": { "dependencies": {