mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-10 20:26:48 +00:00
fix(console): disable the unauthenticated serial console by default
socketiod does not validate the console token, keeps a single global PTY and broadcasts its output to every connected client, so anyone who can reach port 6081 can read and write a serial console that someone opened. It is also broken for legitimate users, since console/views.py now writes signed tokens that socketiod cannot parse. Until it is rewritten, turn the serial console off by default: - Add SERIAL_CONSOLE_ENABLED = False to the settings template; installs whose settings.py lacks it are treated as disabled. - The console view shows an error instead of the xterm page for pty consoles while the setting is off. - socketiod refuses to start unless SERIAL_CONSOLE_ENABLED is True. - Supervisor: autostart=false for socketiod; drop -d (debug) from the supervisor and systemd definitions; fix the systemd unit description; conf/systemd/install.sh no longer suggests enabling socketiod. - README: stop opening port 6081 and document how to enable the serial console on a trusted network.
This commit is contained in:
parent
b1ab1d47fa
commit
1547e58e55
9 changed files with 127 additions and 9 deletions
|
|
@ -17,6 +17,19 @@ if ROOT_PATH not in sys.path:
|
|||
|
||||
django.setup()
|
||||
|
||||
from django.conf import settings as django_settings
|
||||
|
||||
# socketiod does not authenticate connections yet (ROADMAP S-01): any client
|
||||
# that can reach the port can read and write an open serial console. Refuse
|
||||
# to start unless an admin explicitly opts in.
|
||||
if not getattr(django_settings, "SERIAL_CONSOLE_ENABLED", False):
|
||||
sys.stderr.write(
|
||||
"socketiod: the serial console is disabled (SERIAL_CONSOLE_ENABLED is not "
|
||||
"True in settings). It is unauthenticated; only enable it on a trusted "
|
||||
"network.\n"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
import re
|
||||
import tempfile
|
||||
import io
|
||||
|
|
|
|||
57
console/test_socketiod_disabled.py
Normal file
57
console/test_socketiod_disabled.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SOCKETIOD = ROOT / "console" / "socketiod"
|
||||
|
||||
|
||||
class SocketiodDisabledTestCase(unittest.TestCase):
|
||||
"""ROADMAP S-01: socketiod must refuse to start unless explicitly enabled."""
|
||||
|
||||
def test_socketiod_exits_when_serial_console_disabled(self):
|
||||
settings_dir = ROOT / "console" / "_test_settings"
|
||||
settings_dir.mkdir(exist_ok=True)
|
||||
try:
|
||||
(settings_dir / "__init__.py").write_text("")
|
||||
(settings_dir / "disabled.py").write_text(
|
||||
"from webvirtcloud.settings import * # noqa\n"
|
||||
"SERIAL_CONSOLE_ENABLED = False\n"
|
||||
"SOCKETIO_HOST = '127.0.0.1'\n"
|
||||
"SOCKETIO_PORT = 0\n"
|
||||
)
|
||||
env = dict(
|
||||
os.environ,
|
||||
DJANGO_SETTINGS_MODULE="console._test_settings.disabled",
|
||||
PYTHONPATH=str(ROOT),
|
||||
)
|
||||
# Other test modules import novncd, which sets SIGCHLD to SIG_IGN in
|
||||
# this process; with that, the child's exit status is lost (always 0).
|
||||
previous_sigchld = signal.signal(signal.SIGCHLD, signal.SIG_DFL)
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(SOCKETIOD)],
|
||||
cwd=ROOT,
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=20,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.fail("socketiod kept running although SERIAL_CONSOLE_ENABLED=False")
|
||||
finally:
|
||||
signal.signal(signal.SIGCHLD, previous_sigchld)
|
||||
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertIn("SERIAL_CONSOLE_ENABLED", result.stderr)
|
||||
finally:
|
||||
for f in settings_dir.glob("*"):
|
||||
if f.is_file():
|
||||
f.unlink()
|
||||
for d in settings_dir.glob("__pycache__"):
|
||||
for f in d.glob("*"):
|
||||
f.unlink()
|
||||
d.rmdir()
|
||||
settings_dir.rmdir()
|
||||
|
|
@ -121,6 +121,7 @@ class ConsoleViewsTestCase(TestCase):
|
|||
self.assertTemplateUsed(response, "console-vnc-lite.html")
|
||||
self.assertIn("SPICE", str(response.context["console_error"]))
|
||||
|
||||
@override_settings(SERIAL_CONSOLE_ENABLED=True)
|
||||
@patch("console.views.wvmInstance")
|
||||
def test_console_superuser_pty(self, mock_wvm):
|
||||
mock_conn = MagicMock()
|
||||
|
|
@ -134,6 +135,23 @@ class ConsoleViewsTestCase(TestCase):
|
|||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, "console-xterm.html")
|
||||
|
||||
@override_settings(SERIAL_CONSOLE_ENABLED=False)
|
||||
@patch("console.views.wvmInstance")
|
||||
def test_console_pty_refused_when_serial_console_disabled(self, mock_wvm):
|
||||
# ROADMAP S-01: socketiod is unauthenticated, so the serial console is
|
||||
# disabled unless an admin explicitly enables it.
|
||||
mock_conn = MagicMock()
|
||||
mock_conn.get_console_type.return_value = "pty"
|
||||
mock_wvm.return_value = mock_conn
|
||||
|
||||
self.client.force_login(self.admin_user)
|
||||
url = reverse("console") + f"?token={self.token}"
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateNotUsed(response, "console-xterm.html")
|
||||
self.assertIn("Serial console is disabled", str(response.context["console_error"]))
|
||||
|
||||
@patch("console.views.wvmInstance")
|
||||
def test_console_user_assigned_instance_access(self, mock_wvm):
|
||||
mock_conn = MagicMock()
|
||||
|
|
|
|||
|
|
@ -133,6 +133,16 @@ def console(request):
|
|||
if console_type == "vnc":
|
||||
console_page = "console-vnc-" + view_type + ".html"
|
||||
response = render(request, console_page, locals())
|
||||
elif console_type == "pty" and not getattr(
|
||||
settings, "SERIAL_CONSOLE_ENABLED", False
|
||||
):
|
||||
# socketiod does not authenticate connections yet (ROADMAP S-01), so
|
||||
# the serial console stays off unless an admin explicitly enables it.
|
||||
console_error = _(
|
||||
"Serial console is disabled on this server. "
|
||||
"Please switch VM graphics to VNC or ask an administrator."
|
||||
)
|
||||
response = render(request, "console-vnc-lite.html", locals())
|
||||
elif console_type == "pty":
|
||||
socketio_public_host = getattr(settings, "SOCKETIO_PUBLIC_HOST", None)
|
||||
socketio_public_port = getattr(settings, "SOCKETIO_PUBLIC_PORT", 6081)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue