Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 6e89536

Browse files
authored
Add config option to use non-default manhole password and keys (#10643)
1 parent b298de7 commit 6e89536

File tree

9 files changed

+161
-17
lines changed

9 files changed

+161
-17
lines changed

changelog.d/10643.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add config option to use non-default manhole password and keys.

docs/manhole.md

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Note that this will give administrative access to synapse to **all users** with
1111
shell access to the server. It should therefore **not** be enabled in
1212
environments where untrusted users have shell access.
1313

14-
***
14+
## Configuring the manhole
1515

1616
To enable it, first uncomment the `manhole` listener configuration in
1717
`homeserver.yaml`. The configuration is slightly different if you're using docker.
@@ -52,16 +52,37 @@ listeners:
5252
type: manhole
5353
```
5454

55-
#### Accessing synapse manhole
55+
### Security settings
56+
57+
The following config options are available:
58+
59+
- `username` - The username for the manhole (defaults to `matrix`)
60+
- `password` - The password for the manhole (defaults to `rabbithole`)
61+
- `ssh_priv_key` - The path to a private SSH key (defaults to a hardcoded value)
62+
- `ssh_pub_key` - The path to a public SSH key (defaults to a hardcoded value)
63+
64+
For example:
65+
66+
```yaml
67+
manhole_settings:
68+
username: manhole
69+
password: mypassword
70+
ssh_priv_key: "/home/synapse/manhole_keys/id_rsa"
71+
ssh_pub_key: "/home/synapse/manhole_keys/id_rsa.pub"
72+
```
73+
74+
75+
## Accessing synapse manhole
5676

5777
Then restart synapse, and point an ssh client at port 9000 on localhost, using
58-
the username `matrix`:
78+
the username and password configured in `homeserver.yaml` - with the default
79+
configuration, this would be:
5980

6081
```bash
6182
ssh -p9000 matrix@localhost
6283
```
6384

64-
The password is `rabbithole`.
85+
Then enter the password when prompted (the default is `rabbithole`).
6586

6687
This gives a Python REPL in which `hs` gives access to the
6788
`synapse.server.HomeServer` object - which in turn gives access to many other

docs/sample_config.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,24 @@ listeners:
335335
# bind_addresses: ['::1', '127.0.0.1']
336336
# type: manhole
337337

338+
# Connection settings for the manhole
339+
#
340+
manhole_settings:
341+
# The username for the manhole. This defaults to 'matrix'.
342+
#
343+
#username: manhole
344+
345+
# The password for the manhole. This defaults to 'rabbithole'.
346+
#
347+
#password: mypassword
348+
349+
# The private and public SSH key pair used to encrypt the manhole traffic.
350+
# If these are left unset, then hardcoded and non-secret keys are used,
351+
# which could allow traffic to be intercepted if sent over a public network.
352+
#
353+
#ssh_priv_key_path: CONFDIR/id_rsa
354+
#ssh_pub_key_path: CONFDIR/id_rsa.pub
355+
338356
# Forward extremities can build up in a room due to networking delays between
339357
# homeservers. Once this happens in a large room, calculation of the state of
340358
# that room can become quite expensive. To mitigate this, once the number of

synapse/app/_base.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from synapse.app import check_bind_error
3838
from synapse.app.phone_stats_home import start_phone_stats_home
3939
from synapse.config.homeserver import HomeServerConfig
40+
from synapse.config.server import ManholeConfig
4041
from synapse.crypto import context_factory
4142
from synapse.events.presence_router import load_legacy_presence_router
4243
from synapse.events.spamcheck import load_legacy_spam_checkers
@@ -230,7 +231,12 @@ def listen_metrics(bind_addresses, port):
230231
start_http_server(port, addr=host, registry=RegistryProxy)
231232

232233

233-
def listen_manhole(bind_addresses: Iterable[str], port: int, manhole_globals: dict):
234+
def listen_manhole(
235+
bind_addresses: Iterable[str],
236+
port: int,
237+
manhole_settings: ManholeConfig,
238+
manhole_globals: dict,
239+
):
234240
# twisted.conch.manhole 21.1.0 uses "int_from_bytes", which produces a confusing
235241
# warning. It's fixed by https://github.com/twisted/twisted/pull/1522), so
236242
# suppress the warning for now.
@@ -245,7 +251,7 @@ def listen_manhole(bind_addresses: Iterable[str], port: int, manhole_globals: di
245251
listen_tcp(
246252
bind_addresses,
247253
port,
248-
manhole(username="matrix", password="rabbithole", globals=manhole_globals),
254+
manhole(settings=manhole_settings, globals=manhole_globals),
249255
)
250256

251257

synapse/app/generic_worker.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,10 @@ def start_listening(self):
395395
self._listen_http(listener)
396396
elif listener.type == "manhole":
397397
_base.listen_manhole(
398-
listener.bind_addresses, listener.port, manhole_globals={"hs": self}
398+
listener.bind_addresses,
399+
listener.port,
400+
manhole_settings=self.config.server.manhole_settings,
401+
manhole_globals={"hs": self},
399402
)
400403
elif listener.type == "metrics":
401404
if not self.config.enable_metrics:

synapse/app/homeserver.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,10 @@ def start_listening(self):
291291
)
292292
elif listener.type == "manhole":
293293
_base.listen_manhole(
294-
listener.bind_addresses, listener.port, manhole_globals={"hs": self}
294+
listener.bind_addresses,
295+
listener.port,
296+
manhole_settings=self.config.server.manhole_settings,
297+
manhole_globals={"hs": self},
295298
)
296299
elif listener.type == "replication":
297300
services = listen_tcp(

synapse/config/server.py

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,14 @@
2525
import yaml
2626
from netaddr import AddrFormatError, IPNetwork, IPSet
2727

28+
from twisted.conch.ssh.keys import Key
29+
2830
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS
2931
from synapse.util.module_loader import load_module
3032
from synapse.util.stringutils import parse_and_validate_server_name
3133

3234
from ._base import Config, ConfigError
35+
from ._util import validate_config
3336

3437
logger = logging.Logger(__name__)
3538

@@ -216,6 +219,16 @@ class ListenerConfig:
216219
http_options = attr.ib(type=Optional[HttpListenerConfig], default=None)
217220

218221

222+
@attr.s(frozen=True)
223+
class ManholeConfig:
224+
"""Object describing the configuration of the manhole"""
225+
226+
username = attr.ib(type=str, validator=attr.validators.instance_of(str))
227+
password = attr.ib(type=str, validator=attr.validators.instance_of(str))
228+
priv_key = attr.ib(type=Optional[Key])
229+
pub_key = attr.ib(type=Optional[Key])
230+
231+
219232
class ServerConfig(Config):
220233
section = "server"
221234

@@ -649,6 +662,41 @@ class LimitRemoteRoomsConfig:
649662
)
650663
)
651664

665+
manhole_settings = config.get("manhole_settings") or {}
666+
validate_config(
667+
_MANHOLE_SETTINGS_SCHEMA, manhole_settings, ("manhole_settings",)
668+
)
669+
670+
manhole_username = manhole_settings.get("username", "matrix")
671+
manhole_password = manhole_settings.get("password", "rabbithole")
672+
manhole_priv_key_path = manhole_settings.get("ssh_priv_key_path")
673+
manhole_pub_key_path = manhole_settings.get("ssh_pub_key_path")
674+
675+
manhole_priv_key = None
676+
if manhole_priv_key_path is not None:
677+
try:
678+
manhole_priv_key = Key.fromFile(manhole_priv_key_path)
679+
except Exception as e:
680+
raise ConfigError(
681+
f"Failed to read manhole private key file {manhole_priv_key_path}"
682+
) from e
683+
684+
manhole_pub_key = None
685+
if manhole_pub_key_path is not None:
686+
try:
687+
manhole_pub_key = Key.fromFile(manhole_pub_key_path)
688+
except Exception as e:
689+
raise ConfigError(
690+
f"Failed to read manhole public key file {manhole_pub_key_path}"
691+
) from e
692+
693+
self.manhole_settings = ManholeConfig(
694+
username=manhole_username,
695+
password=manhole_password,
696+
priv_key=manhole_priv_key,
697+
pub_key=manhole_pub_key,
698+
)
699+
652700
metrics_port = config.get("metrics_port")
653701
if metrics_port:
654702
logger.warning(METRICS_PORT_WARNING)
@@ -715,7 +763,7 @@ class LimitRemoteRoomsConfig:
715763
if not isinstance(templates_config, dict):
716764
raise ConfigError("The 'templates' section must be a dictionary")
717765

718-
self.custom_template_directory = templates_config.get(
766+
self.custom_template_directory: Optional[str] = templates_config.get(
719767
"custom_template_directory"
720768
)
721769
if self.custom_template_directory is not None and not isinstance(
@@ -727,7 +775,13 @@ def has_tls_listener(self) -> bool:
727775
return any(listener.tls for listener in self.listeners)
728776

729777
def generate_config_section(
730-
self, server_name, data_dir_path, open_private_ports, listeners, **kwargs
778+
self,
779+
server_name,
780+
data_dir_path,
781+
open_private_ports,
782+
listeners,
783+
config_dir_path,
784+
**kwargs,
731785
):
732786
ip_range_blacklist = "\n".join(
733787
" # - '%s'" % ip for ip in DEFAULT_IP_RANGE_BLACKLIST
@@ -1068,6 +1122,24 @@ def generate_config_section(
10681122
# bind_addresses: ['::1', '127.0.0.1']
10691123
# type: manhole
10701124
1125+
# Connection settings for the manhole
1126+
#
1127+
manhole_settings:
1128+
# The username for the manhole. This defaults to 'matrix'.
1129+
#
1130+
#username: manhole
1131+
1132+
# The password for the manhole. This defaults to 'rabbithole'.
1133+
#
1134+
#password: mypassword
1135+
1136+
# The private and public SSH key pair used to encrypt the manhole traffic.
1137+
# If these are left unset, then hardcoded and non-secret keys are used,
1138+
# which could allow traffic to be intercepted if sent over a public network.
1139+
#
1140+
#ssh_priv_key_path: %(config_dir_path)s/id_rsa
1141+
#ssh_pub_key_path: %(config_dir_path)s/id_rsa.pub
1142+
10711143
# Forward extremities can build up in a room due to networking delays between
10721144
# homeservers. Once this happens in a large room, calculation of the state of
10731145
# that room can become quite expensive. To mitigate this, once the number of
@@ -1436,3 +1508,14 @@ def _warn_if_webclient_configured(listeners: Iterable[ListenerConfig]) -> None:
14361508
if name == "webclient":
14371509
logger.warning(NO_MORE_WEB_CLIENT_WARNING)
14381510
return
1511+
1512+
1513+
_MANHOLE_SETTINGS_SCHEMA = {
1514+
"type": "object",
1515+
"properties": {
1516+
"username": {"type": "string"},
1517+
"password": {"type": "string"},
1518+
"ssh_priv_key_path": {"type": "string"},
1519+
"ssh_pub_key_path": {"type": "string"},
1520+
},
1521+
}

synapse/util/manhole.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
-----END RSA PRIVATE KEY-----"""
6262

6363

64-
def manhole(username, password, globals):
64+
def manhole(settings, globals):
6565
"""Starts a ssh listener with password authentication using
6666
the given username and password. Clients connecting to the ssh
6767
listener will find themselves in a colored python shell with
@@ -75,6 +75,15 @@ def manhole(username, password, globals):
7575
Returns:
7676
twisted.internet.protocol.Factory: A factory to pass to ``listenTCP``
7777
"""
78+
username = settings.username
79+
password = settings.password
80+
priv_key = settings.priv_key
81+
if priv_key is None:
82+
priv_key = Key.fromString(PRIVATE_KEY)
83+
pub_key = settings.pub_key
84+
if pub_key is None:
85+
pub_key = Key.fromString(PUBLIC_KEY)
86+
7887
if not isinstance(password, bytes):
7988
password = password.encode("ascii")
8089

@@ -86,8 +95,8 @@ def manhole(username, password, globals):
8695
)
8796

8897
factory = manhole_ssh.ConchFactory(portal.Portal(rlm, [checker]))
89-
factory.publicKeys[b"ssh-rsa"] = Key.fromString(PUBLIC_KEY)
90-
factory.privateKeys[b"ssh-rsa"] = Key.fromString(PRIVATE_KEY)
98+
factory.privateKeys[b"ssh-rsa"] = priv_key
99+
factory.publicKeys[b"ssh-rsa"] = pub_key
91100

92101
return factory
93102

tests/config/test_server.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def test_is_threepid_reserved(self):
3535
def test_unsecure_listener_no_listeners_open_private_ports_false(self):
3636
conf = yaml.safe_load(
3737
ServerConfig().generate_config_section(
38-
"che.org", "/data_dir_path", False, None
38+
"che.org", "/data_dir_path", False, None, config_dir_path="CONFDIR"
3939
)
4040
)
4141

@@ -55,7 +55,7 @@ def test_unsecure_listener_no_listeners_open_private_ports_false(self):
5555
def test_unsecure_listener_no_listeners_open_private_ports_true(self):
5656
conf = yaml.safe_load(
5757
ServerConfig().generate_config_section(
58-
"che.org", "/data_dir_path", True, None
58+
"che.org", "/data_dir_path", True, None, config_dir_path="CONFDIR"
5959
)
6060
)
6161

@@ -89,7 +89,7 @@ def test_listeners_set_correctly_open_private_ports_false(self):
8989

9090
conf = yaml.safe_load(
9191
ServerConfig().generate_config_section(
92-
"this.one.listens", "/data_dir_path", True, listeners
92+
"this.one.listens", "/data_dir_path", True, listeners, "CONFDIR"
9393
)
9494
)
9595

@@ -123,7 +123,7 @@ def test_listeners_set_correctly_open_private_ports_true(self):
123123

124124
conf = yaml.safe_load(
125125
ServerConfig().generate_config_section(
126-
"this.one.listens", "/data_dir_path", True, listeners
126+
"this.one.listens", "/data_dir_path", True, listeners, "CONFDIR"
127127
)
128128
)
129129

0 commit comments

Comments
 (0)