mirror of
https://github.com/element-hq/synapse.git
synced 2024-11-30 18:59:49 +03:00
Add config option to use non-default manhole password and keys (#10643)
This commit is contained in:
parent
b298de780a
commit
6e895366ea
9 changed files with 161 additions and 17 deletions
1
changelog.d/10643.feature
Normal file
1
changelog.d/10643.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Add config option to use non-default manhole password and keys.
|
|
@ -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
|
shell access to the server. It should therefore **not** be enabled in
|
||||||
environments where untrusted users have shell access.
|
environments where untrusted users have shell access.
|
||||||
|
|
||||||
***
|
## Configuring the manhole
|
||||||
|
|
||||||
To enable it, first uncomment the `manhole` listener configuration in
|
To enable it, first uncomment the `manhole` listener configuration in
|
||||||
`homeserver.yaml`. The configuration is slightly different if you're using docker.
|
`homeserver.yaml`. The configuration is slightly different if you're using docker.
|
||||||
|
@ -52,16 +52,37 @@ listeners:
|
||||||
type: manhole
|
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
|
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
|
```bash
|
||||||
ssh -p9000 matrix@localhost
|
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
|
This gives a Python REPL in which `hs` gives access to the
|
||||||
`synapse.server.HomeServer` object - which in turn gives access to many other
|
`synapse.server.HomeServer` object - which in turn gives access to many other
|
||||||
|
|
|
@ -335,6 +335,24 @@ listeners:
|
||||||
# bind_addresses: ['::1', '127.0.0.1']
|
# bind_addresses: ['::1', '127.0.0.1']
|
||||||
# type: manhole
|
# 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
|
# 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
|
# 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
|
# that room can become quite expensive. To mitigate this, once the number of
|
||||||
|
|
|
@ -37,6 +37,7 @@ from synapse.api.constants import MAX_PDU_SIZE
|
||||||
from synapse.app import check_bind_error
|
from synapse.app import check_bind_error
|
||||||
from synapse.app.phone_stats_home import start_phone_stats_home
|
from synapse.app.phone_stats_home import start_phone_stats_home
|
||||||
from synapse.config.homeserver import HomeServerConfig
|
from synapse.config.homeserver import HomeServerConfig
|
||||||
|
from synapse.config.server import ManholeConfig
|
||||||
from synapse.crypto import context_factory
|
from synapse.crypto import context_factory
|
||||||
from synapse.events.presence_router import load_legacy_presence_router
|
from synapse.events.presence_router import load_legacy_presence_router
|
||||||
from synapse.events.spamcheck import load_legacy_spam_checkers
|
from synapse.events.spamcheck import load_legacy_spam_checkers
|
||||||
|
@ -230,7 +231,12 @@ def listen_metrics(bind_addresses, port):
|
||||||
start_http_server(port, addr=host, registry=RegistryProxy)
|
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
|
# 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
|
# warning. It's fixed by https://github.com/twisted/twisted/pull/1522), so
|
||||||
# suppress the warning for now.
|
# suppress the warning for now.
|
||||||
|
@ -245,7 +251,7 @@ def listen_manhole(bind_addresses: Iterable[str], port: int, manhole_globals: di
|
||||||
listen_tcp(
|
listen_tcp(
|
||||||
bind_addresses,
|
bind_addresses,
|
||||||
port,
|
port,
|
||||||
manhole(username="matrix", password="rabbithole", globals=manhole_globals),
|
manhole(settings=manhole_settings, globals=manhole_globals),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -395,7 +395,10 @@ class GenericWorkerServer(HomeServer):
|
||||||
self._listen_http(listener)
|
self._listen_http(listener)
|
||||||
elif listener.type == "manhole":
|
elif listener.type == "manhole":
|
||||||
_base.listen_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":
|
elif listener.type == "metrics":
|
||||||
if not self.config.enable_metrics:
|
if not self.config.enable_metrics:
|
||||||
|
|
|
@ -291,7 +291,10 @@ class SynapseHomeServer(HomeServer):
|
||||||
)
|
)
|
||||||
elif listener.type == "manhole":
|
elif listener.type == "manhole":
|
||||||
_base.listen_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":
|
elif listener.type == "replication":
|
||||||
services = listen_tcp(
|
services = listen_tcp(
|
||||||
|
|
|
@ -25,11 +25,14 @@ import attr
|
||||||
import yaml
|
import yaml
|
||||||
from netaddr import AddrFormatError, IPNetwork, IPSet
|
from netaddr import AddrFormatError, IPNetwork, IPSet
|
||||||
|
|
||||||
|
from twisted.conch.ssh.keys import Key
|
||||||
|
|
||||||
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS
|
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS
|
||||||
from synapse.util.module_loader import load_module
|
from synapse.util.module_loader import load_module
|
||||||
from synapse.util.stringutils import parse_and_validate_server_name
|
from synapse.util.stringutils import parse_and_validate_server_name
|
||||||
|
|
||||||
from ._base import Config, ConfigError
|
from ._base import Config, ConfigError
|
||||||
|
from ._util import validate_config
|
||||||
|
|
||||||
logger = logging.Logger(__name__)
|
logger = logging.Logger(__name__)
|
||||||
|
|
||||||
|
@ -216,6 +219,16 @@ class ListenerConfig:
|
||||||
http_options = attr.ib(type=Optional[HttpListenerConfig], default=None)
|
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):
|
class ServerConfig(Config):
|
||||||
section = "server"
|
section = "server"
|
||||||
|
|
||||||
|
@ -649,6 +662,41 @@ class ServerConfig(Config):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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")
|
metrics_port = config.get("metrics_port")
|
||||||
if metrics_port:
|
if metrics_port:
|
||||||
logger.warning(METRICS_PORT_WARNING)
|
logger.warning(METRICS_PORT_WARNING)
|
||||||
|
@ -715,7 +763,7 @@ class ServerConfig(Config):
|
||||||
if not isinstance(templates_config, dict):
|
if not isinstance(templates_config, dict):
|
||||||
raise ConfigError("The 'templates' section must be a dictionary")
|
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"
|
"custom_template_directory"
|
||||||
)
|
)
|
||||||
if self.custom_template_directory is not None and not isinstance(
|
if self.custom_template_directory is not None and not isinstance(
|
||||||
|
@ -727,7 +775,13 @@ class ServerConfig(Config):
|
||||||
return any(listener.tls for listener in self.listeners)
|
return any(listener.tls for listener in self.listeners)
|
||||||
|
|
||||||
def generate_config_section(
|
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(
|
ip_range_blacklist = "\n".join(
|
||||||
" # - '%s'" % ip for ip in DEFAULT_IP_RANGE_BLACKLIST
|
" # - '%s'" % ip for ip in DEFAULT_IP_RANGE_BLACKLIST
|
||||||
|
@ -1068,6 +1122,24 @@ class ServerConfig(Config):
|
||||||
# bind_addresses: ['::1', '127.0.0.1']
|
# bind_addresses: ['::1', '127.0.0.1']
|
||||||
# type: manhole
|
# 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
|
# 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
|
# 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
|
# 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:
|
||||||
if name == "webclient":
|
if name == "webclient":
|
||||||
logger.warning(NO_MORE_WEB_CLIENT_WARNING)
|
logger.warning(NO_MORE_WEB_CLIENT_WARNING)
|
||||||
return
|
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"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
|
@ -61,7 +61,7 @@ EddTrx3TNpr1D5m/f+6mnXWrc8u9y1+GNx9yz889xMjIBTBI9KqaaOs=
|
||||||
-----END RSA PRIVATE KEY-----"""
|
-----END RSA PRIVATE KEY-----"""
|
||||||
|
|
||||||
|
|
||||||
def manhole(username, password, globals):
|
def manhole(settings, globals):
|
||||||
"""Starts a ssh listener with password authentication using
|
"""Starts a ssh listener with password authentication using
|
||||||
the given username and password. Clients connecting to the ssh
|
the given username and password. Clients connecting to the ssh
|
||||||
listener will find themselves in a colored python shell with
|
listener will find themselves in a colored python shell with
|
||||||
|
@ -75,6 +75,15 @@ def manhole(username, password, globals):
|
||||||
Returns:
|
Returns:
|
||||||
twisted.internet.protocol.Factory: A factory to pass to ``listenTCP``
|
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):
|
if not isinstance(password, bytes):
|
||||||
password = password.encode("ascii")
|
password = password.encode("ascii")
|
||||||
|
|
||||||
|
@ -86,8 +95,8 @@ def manhole(username, password, globals):
|
||||||
)
|
)
|
||||||
|
|
||||||
factory = manhole_ssh.ConchFactory(portal.Portal(rlm, [checker]))
|
factory = manhole_ssh.ConchFactory(portal.Portal(rlm, [checker]))
|
||||||
factory.publicKeys[b"ssh-rsa"] = Key.fromString(PUBLIC_KEY)
|
factory.privateKeys[b"ssh-rsa"] = priv_key
|
||||||
factory.privateKeys[b"ssh-rsa"] = Key.fromString(PRIVATE_KEY)
|
factory.publicKeys[b"ssh-rsa"] = pub_key
|
||||||
|
|
||||||
return factory
|
return factory
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ class ServerConfigTestCase(unittest.TestCase):
|
||||||
def test_unsecure_listener_no_listeners_open_private_ports_false(self):
|
def test_unsecure_listener_no_listeners_open_private_ports_false(self):
|
||||||
conf = yaml.safe_load(
|
conf = yaml.safe_load(
|
||||||
ServerConfig().generate_config_section(
|
ServerConfig().generate_config_section(
|
||||||
"che.org", "/data_dir_path", False, None
|
"che.org", "/data_dir_path", False, None, config_dir_path="CONFDIR"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ class ServerConfigTestCase(unittest.TestCase):
|
||||||
def test_unsecure_listener_no_listeners_open_private_ports_true(self):
|
def test_unsecure_listener_no_listeners_open_private_ports_true(self):
|
||||||
conf = yaml.safe_load(
|
conf = yaml.safe_load(
|
||||||
ServerConfig().generate_config_section(
|
ServerConfig().generate_config_section(
|
||||||
"che.org", "/data_dir_path", True, None
|
"che.org", "/data_dir_path", True, None, config_dir_path="CONFDIR"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -89,7 +89,7 @@ class ServerConfigTestCase(unittest.TestCase):
|
||||||
|
|
||||||
conf = yaml.safe_load(
|
conf = yaml.safe_load(
|
||||||
ServerConfig().generate_config_section(
|
ServerConfig().generate_config_section(
|
||||||
"this.one.listens", "/data_dir_path", True, listeners
|
"this.one.listens", "/data_dir_path", True, listeners, "CONFDIR"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -123,7 +123,7 @@ class ServerConfigTestCase(unittest.TestCase):
|
||||||
|
|
||||||
conf = yaml.safe_load(
|
conf = yaml.safe_load(
|
||||||
ServerConfig().generate_config_section(
|
ServerConfig().generate_config_section(
|
||||||
"this.one.listens", "/data_dir_path", True, listeners
|
"this.one.listens", "/data_dir_path", True, listeners, "CONFDIR"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue