Skip to content

Commit

Permalink
Add macaroon_secret_key_path config option (element-hq#17983)
Browse files Browse the repository at this point in the history
Another config option on my quest to a `*_path` variant for every
secret. This time it’s `macaroon_secret_key_path`.

Reading secrets from files has the security advantage of separating the secrets from the config. It also simplifies secrets management in Kubernetes. Also useful to NixOS users.
  • Loading branch information
V02460 authored Dec 17, 2024
1 parent 3d60a58 commit 57bf449
Show file tree
Hide file tree
Showing 5 changed files with 51 additions and 15 deletions.
1 change: 1 addition & 0 deletions changelog.d/17983.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add `macaroon_secret_key_path` config option.
16 changes: 16 additions & 0 deletions docs/usage/configuration/config_documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -3091,6 +3091,22 @@ Example configuration:
```yaml
macaroon_secret_key: <PRIVATE STRING>
```
---
### `macaroon_secret_key_path`

An alternative to [`macaroon_secret_key`](#macaroon_secret_key):
allows the secret key to be specified in an external file.

The file should be a plain text file, containing only the secret key.
Synapse reads the secret key from the given file once at startup.

Example configuration:
```yaml
macaroon_secret_key_path: /path/to/secrets/file
```

_Added in Synapse 1.121.0._

---
### `form_secret`

Expand Down
21 changes: 16 additions & 5 deletions synapse/config/key.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
from synapse.types import JsonDict
from synapse.util.stringutils import random_string, random_string_with_symbols

from ._base import Config, ConfigError
from ._base import Config, ConfigError, read_file

if TYPE_CHECKING:
from signedjson.key import VerifyKeyWithExpiry
Expand Down Expand Up @@ -91,6 +91,11 @@
'suppress_key_server_warning' to 'true' in homeserver.yaml.
--------------------------------------------------------------------------------"""

CONFLICTING_MACAROON_SECRET_KEY_OPTS_ERROR = """\
Conflicting options 'macaroon_secret_key' and 'macaroon_secret_key_path' are
both defined in config file.
"""

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -166,10 +171,16 @@ def read_config(
)
)

macaroon_secret_key: Optional[str] = config.get(
"macaroon_secret_key", self.root.registration.registration_shared_secret
)

macaroon_secret_key = config.get("macaroon_secret_key")
macaroon_secret_key_path = config.get("macaroon_secret_key_path")
if macaroon_secret_key_path:
if macaroon_secret_key:
raise ConfigError(CONFLICTING_MACAROON_SECRET_KEY_OPTS_ERROR)
macaroon_secret_key = read_file(
macaroon_secret_key_path, "macaroon_secret_key_path"
).strip()
if not macaroon_secret_key:
macaroon_secret_key = self.root.registration.registration_shared_secret
if not macaroon_secret_key:
# Unfortunately, there are people out there that don't have this
# set. Lets just be "nice" and derive one from their secret key.
Expand Down
23 changes: 15 additions & 8 deletions tests/config/test_load.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@

class ConfigLoadingFileTestCase(ConfigFileTestCase):
def test_load_fails_if_server_name_missing(self) -> None:
self.generate_config_and_remove_lines_containing("server_name")
self.generate_config_and_remove_lines_containing(["server_name"])
with self.assertRaises(ConfigError):
HomeServerConfig.load_config("", ["-c", self.config_file])
with self.assertRaises(ConfigError):
Expand Down Expand Up @@ -76,7 +76,7 @@ def test_generates_and_loads_macaroon_secret_key(self) -> None:
)

def test_load_succeeds_if_macaroon_secret_key_missing(self) -> None:
self.generate_config_and_remove_lines_containing("macaroon")
self.generate_config_and_remove_lines_containing(["macaroon"])
config1 = HomeServerConfig.load_config("", ["-c", self.config_file])
config2 = HomeServerConfig.load_config("", ["-c", self.config_file])
config3 = HomeServerConfig.load_or_generate_config("", ["-c", self.config_file])
Expand Down Expand Up @@ -111,7 +111,7 @@ def test_disable_registration(self) -> None:
self.assertTrue(config3.registration.enable_registration)

def test_stats_enabled(self) -> None:
self.generate_config_and_remove_lines_containing("enable_metrics")
self.generate_config_and_remove_lines_containing(["enable_metrics"])
self.add_lines_to_config(["enable_metrics: true"])

# The default Metrics Flags are off by default.
Expand All @@ -131,6 +131,7 @@ def test_depreciated_identity_server_flag_throws_error(self) -> None:
[
"turn_shared_secret_path: /does/not/exist",
"registration_shared_secret_path: /does/not/exist",
"macaroon_secret_key_path: /does/not/exist",
*["redis:\n enabled: true\n password_path: /does/not/exist"]
* (hiredis is not None),
]
Expand All @@ -146,16 +147,20 @@ def test_secret_files_missing(self, config_str: str) -> None:
[
(
"turn_shared_secret_path: {}",
lambda c: c.voip.turn_shared_secret,
lambda c: c.voip.turn_shared_secret.encode("utf-8"),
),
(
"registration_shared_secret_path: {}",
lambda c: c.registration.registration_shared_secret,
lambda c: c.registration.registration_shared_secret.encode("utf-8"),
),
(
"macaroon_secret_key_path: {}",
lambda c: c.key.macaroon_secret_key,
),
*[
(
"redis:\n enabled: true\n password_path: {}",
lambda c: c.redis.redis_password,
lambda c: c.redis.redis_password.encode("utf-8"),
)
]
* (hiredis is not None),
Expand All @@ -164,11 +169,13 @@ def test_secret_files_missing(self, config_str: str) -> None:
def test_secret_files_existing(
self, config_line: str, get_secret: Callable[[RootConfig], str]
) -> None:
self.generate_config_and_remove_lines_containing("registration_shared_secret")
self.generate_config_and_remove_lines_containing(
["registration_shared_secret", "macaroon_secret_key"]
)
with tempfile.NamedTemporaryFile(buffering=0) as secret_file:
secret_file.write(b"53C237")

self.add_lines_to_config(["", config_line.format(secret_file.name)])
config = HomeServerConfig.load_config("", ["-c", self.config_file])

self.assertEqual(get_secret(config), "53C237")
self.assertEqual(get_secret(config), b"53C237")
5 changes: 3 additions & 2 deletions tests/config/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,13 @@ def generate_config(self) -> None:
],
)

def generate_config_and_remove_lines_containing(self, needle: str) -> None:
def generate_config_and_remove_lines_containing(self, needles: list[str]) -> None:
self.generate_config()

with open(self.config_file) as f:
contents = f.readlines()
contents = [line for line in contents if needle not in line]
for needle in needles:
contents = [line for line in contents if needle not in line]
with open(self.config_file, "w") as f:
f.write("".join(contents))

Expand Down

0 comments on commit 57bf449

Please sign in to comment.