Skip to content

Commit

Permalink
chore: create TLS certs in a consistent manner (testcontainers#2478)
Browse files Browse the repository at this point in the history
* fix: remove suspicious filepath.Join

* chore: fix lint

* fix: handle error

* chore: reverse assertion for lint

* feat: support generating TLS certificates on the fly

* chore: apply to cockroachdb

* chore: support saving the cert and priv key files to disk

* chore: apply to rabbitmq

* chore: simplify

* chore: use in redpanda module

* chore: lint

* chore: set validFrom internally

* fix: properly use the new API in redpanda

* docs: document the TLS helpers

* chore: simplify WithParent to accept the struct directly

* chore: use tlscert package instead

* fix: use non-deprecated API

* docs: update

* docs: fix examples

* chore: use released version of tlscert

* fix: add common name for the node cert
  • Loading branch information
mdelapenya committed Apr 23, 2024
1 parent bb578a4 commit ca2ea86
Show file tree
Hide file tree
Showing 19 changed files with 159 additions and 294 deletions.
2 changes: 1 addition & 1 deletion container.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,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 {
return err
}

err = c.provider.client.CopyToContainer(ctx, c.ID, "/", buffer, types.CopyToContainerOptions{})
if err != nil {
Expand Down Expand Up @@ -1520,7 +1523,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())
}
17 changes: 17 additions & 0 deletions docs/features/tls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# TLS certificates

Interacting with services that require TLS certificates is a common issue when working with containers. You can create one or more on-the-fly certificates in order to communicate with your services.

_Testcontainers for Go_ uses a library to generate certificates on-the-fly. This library is called [tlscert](https://github.com/mdelapenya/tlscert).

### 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-->
[Create a self-signed certificate](../../modules/cockroachdb/certs.go) inside_block:exampleSelfSignedCert
[Sign a self-signed certificate](../../modules/cockroachdb/certs.go) inside_block:exampleSignSelfSignedCert
<!--/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
175 changes: 42 additions & 133 deletions modules/cockroachdb/certs.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
package cockroachdb

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

"github.com/mdelapenya/tlscert"
)

type TLSConfig struct {
Expand All @@ -21,138 +19,49 @@ type TLSConfig struct {

// NewTLSConfig creates a new TLSConfig capable of running CockroachDB & connecting over TLS.
func NewTLSConfig() (*TLSConfig, error) {
caCert, caKey, err := generateCA()
if err != nil {
return nil, err
// exampleSelfSignedCert {
caCert := tlscert.SelfSignedFromRequest(tlscert.Request{
Name: "ca",
SubjectCommonName: "Cockroach Test CA",
Host: "localhost,127.0.0.1",
IsCA: true,
ValidFor: time.Hour,
})
if caCert == nil {
return nil, fmt.Errorf("failed to generate CA certificate")
}

nodeCert, nodeKey, err := generateNode(caCert, caKey)
if err != nil {
return nil, err
// }

// exampleSignSelfSignedCert {
nodeCert := tlscert.SelfSignedFromRequest(tlscert.Request{
Name: "node",
SubjectCommonName: "node",
Host: "localhost,127.0.0.1",
IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback},
ValidFor: time.Hour,
Parent: caCert, // using the CA certificate as parent
})
if nodeCert == nil {
return nil, fmt.Errorf("failed to generate node certificate")
}

clientCert, clientKey, err := generateClient(caCert, caKey)
if err != nil {
return nil, err
// }

clientCert := tlscert.SelfSignedFromRequest(tlscert.Request{
Name: "client",
SubjectCommonName: defaultUser,
Host: "localhost,127.0.0.1",
ValidFor: time.Hour,
Parent: caCert, // using the CA certificate as parent
})
if clientCert == nil {
return nil, fmt.Errorf("failed to generate client certificate")
}

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
}
1 change: 1 addition & 0 deletions modules/cockroachdb/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ require (
github.com/kr/text v0.2.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mdelapenya/tlscert v0.1.0
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/moby/sys/user v0.1.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions modules/cockroachdb/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mdelapenya/tlscert v0.1.0 h1:YTpF579PYUX475eOL+6zyEO3ngLTOUWck78NBuJVXaM=
github.com/mdelapenya/tlscert v0.1.0/go.mod h1:wrbyM/DwbFCeCeqdPX/8c6hNOqQgbf0rUDErE1uD+64=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
Expand Down
38 changes: 34 additions & 4 deletions modules/rabbitmq/examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import (
"fmt"
"io"
"log"
"path/filepath"
"os"
"strings"

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

"github.com/testcontainers/testcontainers-go"
Expand Down Expand Up @@ -89,10 +90,39 @@ 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
caCert := tlscert.SelfSignedFromRequest(tlscert.Request{
Name: "ca",
Host: "localhost,127.0.0.1",
IsCA: true,
ParentDir: certDirs,
})
if caCert == nil {
log.Fatal("failed to generate CA certificate")
}

cert := tlscert.SelfSignedFromRequest(tlscert.Request{
Name: "client",
Host: "localhost,127.0.0.1",
IsCA: true,
Parent: caCert,
ParentDir: certDirs,
})
if cert == nil {
log.Fatal("failed to generate certificate")
}

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
1 change: 1 addition & 0 deletions modules/rabbitmq/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ require (
github.com/klauspost/compress v1.16.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mdelapenya/tlscert v0.1.0
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/moby/sys/user v0.1.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions modules/rabbitmq/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mdelapenya/tlscert v0.1.0 h1:YTpF579PYUX475eOL+6zyEO3ngLTOUWck78NBuJVXaM=
github.com/mdelapenya/tlscert v0.1.0/go.mod h1:wrbyM/DwbFCeCeqdPX/8c6hNOqQgbf0rUDErE1uD+64=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
Expand Down
Loading

0 comments on commit ca2ea86

Please sign in to comment.