Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: create TLS certs in a consistent manner #2478

Merged
merged 21 commits into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion container.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ func (c *ContainerRequest) GetContext() (io.Reader, error) {

if dockerIgnoreExists {
// only add .dockerignore if it exists
includes = append(includes, filepath.Join(".dockerignore"))
includes = append(includes, ".dockerignore")
}

includes = append(includes, c.GetDockerfile())
Expand Down
4 changes: 3 additions & 1 deletion docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,9 @@ func (c *DockerContainer) CopyToContainer(ctx context.Context, fileContent []byt

func (c *DockerContainer) copyToContainer(ctx context.Context, fileContent func(tw io.Writer) error, fileContentSize int64, containerFilePath string, fileMode int64) error {
buffer, err := tarFile(containerFilePath, fileContent, fileContentSize, fileMode)
if err != nil {
mdelapenya marked this conversation as resolved.
Show resolved Hide resolved
return err
}

err = c.provider.client.CopyToContainer(ctx, c.ID, "/", buffer, types.CopyToContainerOptions{})
if err != nil {
Expand Down Expand Up @@ -1506,7 +1509,6 @@ func (p *DockerProvider) getDefaultNetwork(ctx context.Context, cli client.APICl
Attachable: true,
Labels: core.DefaultLabels(core.SessionID()),
})

if err != nil {
return "", err
}
Expand Down
5 changes: 3 additions & 2 deletions docker_exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/docker/docker/pkg/stdcopy"
"github.com/stretchr/testify/require"

tcexec "github.com/testcontainers/testcontainers-go/exec"
)

Expand Down Expand Up @@ -133,6 +134,6 @@ func TestExecWithNonMultiplexedResponse(t *testing.T) {
require.NotNil(t, stdout)
require.NotNil(t, stderr)

require.Equal(t, stdout.String(), "stdout\n")
require.Equal(t, stderr.String(), "stderr\n")
require.Equal(t, "stdout\n", stdout.String())
require.Equal(t, "stderr\n", stderr.String())
}
44 changes: 44 additions & 0 deletions docs/features/tls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# TLS certificates

_Testcontainers for Go_ provides a way to interact with services that require TLS certificates.
You can create one or more on-the-fly certificates in order to communicate with your services.

To create a certificate, you can use the `tctls.Certificate` struct, where the `tctls` namespace is imported from the `github.com/testcontainers/testcontainers-go/tls` package, to avoid conflicts with the `tls` package from the Go standard library.

The `tctls.Certificate` struct has the following fields:

<!--codeinclude-->
[Certificate struct](../../tls/generate.go) inside_block:testcontainersTLSCertificate
<!--/codeinclude-->

You can generate a certificate by calling the `tctls.GenerateCert` function. This function receives a variadic argument of functional options that allow you to customize the certificate:

- `tctls.WithSubjectCommonName`: sets the subject's common name of the certificate.
- `tctls.WithHost`: sets the hostnames that the certificate will be valid for. In the case the passed string contains comma-separated values,
it will be split into multiple hostnames and IPs. Each hostname and IP will be trimmed of whitespace, and if the value is an IP,
it will be added to the IPAddresses field of the certificate, after the ones passed with the WithIPAddresses option.
Otherwise, it will be added to the DNSNames field.
- `tctls.WithValidFor`: sets the duration that the certificate will be valid for. By default, the certificate is valid for 365 days.
- `tctls.AsCA`: sets the certificate as a Certificate Authority (CA). This option is disabled by default.
When passed, the KeyUsage field of the certificate will append the x509.KeyUsageCertSign usage.
- `tctls.WithParent`: sets the parent certificate of the certificate. This option is disabled by default.
When passed, the parent certificate will be used to sign the generated certificate,
and the issuer of the certificate will be set to the common name of the parent certificate.
- `tctls.AsPem`: sets the certificate to be returned as PEM bytes. It will include the private key in the `KeyBytes` field of the Certificate struct.
- `tctls.WithIPAddresses`: sets the IP addresses that the certificate will be valid for. The IPs passed with this option will be added
first to the IPAddresses field of the certificate: those coming from the `WithHost` option will be added after them.
- `tctls.WithSaveToFile`: sets the parent directory where the certificate and its private key will be saved. Both the certificate and its private key will be saved in separate files, using an random UUID as part of the filename. E.g., `cert-<UUID>.pem` and `key-<UUID>.pem`.

!!! note
If the `WithSaveToFile` option is passed, it will automatically set the `AsPem` option, as we need to the private key bytes too.

### Examples

In the following example we are going to start an HTTP server with a self-signed certificate.
It exposes one single handler that will return a simple message when accessed.
The example will also create a client that will connect to the server using the generated certificate,
demonstrating how to use the generated certificate to communicate with a service.

<!--codeinclude-->
[Use a certificate](../../tls/examples_test.go) inside_block:ExampleGenerateCert
<!--/codeinclude-->
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ nav:
- features/files_and_mounts.md
- features/creating_networks.md
- features/networking.md
- features/tls.md
- features/garbage_collector.md
- features/build_from_dockerfile.md
- features/docker_auth.md
Expand Down
155 changes: 30 additions & 125 deletions modules/cockroachdb/certs.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
package cockroachdb

import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"net"
"time"

tctls "github.com/testcontainers/testcontainers-go/tls"
)

type TLSConfig struct {
Expand All @@ -21,138 +18,46 @@ type TLSConfig struct {

// NewTLSConfig creates a new TLSConfig capable of running CockroachDB & connecting over TLS.
func NewTLSConfig() (*TLSConfig, error) {
caCert, caKey, err := generateCA()
caCert, err := tctls.GenerateCert(
tctls.WithHost("localhost"),
tctls.WithSubjectCommonName("Cockroach Test CA"),
tctls.AsCA(),
tctls.WithValidFor(time.Hour),
)
if err != nil {
return nil, err
}

nodeCert, nodeKey, err := generateNode(caCert, caKey)
nodeCert, err := tctls.GenerateCert(
tctls.WithHost("localhost"), // the host will be passed as DNSNames
tctls.WithSubjectCommonName("node"),
tctls.AsCA(),
tctls.WithIPAddresses(net.IPv4(127, 0, 0, 1), net.IPv6loopback),
tctls.WithValidFor(time.Hour),
tctls.AsPem(),
tctls.WithParent(caCert.Cert, caCert.Key),
)
if err != nil {
return nil, err
}

clientCert, clientKey, err := generateClient(caCert, caKey)
clientCert, err := tctls.GenerateCert(
tctls.WithHost("localhost"),
tctls.WithSubjectCommonName(defaultUser),
tctls.AsCA(),
tctls.WithValidFor(time.Hour),
tctls.AsPem(),
tctls.WithParent(caCert.Cert, caCert.Key),
)
if err != nil {
return nil, err
}

return &TLSConfig{
CACert: caCert,
NodeCert: nodeCert,
NodeKey: nodeKey,
ClientCert: clientCert,
ClientKey: clientKey,
CACert: caCert.Cert,
NodeCert: nodeCert.Bytes,
NodeKey: nodeCert.KeyBytes,
ClientCert: clientCert.Bytes,
ClientKey: clientCert.KeyBytes,
}, nil
}

func generateCA() (*x509.Certificate, *rsa.PrivateKey, error) {
template := x509.Certificate{
SerialNumber: big.NewInt(2019),
Subject: pkix.Name{
CommonName: "Cockroach Test CA",
},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
BasicConstraintsValid: true,
IsCA: true,
}

caPrivKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, nil, err
}

caBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, caPrivKey.Public(), caPrivKey)
if err != nil {
return nil, nil, err
}

caCert, err := x509.ParseCertificate(caBytes)
if err != nil {
return nil, nil, err
}

return caCert, caPrivKey, nil
}

func generateNode(caCert *x509.Certificate, caKey *rsa.PrivateKey) ([]byte, []byte, error) {
template := x509.Certificate{
SerialNumber: big.NewInt(2019),
Subject: pkix.Name{
CommonName: "node",
},
DNSNames: []string{"localhost"},
IPAddresses: []net.IP{
net.IPv4(127, 0, 0, 1),
net.IPv6loopback,
},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{
x509.ExtKeyUsageServerAuth,
x509.ExtKeyUsageClientAuth,
},
BasicConstraintsValid: true,
}

certPrivKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, nil, err
}

certBytes, err := x509.CreateCertificate(rand.Reader, &template, caCert, certPrivKey.Public(), caKey)
if err != nil {
return nil, nil, err
}

cert := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: certBytes,
})
certKey := pem.EncodeToMemory(&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey),
})

return cert, certKey, nil
}

func generateClient(caCert *x509.Certificate, caKey *rsa.PrivateKey) ([]byte, []byte, error) {
template := x509.Certificate{
SerialNumber: big.NewInt(2019),
Subject: pkix.Name{
CommonName: defaultUser,
},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{
x509.ExtKeyUsageServerAuth,
x509.ExtKeyUsageClientAuth,
},
BasicConstraintsValid: true,
}

certPrivKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, nil, err
}

certBytes, err := x509.CreateCertificate(rand.Reader, &template, caCert, certPrivKey.Public(), caKey)
if err != nil {
return nil, nil, err
}

cert := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: certBytes,
})
certKey := pem.EncodeToMemory(&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey),
})

return cert, certKey, nil
}
28 changes: 24 additions & 4 deletions modules/rabbitmq/examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import (
"fmt"
"io"
"log"
"path/filepath"
"os"
"strings"

amqp "github.com/rabbitmq/amqp091-go"

"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/rabbitmq"
tctls "github.com/testcontainers/testcontainers-go/tls"
)

func ExampleRunContainer() {
Expand Down Expand Up @@ -89,10 +90,29 @@ func ExampleRunContainer_withSSL() {
// enableSSL {
ctx := context.Background()

tmpDir := os.TempDir()
certDirs := tmpDir + "/rabbitmq"
if err := os.MkdirAll(certDirs, 0755); err != nil {
log.Fatalf("failed to create temporary directory: %s", err)
}
defer os.RemoveAll(certDirs)

// generates the CA certificate and the certificate
// using TestContainers TLS helper functions.
caCert, err := tctls.GenerateCert(tctls.WithHost("localhost"), tctls.AsCA(), tctls.WithSaveToFile(certDirs))
if err != nil {
log.Fatalf("failed to generate CA certificate: %s", err)
}

cert, err := tctls.GenerateCert(tctls.WithHost("localhost"), tctls.WithParent(caCert.Cert, caCert.Key), tctls.WithSaveToFile(certDirs))
if err != nil {
log.Fatalf("failed to generate certificate: %s", err)
}

sslSettings := rabbitmq.SSLSettings{
CACertFile: filepath.Join("testdata", "certs", "server_ca.pem"),
CertFile: filepath.Join("testdata", "certs", "server_cert.pem"),
KeyFile: filepath.Join("testdata", "certs", "server_key.pem"),
CACertFile: caCert.CertPath,
CertFile: cert.CertPath,
KeyFile: cert.KeyPath,
VerificationMode: rabbitmq.SSLVerificationModePeer,
FailIfNoCert: true,
VerificationDepth: 1,
Expand Down
20 changes: 16 additions & 4 deletions modules/rabbitmq/rabbitmq_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import (
"fmt"
"io"
"io/ioutil"
"path/filepath"
"strings"
"testing"

amqp "github.com/rabbitmq/amqp091-go"

"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/rabbitmq"
tctls "github.com/testcontainers/testcontainers-go/tls"
)

func TestRunContainer_connectUsingAmqp(t *testing.T) {
Expand Down Expand Up @@ -49,10 +49,22 @@ func TestRunContainer_connectUsingAmqp(t *testing.T) {
func TestRunContainer_connectUsingAmqps(t *testing.T) {
ctx := context.Background()

tmpDir := t.TempDir()

caCert, err := tctls.GenerateCert(tctls.WithHost("localhost"), tctls.AsCA(), tctls.WithSaveToFile(tmpDir))
if err != nil {
t.Fatalf("failed to generate CA certificate: %s", err)
}

cert, err := tctls.GenerateCert(tctls.WithHost("localhost"), tctls.WithParent(caCert.Cert, caCert.Key), tctls.WithSaveToFile(tmpDir))
if err != nil {
t.Fatalf("failed to generate certificate: %s", err)
}

sslSettings := rabbitmq.SSLSettings{
CACertFile: filepath.Join("testdata", "certs", "server_ca.pem"),
CertFile: filepath.Join("testdata", "certs", "server_cert.pem"),
KeyFile: filepath.Join("testdata", "certs", "server_key.pem"),
CACertFile: caCert.CertPath,
CertFile: cert.CertPath,
KeyFile: cert.KeyPath,
VerificationMode: rabbitmq.SSLVerificationModePeer,
FailIfNoCert: false,
VerificationDepth: 1,
Expand Down
20 changes: 0 additions & 20 deletions modules/rabbitmq/testdata/certs/server_ca.pem

This file was deleted.

Loading
Loading