diff --git a/lib/charms/pgbouncer_k8s/v0/pgb.py b/lib/charms/pgbouncer_k8s/v0/pgb.py index 7fb76a764..2efb17b0e 100644 --- a/lib/charms/pgbouncer_k8s/v0/pgb.py +++ b/lib/charms/pgbouncer_k8s/v0/pgb.py @@ -36,6 +36,7 @@ user = postgres max_client_conn = 10000 ignore_startup_parameters = extra_float_digits +server_tls_sslmode = prefer so_reuseport = 1 unix_socket_dir = /var/lib/postgresql/pgbouncer pool_mode = session @@ -87,6 +88,7 @@ "user": "postgres", "max_client_conn": "10000", "ignore_startup_parameters": "extra_float_digits", + "server_tls_sslmode": "prefer", "so_reuseport": "1", "unix_socket_dir": PGB_DIR, }, diff --git a/src/relations/peers.py b/src/relations/peers.py index c0b262e58..89887b791 100644 --- a/src/relations/peers.py +++ b/src/relations/peers.py @@ -30,6 +30,7 @@ │ │ │ user = postgres │ │ │ │ │ max_client_conn = 10000 │ │ │ │ │ ignore_startup_parameters = extra_float_digits │ │ +│ │ │ server_tls_sslmode = prefer │ │ │ │ │ so_reuseport = 1 │ │ │ │ │ unix_socket_dir = /var/lib/postgresql/pgbouncer │ │ │ │ │ pool_mode = session │ │ diff --git a/tests/integration/helpers/helpers.py b/tests/integration/helpers/helpers.py index ca40c4031..de1c3d4b5 100644 --- a/tests/integration/helpers/helpers.py +++ b/tests/integration/helpers/helpers.py @@ -131,16 +131,6 @@ async def run_sql(ops_test, unit_name, command, pgpass, user, host, port, dbname return await ops_test.juju(*run_cmd.split(" "), cmd) -def get_backend_relation(ops_test: OpsTest): - """Gets the backend-database relation used to connect pgbouncer to the backend.""" - app_name = ops_test.model.applications[PGB].name - for rel in ops_test.model.relations: - if app_name in rel.endpoints and "postgresql" in rel.endpoints: - return rel - - return None - - def get_legacy_relation_username(ops_test: OpsTest, relation_id: int): """Gets a username as it should be generated in the db and db-admin legacy relations.""" app_name = ops_test.model.applications[PGB].name diff --git a/tests/integration/helpers/postgresql_helpers.py b/tests/integration/helpers/postgresql_helpers.py index d34182e2a..f20add6c3 100644 --- a/tests/integration/helpers/postgresql_helpers.py +++ b/tests/integration/helpers/postgresql_helpers.py @@ -6,6 +6,7 @@ from typing import List import psycopg2 +import requests as requests import yaml from pytest_operator.plugin import OpsTest @@ -127,6 +128,21 @@ async def check_databases_creation( assert len(output) +def enable_connections_logging(ops_test: OpsTest, unit_name: str) -> None: + """Turn on the log of all connections made to a PostgreSQL instance. + + Args: + ops_test: The ops test framework instance + unit_name: The name of the unit to turn on the connection logs + """ + unit_address = get_unit_address(ops_test, unit_name) + requests.patch( + f"https://{unit_address}:8008/config", + json={"postgresql": {"parameters": {"log_connections": True}}}, + verify=False, + ) + + async def execute_query_on_unit( unit_address: str, user: str, @@ -153,6 +169,20 @@ async def execute_query_on_unit( return output +async def get_postgres_primary(ops_test: OpsTest) -> str: + """Get the PostgreSQL primary unit. + + Args: + ops_test: ops_test instance. + + Returns: + the current PostgreSQL primary unit. + """ + action = await ops_test.model.units.get(f"{PG}/0").run_action("get-primary") + action = await action.wait() + return action.results["primary"] + + def get_unit_address(ops_test: OpsTest, unit_name: str) -> str: """Get unit IP address. @@ -164,3 +194,23 @@ def get_unit_address(ops_test: OpsTest, unit_name: str) -> str: IP address of the unit """ return ops_test.model.units.get(unit_name).public_address + + +async def run_command_on_unit(ops_test: OpsTest, unit_name: str, command: str) -> str: + """Run a command on a specific unit. + + Args: + ops_test: The ops test framework instance + unit_name: The name of the unit to run the command on + command: The command to run + + Returns: + the command output if it succeeds, otherwise raises an exception. + """ + complete_command = f"run --unit {unit_name} -- {command}" + return_code, stdout, _ = await ops_test.juju(*complete_command.split()) + if return_code != 0: + raise Exception( + "Expected command %s to succeed instead it failed: %s", command, return_code + ) + return stdout diff --git a/tests/integration/relations/test_backend_database.py b/tests/integration/relations/test_backend_database.py index 4f09047ca..ac8a92601 100644 --- a/tests/integration/relations/test_backend_database.py +++ b/tests/integration/relations/test_backend_database.py @@ -15,15 +15,23 @@ get_app_relation_databag, get_backend_user_pass, get_cfg, + wait_for_relation_joined_between, wait_for_relation_removed_between, ) -from tests.integration.helpers.postgresql_helpers import check_database_users_existence +from tests.integration.helpers.postgresql_helpers import ( + check_database_users_existence, + enable_connections_logging, + get_postgres_primary, + run_command_on_unit, +) logger = logging.getLogger(__name__) METADATA = yaml.safe_load(Path("./metadata.yaml").read_text()) +MAILMAN3 = "mailman3-core" PGB = METADATA["name"] PG = "postgresql" +TLS = "tls-certificates-operator" RELATION = "backend-database" @@ -70,3 +78,42 @@ async def test_relate_pgbouncer_to_postgres(ops_test: OpsTest): cfg = await get_cfg(ops_test, f"{PGB}/0") logger.info(cfg.render()) + + +@pytest.mark.backend +async def test_tls_encrypted_connection_to_postgres(ops_test: OpsTest): + async with ops_test.fast_forward(): + # Relate PgBouncer to PostgreSQL. + relation = await ops_test.model.add_relation(f"{PGB}:{RELATION}", f"{PG}:database") + wait_for_relation_joined_between(ops_test, PG, PGB) + await ops_test.model.wait_for_idle(apps=[PGB, PG], status="active", timeout=1000) + pgb_user, _ = await get_backend_user_pass(ops_test, relation) + + # Deploy TLS Certificates operator. + config = {"generate-self-signed-certificates": "true", "ca-common-name": "Test CA"} + await ops_test.model.deploy(TLS, channel="edge", config=config) + await ops_test.model.wait_for_idle(apps=[TLS], status="active", timeout=1000) + + # Relate it to the PostgreSQL to enable TLS. + await ops_test.model.relate(PG, TLS) + await ops_test.model.wait_for_idle(status="active", timeout=1000) + + # Enable additional logs on the PostgreSQL instance to check TLS + # being used in a later step. + enable_connections_logging(ops_test, f"{PG}/0") + + # Deploy an app and relate it to PgBouncer to open a connection + # between PgBouncer and PostgreSQL. + await ops_test.model.deploy(MAILMAN3) + await ops_test.model.add_relation(f"{PGB}:db", f"{MAILMAN3}:db") + await ops_test.model.wait_for_idle(apps=[PG, PGB, MAILMAN3], status="active", timeout=1000) + + # Check the logs to ensure TLS is being used by PgBouncer. + postgresql_primary_unit = await get_postgres_primary(ops_test) + logs = await run_command_on_unit( + ops_test, postgresql_primary_unit, "journalctl -u patroni.service" + ) + assert ( + f"connection authorized: user={pgb_user} database=mailman3 SSL enabled" + " (protocol=TLSv1.3, cipher=TLS_AES_256_GCM_SHA384, bits=256, compression=off)" in logs + ), "TLS is not being used on connections to PostgreSQL" diff --git a/tox.ini b/tox.ini index 706716d5e..b4dcb2189 100644 --- a/tox.ini +++ b/tox.ini @@ -124,6 +124,7 @@ deps = psycopg2-binary pytest-operator mailmanclient + requests -r{toxinidir}/requirements.txt commands = pytest -v --tb native --ignore={[vars]tst_path}unit --log-cli-level=INFO -s {posargs} \ @@ -137,6 +138,7 @@ deps = psycopg2-binary pytest-operator mailmanclient + requests -r{toxinidir}/requirements.txt commands = pytest -v --tb native --ignore={[vars]tst_path}unit --log-cli-level=INFO -s {posargs} \