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

Add config option to use non-default manhole password and keys #10643

Merged
merged 14 commits into from
Sep 6, 2021
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