Skip to content

Commit

Permalink
shell: Require confirmation before connecting to remote machines
Browse files Browse the repository at this point in the history
  • Loading branch information
mvollmer committed Sep 13, 2024
1 parent 6592eda commit c35a131
Show file tree
Hide file tree
Showing 16 changed files with 472 additions and 144 deletions.
1 change: 1 addition & 0 deletions doc/guide/Makefile-guide.am
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ GUIDE_INCLUDES = \
doc/guide/cockpit-session.xml \
doc/guide/cockpit-spawn.xml \
doc/guide/cockpit-util.xml \
doc/guide/multi-host.xml \
doc/guide/authentication.xml \
doc/guide/embedding.xml \
doc/guide/feature-firewall.xml \
Expand Down
1 change: 1 addition & 0 deletions doc/guide/cockpit-guide.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<xi:include href="https.xml"/>
<xi:include href="listen.xml"/>
<xi:include href="startup.xml"/>
<xi:include href="multi-host.xml"/>
<xi:include href="authentication.xml"/>
<xi:include href="sso.xml"/>
<xi:include href="cert-authentication.xml"/>
Expand Down
54 changes: 54 additions & 0 deletions doc/guide/multi-host.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?xml version="1.0"?>
<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
"http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
<chapter id="multi-host">
<title>
Managing multiple hosts at the same time
</title>

<para>
Cockpit allows you to access multiple hosts in a single session,
by establishing SSH connections to other hosts. This is quite
similar to logging into these other hosts using the "ssh" command
on the command line, with one very important difference:
</para>
<para>
Code from the local host and all the remote hosts run at the same
time, in the same browser context. They are not sufficiently
isolated from each other in the browser. All code effectively has
the same privileges as the primary session on the local host.
</para>
<para>
Thus, <emphasis>you should only only connect to remote hosts that
you trust</emphasis>. You must be sure that none of the hosts that
you connect to will cause Cockpit to load malicious JavaScript
code into your browser.
</para>
<para>
Going forward, Cockpit will try to provide sufficient isolation to
make it safe to manage multiple hosts in a single Cockpit
session. But until we get there, Cockpit will at least warn you
before connecting to more than one host. It is also possible to
disable multiple hosts entirely, and some operating systems do
this already by default.
</para>
<para>
You can prevent loading of JavaScript, HTML, etc from more than
one host by adding this to <filename>cockpit.conf</filename>:
</para>
<programlisting>
[WebService]
AllowMultiHost=false
</programlisting>
<para>
When you allow multiple hosts in a single Cockpit session by
setting <code>AllowMultiHost</code> to true, then the user will be
warned once per session, before connecting to the second host. If
that is still too much, you can switch it off completely by adding
the following to <filename>cockpit.conf</filename>:
</para>
<programlisting>
[Session]
WarnBeforeConnecting=false
</programlisting>
</chapter>
12 changes: 12 additions & 0 deletions doc/man/cockpit.conf.xml
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,18 @@ IdleTimeout=15
<para>When not specified, there is no idle timeout by default.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>WarnBeforeConnecting</option></term>
<listitem>
<para>Whether to warn before connecting to remote hosts from the Shell. Defaults to true</para>
<informalexample>
<programlisting language="ini">
[Session]
WarnBeforeConnecting=false
</programlisting>
</informalexample>
</listitem>
</varlistentry>
</variablelist>
</refsect1>

Expand Down
10 changes: 10 additions & 0 deletions pkg/shell/base_index.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,16 @@ function Frames(index, setupIdleResetTimers) {

/* Need to create a new frame */
if (!frame) {
/* Never create new frames for machines that are not
connected yet. That would open a channel to them (for
loading the URL), which woould trigger the bridge to
attempt a log in. We want all logins to happen in a
single place (in hosts.jsx) so that we can get the
options right, and show a warning dialog.
*/
if (host != "localhost" && machine.state !== "connected")
return null;

new_frame = true;
frame = document.createElement("iframe");
frame.setAttribute("class", "container-frame");
Expand Down
117 changes: 95 additions & 22 deletions pkg/shell/hosts.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { Tooltip } from "@patternfly/react-core/dist/esm/components/Tooltip";

import 'polyfills';
import { CockpitNav, CockpitNavItem } from "./nav.jsx";
import { HostModal } from "./hosts_dialog.jsx";
import { HostModal, try2Connect, codes } from "./hosts_dialog.jsx";
import { useLoggedInUser } from "hooks";

const _ = cockpit.gettext;
Expand Down Expand Up @@ -73,8 +73,8 @@ export class CockpitHosts extends React.Component {
editing: false,
current_user: "",
current_key: props.machine.key,
show_modal: false,
edit_machine: null,
modal_properties: null,
modal_callback: null,
};

this.toggleMenu = this.toggleMenu.bind(this);
Expand All @@ -89,6 +89,16 @@ export class CockpitHosts extends React.Component {
cockpit.user().then(user => {
this.setState({ current_user: user.name || "" });
}).catch(exc => console.log(exc));

window.trigger_connection_flow = machine => {
if (!this.state.modal_properties)
this.connectHost(machine);
};
this.props.index.navigate(null, true);
}

componentWillUnmount() {
window.trigger_connection_flow = null;
}

static getDerivedStateFromProps(nextProps, prevState) {
Expand Down Expand Up @@ -116,12 +126,80 @@ export class CockpitHosts extends React.Component {
});
}

onAddNewHost() {
this.setState({ show_modal: true });
showModal(properties) {
return new Promise((resolve, reject) => {
this.setState({ modal_properties: properties,
modal_callback: result => { resolve(result); return Promise.resolve() },
});
});
}

async onAddNewHost() {
await this.showModal({ });
}

onHostEdit(event, machine) {
this.setState({ show_modal: true, edit_machine: machine });
async onHostEdit(event, machine) {
const connection_string = await this.showModal({ address: machine.address });
if (connection_string) {
const parts = this.props.machines.split_connection_string(connection_string);
const addr = this.props.hostAddr({ host: parts.address }, true);
if (machine == this.props.machine && parts.address != machine.address) {
this.props.loader.connect(parts.address);
this.props.jump(addr);
}
}
}

async connectHost(machine) {
if (machine.address == "localhost" || machine.state == "connected" || machine.state == "connecting")
return machine.connection_string;

let connection_string = null;

if (machine.problem && codes[machine.problem]) {
// trouble shooting
connection_string = await this.showModal({
address: machine.address,
template: codes[machine.problem],
});
} else if (!window.sessionStorage.getItem("connection-warning-shown")) {
// connect by launching into the "Connection warning" dialog.
connection_string = await this.showModal({
address: machine.address,
template: "connect"
});
} else {
// Try to connect without any dialog
try {
await try2Connect(this.props.machines, machine.connection_string);
connection_string = machine.connection_string;
} catch (err) {
// continue with troubleshooting in the dialog
connection_string = await this.showModal({
address: machine.address,
template: codes[err.problem] || "change-port",
error_options: err,
});
}
}

if (connection_string) {
// make the rest of the shell aware that the machine is now connected
const parts = this.props.machines.split_connection_string(connection_string);
this.props.loader.connect(parts.address);
this.props.index.navigate();
}

return connection_string;
}

async onHostSwitch(machine) {
const connection_string = await this.connectHost(machine);
if (connection_string) {
const parts = this.props.machines.split_connection_string(connection_string);
const addr = this.props.hostAddr({ host: parts.address }, true);
this.props.jump(addr);
}
}

onEditHosts() {
Expand Down Expand Up @@ -180,7 +258,7 @@ export class CockpitHosts extends React.Component {
header={(m.user ? m.user : this.state.current_user) + " @"}
status={m.state === "failed" ? { type: "error", title: _("Connection error") } : null}
className={m.state}
jump={this.props.jump}
jump={() => this.onHostSwitch(m)}
actions={<>
<Tooltip content={_("Edit")} position="right">
<Button isDisabled={m.address === "localhost"} className="nav-action" hidden={!editing} onClick={e => this.onHostEdit(e, m)} key={m.label + "edit"} variant="secondary"><EditIcon /></Button>
Expand Down Expand Up @@ -240,20 +318,13 @@ export class CockpitHosts extends React.Component {
</HostsSelector>
}
</div>
{this.state.show_modal &&
<HostModal machines_ins={this.props.machines}
onClose={() => this.setState({ show_modal: false, edit_machine: null })}
address={this.state.edit_machine ? this.state.edit_machine.address : null}
caller_callback={this.state.edit_machine
? (new_connection_string) => {
const parts = this.props.machines.split_connection_string(new_connection_string);
if (this.state.edit_machine == this.props.machine && parts.address != this.state.edit_machine.address) {
const addr = this.props.hostAddr({ host: parts.address }, true);
this.props.jump(addr);
}
return Promise.resolve();
}
: null } />
{this.state.modal_properties &&
<HostModal machines_ins={this.props.machines}
onClose={() => this.setState({ modal_properties: null })}
{...this.state.modal_properties}
caller_callback={this.state.modal_callback}
caller_cancelled={() => this.state.modal_callback(null)}
/>
}
</>
);
Expand All @@ -263,6 +334,8 @@ export class CockpitHosts extends React.Component {
CockpitHosts.propTypes = {
machine: PropTypes.object.isRequired,
machines: PropTypes.object.isRequired,
index: PropTypes.object.isRequired,
loader: PropTypes.object.isRequired,
selector: PropTypes.string.isRequired,
hostAddr: PropTypes.func.isRequired,
jump: PropTypes.func.isRequired,
Expand Down
Loading

0 comments on commit c35a131

Please sign in to comment.