Skip to content

Commit 914dd25

Browse files
committed
config.TLS: Inline PEM-encoded Certs, Keys and CAs
Introduced support for raw encoded PEM data instead of filenames for TLS.Cert, TLS.Key and TLS.Ca. This should make it easier to get rid of the additional entrypoint program for docker-icingadb and allow direct use of Icinga DB with its environment variable support. In addition to the implementation, new tests were written with the goal of complete coverage. A consumer of this type, database.Config, got another test case using PEMs both as environment variables and as YAML
1 parent cf9d730 commit 914dd25

File tree

3 files changed

+235
-17
lines changed

3 files changed

+235
-17
lines changed

config/tls.go

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package config
33
import (
44
"crypto/tls"
55
"crypto/x509"
6+
"encoding/pem"
67
"github.com/pkg/errors"
78
"os"
89
)
@@ -34,13 +35,15 @@ type TLS struct {
3435
// Enable indicates whether TLS is enabled.
3536
Enable bool `yaml:"tls" env:"TLS"`
3637

37-
// Cert is the path to the TLS certificate file. If provided, Key must also be specified.
38+
// Cert is either the path to the TLS certificate file or a raw PEM-encoded string representing it.
39+
// If provided, Key must also be specified.
3840
Cert string `yaml:"cert" env:"CERT"`
3941

40-
// Key is the path to the TLS key file. If specified, Cert must also be provided.
42+
// Key is either the path to the TLS key file or a raw PEM-encoded string representing it.
43+
// If specified, Cert must also be provided.
4144
Key string `yaml:"key" env:"KEY"`
4245

43-
// Ca is the path to the CA certificate file.
46+
// Ca is either the path to the CA certificate file or a raw PEM-encoded string representing it.
4447
Ca string `yaml:"ca" env:"CA"`
4548

4649
// Insecure indicates whether to skip verification of the server's certificate chain and host name.
@@ -49,6 +52,20 @@ type TLS struct {
4952
Insecure bool `yaml:"insecure" env:"INSECURE"`
5053
}
5154

55+
// loadPemOrFile either returns a PEM from within the string or treats it as a file, returning its content.
56+
func loadPemOrFile(pemOrFile string) ([]byte, error) {
57+
block, _ := pem.Decode([]byte(pemOrFile))
58+
if block != nil {
59+
return []byte(pemOrFile), nil
60+
}
61+
62+
data, err := os.ReadFile(pemOrFile)
63+
if err != nil {
64+
return nil, err
65+
}
66+
return data, nil
67+
}
68+
5269
// MakeConfig assembles a [*tls.Config] from the TLS struct and the provided serverName.
5370
// It returns a configured *tls.Config or an error if there are issues with the provided TLS settings.
5471
// If TLS is not enabled (t.Enable is false), it returns nil without an error.
@@ -58,31 +75,37 @@ func (t *TLS) MakeConfig(serverName string) (*tls.Config, error) {
5875
}
5976

6077
tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12}
61-
if t.Cert == "" {
62-
if t.Key != "" {
63-
return nil, errors.New("private key given, but client certificate missing")
64-
}
65-
} else if t.Key == "" {
78+
if t.Cert == "" && t.Key != "" {
79+
return nil, errors.New("private key given, but client certificate missing")
80+
} else if t.Cert != "" && t.Key == "" {
6681
return nil, errors.New("client certificate given, but private key missing")
67-
} else {
68-
crt, err := tls.LoadX509KeyPair(t.Cert, t.Key)
82+
} else if t.Cert != "" && t.Key != "" {
83+
certPem, err := loadPemOrFile(t.Cert)
84+
if err != nil {
85+
return nil, errors.Wrap(err, "can't load X.509 client certificate")
86+
}
87+
keyPem, err := loadPemOrFile(t.Key)
6988
if err != nil {
70-
return nil, errors.Wrap(err, "can't load X.509 key pair")
89+
return nil, errors.Wrap(err, "can't load X.509 private key")
7190
}
7291

92+
crt, err := tls.X509KeyPair(certPem, keyPem)
93+
if err != nil {
94+
return nil, errors.Wrap(err, "can't parse client certificate and private key into an X.509 key pair")
95+
}
7396
tlsConfig.Certificates = []tls.Certificate{crt}
7497
}
7598

7699
if t.Insecure {
77100
tlsConfig.InsecureSkipVerify = true
78101
} else if t.Ca != "" {
79-
raw, err := os.ReadFile(t.Ca)
102+
caPem, err := loadPemOrFile(t.Ca)
80103
if err != nil {
81-
return nil, errors.Wrap(err, "can't read CA file")
104+
return nil, errors.Wrap(err, "can't load X.509 CA certificate")
82105
}
83106

84107
tlsConfig.RootCAs = x509.NewCertPool()
85-
if !tlsConfig.RootCAs.AppendCertsFromPEM(raw) {
108+
if !tlsConfig.RootCAs.AppendCertsFromPEM(caPem) {
86109
return nil, errors.New("can't parse CA file")
87110
}
88111
}

config/tls_test.go

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,37 @@ import (
1616
"time"
1717
)
1818

19+
func Test_loadPemOrFile(t *testing.T) {
20+
cert, _, err := generateCert("cert", generateCertOptions{})
21+
require.NoError(t, err)
22+
certPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})
23+
24+
certFile, err := os.CreateTemp("", "cert-*.pem")
25+
require.NoError(t, err)
26+
defer func(name string) {
27+
_ = os.Remove(name)
28+
}(certFile.Name())
29+
_, err = certFile.Write(certPem)
30+
require.NoError(t, err)
31+
32+
t.Run("Load raw PEM", func(t *testing.T) {
33+
out, err := loadPemOrFile(string(certPem))
34+
require.NoError(t, err)
35+
require.Equal(t, certPem, out)
36+
})
37+
38+
t.Run("Load file", func(t *testing.T) {
39+
out, err := loadPemOrFile(certFile.Name())
40+
require.NoError(t, err)
41+
require.Equal(t, certPem, out)
42+
})
43+
44+
t.Run("Invalid file", func(t *testing.T) {
45+
_, err := loadPemOrFile("/dev/null/nonexistent")
46+
require.Error(t, err)
47+
})
48+
}
49+
1950
func TestTLS_MakeConfig(t *testing.T) {
2051
t.Run("TLS disabled", func(t *testing.T) {
2152
tlsConfig := &TLS{Enable: false}
@@ -48,13 +79,13 @@ func TestTLS_MakeConfig(t *testing.T) {
4879
t.Run("Missing client certificate", func(t *testing.T) {
4980
tlsConfig := &TLS{Enable: true, Key: "test.key"}
5081
_, err := tlsConfig.MakeConfig("icinga.com")
51-
require.Error(t, err)
82+
require.ErrorContains(t, err, "client certificate missing")
5283
})
5384

5485
t.Run("Missing private key", func(t *testing.T) {
5586
tlsConfig := &TLS{Enable: true, Cert: "test.crt"}
5687
_, err := tlsConfig.MakeConfig("icinga.com")
57-
require.Error(t, err)
88+
require.ErrorContains(t, err, "private key missing")
5889
})
5990

6091
t.Run("x509", func(t *testing.T) {
@@ -93,7 +124,7 @@ func TestTLS_MakeConfig(t *testing.T) {
93124
defer func(name string) {
94125
_ = os.Remove(name)
95126
}(corruptFile.Name())
96-
err = os.WriteFile(corruptFile.Name(), []byte("corrupt PEM"), 0600)
127+
err = os.WriteFile(corruptFile.Name(), []byte("-----BEGIN CORRUPT-----\nOOPS\n-----END CORRUPT-----"), 0600)
97128
require.NoError(t, err)
98129

99130
t.Run("Valid certificate and key", func(t *testing.T) {
@@ -104,6 +135,30 @@ func TestTLS_MakeConfig(t *testing.T) {
104135
require.Len(t, config.Certificates, 1)
105136
})
106137

138+
t.Run("Valid certificate and key as PEM", func(t *testing.T) {
139+
certRaw, err := os.ReadFile(certFile.Name())
140+
require.NoError(t, err)
141+
keyRaw, err := os.ReadFile(keyFile.Name())
142+
require.NoError(t, err)
143+
144+
tlsConfig := &TLS{Enable: true, Cert: string(certRaw), Key: string(keyRaw)}
145+
config, err := tlsConfig.MakeConfig("icinga.com")
146+
require.NoError(t, err)
147+
require.NotNil(t, config)
148+
require.Len(t, config.Certificates, 1)
149+
})
150+
151+
t.Run("Valid certificate and key, mixed file and PEM", func(t *testing.T) {
152+
keyRaw, err := os.ReadFile(keyFile.Name())
153+
require.NoError(t, err)
154+
155+
tlsConfig := &TLS{Enable: true, Cert: certFile.Name(), Key: string(keyRaw)}
156+
config, err := tlsConfig.MakeConfig("icinga.com")
157+
require.NoError(t, err)
158+
require.NotNil(t, config)
159+
require.Len(t, config.Certificates, 1)
160+
})
161+
107162
t.Run("Mismatched certificate and key", func(t *testing.T) {
108163
_key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
109164
require.NoError(t, err)
@@ -149,6 +204,17 @@ func TestTLS_MakeConfig(t *testing.T) {
149204
require.Error(t, err)
150205
})
151206

207+
t.Run("Corrupt certificate as PEM", func(t *testing.T) {
208+
corruptRaw, err := os.ReadFile(corruptFile.Name())
209+
require.NoError(t, err)
210+
keyRaw, err := os.ReadFile(keyFile.Name())
211+
require.NoError(t, err)
212+
213+
tlsConfig := &TLS{Enable: true, Cert: string(corruptRaw), Key: string(keyRaw)}
214+
_, err = tlsConfig.MakeConfig("icinga.com")
215+
require.Error(t, err)
216+
})
217+
152218
t.Run("Invalid key path", func(t *testing.T) {
153219
tlsConfig := &TLS{Enable: true, Cert: certFile.Name(), Key: "nonexistent.key"}
154220
_, err := tlsConfig.MakeConfig("icinga.com")
@@ -184,6 +250,17 @@ func TestTLS_MakeConfig(t *testing.T) {
184250
require.NotNil(t, config.RootCAs)
185251
})
186252

253+
t.Run("Valid CA as PEM", func(t *testing.T) {
254+
caRaw, err := os.ReadFile(caFile.Name())
255+
require.NoError(t, err)
256+
257+
tlsConfig := &TLS{Enable: true, Ca: string(caRaw)}
258+
config, err := tlsConfig.MakeConfig("icinga.com")
259+
require.NoError(t, err)
260+
require.NotNil(t, config)
261+
require.NotNil(t, config.RootCAs)
262+
})
263+
187264
t.Run("Invalid CA path", func(t *testing.T) {
188265
tlsConfig := &TLS{Enable: true, Ca: "nonexistent.ca"}
189266
_, err := tlsConfig.MakeConfig("icinga.com")

database/config_test.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,124 @@ ca: ca.pem`,
161161
},
162162
},
163163
},
164+
{
165+
Name: "TLS with raw PEM",
166+
Data: testutils.ConfigTestData{
167+
Yaml: minimalYaml + `
168+
tls: true
169+
cert: |-
170+
-----BEGIN CERTIFICATE-----
171+
MIIBhTCCASugAwIBAgIQIRi6zePL6mKjOipn+dNuaTAKBggqhkjOPQQDAjASMRAw
172+
DgYDVQQKEwdBY21lIENvMB4XDTE3MTAyMDE5NDMwNloXDTE4MTAyMDE5NDMwNlow
173+
EjEQMA4GA1UEChMHQWNtZSBDbzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABD0d
174+
7VNhbWvZLWPuj/RtHFjvtJBEwOkhbN/BnnE8rnZR8+sbwnc/KhCk3FhnpHZnQz7B
175+
5aETbbIgmuvewdjvSBSjYzBhMA4GA1UdDwEB/wQEAwICpDATBgNVHSUEDDAKBggr
176+
BgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MCkGA1UdEQQiMCCCDmxvY2FsaG9zdDo1
177+
NDUzgg4xMjcuMC4wLjE6NTQ1MzAKBggqhkjOPQQDAgNIADBFAiEA2zpJEPQyz6/l
178+
Wf86aX6PepsntZv2GYlA5UpabfT2EZICICpJ5h/iI+i341gBmLiAFQOyTDT+/wQc
179+
6MF9+Yw1Yy0t
180+
-----END CERTIFICATE-----
181+
key: |-
182+
-----BEGIN EC PRIVATE KEY-----
183+
MHcCAQEEIIrYSSNQFaA2Hwf1duRSxKtLYX5CB04fSeQ6tF1aY/PuoAoGCCqGSM49
184+
AwEHoUQDQgAEPR3tU2Fta9ktY+6P9G0cWO+0kETA6SFs38GecTyudlHz6xvCdz8q
185+
EKTcWGekdmdDPsHloRNtsiCa697B2O9IFA==
186+
-----END EC PRIVATE KEY-----
187+
ca: |-
188+
-----BEGIN CERTIFICATE-----
189+
MIICSTCCAfOgAwIBAgIUcmQfIJAvbxdVm0PFanS4FWH71Z0wDQYJKoZIhvcNAQEL
190+
BQAweTELMAkGA1UEBhMCREUxEjAQBgNVBAgMCUZyYW5jb25pYTESMBAGA1UEBwwJ
191+
TnVyZW1iZXJnMUIwQAYDVQQKDDlIb25lc3QgTWFya3VzJyBVc2VkIE51Y2xlYXIg
192+
UG93ZXIgUGxhbnRzIGFuZCBDZXJ0aWZpY2F0ZXMwHhcNMjUwMzA1MDk0ODIwWhcN
193+
MjUwMzA2MDk0ODIwWjB5MQswCQYDVQQGEwJERTESMBAGA1UECAwJRnJhbmNvbmlh
194+
MRIwEAYDVQQHDAlOdXJlbWJlcmcxQjBABgNVBAoMOUhvbmVzdCBNYXJrdXMnIFVz
195+
ZWQgTnVjbGVhciBQb3dlciBQbGFudHMgYW5kIENlcnRpZmljYXRlczBcMA0GCSqG
196+
SIb3DQEBAQUAA0sAMEgCQQCeEGX2IolvELSUjC1DqvJRbTs4DKwE8ZZHDAGrc5K9
197+
DFrLKvkwgfv3g9R2NJE5o/A5vBLq22IDCFdI26M6t10HAgMBAAGjUzBRMB0GA1Ud
198+
DgQWBBQn+dCzVtAzYOGC8tIi9JLmRbWI7jAfBgNVHSMEGDAWgBQn+dCzVtAzYOGC
199+
8tIi9JLmRbWI7jAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA0EAlA27
200+
ti1NKC+o+iZtyU8I/32aPaFme1+eQNIxvqXfw49jSM/FyDjhfZ0XlAxmK6tzF3mM
201+
LJZsYbxapLeyWoA05Q==
202+
-----END CERTIFICATE-----
203+
`,
204+
Env: withMinimalEnv(map[string]string{
205+
"TLS": "1",
206+
"CERT": `-----BEGIN CERTIFICATE-----
207+
MIIBhTCCASugAwIBAgIQIRi6zePL6mKjOipn+dNuaTAKBggqhkjOPQQDAjASMRAw
208+
DgYDVQQKEwdBY21lIENvMB4XDTE3MTAyMDE5NDMwNloXDTE4MTAyMDE5NDMwNlow
209+
EjEQMA4GA1UEChMHQWNtZSBDbzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABD0d
210+
7VNhbWvZLWPuj/RtHFjvtJBEwOkhbN/BnnE8rnZR8+sbwnc/KhCk3FhnpHZnQz7B
211+
5aETbbIgmuvewdjvSBSjYzBhMA4GA1UdDwEB/wQEAwICpDATBgNVHSUEDDAKBggr
212+
BgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MCkGA1UdEQQiMCCCDmxvY2FsaG9zdDo1
213+
NDUzgg4xMjcuMC4wLjE6NTQ1MzAKBggqhkjOPQQDAgNIADBFAiEA2zpJEPQyz6/l
214+
Wf86aX6PepsntZv2GYlA5UpabfT2EZICICpJ5h/iI+i341gBmLiAFQOyTDT+/wQc
215+
6MF9+Yw1Yy0t
216+
-----END CERTIFICATE-----`,
217+
"KEY": `-----BEGIN EC PRIVATE KEY-----
218+
MHcCAQEEIIrYSSNQFaA2Hwf1duRSxKtLYX5CB04fSeQ6tF1aY/PuoAoGCCqGSM49
219+
AwEHoUQDQgAEPR3tU2Fta9ktY+6P9G0cWO+0kETA6SFs38GecTyudlHz6xvCdz8q
220+
EKTcWGekdmdDPsHloRNtsiCa697B2O9IFA==
221+
-----END EC PRIVATE KEY-----`,
222+
"CA": `-----BEGIN CERTIFICATE-----
223+
MIICSTCCAfOgAwIBAgIUcmQfIJAvbxdVm0PFanS4FWH71Z0wDQYJKoZIhvcNAQEL
224+
BQAweTELMAkGA1UEBhMCREUxEjAQBgNVBAgMCUZyYW5jb25pYTESMBAGA1UEBwwJ
225+
TnVyZW1iZXJnMUIwQAYDVQQKDDlIb25lc3QgTWFya3VzJyBVc2VkIE51Y2xlYXIg
226+
UG93ZXIgUGxhbnRzIGFuZCBDZXJ0aWZpY2F0ZXMwHhcNMjUwMzA1MDk0ODIwWhcN
227+
MjUwMzA2MDk0ODIwWjB5MQswCQYDVQQGEwJERTESMBAGA1UECAwJRnJhbmNvbmlh
228+
MRIwEAYDVQQHDAlOdXJlbWJlcmcxQjBABgNVBAoMOUhvbmVzdCBNYXJrdXMnIFVz
229+
ZWQgTnVjbGVhciBQb3dlciBQbGFudHMgYW5kIENlcnRpZmljYXRlczBcMA0GCSqG
230+
SIb3DQEBAQUAA0sAMEgCQQCeEGX2IolvELSUjC1DqvJRbTs4DKwE8ZZHDAGrc5K9
231+
DFrLKvkwgfv3g9R2NJE5o/A5vBLq22IDCFdI26M6t10HAgMBAAGjUzBRMB0GA1Ud
232+
DgQWBBQn+dCzVtAzYOGC8tIi9JLmRbWI7jAfBgNVHSMEGDAWgBQn+dCzVtAzYOGC
233+
8tIi9JLmRbWI7jAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA0EAlA27
234+
ti1NKC+o+iZtyU8I/32aPaFme1+eQNIxvqXfw49jSM/FyDjhfZ0XlAxmK6tzF3mM
235+
LJZsYbxapLeyWoA05Q==
236+
-----END CERTIFICATE-----`,
237+
}),
238+
},
239+
Expected: Config{
240+
Type: "pgsql",
241+
Host: "localhost",
242+
User: "icinga",
243+
Database: "icingadb",
244+
Password: "secret",
245+
Options: defaultOptions,
246+
TlsOptions: config.TLS{
247+
Enable: true,
248+
Cert: `-----BEGIN CERTIFICATE-----
249+
MIIBhTCCASugAwIBAgIQIRi6zePL6mKjOipn+dNuaTAKBggqhkjOPQQDAjASMRAw
250+
DgYDVQQKEwdBY21lIENvMB4XDTE3MTAyMDE5NDMwNloXDTE4MTAyMDE5NDMwNlow
251+
EjEQMA4GA1UEChMHQWNtZSBDbzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABD0d
252+
7VNhbWvZLWPuj/RtHFjvtJBEwOkhbN/BnnE8rnZR8+sbwnc/KhCk3FhnpHZnQz7B
253+
5aETbbIgmuvewdjvSBSjYzBhMA4GA1UdDwEB/wQEAwICpDATBgNVHSUEDDAKBggr
254+
BgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MCkGA1UdEQQiMCCCDmxvY2FsaG9zdDo1
255+
NDUzgg4xMjcuMC4wLjE6NTQ1MzAKBggqhkjOPQQDAgNIADBFAiEA2zpJEPQyz6/l
256+
Wf86aX6PepsntZv2GYlA5UpabfT2EZICICpJ5h/iI+i341gBmLiAFQOyTDT+/wQc
257+
6MF9+Yw1Yy0t
258+
-----END CERTIFICATE-----`,
259+
Key: `-----BEGIN EC PRIVATE KEY-----
260+
MHcCAQEEIIrYSSNQFaA2Hwf1duRSxKtLYX5CB04fSeQ6tF1aY/PuoAoGCCqGSM49
261+
AwEHoUQDQgAEPR3tU2Fta9ktY+6P9G0cWO+0kETA6SFs38GecTyudlHz6xvCdz8q
262+
EKTcWGekdmdDPsHloRNtsiCa697B2O9IFA==
263+
-----END EC PRIVATE KEY-----`,
264+
Ca: `-----BEGIN CERTIFICATE-----
265+
MIICSTCCAfOgAwIBAgIUcmQfIJAvbxdVm0PFanS4FWH71Z0wDQYJKoZIhvcNAQEL
266+
BQAweTELMAkGA1UEBhMCREUxEjAQBgNVBAgMCUZyYW5jb25pYTESMBAGA1UEBwwJ
267+
TnVyZW1iZXJnMUIwQAYDVQQKDDlIb25lc3QgTWFya3VzJyBVc2VkIE51Y2xlYXIg
268+
UG93ZXIgUGxhbnRzIGFuZCBDZXJ0aWZpY2F0ZXMwHhcNMjUwMzA1MDk0ODIwWhcN
269+
MjUwMzA2MDk0ODIwWjB5MQswCQYDVQQGEwJERTESMBAGA1UECAwJRnJhbmNvbmlh
270+
MRIwEAYDVQQHDAlOdXJlbWJlcmcxQjBABgNVBAoMOUhvbmVzdCBNYXJrdXMnIFVz
271+
ZWQgTnVjbGVhciBQb3dlciBQbGFudHMgYW5kIENlcnRpZmljYXRlczBcMA0GCSqG
272+
SIb3DQEBAQUAA0sAMEgCQQCeEGX2IolvELSUjC1DqvJRbTs4DKwE8ZZHDAGrc5K9
273+
DFrLKvkwgfv3g9R2NJE5o/A5vBLq22IDCFdI26M6t10HAgMBAAGjUzBRMB0GA1Ud
274+
DgQWBBQn+dCzVtAzYOGC8tIi9JLmRbWI7jAfBgNVHSMEGDAWgBQn+dCzVtAzYOGC
275+
8tIi9JLmRbWI7jAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA0EAlA27
276+
ti1NKC+o+iZtyU8I/32aPaFme1+eQNIxvqXfw49jSM/FyDjhfZ0XlAxmK6tzF3mM
277+
LJZsYbxapLeyWoA05Q==
278+
-----END CERTIFICATE-----`,
279+
},
280+
},
281+
},
164282
{
165283
Name: "max_connections cannot be 0",
166284
Data: testutils.ConfigTestData{

0 commit comments

Comments
 (0)