Skip to content

Commit e8e0f0f

Browse files
authored
Add config option redis.password_path (#17717)
Adds the option to load the Redis password from a file, instead of giving it in the config directly. The code is similar to how it’s done for `registration_shared_secret_path`. I changed the example in the documentation to represent the best practice regarding the handling of secrets. Reading secrets from files has the security advantage of separating the secrets from the config. It also simplifies secrets management in Kubernetes.
1 parent beb7a95 commit e8e0f0f

File tree

4 files changed

+81
-2
lines changed

4 files changed

+81
-2
lines changed

changelog.d/17717.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add config option `redis.password_path`.

docs/usage/configuration/config_documentation.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4524,6 +4524,9 @@ This setting has the following sub-options:
45244524
* `path`: The full path to a local Unix socket file. **If this is used, `host` and
45254525
`port` are ignored.** Defaults to `/tmp/redis.sock'
45264526
* `password`: Optional password if configured on the Redis instance.
4527+
* `password_path`: Alternative to `password`, reading the password from an
4528+
external file. The file should be a plain text file, containing only the
4529+
password. Synapse reads the password from the given file once at startup.
45274530
* `dbid`: Optional redis dbid if needs to connect to specific redis logical db.
45284531
* `use_tls`: Whether to use tls connection. Defaults to false.
45294532
* `certificate_file`: Optional path to the certificate file
@@ -4537,13 +4540,16 @@ This setting has the following sub-options:
45374540

45384541
_Changed in Synapse 1.85.0: Added path option to use a local Unix socket_
45394542

4543+
_Changed in Synapse 1.116.0: Added password\_path_
4544+
45404545
Example configuration:
45414546
```yaml
45424547
redis:
45434548
enabled: true
45444549
host: localhost
45454550
port: 6379
4546-
password: <secret_password>
4551+
password_path: <path_to_the_password_file>
4552+
# OR password: <secret_password>
45474553
dbid: <dbid>
45484554
#use_tls: True
45494555
#certificate_file: <path_to_the_certificate_file>

synapse/config/redis.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,15 @@
2121

2222
from typing import Any
2323

24-
from synapse.config._base import Config
24+
from synapse.config._base import Config, ConfigError, read_file
2525
from synapse.types import JsonDict
2626
from synapse.util.check_dependencies import check_requirements
2727

28+
CONFLICTING_PASSWORD_OPTS_ERROR = """\
29+
You have configured both `redis.password` and `redis.password_path`.
30+
These are mutually incompatible.
31+
"""
32+
2833

2934
class RedisConfig(Config):
3035
section = "redis"
@@ -43,6 +48,17 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
4348
self.redis_path = redis_config.get("path", None)
4449
self.redis_dbid = redis_config.get("dbid", None)
4550
self.redis_password = redis_config.get("password")
51+
redis_password_path = redis_config.get("password_path")
52+
if redis_password_path:
53+
if self.redis_password:
54+
raise ConfigError(CONFLICTING_PASSWORD_OPTS_ERROR)
55+
self.redis_password = read_file(
56+
redis_password_path,
57+
(
58+
"redis",
59+
"password_path",
60+
),
61+
).strip()
4662

4763
self.redis_use_tls = redis_config.get("use_tls", False)
4864
self.redis_certificate = redis_config.get("certificate_file", None)

tests/config/test_load.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,23 @@
1919
# [This file includes modifications made by New Vector Limited]
2020
#
2121
#
22+
import tempfile
23+
from typing import Callable
24+
2225
import yaml
26+
from parameterized import parameterized
2327

2428
from synapse.config import ConfigError
29+
from synapse.config._base import RootConfig
2530
from synapse.config.homeserver import HomeServerConfig
2631

2732
from tests.config.utils import ConfigFileTestCase
2833

34+
try:
35+
import hiredis
36+
except ImportError:
37+
hiredis = None # type: ignore
38+
2939

3040
class ConfigLoadingFileTestCase(ConfigFileTestCase):
3141
def test_load_fails_if_server_name_missing(self) -> None:
@@ -116,3 +126,49 @@ def test_depreciated_identity_server_flag_throws_error(self) -> None:
116126
self.add_lines_to_config(["trust_identity_server_for_password_resets: true"])
117127
with self.assertRaises(ConfigError):
118128
HomeServerConfig.load_config("", ["-c", self.config_file])
129+
130+
@parameterized.expand(
131+
[
132+
"turn_shared_secret_path: /does/not/exist",
133+
"registration_shared_secret_path: /does/not/exist",
134+
*["redis:\n enabled: true\n password_path: /does/not/exist"]
135+
* (hiredis is not None),
136+
]
137+
)
138+
def test_secret_files_missing(self, config_str: str) -> None:
139+
self.generate_config()
140+
self.add_lines_to_config(["", config_str])
141+
142+
with self.assertRaises(ConfigError):
143+
HomeServerConfig.load_config("", ["-c", self.config_file])
144+
145+
@parameterized.expand(
146+
[
147+
(
148+
"turn_shared_secret_path: {}",
149+
lambda c: c.voip.turn_shared_secret,
150+
),
151+
(
152+
"registration_shared_secret_path: {}",
153+
lambda c: c.registration.registration_shared_secret,
154+
),
155+
*[
156+
(
157+
"redis:\n enabled: true\n password_path: {}",
158+
lambda c: c.redis.redis_password,
159+
)
160+
]
161+
* (hiredis is not None),
162+
]
163+
)
164+
def test_secret_files_existing(
165+
self, config_line: str, get_secret: Callable[[RootConfig], str]
166+
) -> None:
167+
self.generate_config_and_remove_lines_containing("registration_shared_secret")
168+
with tempfile.NamedTemporaryFile(buffering=0) as secret_file:
169+
secret_file.write(b"53C237")
170+
171+
self.add_lines_to_config(["", config_line.format(secret_file.name)])
172+
config = HomeServerConfig.load_config("", ["-c", self.config_file])
173+
174+
self.assertEqual(get_secret(config), "53C237")

0 commit comments

Comments
 (0)