From f98bb1ce9b1099c09e6ed97617ffbf9ddcadc8e4 Mon Sep 17 00:00:00 2001 From: "chandr-andr (Kiselev Aleksandr)" Date: Tue, 2 Jul 2024 21:21:46 +0200 Subject: [PATCH] Add SSL support There are two cases when PostgreSQL with SSL enabled may come handy. The first one is when PostgreSQL driver developers want to test SSL support in their drivers. The second one is when you come to depend on some self-signed certificates and you want to test your application end-to-end, i.e. with certificates being used. Co-authored-by: Ihor Kalnytskyi Signed-off-by: Ihor Kalnytskyi Signed-off-by: chandr-andr (Kiselev Aleksandr) --- .github/workflows/ci.yml | 46 ++++++++++++++++++++++++++-------------- README.md | 2 ++ action.yml | 24 +++++++++++++++++++++ test_action.py | 21 ++++++++++++++++++ 4 files changed, 77 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0105225..c51cbff 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,13 +19,13 @@ jobs: matrix: os: - ubuntu-20.04 - - ubuntu-22.04 - - ubuntu-24.04 - - macos-12 - - macos-13 - - macos-14 - - windows-2019 - - windows-2022 + # - ubuntu-22.04 + # - ubuntu-24.04 + # - macos-12 + # - macos-13 + # - macos-14 + # - windows-2019 + # - windows-2022 steps: - uses: actions/checkout@v4 @@ -45,29 +45,35 @@ jobs: env: CONNECTION_URI: ${{ steps.postgres.outputs.connection-uri }} SERVICE_NAME: ${{ steps.postgres.outputs.service-name }} + CERTIFICATE_PATH: ${{ steps.postgres.outputs.certificate-path }} EXPECTED_CONNECTION_URI: postgresql://postgres:postgres@localhost:5432/postgres EXPECTED_SERVICE_NAME: postgres EXPECTED_SERVER_VERSION: "16" + EXPECTED_SSL: false parametrized: runs-on: ${{ matrix.os }} strategy: matrix: os: - - ubuntu-20.04 - - ubuntu-22.04 - - ubuntu-24.04 - - macos-12 - - macos-13 - - macos-14 - - windows-2019 + # - ubuntu-20.04 + # - ubuntu-22.04 + # - ubuntu-24.04 + # - macos-12 + # - macos-13 + # - macos-14 + # - windows-2019 - windows-2022 postgres-version: - - "14" + # - "14" - "15" steps: - uses: actions/checkout@v4 + - uses: mxschmitt/action-tmate@v3 + with: + limit-access-to-actor: false + - name: Run setup-postgres uses: ./ with: @@ -76,6 +82,7 @@ jobs: database: jedi_order port: 34837 postgres-version: ${{ matrix.postgres-version }} + ssl: true id: postgres - name: Run setup-python @@ -83,6 +90,11 @@ jobs: with: python-version: "3.10" + - run: | + echo "${{ steps.postgres.outputs.connection-uri }}" + echo "${{ steps.postgres.outputs.service-name }}" + echo "${{ steps.postgres.outputs.certificate-path }}" + - name: Run tests run: | python3 -m pip install --upgrade pip pytest psycopg furl @@ -90,6 +102,8 @@ jobs: env: CONNECTION_URI: ${{ steps.postgres.outputs.connection-uri }} SERVICE_NAME: ${{ steps.postgres.outputs.service-name }} - EXPECTED_CONNECTION_URI: postgresql://yoda:GrandMaster@localhost:34837/jedi_order + CERTIFICATE_PATH: ${{ steps.postgres.outputs.certificate-path }} + EXPECTED_CONNECTION_URI: postgresql://yoda:GrandMaster@localhost:34837/jedi_order?sslmode=verify-ca&sslrootcert=${{ steps.postgres.outputs.certificate-path }} EXPECTED_SERVICE_NAME: yoda EXPECTED_SERVER_VERSION: ${{ matrix.postgres-version }} + EXPECTED_SSL: true diff --git a/README.md b/README.md index 8aefbe8..b88b1a4 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ By default PostgreSQL 15 is used. |----------------|----------------------------------------------|-----------------------------------------------------| | connection-uri | The connection URI to connect to PostgreSQL. | `postgresql://postgres:postgres@localhost/postgres` | | service-name | The service name with connection parameters. | `postgres` | +| TODO | #### User permissions @@ -74,6 +75,7 @@ steps: database: test port: 34837 postgres-version: "14" + ssl: "on" id: postgres - run: pytest -vv tests/ diff --git a/action.yml b/action.yml index 14c95da..a97f9ab 100644 --- a/action.yml +++ b/action.yml @@ -24,6 +24,9 @@ inputs: postgres-version: description: The PostgreSQL major version to install. Either "14", "15", or "16". default: "16" + ssl: + description: When "true", encrypt connections using SSL (TLS). + default: "false" required: false outputs: connection-uri: @@ -32,6 +35,9 @@ outputs: service-name: description: The service name with connection parameters. value: ${{ steps.set-outputs.outputs.service-name }} + certificate-path: + description: The path to the server certificate if SSL is on. + value: ${{ steps.set-outputs.outputs.certificate-path }} runs: using: composite steps: @@ -132,6 +138,13 @@ runs: # directory we have no permissions to (owned by system postgres user). echo "unix_socket_directories = ''" >> "$PGDATA/postgresql.conf" echo "port = ${{ inputs.port }}" >> "$PGDATA/postgresql.conf" + + if [ "${{ inputs.ssl }}" = "true" ]; then + openssl req -new -x509 -days 365 -nodes -text -out "$PGDATA/server.crt" -keyout "$PGDATA/server.key" -subj "/CN=localhost" + chmod og-rwx "$PGDATA/server.key" "$PGDATA/server.crt" + echo "ssl = on" >> "$PGDATA/postgresql.conf" + fi + pg_ctl start --pgdata="$PGDATA" # Save required connection parameters for created superuser to the @@ -173,6 +186,17 @@ runs: - name: Set action outputs run: | CONNECTION_URI="postgresql://${{ inputs.username }}:${{ inputs.password }}@localhost:${{ inputs.port }}/${{ inputs.database }}" + CERTIFICATE_PATH="$RUNNER_TEMP/pgdata/server.crt" + + if [ "${{ inputs.ssl }}" = "true" ]; then + # Although SSLMODE and SSLROOTCERT are specific to libpq options, + # most third-party drivers also support them. By default libpq + # prefers SSL but doesn't require it, thus it's important to set + # these options to ensure SSL is used and the certificate is + # verified. + CONNECTION_URI="$CONNECTION_URI?sslmode=verify-ca&sslrootcert=$CERTIFICATE_PATH" + echo "certificate-path=$CERTIFICATE_PATH" >> $GITHUB_OUTPUT + fi echo "connection-uri=$CONNECTION_URI" >> $GITHUB_OUTPUT echo "service-name=${{ inputs.username }}" >> $GITHUB_OUTPUT diff --git a/test_action.py b/test_action.py index 809b2aa..52095b1 100644 --- a/test_action.py +++ b/test_action.py @@ -87,6 +87,20 @@ def test_service_name(service_name: str): assert service_name == os.getenv("EXPECTED_SERVICE_NAME") +def test_certificate_path(): + """Test that CERTIFICATE_PATH points to the certificate.""" + + certificate_path = os.getenv("CERTIFICATE_PATH") + + if os.getenv("EXPECTED_SSL") == "true": + assert certificate_path + certificate_text = pathlib.Path(certificate_path).read_text() + assert "Subject: CN = localhost" in certificate_text + assert "-----BEGIN CERTIFICATE-----" in certificate_text + else: + assert not certificate_path + + def test_server_encoding(connection: psycopg.Connection): """Test that PostgreSQL's encoding matches the one we passed to initdb.""" @@ -147,6 +161,13 @@ def test_server_version(connection: psycopg.Connection): assert server_version.split(".")[0] == os.getenv("EXPECTED_SERVER_VERSION") +def test_server_ssl(connection: psycopg.Connection): + """Test that connection is SSL encrypted.""" + + expected = os.getenv("EXPECTED_SSL") == "true" + assert connection.info.pgconn.ssl_in_use is expected + + def test_user_permissions(connection: psycopg.Connection): """Test that a user has super/createdb permissions."""