Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/10643.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add config option to use non-default manhole password and keys.
29 changes: 25 additions & 4 deletions docs/manhole.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Note that this will give administrative access to synapse to **all users** with
shell access to the server. It should therefore **not** be enabled in
environments where untrusted users have shell access.

***
## Configuring the manhole

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

#### Accessing synapse manhole
### Security settings

The following config options are available:

- `username` - The username for the manhole (defaults to `matrix`)
- `password` - The password for the manhole (defaults to `rabbithole`)
- `ssh_priv_key` - The path to a private SSH key (defaults to a hardcoded value)
- `ssh_pub_key` - The path to a public SSH key (defaults to a hardcoded value)

For example:

```yaml
manhole_settings:
username: manhole
password: mypassword
ssh_priv_key: "/home/synapse/manhole_keys/id_rsa"
ssh_pub_key: "/home/synapse/manhole_keys/id_rsa.pub"
```


## Accessing synapse manhole

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

```bash
ssh -p9000 matrix@localhost
```

The password is `rabbithole`.
Then enter the password when prompted (the default is `rabbithole`).

This gives a Python REPL in which `hs` gives access to the
`synapse.server.HomeServer` object - which in turn gives access to many other
Expand Down
18 changes: 18 additions & 0 deletions docs/sample_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,24 @@ listeners:
# bind_addresses: ['::1', '127.0.0.1']
# type: manhole

# Connection settings for the manhole
#
manhole_settings:
# The username for the manhole. This defaults to 'matrix'.
#
#username: manhole

# The password for the manhole. This defaults to 'rabbithole'.
#
#password: mypassword

# The private and public SSH key pair used to encrypt the manhole traffic.
# If these are left unset, then hardcoded and non-secret keys are used,
# which could allow traffic to be intercepted if sent over a public network.
#
#ssh_priv_key_path: CONFDIR/id_rsa
#ssh_pub_key_path: CONFDIR/id_rsa.pub

# Forward extremities can build up in a room due to networking delays between
# homeservers. Once this happens in a large room, calculation of the state of
# that room can become quite expensive. To mitigate this, once the number of
Expand Down
10 changes: 8 additions & 2 deletions synapse/app/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from synapse.app import check_bind_error
from synapse.app.phone_stats_home import start_phone_stats_home
from synapse.config.homeserver import HomeServerConfig
from synapse.config.server import ManholeConfig
from synapse.crypto import context_factory
from synapse.events.presence_router import load_legacy_presence_router
from synapse.events.spamcheck import load_legacy_spam_checkers
Expand Down Expand Up @@ -229,7 +230,12 @@ def listen_metrics(bind_addresses, port):
start_http_server(port, addr=host, registry=RegistryProxy)


def listen_manhole(bind_addresses: Iterable[str], port: int, manhole_globals: dict):
def listen_manhole(
bind_addresses: Iterable[str],
port: int,
manhole_settings: ManholeConfig,
manhole_globals: dict,
):
# twisted.conch.manhole 21.1.0 uses "int_from_bytes", which produces a confusing
# warning. It's fixed by https://github.com/twisted/twisted/pull/1522), so
# suppress the warning for now.
Expand All @@ -244,7 +250,7 @@ def listen_manhole(bind_addresses: Iterable[str], port: int, manhole_globals: di
listen_tcp(
bind_addresses,
port,
manhole(username="matrix", password="rabbithole", globals=manhole_globals),
manhole(settings=manhole_settings, globals=manhole_globals),
)


Expand Down
5 changes: 4 additions & 1 deletion synapse/app/generic_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,10 @@ def start_listening(self):
self._listen_http(listener)
elif listener.type == "manhole":
_base.listen_manhole(
listener.bind_addresses, listener.port, manhole_globals={"hs": self}
listener.bind_addresses,
listener.port,
manhole_settings=self.config.server.manhole_settings,
manhole_globals={"hs": self},
)
elif listener.type == "metrics":
if not self.config.enable_metrics:
Expand Down
5 changes: 4 additions & 1 deletion synapse/app/homeserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,10 @@ def start_listening(self):
)
elif listener.type == "manhole":
_base.listen_manhole(
listener.bind_addresses, listener.port, manhole_globals={"hs": self}
listener.bind_addresses,
listener.port,
manhole_settings=self.config.server.manhole_settings,
manhole_globals={"hs": self},
)
elif listener.type == "replication":
services = listen_tcp(
Expand Down
87 changes: 85 additions & 2 deletions synapse/config/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,14 @@
import yaml
from netaddr import AddrFormatError, IPNetwork, IPSet

from twisted.conch.ssh.keys import Key

from synapse.api.room_versions import KNOWN_ROOM_VERSIONS
from synapse.util.module_loader import load_module
from synapse.util.stringutils import parse_and_validate_server_name

from ._base import Config, ConfigError
from ._util import validate_config

logger = logging.Logger(__name__)

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


@attr.s(frozen=True)
class ManholeConfig:
"""Object describing the configuration of the manhole"""

username = attr.ib(type=str, validator=attr.validators.instance_of(str))
password = attr.ib(type=str, validator=attr.validators.instance_of(str))
priv_key = attr.ib(type=Optional[Key])
pub_key = attr.ib(type=Optional[Key])


class ServerConfig(Config):
section = "server"

Expand Down Expand Up @@ -649,6 +662,41 @@ class LimitRemoteRoomsConfig:
)
)

manhole_settings = config.get("manhole_settings") or {}
validate_config(
_MANHOLE_SETTINGS_SCHEMA, manhole_settings, ("manhole_settings",)
)

manhole_username = manhole_settings.get("username", "matrix")
manhole_password = manhole_settings.get("password", "rabbithole")
manhole_priv_key_path = manhole_settings.get("ssh_priv_key_path")
manhole_pub_key_path = manhole_settings.get("ssh_pub_key_path")

manhole_priv_key = None
if manhole_priv_key_path is not None:
try:
manhole_priv_key = Key.fromFile(manhole_priv_key_path)
except Exception as e:
raise ConfigError(
f"Failed to read manhole private key file {manhole_priv_key_path}"
) from e

manhole_pub_key = None
if manhole_pub_key_path is not None:
try:
manhole_pub_key = Key.fromFile(manhole_pub_key_path)
except Exception as e:
raise ConfigError(
f"Failed to read manhole public key file {manhole_pub_key_path}"
) from e

self.manhole_settings = ManholeConfig(
username=manhole_username,
password=manhole_password,
priv_key=manhole_priv_key,
pub_key=manhole_pub_key,
)

metrics_port = config.get("metrics_port")
if metrics_port:
logger.warning(METRICS_PORT_WARNING)
Expand Down Expand Up @@ -715,7 +763,7 @@ class LimitRemoteRoomsConfig:
if not isinstance(templates_config, dict):
raise ConfigError("The 'templates' section must be a dictionary")

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

def generate_config_section(
self, server_name, data_dir_path, open_private_ports, listeners, **kwargs
self,
server_name,
data_dir_path,
open_private_ports,
listeners,
config_dir_path,
**kwargs,
):
ip_range_blacklist = "\n".join(
" # - '%s'" % ip for ip in DEFAULT_IP_RANGE_BLACKLIST
Expand Down Expand Up @@ -1068,6 +1122,24 @@ def generate_config_section(
# bind_addresses: ['::1', '127.0.0.1']
# type: manhole

# Connection settings for the manhole
#
manhole_settings:
# The username for the manhole. This defaults to 'matrix'.
#
#username: manhole

# The password for the manhole. This defaults to 'rabbithole'.
#
#password: mypassword

# The private and public SSH key pair used to encrypt the manhole traffic.
# If these are left unset, then hardcoded and non-secret keys are used,
# which could allow traffic to be intercepted if sent over a public network.
#
#ssh_priv_key_path: %(config_dir_path)s/id_rsa
#ssh_pub_key_path: %(config_dir_path)s/id_rsa.pub

# Forward extremities can build up in a room due to networking delays between
# homeservers. Once this happens in a large room, calculation of the state of
# that room can become quite expensive. To mitigate this, once the number of
Expand Down Expand Up @@ -1436,3 +1508,14 @@ def _warn_if_webclient_configured(listeners: Iterable[ListenerConfig]) -> None:
if name == "webclient":
logger.warning(NO_MORE_WEB_CLIENT_WARNING)
return


_MANHOLE_SETTINGS_SCHEMA = {
"type": "object",
"properties": {
"username": {"type": "string"},
"password": {"type": "string"},
"ssh_priv_key_path": {"type": "string"},
"ssh_pub_key_path": {"type": "string"},
},
}
15 changes: 12 additions & 3 deletions synapse/util/manhole.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
-----END RSA PRIVATE KEY-----"""


def manhole(username, password, globals):
def manhole(settings, globals):
"""Starts a ssh listener with password authentication using
the given username and password. Clients connecting to the ssh
listener will find themselves in a colored python shell with
Expand All @@ -75,6 +75,15 @@ def manhole(username, password, globals):
Returns:
twisted.internet.protocol.Factory: A factory to pass to ``listenTCP``
"""
username = settings.username
password = settings.password
priv_key = settings.priv_key
if priv_key is None:
priv_key = Key.fromString(PRIVATE_KEY)
pub_key = settings.pub_key
if pub_key is None:
pub_key = Key.fromString(PUBLIC_KEY)

if not isinstance(password, bytes):
password = password.encode("ascii")

Expand All @@ -86,8 +95,8 @@ def manhole(username, password, globals):
)

factory = manhole_ssh.ConchFactory(portal.Portal(rlm, [checker]))
factory.publicKeys[b"ssh-rsa"] = Key.fromString(PUBLIC_KEY)
factory.privateKeys[b"ssh-rsa"] = Key.fromString(PRIVATE_KEY)
factory.privateKeys[b"ssh-rsa"] = priv_key
factory.publicKeys[b"ssh-rsa"] = pub_key

return factory

Expand Down
8 changes: 4 additions & 4 deletions tests/config/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def test_is_threepid_reserved(self):
def test_unsecure_listener_no_listeners_open_private_ports_false(self):
conf = yaml.safe_load(
ServerConfig().generate_config_section(
"che.org", "/data_dir_path", False, None
"che.org", "/data_dir_path", False, None, config_dir_path="CONFDIR"
)
)

Expand All @@ -55,7 +55,7 @@ def test_unsecure_listener_no_listeners_open_private_ports_false(self):
def test_unsecure_listener_no_listeners_open_private_ports_true(self):
conf = yaml.safe_load(
ServerConfig().generate_config_section(
"che.org", "/data_dir_path", True, None
"che.org", "/data_dir_path", True, None, config_dir_path="CONFDIR"
)
)

Expand Down Expand Up @@ -89,7 +89,7 @@ def test_listeners_set_correctly_open_private_ports_false(self):

conf = yaml.safe_load(
ServerConfig().generate_config_section(
"this.one.listens", "/data_dir_path", True, listeners
"this.one.listens", "/data_dir_path", True, listeners, "CONFDIR"
)
)

Expand Down Expand Up @@ -123,7 +123,7 @@ def test_listeners_set_correctly_open_private_ports_true(self):

conf = yaml.safe_load(
ServerConfig().generate_config_section(
"this.one.listens", "/data_dir_path", True, listeners
"this.one.listens", "/data_dir_path", True, listeners, "CONFDIR"
)
)

Expand Down