Skip to content
Open
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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ The service depends on the following Environment Variables to be set:
- MF_METADATA_DB_USER [defaults to postgres]
- MF_METADATA_DB_PSWD [defaults to postgres]
- MF_METADATA_DB_NAME [defaults to postgres]
- MF_METADATA_DB_SSL_MODE [optional; `allow`, `prefer`, `require`, `verify-ca`, or `verify-full`. Unset keeps `sslmode=disable`]
- MF_METADATA_DB_SSL_CERT_PATH / MF_METADATA_DB_SSL_KEY_PATH / MF_METADATA_DB_SSL_ROOT_CERT_PATH [optional paths inside the process/container]
- MF_METADATA_DB_SSL_ROOT_CERT [optional goose-specific root cert path; if both are set, goose prefers this over `MF_METADATA_DB_SSL_ROOT_CERT_PATH`]

Custom CA/client certificates are not bundled. Mount them into the container and point the path variables at those mount points. For AWS RDS, `MF_METADATA_DB_SSL_MODE=prefer` is typically sufficient unless you use custom certificates.

Optionally you can also overrider the host and port the service runs on

Expand Down
5 changes: 5 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ services:
- MF_METADATA_DB_USER=postgres
- MF_METADATA_DB_PSWD=postgres
- MF_METADATA_DB_NAME=postgres
- MF_METADATA_DB_SSL_MODE=${MF_METADATA_DB_SSL_MODE:-}
- MF_METADATA_DB_SSL_CERT_PATH=${MF_METADATA_DB_SSL_CERT_PATH:-}
- MF_METADATA_DB_SSL_KEY_PATH=${MF_METADATA_DB_SSL_KEY_PATH:-}
- MF_METADATA_DB_SSL_ROOT_CERT=${MF_METADATA_DB_SSL_ROOT_CERT:-}
- MF_METADATA_DB_SSL_ROOT_CERT_PATH=${MF_METADATA_DB_SSL_ROOT_CERT_PATH:-}
- MF_MIGRATION_ENDPOINTS_ENABLED=1
- MF_METADATA_PORT=${MF_METADATA_PORT:-8080}
- MF_METADATA_HOST=${MF_METADATA_HOST:-0.0.0.0}
Expand Down
11 changes: 7 additions & 4 deletions run_goose.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,18 @@ def main():
ssl_mode = os.environ.get("MF_METADATA_DB_SSL_MODE")
ssl_cert_path = os.environ.get("MF_METADATA_DB_SSL_CERT_PATH")
ssl_key_path = os.environ.get("MF_METADATA_DB_SSL_KEY_PATH")
ssl_root_cert_path = os.environ.get("MF_METADATA_DB_SSL_ROOT_CERT")
# Prefer the historical goose var; fall back to DBConfiguration's SSL_ROOT_CERT_PATH.
ssl_root_cert_path = os.environ.get(
"MF_METADATA_DB_SSL_ROOT_CERT"
) or os.environ.get("MF_METADATA_DB_SSL_ROOT_CERT_PATH")

if ssl_mode in ["allow", "prefer", "require", "verify-ca", "verify-full"]:
ssl_query = f"sslmode={ssl_mode}"
if ssl_cert_path is not None:
if ssl_cert_path:
ssl_query = f"{ssl_query}&sslcert={ssl_cert_path}"
if ssl_key_path is not None:
if ssl_key_path:
ssl_query = f"{ssl_query}&sslkey={ssl_key_path}"
if ssl_root_cert_path is not None:
if ssl_root_cert_path:
ssl_query = f"{ssl_query}&sslrootcert={ssl_root_cert_path}"
else:
ssl_query = f"sslmode=disable"
Expand Down
14 changes: 9 additions & 5 deletions services/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,11 +249,15 @@ def __init__(
self._user = os.environ.get(prefix + "USER", user)
self._password = os.environ.get(prefix + "PSWD", password)
self._database_name = os.environ.get(prefix + "NAME", database_name)
self._ssl_mode = os.environ.get(prefix + "SSL_MODE", ssl_mode)
self._ssl_cert_path = os.environ.get(prefix + "SSL_CERT_PATH", ssl_cert_path)
self._ssl_key_path = os.environ.get(prefix + "SSL_KEY_PATH", ssl_key_path)
self._ssl_root_cert_path = os.environ.get(
prefix + "SSL_ROOT_CERT_PATH", ssl_root_cert_path
self._ssl_mode = os.environ.get(prefix + "SSL_MODE", ssl_mode) or ssl_mode
self._ssl_cert_path = (
os.environ.get(prefix + "SSL_CERT_PATH", ssl_cert_path) or None
)
self._ssl_key_path = (
os.environ.get(prefix + "SSL_KEY_PATH", ssl_key_path) or None
)
self._ssl_root_cert_path = (
os.environ.get(prefix + "SSL_ROOT_CERT_PATH", ssl_root_cert_path) or None
)
conn_str_required_values = [
self._host,
Expand Down
152 changes: 152 additions & 0 deletions services/utils/tests/unit_tests/run_goose_ssl_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import os
import sys
from pathlib import Path
from unittest.mock import MagicMock

import pytest

pytestmark = [pytest.mark.unit_tests]

REPO_ROOT = Path(__file__).resolve().parents[4]
COMPOSE_FILE = REPO_ROOT / "docker-compose.yml"
sys.path.insert(0, str(REPO_ROOT))
import run_goose # noqa: E402

SSL_ENV_KEYS = (
"MF_METADATA_DB_SSL_MODE",
"MF_METADATA_DB_SSL_CERT_PATH",
"MF_METADATA_DB_SSL_KEY_PATH",
"MF_METADATA_DB_SSL_ROOT_CERT",
"MF_METADATA_DB_SSL_ROOT_CERT_PATH",
)

DB_ENV = {
"MF_METADATA_DB_USER": "postgres",
"MF_METADATA_DB_PSWD": "postgres",
"MF_METADATA_DB_HOST": "db",
"MF_METADATA_DB_PORT": "5432",
"MF_METADATA_DB_NAME": "postgres",
}


def _dsn_from_main(monkeypatch, extra_env=None):
env = dict(DB_ENV)
if extra_env:
env.update(extra_env)
monkeypatch.setattr(os, "environ", env)
monkeypatch.setattr(run_goose, "wait_for_postgres", lambda *args, **kwargs: None)
captured = {}

def fake_popen(args):
captured["dsn"] = args[4]
proc = MagicMock()
proc.wait.return_value = 0
return proc

monkeypatch.setattr(run_goose, "Popen", fake_popen)
monkeypatch.setattr("sys.argv", ["run_goose.py", "--wait", "0"])
run_goose.main()
return captured["dsn"]


def test_docker_compose_passes_through_ssl_env_without_defaults():
text = COMPOSE_FILE.read_text()
for key in SSL_ENV_KEYS:
assert f"{key}=${{{key}:-}}" in text
assert "MF_METADATA_DB_HOST=db" in text
assert "MF_METADATA_DB_USER=postgres" in text
assert "global-bundle.pem" not in text
assert "rds-ca" not in text.lower()


def test_run_goose_unset_ssl_disables_ssl(monkeypatch):
dsn = _dsn_from_main(monkeypatch)
assert dsn.endswith("?sslmode=disable")
assert "sslcert=" not in dsn


def test_run_goose_empty_ssl_env_does_not_add_cert_args(monkeypatch):
dsn = _dsn_from_main(
monkeypatch,
{
"MF_METADATA_DB_SSL_MODE": "",
"MF_METADATA_DB_SSL_CERT_PATH": "",
"MF_METADATA_DB_SSL_KEY_PATH": "",
"MF_METADATA_DB_SSL_ROOT_CERT": "",
"MF_METADATA_DB_SSL_ROOT_CERT_PATH": "",
},
)
assert dsn.endswith("?sslmode=disable")
assert "sslcert=" not in dsn
assert "sslkey=" not in dsn
assert "sslrootcert=" not in dsn


def test_run_goose_ssl_mode_prefer(monkeypatch):
dsn = _dsn_from_main(monkeypatch, {"MF_METADATA_DB_SSL_MODE": "prefer"})
assert "sslmode=prefer" in dsn
assert "sslcert=" not in dsn


def test_run_goose_ssl_cert_paths(monkeypatch):
dsn = _dsn_from_main(
monkeypatch,
{
"MF_METADATA_DB_SSL_MODE": "verify-ca",
"MF_METADATA_DB_SSL_CERT_PATH": "/certs/client.crt",
"MF_METADATA_DB_SSL_KEY_PATH": "/certs/client.key",
"MF_METADATA_DB_SSL_ROOT_CERT": "/certs/root.crt",
},
)
assert "sslmode=verify-ca" in dsn
assert "sslcert=/certs/client.crt" in dsn
assert "sslkey=/certs/client.key" in dsn
assert "sslrootcert=/certs/root.crt" in dsn


def test_run_goose_ssl_root_cert_only(monkeypatch):
dsn = _dsn_from_main(
monkeypatch,
{
"MF_METADATA_DB_SSL_MODE": "require",
"MF_METADATA_DB_SSL_ROOT_CERT": "/certs/goose-root.crt",
},
)
assert "sslrootcert=/certs/goose-root.crt" in dsn


def test_run_goose_ssl_root_cert_path_alias(monkeypatch):
dsn = _dsn_from_main(
monkeypatch,
{
"MF_METADATA_DB_SSL_MODE": "require",
"MF_METADATA_DB_SSL_ROOT_CERT_PATH": "/certs/root.crt",
},
)
assert "sslmode=require" in dsn
assert "sslrootcert=/certs/root.crt" in dsn


def test_run_goose_ssl_root_cert_prefers_historical_var_when_both_set(monkeypatch):
dsn = _dsn_from_main(
monkeypatch,
{
"MF_METADATA_DB_SSL_MODE": "require",
"MF_METADATA_DB_SSL_ROOT_CERT": "/certs/goose-root.crt",
"MF_METADATA_DB_SSL_ROOT_CERT_PATH": "/certs/service-root.crt",
},
)
assert "sslrootcert=/certs/goose-root.crt" in dsn
assert "sslrootcert=/certs/service-root.crt" not in dsn


def test_run_goose_ssl_root_cert_empty_falls_back_to_path(monkeypatch):
dsn = _dsn_from_main(
monkeypatch,
{
"MF_METADATA_DB_SSL_MODE": "require",
"MF_METADATA_DB_SSL_ROOT_CERT": "",
"MF_METADATA_DB_SSL_ROOT_CERT_PATH": "/certs/service-root.crt",
},
)
assert "sslrootcert=/certs/service-root.crt" in dsn
61 changes: 61 additions & 0 deletions services/utils/tests/unit_tests/utils_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,67 @@ def test_db_conf_env_custom_prefix():
assert db_conf.timeout == 5


def test_db_conf_ssl_env_prefer_without_certs():
with set_env(
{
"MF_METADATA_DB_HOST": "db",
"MF_METADATA_DB_PORT": "5432",
"MF_METADATA_DB_USER": "postgres",
"MF_METADATA_DB_PSWD": "postgres",
"MF_METADATA_DB_NAME": "postgres",
"MF_METADATA_DB_SSL_MODE": "prefer",
}
):
db_conf = DBConfiguration()
assert "sslmode=prefer" in db_conf.connection_string_url()
assert "sslcert=" not in db_conf.connection_string_url()
assert "sslkey=" not in db_conf.connection_string_url()
assert "sslrootcert=" not in db_conf.connection_string_url()
assert "sslmode=prefer" in db_conf.get_dsn()
assert "sslcert" not in db_conf.get_dsn()


def test_db_conf_ssl_env_cert_paths():
with set_env(
{
"MF_METADATA_DB_HOST": "db",
"MF_METADATA_DB_PORT": "5432",
"MF_METADATA_DB_USER": "postgres",
"MF_METADATA_DB_PSWD": "postgres",
"MF_METADATA_DB_NAME": "postgres",
"MF_METADATA_DB_SSL_MODE": "verify-full",
"MF_METADATA_DB_SSL_CERT_PATH": "/certs/client.crt",
"MF_METADATA_DB_SSL_KEY_PATH": "/certs/client.key",
"MF_METADATA_DB_SSL_ROOT_CERT_PATH": "/certs/root.crt",
}
):
url = DBConfiguration().connection_string_url()
assert "sslmode=verify-full" in url
assert "sslcert=/certs/client.crt" in url
assert "sslkey=/certs/client.key" in url
assert "sslrootcert=/certs/root.crt" in url


def test_db_conf_ssl_empty_env_keeps_disable():
with set_env(
{
"MF_METADATA_DB_HOST": "db",
"MF_METADATA_DB_PORT": "5432",
"MF_METADATA_DB_USER": "postgres",
"MF_METADATA_DB_PSWD": "postgres",
"MF_METADATA_DB_NAME": "postgres",
"MF_METADATA_DB_SSL_MODE": "",
"MF_METADATA_DB_SSL_CERT_PATH": "",
"MF_METADATA_DB_SSL_KEY_PATH": "",
"MF_METADATA_DB_SSL_ROOT_CERT_PATH": "",
}
):
db_conf = DBConfiguration()
assert db_conf.connection_string_url().endswith("?sslmode=disable")
assert "sslcert" not in db_conf.get_dsn()
assert "sslmode" not in db_conf.get_dsn()


def test_db_conf_env_dsn():
with set_env({"MF_METADATA_DB_DSN": "foo"}):
# Should use default dsn with invalid dsn in environment
Expand Down