Browse Source

Provide SSH key option to specify client private key

This is to make it possible for admins to enable password-less
auth at the expense of security.

In that situation:
Server <=> Wetty <=> Client

- Link between Wetty and the SSH Server is secured by SSH.
- Wetty knows a private key to connect to the SSH Server, using an SSH
  User.
- Client only needs to know how to access Wetty to run commands
  on the SSH Host.
- Client would not be able to retrieve the private key from Wetty to
  login directly to the SSH Server.

This basically means that anything that reaches Wetty needs to be
trusted as it would be able to execute commands on the remote SSH server,
so admins should be really careful when using that feature.
pull/140/head
Bertrand Roussel 6 years ago
committed by Cian Butler
parent
commit
9995358752
  1. 5
      Dockerfile
  2. 17
      cli.mjs
  3. 26
      wetty.mjs

5
Dockerfile

@ -2,8 +2,9 @@ FROM node:8-alpine
MAINTAINER butlerx@notthe.cloud MAINTAINER butlerx@notthe.cloud
WORKDIR /app WORKDIR /app
RUN adduser -D -h /home/term -s /bin/sh term && \ RUN adduser -D -h /home/term -s /bin/sh term && \
echo "term:term" | chpasswd ( echo "term:term" | chpasswd ) && \
apk add --update build-base python openssh-client
EXPOSE 3000 EXPOSE 3000
COPY . /app COPY . /app
RUN apk add --update build-base python openssh-client && yarn RUN yarn
CMD node bin CMD node bin

17
cli.mjs

@ -29,6 +29,10 @@ const opts = optimist
demand : false, demand : false,
description: 'defaults to "password", you can use "publickey,password" instead', description: 'defaults to "password", you can use "publickey,password" instead',
}, },
sshkey: {
demand : false,
description: 'path to an optional client private key (connection will be password-less and insecure!)',
},
port: { port: {
demand : false, demand : false,
alias : 'p', alias : 'p',
@ -51,6 +55,7 @@ const sshuser = opts.sshuser || process.env.SSHUSER || '';
const sshhost = opts.sshhost || process.env.SSHHOST || 'localhost'; const sshhost = opts.sshhost || process.env.SSHHOST || 'localhost';
const sshauth = opts.sshauth || process.env.SSHAUTH || 'password,keyboard-interactive'; const sshauth = opts.sshauth || process.env.SSHAUTH || 'password,keyboard-interactive';
const sshport = opts.sshport || process.env.SSHPORT || 22; const sshport = opts.sshport || process.env.SSHPORT || 22;
const sshkey = opts.sshkey || process.env.SSHKEY || '';
const port = opts.port || process.env.PORT || 3000; const port = opts.port || process.env.PORT || 3000;
loadSSL(opts) loadSSL(opts)
@ -62,11 +67,21 @@ loadSSL(opts)
process.exit(1); process.exit(1);
}); });
const sshkeyWarning =
`!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
! Password-less auth enabled using private key from \'%s\'.
! This is dangerous, anything that reaches the wetty server
! will be able to run remote operations without authentication.
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!`;
if(sshkey) {
console.warn(sshkeyWarning);
}
process.on('uncaughtException', err => { process.on('uncaughtException', err => {
console.error(`Error: ${err}`); console.error(`Error: ${err}`);
}); });
const tty = wetty(port, sshuser, sshhost, sshport, sshauth, opts.ssl); const tty = wetty(port, sshuser, sshhost, sshport, sshauth, sshkey, opts.ssl);
tty.on('exit', code => { tty.on('exit', code => {
console.log(`exit with code: ${code}`); console.log(`exit with code: ${code}`);
}); });

26
wetty.mjs

@ -36,33 +36,37 @@ function createServer(port, sslopts) {
}); });
} }
function getCommand(socket, sshuser, sshhost, sshport, sshauth) { function getCommand(socket, sshuser, sshhost, sshport, sshauth, sshkey) {
const { request } = socket; const { request } = socket;
const match = request.headers.referer.match('.+/ssh/.+$'); const match = request.headers.referer.match('.+/ssh/.+$');
const sshAddress = sshuser ? `${sshuser}@${sshhost}` : sshhost; const sshAddress = sshuser ? `${sshuser}@${sshhost}` : sshhost;
const ssh = match ? `${match[0].split('/ssh/').pop()}@${sshhost}` : sshAddress; const ssh = match ? `${match[0].split('/ssh/').pop()}@${sshhost}` : sshAddress;
const sshRemoteOptsBase = [
path.join(__dirname, 'bin/ssh'),
ssh,
'-p',
sshport,
'-o',
`PreferredAuthentications=${sshauth}`,
]
const sshRemoteOpts = sshkey ? sshRemoteOptsBase.concat(['-i', sshkey])
: sshRemoteOptsBase
return [ return [
process.getuid() === 0 && sshhost === 'localhost' process.getuid() === 0 && sshhost === 'localhost'
? ['login', '-h', socket.client.conn.remoteAddress.split(':')[3]] ? ['login', '-h', socket.client.conn.remoteAddress.split(':')[3]]
: [ : sshRemoteOpts
path.join(__dirname, 'bin/ssh'), ,
ssh,
'-p',
sshport,
'-o',
`PreferredAuthentications=${sshauth}`,
],
ssh, ssh,
]; ];
} }
export default function start(port, sshuser, sshhost, sshport, sshauth, sslopts) { export default function start(port, sshuser, sshhost, sshport, sshauth, sshkey, sslopts) {
const events = new EventEmitter(); const events = new EventEmitter();
const io = server(createServer(port, sslopts), { path: '/wetty/socket.io' }); const io = server(createServer(port, sslopts), { path: '/wetty/socket.io' });
io.on('connection', socket => { io.on('connection', socket => {
console.log(`${new Date()} Connection accepted.`); console.log(`${new Date()} Connection accepted.`);
const [args, ssh] = getCommand(socket, sshuser, sshhost, sshport, sshauth); const [args, ssh] = getCommand(socket, sshuser, sshhost, sshport, sshauth, sshkey);
const term = spawn('/usr/bin/env', args, { const term = spawn('/usr/bin/env', args, {
name: 'xterm-256color', name: 'xterm-256color',
cols: 80, cols: 80,

Loading…
Cancel
Save