Skip to content

Commit

Permalink
Merge pull request #22 from canonical/tls-postgresql
Browse files Browse the repository at this point in the history
Add TLS encryption between PgBouncer and PostgreSQL
  • Loading branch information
marceloneppel authored Oct 5, 2022
2 parents 9a0b2b9 + 7b88f9d commit efc2cdf
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 11 deletions.
2 changes: 2 additions & 0 deletions lib/charms/pgbouncer_k8s/v0/pgb.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
},
Expand Down
1 change: 1 addition & 0 deletions src/relations/peers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 │ │
Expand Down
10 changes: 0 additions & 10 deletions tests/integration/helpers/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
50 changes: 50 additions & 0 deletions tests/integration/helpers/postgresql_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from typing import List

import psycopg2
import requests as requests
import yaml
from pytest_operator.plugin import OpsTest

Expand Down Expand Up @@ -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,
Expand All @@ -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.
Expand All @@ -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
49 changes: 48 additions & 1 deletion tests/integration/relations/test_backend_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"


Expand Down Expand Up @@ -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"
2 changes: 2 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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} \
Expand All @@ -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} \
Expand Down

0 comments on commit efc2cdf

Please sign in to comment.