Skip to content

Commit 0199df9

Browse files
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 <[email protected]> Signed-off-by: Ihor Kalnytskyi <[email protected]> Signed-off-by: chandr-andr (Kiselev Aleksandr) <[email protected]>
1 parent 50da8e8 commit 0199df9

File tree

4 files changed

+77
-7
lines changed

4 files changed

+77
-7
lines changed

.github/workflows/ci.yml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,16 @@ jobs:
4040

4141
- name: Run tests
4242
run: |
43-
python3 -m pip install --upgrade pip pytest psycopg furl
43+
python3 -m pip install --upgrade pip pytest psycopg furl cryptography
4444
python3 -m pytest -vv test_action.py
4545
env:
4646
CONNECTION_URI: ${{ steps.postgres.outputs.connection-uri }}
4747
SERVICE_NAME: ${{ steps.postgres.outputs.service-name }}
48+
CERTIFICATE_PATH: ${{ steps.postgres.outputs.certificate-path }}
4849
EXPECTED_CONNECTION_URI: postgresql://postgres:postgres@localhost:5432/postgres
4950
EXPECTED_SERVICE_NAME: postgres
5051
EXPECTED_SERVER_VERSION: "16"
52+
EXPECTED_SSL: false
5153

5254
parametrized:
5355
runs-on: ${{ matrix.os }}
@@ -76,6 +78,7 @@ jobs:
7678
database: jedi_order
7779
port: 34837
7880
postgres-version: ${{ matrix.postgres-version }}
81+
ssl: true
7982
id: postgres
8083

8184
- name: Run setup-python
@@ -85,11 +88,13 @@ jobs:
8588

8689
- name: Run tests
8790
run: |
88-
python3 -m pip install --upgrade pip pytest psycopg furl
91+
python3 -m pip install --upgrade pip pytest psycopg furl cryptography
8992
python3 -m pytest -vv test_action.py
9093
env:
9194
CONNECTION_URI: ${{ steps.postgres.outputs.connection-uri }}
9295
SERVICE_NAME: ${{ steps.postgres.outputs.service-name }}
93-
EXPECTED_CONNECTION_URI: postgresql://yoda:GrandMaster@localhost:34837/jedi_order
96+
CERTIFICATE_PATH: ${{ steps.postgres.outputs.certificate-path }}
97+
EXPECTED_CONNECTION_URI: postgresql://yoda:GrandMaster@localhost:34837/jedi_order?sslmode=verify-ca&sslrootcert=${{ steps.postgres.outputs.certificate-path }}
9498
EXPECTED_SERVICE_NAME: yoda
9599
EXPECTED_SERVER_VERSION: ${{ matrix.postgres-version }}
100+
EXPECTED_SSL: true

README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ key features:
1010
* Runs on Linux, macOS and Windows action runners.
1111
* Adds PostgreSQL [client applications][1] to `PATH`.
1212
* PostgreSQL version can be parametrized.
13+
* Supports SSL if needed.
1314
* Easy [to verify][2] that it DOES NOT contain malicious code.
1415

1516
By default PostgreSQL 15 is used.
@@ -44,10 +45,11 @@ By default PostgreSQL 15 is used.
4445

4546
#### Outputs
4647

47-
| Key | Description | Example |
48-
|----------------|----------------------------------------------|-----------------------------------------------------|
49-
| connection-uri | The connection URI to connect to PostgreSQL. | `postgresql://postgres:postgres@localhost/postgres` |
50-
| service-name | The service name with connection parameters. | `postgres` |
48+
| Key | Description | Example |
49+
|------------------|--------------------------------------------------|-----------------------------------------------------|
50+
| connection-uri | The connection URI to connect to PostgreSQL. | `postgresql://postgres:postgres@localhost/postgres` |
51+
| service-name | The service name with connection parameters. | `postgres` |
52+
| certificate-path | The path to the server certificate if SSL is on. | `/home/runner/work/_temp/pgdata/server.crt` |
5153

5254
#### User permissions
5355

@@ -74,6 +76,7 @@ steps:
7476
database: test
7577
port: 34837
7678
postgres-version: "14"
79+
ssl: "on"
7780
id: postgres
7881

7982
- run: pytest -vv tests/

action.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ inputs:
2424
postgres-version:
2525
description: The PostgreSQL major version to install. Either "14", "15", or "16".
2626
default: "16"
27+
ssl:
28+
description: When "true", encrypt connections using SSL (TLS).
29+
default: "false"
2730
required: false
2831
outputs:
2932
connection-uri:
@@ -32,6 +35,9 @@ outputs:
3235
service-name:
3336
description: The service name with connection parameters.
3437
value: ${{ steps.set-outputs.outputs.service-name }}
38+
certificate-path:
39+
description: The path to the server certificate if SSL is on.
40+
value: ${{ steps.set-outputs.outputs.certificate-path }}
3541
runs:
3642
using: composite
3743
steps:
@@ -132,6 +138,23 @@ runs:
132138
# directory we have no permissions to (owned by system postgres user).
133139
echo "unix_socket_directories = ''" >> "$PGDATA/postgresql.conf"
134140
echo "port = ${{ inputs.port }}" >> "$PGDATA/postgresql.conf"
141+
142+
if [ "${{ inputs.ssl }}" = "true" ]; then
143+
# On Windows, bash runs on top of MSYS2, which automatically converts
144+
# Unix paths to Windows paths for every argument that appears to be a
145+
# path. This behavior breaks the openssl invocation because the
146+
# subject argument is mistakenly converted when it should not be.
147+
# Therefore, we need to exclude it from the path conversion process
148+
# by setting the MSYS2_ARG_CONV_EXCL environment variable.
149+
#
150+
# https://www.msys2.org/docs/filesystem-paths/#automatic-unix-windows-path-conversion
151+
export MSYS2_ARG_CONV_EXCL="/CN"
152+
openssl req -new -x509 -days 365 -nodes -text -subj "/CN=localhost" \
153+
-out "$PGDATA/server.crt" -keyout "$PGDATA/server.key"
154+
chmod og-rwx "$PGDATA/server.key" "$PGDATA/server.crt"
155+
echo "ssl = on" >> "$PGDATA/postgresql.conf"
156+
fi
157+
135158
pg_ctl start --pgdata="$PGDATA"
136159
137160
# Save required connection parameters for created superuser to the
@@ -154,6 +177,12 @@ runs:
154177
password=${{ inputs.password }}
155178
dbname=${{ inputs.database }}
156179
EOF
180+
181+
if [ "${{ inputs.ssl }}" = "true" ]; then
182+
echo "sslmode=verify-ca" >> "$PGDATA/pg_service.conf"
183+
echo "sslrootcert=$PGDATA/server.crt" >> "$PGDATA/pg_service.conf"
184+
fi
185+
157186
echo "PGSERVICEFILE=$PGDATA/pg_service.conf" >> $GITHUB_ENV
158187
shell: bash
159188

@@ -173,6 +202,17 @@ runs:
173202
- name: Set action outputs
174203
run: |
175204
CONNECTION_URI="postgresql://${{ inputs.username }}:${{ inputs.password }}@localhost:${{ inputs.port }}/${{ inputs.database }}"
205+
CERTIFICATE_PATH="$RUNNER_TEMP/pgdata/server.crt"
206+
207+
if [ "${{ inputs.ssl }}" = "true" ]; then
208+
# Although SSLMODE and SSLROOTCERT are specific to libpq options,
209+
# most third-party drivers also support them. By default libpq
210+
# prefers SSL but doesn't require it, thus it's important to set
211+
# these options to ensure SSL is used and the certificate is
212+
# verified.
213+
CONNECTION_URI="$CONNECTION_URI?sslmode=verify-ca&sslrootcert=$CERTIFICATE_PATH"
214+
echo "certificate-path=$CERTIFICATE_PATH" >> $GITHUB_OUTPUT
215+
fi
176216
177217
echo "connection-uri=$CONNECTION_URI" >> $GITHUB_OUTPUT
178218
echo "service-name=${{ inputs.username }}" >> $GITHUB_OUTPUT

test_action.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import subprocess
55
import typing as t
66

7+
import cryptography.x509 as x509
78
import psycopg
89
import furl
910
import pytest
@@ -87,6 +88,20 @@ def test_service_name(service_name: str):
8788
assert service_name == os.getenv("EXPECTED_SERVICE_NAME")
8889

8990

91+
def test_certificate_path():
92+
"""Test that CERTIFICATE_PATH points to the certificate."""
93+
94+
certificate_path = os.getenv("CERTIFICATE_PATH")
95+
96+
if os.getenv("EXPECTED_SSL") == "true":
97+
assert certificate_path
98+
certificate_bytes = pathlib.Path(certificate_path).read_bytes()
99+
certificate = x509.load_pem_x509_certificate(certificate_bytes)
100+
assert certificate.subject.rfc4514_string() == "CN=localhost"
101+
else:
102+
assert not certificate_path
103+
104+
90105
def test_server_encoding(connection: psycopg.Connection):
91106
"""Test that PostgreSQL's encoding matches the one we passed to initdb."""
92107

@@ -147,6 +162,13 @@ def test_server_version(connection: psycopg.Connection):
147162
assert server_version.split(".")[0] == os.getenv("EXPECTED_SERVER_VERSION")
148163

149164

165+
def test_server_ssl(connection: psycopg.Connection):
166+
"""Test that connection is SSL encrypted."""
167+
168+
expected = os.getenv("EXPECTED_SSL") == "true"
169+
assert connection.info.pgconn.ssl_in_use is expected
170+
171+
150172
def test_user_permissions(connection: psycopg.Connection):
151173
"""Test that a user has super/createdb permissions."""
152174

0 commit comments

Comments
 (0)