Skip to content

Commit 3330dc1

Browse files
bearritobstraussermdelapenyastevenh
authored
feat(postgres): ssl for postgres (#2473)
* SSL for postgres * Add entrypoint wrapper * Add in init so we can test ssl+init path * Remove unused fields from options * Remove unused consts * Separate entrypoint from ssl * Use external cert generation * Make entrypoint not-optional * Add docstring * Spaces to tab in entrypoint * Add postgres ssl docs * Remove WithEntrypoint * Update docs/modules/postgres.md Co-authored-by: Manuel de la Peña <[email protected]> * Update docs/modules/postgres.md Co-authored-by: Manuel de la Peña <[email protected]> * Update docs/modules/postgres.md Co-authored-by: Manuel de la Peña <[email protected]> * Update modules/postgres/postgres_test.go Co-authored-by: Manuel de la Peña <[email protected]> * Update modules/postgres/postgres_test.go Co-authored-by: Manuel de la Peña <[email protected]> * Embed resources + Use custom conf automatically * Update docs/modules/postgres.md Co-authored-by: Manuel de la Peña <[email protected]> * Update docs/modules/postgres.md Co-authored-by: Manuel de la Peña <[email protected]> * Update docs/modules/postgres.md Co-authored-by: Manuel de la Peña <[email protected]> * Update modules/postgres/postgres_test.go Co-authored-by: Manuel de la Peña <[email protected]> * Update modules/postgres/postgres_test.go Co-authored-by: Manuel de la Peña <[email protected]> * Update modules/postgres/postgres_test.go Co-authored-by: Manuel de la Peña <[email protected]> * Update modules/postgres/postgres_test.go Co-authored-by: Manuel de la Peña <[email protected]> * Revert to use passed in conf * Update doc for required conf * Error checking in the customizer * Few formatting fix * Use non-nil error when err is nil * Update modules/postgres/postgres_test.go Co-authored-by: Steven Hartland <[email protected]> * Update modules/postgres/postgres_test.go Co-authored-by: Steven Hartland <[email protected]> * Update modules/postgres/postgres.go Co-authored-by: Steven Hartland <[email protected]> * Update modules/postgres/postgres.go Co-authored-by: Steven Hartland <[email protected]> * Update modules/postgres/postgres_test.go Co-authored-by: Steven Hartland <[email protected]> * Addresses review modulo cleanup * Remove unused type * Use ContainerCleanup * Lint pass * Add t.Helper and Linting * Remove SSLSetting struct, use raw paths * Use single command for chown key material * docs: remove spaces * fix: use non-deprecated APIs * chore: rename variable --------- Co-authored-by: bstrausser <[email protected]> Co-authored-by: Manuel de la Peña <[email protected]> Co-authored-by: Steven Hartland <[email protected]> Co-authored-by: Manuel de la Peña <[email protected]>
1 parent 6ec91f1 commit 3330dc1

File tree

7 files changed

+263
-0
lines changed

7 files changed

+263
-0
lines changed

docs/modules/postgres.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,35 @@ An example of a `*.sh` script that creates a user and database is shown below:
7474
7575
In the case you have a custom config file for Postgres, it's possible to copy that file into the container before it's started, using the `WithConfigFile(cfgPath string)` function.
7676
77+
This function can be used `WithSSLSettings` but requires your configuration correctly sets the SSL properties. See the below section for more information.
78+
7779
!!!tip
7880
For information on what is available to configure, see the [PostgreSQL docs](https://www.postgresql.org/docs/14/runtime-config.html) for the specific version of PostgreSQL that you are running.
7981
82+
#### SSL Configuration
83+
84+
- Not available until the next release of testcontainers-go <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>
85+
86+
If you would like to use SSL with the container you can use the `WithSSLSettings`. This function accepts a `SSLSettings` which has the required secret material, namely the ca-certificate, server certificate and key. The container will copy this material to `/tmp/testcontainers-go/postgres/ca_cert.pem`, `/tmp/testcontainers-go/postgres/server.cert` and `/tmp/testcontainers-go/postgres/server.key`
87+
88+
This function requires a custom postgres configuration file that enables SSL and correctly sets the paths on the key material.
89+
90+
If you use this function by itself or in conjuction with `WithConfigFile` your custom conf must set the require ssl fields. The configuration must correctly align the key material provided via `SSLSettings` with the server configuration, namely the paths. Your configuration will need to contain the following:
91+
92+
```
93+
ssl = on
94+
ssl_ca_file = '/tmp/testcontainers-go/postgres/ca_cert.pem'
95+
ssl_cert_file = '/tmp/testcontainers-go/postgres/server.cert'
96+
ssl_key_file = '/tmp/testcontainers-go/postgres/server.key'
97+
```
98+
99+
!!!warning
100+
This function assumes the postgres user in the container is `postgres`
101+
102+
There is no current support for mutual authentication.
103+
104+
The `SSLSettings` function will modify the container `entrypoint`. This is done so that key material copied over to the container is chowned by `postgres`. All other container arguments will be passed through to the original container entrypoint.
105+
80106
### Container Methods
81107
82108
#### ConnectionString

modules/postgres/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/docker/go-connections v0.5.0
77
github.com/jackc/pgx/v5 v5.5.4
88
github.com/lib/pq v1.10.9
9+
github.com/mdelapenya/tlscert v0.1.0
910
github.com/stretchr/testify v1.9.0
1011
github.com/testcontainers/testcontainers-go v0.34.0
1112

modules/postgres/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ
7171
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
7272
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
7373
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
74+
github.com/mdelapenya/tlscert v0.1.0 h1:YTpF579PYUX475eOL+6zyEO3ngLTOUWck78NBuJVXaM=
75+
github.com/mdelapenya/tlscert v0.1.0/go.mod h1:wrbyM/DwbFCeCeqdPX/8c6hNOqQgbf0rUDErE1uD+64=
7476
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
7577
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
7678
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=

modules/postgres/postgres.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package postgres
33
import (
44
"context"
55
"database/sql"
6+
_ "embed"
67
"errors"
78
"fmt"
89
"io"
@@ -19,6 +20,9 @@ const (
1920
defaultSnapshotName = "migrated_template"
2021
)
2122

23+
//go:embed resources/customEntrypoint.sh
24+
var embeddedCustomEntrypoint string
25+
2226
// PostgresContainer represents the postgres container type used in the module
2327
type PostgresContainer struct {
2428
testcontainers.Container
@@ -205,6 +209,43 @@ func WithSnapshotName(name string) SnapshotOption {
205209
}
206210
}
207211

212+
// WithSSLSettings configures the Postgres server to run with the provided CA Chain
213+
// This will not function if the corresponding postgres conf is not correctly configured.
214+
// Namely the paths below must match what is set in the conf file
215+
func WithSSLCert(caCertFile string, certFile string, keyFile string) testcontainers.CustomizeRequestOption {
216+
const defaultPermission = 0o600
217+
218+
return func(req *testcontainers.GenericContainerRequest) error {
219+
const entrypointPath = "/usr/local/bin/docker-entrypoint-ssl.bash"
220+
221+
req.Files = append(req.Files,
222+
testcontainers.ContainerFile{
223+
HostFilePath: caCertFile,
224+
ContainerFilePath: "/tmp/testcontainers-go/postgres/ca_cert.pem",
225+
FileMode: defaultPermission,
226+
},
227+
testcontainers.ContainerFile{
228+
HostFilePath: certFile,
229+
ContainerFilePath: "/tmp/testcontainers-go/postgres/server.cert",
230+
FileMode: defaultPermission,
231+
},
232+
testcontainers.ContainerFile{
233+
HostFilePath: keyFile,
234+
ContainerFilePath: "/tmp/testcontainers-go/postgres/server.key",
235+
FileMode: defaultPermission,
236+
},
237+
testcontainers.ContainerFile{
238+
Reader: strings.NewReader(embeddedCustomEntrypoint),
239+
ContainerFilePath: entrypointPath,
240+
FileMode: defaultPermission,
241+
},
242+
)
243+
req.Entrypoint = []string{"sh", entrypointPath}
244+
245+
return nil
246+
}
247+
}
248+
208249
// Snapshot takes a snapshot of the current state of the database as a template, which can then be restored using
209250
// the Restore method. By default, the snapshot will be created under a database called migrated_template, you can
210251
// customize the snapshot name with the options.

modules/postgres/postgres_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package postgres_test
33
import (
44
"context"
55
"database/sql"
6+
"errors"
67
"fmt"
8+
"os"
79
"path/filepath"
810
"testing"
911
"time"
@@ -12,6 +14,8 @@ import (
1214
"github.com/jackc/pgx/v5"
1315
_ "github.com/jackc/pgx/v5/stdlib"
1416
_ "github.com/lib/pq"
17+
"github.com/mdelapenya/tlscert"
18+
"github.com/stretchr/testify/assert"
1519
"github.com/stretchr/testify/require"
1620

1721
"github.com/testcontainers/testcontainers-go"
@@ -25,6 +29,40 @@ const (
2529
password = "password"
2630
)
2731

32+
func createSSLCerts(t *testing.T) (*tlscert.Certificate, *tlscert.Certificate, error) {
33+
t.Helper()
34+
tmpDir := t.TempDir()
35+
certsDir := tmpDir + "/certs"
36+
37+
require.NoError(t, os.MkdirAll(certsDir, 0o755))
38+
39+
t.Cleanup(func() {
40+
require.NoError(t, os.RemoveAll(tmpDir))
41+
})
42+
43+
caCert := tlscert.SelfSignedFromRequest(tlscert.Request{
44+
Host: "localhost",
45+
Name: "ca-cert",
46+
ParentDir: certsDir,
47+
})
48+
49+
if caCert == nil {
50+
return caCert, nil, errors.New("unable to create CA Authority")
51+
}
52+
53+
cert := tlscert.SelfSignedFromRequest(tlscert.Request{
54+
Host: "localhost",
55+
Name: "client-cert",
56+
Parent: caCert,
57+
ParentDir: certsDir,
58+
})
59+
if cert == nil {
60+
return caCert, cert, errors.New("unable to create Server Certificates")
61+
}
62+
63+
return caCert, cert, nil
64+
}
65+
2866
func TestPostgres(t *testing.T) {
2967
ctx := context.Background()
3068

@@ -171,6 +209,56 @@ func TestWithConfigFile(t *testing.T) {
171209
defer db.Close()
172210
}
173211

212+
func TestWithSSL(t *testing.T) {
213+
ctx := context.Background()
214+
215+
caCert, serverCerts, err := createSSLCerts(t)
216+
require.NoError(t, err)
217+
218+
ctr, err := postgres.Run(ctx,
219+
"postgres:16-alpine",
220+
postgres.WithConfigFile(filepath.Join("testdata", "postgres-ssl.conf")),
221+
postgres.WithInitScripts(filepath.Join("testdata", "init-user-db.sh")),
222+
postgres.WithDatabase(dbname),
223+
postgres.WithUsername(user),
224+
postgres.WithPassword(password),
225+
testcontainers.WithWaitStrategy(wait.ForLog("database system is ready to accept connections").WithOccurrence(2).WithStartupTimeout(5*time.Second)),
226+
postgres.WithSSLCert(caCert.CertPath, serverCerts.CertPath, serverCerts.KeyPath),
227+
)
228+
229+
testcontainers.CleanupContainer(t, ctr)
230+
require.NoError(t, err)
231+
232+
connStr, err := ctr.ConnectionString(ctx, "sslmode=require")
233+
require.NoError(t, err)
234+
235+
db, err := sql.Open("postgres", connStr)
236+
require.NoError(t, err)
237+
assert.NotNil(t, db)
238+
defer db.Close()
239+
240+
result, err := db.Exec("SELECT * FROM testdb;")
241+
require.NoError(t, err)
242+
assert.NotNil(t, result)
243+
}
244+
245+
func TestSSLValidatesKeyMaterialPath(t *testing.T) {
246+
ctx := context.Background()
247+
248+
_, err := postgres.Run(ctx,
249+
"postgres:16-alpine",
250+
postgres.WithConfigFile(filepath.Join("testdata", "postgres-ssl.conf")),
251+
postgres.WithInitScripts(filepath.Join("testdata", "init-user-db.sh")),
252+
postgres.WithDatabase(dbname),
253+
postgres.WithUsername(user),
254+
postgres.WithPassword(password),
255+
testcontainers.WithWaitStrategy(wait.ForLog("database system is ready to accept connections").WithOccurrence(2).WithStartupTimeout(5*time.Second)),
256+
postgres.WithSSLCert("", "", ""),
257+
)
258+
259+
require.Error(t, err, "Error should not have been nil. Container creation should have failed due to empty key material")
260+
}
261+
174262
func TestWithInitScript(t *testing.T) {
175263
ctx := context.Background()
176264

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#!/usr/bin/env bash
2+
set -Eeo pipefail
3+
4+
5+
pUID=$(id -u postgres)
6+
pGID=$(id -g postgres)
7+
8+
if [ -z "$pUID" ]
9+
then
10+
echo "Unable to find postgres user id, required in order to chown key material"
11+
exit 1
12+
fi
13+
14+
if [ -z "$pGID" ]
15+
then
16+
echo "Unable to find postgres group id, required in order to chown key material"
17+
exit 1
18+
fi
19+
20+
chown "$pUID":"$pGID" \
21+
/tmp/testcontainers-go/postgres/ca_cert.pem \
22+
/tmp/testcontainers-go/postgres/server.cert \
23+
/tmp/testcontainers-go/postgres/server.key
24+
25+
/usr/local/bin/docker-entrypoint.sh "$@"
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# -----------------------------
2+
# PostgreSQL configuration file
3+
# -----------------------------
4+
#
5+
# This file consists of lines of the form:
6+
#
7+
# name = value
8+
#
9+
# (The "=" is optional.) Whitespace may be used. Comments are introduced with
10+
# "#" anywhere on a line. The complete list of parameter names and allowed
11+
# values can be found in the PostgreSQL documentation.
12+
#
13+
# The commented-out settings shown in this file represent the default values.
14+
# Re-commenting a setting is NOT sufficient to revert it to the default value;
15+
# you need to reload the server.
16+
#
17+
# This file is read on server startup and when the server receives a SIGHUP
18+
# signal. If you edit the file on a running system, you have to SIGHUP the
19+
# server for the changes to take effect, run "pg_ctl reload", or execute
20+
# "SELECT pg_reload_conf()". Some parameters, which are marked below,
21+
# require a server shutdown and restart to take effect.
22+
#
23+
# Any parameter can also be given as a command-line option to the server, e.g.,
24+
# "postgres -c log_connections=on". Some parameters can be changed at run time
25+
# with the "SET" SQL command.
26+
#
27+
# Memory units: B = bytes Time units: ms = milliseconds
28+
# kB = kilobytes s = seconds
29+
# MB = megabytes min = minutes
30+
# GB = gigabytes h = hours
31+
# TB = terabytes d = days
32+
33+
34+
#------------------------------------------------------------------------------
35+
# FILE LOCATIONS
36+
#------------------------------------------------------------------------------
37+
38+
# The default values of these variables are driven from the -D command-line
39+
# option or PGDATA environment variable, represented here as ConfigDir.
40+
41+
#data_directory = 'ConfigDir' # use data in another directory
42+
# (change requires restart)
43+
#hba_file = 'ConfigDir/pg_hba.conf' # host-based authentication file
44+
# (change requires restart)
45+
#ident_file = 'ConfigDir/pg_ident.conf' # ident configuration file
46+
# (change requires restart)
47+
48+
# If external_pid_file is not explicitly set, no extra PID file is written.
49+
#external_pid_file = '' # write an extra PID file
50+
# (change requires restart)
51+
52+
53+
#------------------------------------------------------------------------------
54+
# CONNECTIONS AND AUTHENTICATION
55+
#------------------------------------------------------------------------------
56+
57+
# - Connection Settings -
58+
59+
listen_addresses = '*'
60+
# comma-separated list of addresses;
61+
# defaults to 'localhost'; use '*' for all
62+
# (change requires restart)
63+
#port = 5432 # (change requires restart)
64+
#max_connections = 100 # (change requires restart)
65+
66+
# - SSL -
67+
68+
ssl = on
69+
ssl_ca_file = '/tmp/testcontainers-go/postgres/ca_cert.pem'
70+
ssl_cert_file = '/tmp/testcontainers-go/postgres/server.cert'
71+
#ssl_crl_file = ''
72+
ssl_key_file = '/tmp/testcontainers-go/postgres/server.key'
73+
#ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL' # allowed SSL ciphers
74+
#ssl_prefer_server_ciphers = on
75+
#ssl_ecdh_curve = 'prime256v1'
76+
#ssl_dh_params_file = ''
77+
#ssl_passphrase_command = ''
78+
#ssl_passphrase_command_supports_reload = off
79+
80+

0 commit comments

Comments
 (0)