Skip to content

Commit b9a288c

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 b9a288c

File tree

3 files changed

+224
-15
lines changed

3 files changed

+224
-15
lines changed

config/tls.go

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ package config
33
import (
44
"crypto/tls"
55
"crypto/x509"
6+
"fmt"
67
"github.com/pkg/errors"
78
"os"
9+
"strings"
810
)
911

1012
// TLS represents configuration for a TLS client.
@@ -34,13 +36,15 @@ type TLS struct {
3436
// Enable indicates whether TLS is enabled.
3537
Enable bool `yaml:"tls" env:"TLS"`
3638

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

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

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

4650
// Insecure indicates whether to skip verification of the server's certificate chain and host name.
@@ -57,6 +61,9 @@ func (t *TLS) MakeConfig(serverName string) (*tls.Config, error) {
5761
return nil, nil
5862
}
5963

64+
isRawPem := func(s string) bool { return strings.HasPrefix(strings.TrimSpace(s), "-----BEGIN") }
65+
strToPem := func(s string) []byte { return []byte(strings.TrimSpace(s)) }
66+
6067
tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12}
6168
if t.Cert == "" {
6269
if t.Key != "" {
@@ -65,24 +72,39 @@ func (t *TLS) MakeConfig(serverName string) (*tls.Config, error) {
6572
} else if t.Key == "" {
6673
return nil, errors.New("client certificate given, but private key missing")
6774
} else {
68-
crt, err := tls.LoadX509KeyPair(t.Cert, t.Key)
69-
if err != nil {
70-
return nil, errors.Wrap(err, "can't load X.509 key pair")
75+
if c, k := isRawPem(t.Cert), isRawPem(t.Key); c != k {
76+
return nil, fmt.Errorf("either both certificate and key are PEM or none; is PEM certificate=%t, key=%t", c, k)
77+
} else if c {
78+
crt, err := tls.X509KeyPair(strToPem(t.Cert), strToPem(t.Key))
79+
if err != nil {
80+
return nil, errors.Wrap(err, "can't load X.509 key pair from raw PEM")
81+
}
82+
tlsConfig.Certificates = []tls.Certificate{crt}
83+
} else {
84+
crt, err := tls.LoadX509KeyPair(t.Cert, t.Key)
85+
if err != nil {
86+
return nil, errors.Wrap(err, "can't load X.509 key pair from files")
87+
}
88+
tlsConfig.Certificates = []tls.Certificate{crt}
7189
}
72-
73-
tlsConfig.Certificates = []tls.Certificate{crt}
7490
}
7591

7692
if t.Insecure {
7793
tlsConfig.InsecureSkipVerify = true
7894
} else if t.Ca != "" {
79-
raw, err := os.ReadFile(t.Ca)
80-
if err != nil {
81-
return nil, errors.Wrap(err, "can't read CA file")
95+
var ca []byte
96+
if isRawPem(t.Ca) {
97+
ca = strToPem(t.Ca)
98+
} else {
99+
raw, err := os.ReadFile(t.Ca)
100+
if err != nil {
101+
return nil, errors.Wrap(err, "can't read CA file")
102+
}
103+
ca = raw
82104
}
83105

84106
tlsConfig.RootCAs = x509.NewCertPool()
85-
if !tlsConfig.RootCAs.AppendCertsFromPEM(raw) {
107+
if !tlsConfig.RootCAs.AppendCertsFromPEM(ca) {
86108
return nil, errors.New("can't parse CA file")
87109
}
88110
}

config/tls_test.go

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,47 @@ func TestTLS_MakeConfig(t *testing.T) {
4848
t.Run("Missing client certificate", func(t *testing.T) {
4949
tlsConfig := &TLS{Enable: true, Key: "test.key"}
5050
_, err := tlsConfig.MakeConfig("icinga.com")
51-
require.Error(t, err)
51+
require.ErrorContains(t, err, "client certificate missing")
5252
})
5353

5454
t.Run("Missing private key", func(t *testing.T) {
5555
tlsConfig := &TLS{Enable: true, Cert: "test.crt"}
5656
_, err := tlsConfig.MakeConfig("icinga.com")
57-
require.Error(t, err)
57+
require.ErrorContains(t, err, "private key missing")
58+
})
59+
60+
t.Run("Cert is file, Key is PEM", func(t *testing.T) {
61+
tlsConfig := &TLS{
62+
Enable: true,
63+
Cert: "test.crt",
64+
Key: `-----BEGIN EC PRIVATE KEY-----
65+
MHcCAQEEIIrYSSNQFaA2Hwf1duRSxKtLYX5CB04fSeQ6tF1aY/PuoAoGCCqGSM49
66+
AwEHoUQDQgAEPR3tU2Fta9ktY+6P9G0cWO+0kETA6SFs38GecTyudlHz6xvCdz8q
67+
EKTcWGekdmdDPsHloRNtsiCa697B2O9IFA==
68+
-----END EC PRIVATE KEY----`,
69+
}
70+
_, err := tlsConfig.MakeConfig("icinga.com")
71+
require.ErrorContains(t, err, "either both certificate and key are PEM or none; is PEM certificate=false, key=true")
72+
})
73+
74+
t.Run("Cert is PEM, Key is file", func(t *testing.T) {
75+
tlsConfig := &TLS{
76+
Enable: true,
77+
Cert: `-----BEGIN CERTIFICATE-----
78+
MIIBhTCCASugAwIBAgIQIRi6zePL6mKjOipn+dNuaTAKBggqhkjOPQQDAjASMRAw
79+
DgYDVQQKEwdBY21lIENvMB4XDTE3MTAyMDE5NDMwNloXDTE4MTAyMDE5NDMwNlow
80+
EjEQMA4GA1UEChMHQWNtZSBDbzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABD0d
81+
7VNhbWvZLWPuj/RtHFjvtJBEwOkhbN/BnnE8rnZR8+sbwnc/KhCk3FhnpHZnQz7B
82+
5aETbbIgmuvewdjvSBSjYzBhMA4GA1UdDwEB/wQEAwICpDATBgNVHSUEDDAKBggr
83+
BgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MCkGA1UdEQQiMCCCDmxvY2FsaG9zdDo1
84+
NDUzgg4xMjcuMC4wLjE6NTQ1MzAKBggqhkjOPQQDAgNIADBFAiEA2zpJEPQyz6/l
85+
Wf86aX6PepsntZv2GYlA5UpabfT2EZICICpJ5h/iI+i341gBmLiAFQOyTDT+/wQc
86+
6MF9+Yw1Yy0t
87+
-----END CERTIFICATE-----`,
88+
Key: "test.key",
89+
}
90+
_, err := tlsConfig.MakeConfig("icinga.com")
91+
require.ErrorContains(t, err, "either both certificate and key are PEM or none; is PEM certificate=true, key=false")
5892
})
5993

6094
t.Run("x509", func(t *testing.T) {
@@ -93,7 +127,7 @@ func TestTLS_MakeConfig(t *testing.T) {
93127
defer func(name string) {
94128
_ = os.Remove(name)
95129
}(corruptFile.Name())
96-
err = os.WriteFile(corruptFile.Name(), []byte("corrupt PEM"), 0600)
130+
err = os.WriteFile(corruptFile.Name(), []byte("-----BEGIN CORRUPT-----\nOOPS\n-----END CORRUPT-----"), 0600)
97131
require.NoError(t, err)
98132

99133
t.Run("Valid certificate and key", func(t *testing.T) {
@@ -104,6 +138,19 @@ func TestTLS_MakeConfig(t *testing.T) {
104138
require.Len(t, config.Certificates, 1)
105139
})
106140

141+
t.Run("Valid certificate and key as PEM", func(t *testing.T) {
142+
certRaw, err := os.ReadFile(certFile.Name())
143+
require.NoError(t, err)
144+
keyRaw, err := os.ReadFile(keyFile.Name())
145+
require.NoError(t, err)
146+
147+
tlsConfig := &TLS{Enable: true, Cert: string(certRaw), Key: string(keyRaw)}
148+
config, err := tlsConfig.MakeConfig("icinga.com")
149+
require.NoError(t, err)
150+
require.NotNil(t, config)
151+
require.Len(t, config.Certificates, 1)
152+
})
153+
107154
t.Run("Mismatched certificate and key", func(t *testing.T) {
108155
_key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
109156
require.NoError(t, err)
@@ -149,6 +196,17 @@ func TestTLS_MakeConfig(t *testing.T) {
149196
require.Error(t, err)
150197
})
151198

199+
t.Run("Corrupt certificate as PEM", func(t *testing.T) {
200+
corruptRaw, err := os.ReadFile(corruptFile.Name())
201+
require.NoError(t, err)
202+
keyRaw, err := os.ReadFile(keyFile.Name())
203+
require.NoError(t, err)
204+
205+
tlsConfig := &TLS{Enable: true, Cert: string(corruptRaw), Key: string(keyRaw)}
206+
_, err = tlsConfig.MakeConfig("icinga.com")
207+
require.Error(t, err)
208+
})
209+
152210
t.Run("Invalid key path", func(t *testing.T) {
153211
tlsConfig := &TLS{Enable: true, Cert: certFile.Name(), Key: "nonexistent.key"}
154212
_, err := tlsConfig.MakeConfig("icinga.com")
@@ -184,6 +242,17 @@ func TestTLS_MakeConfig(t *testing.T) {
184242
require.NotNil(t, config.RootCAs)
185243
})
186244

245+
t.Run("Valid CA as PEM", func(t *testing.T) {
246+
caRaw, err := os.ReadFile(caFile.Name())
247+
require.NoError(t, err)
248+
249+
tlsConfig := &TLS{Enable: true, Ca: string(caRaw)}
250+
config, err := tlsConfig.MakeConfig("icinga.com")
251+
require.NoError(t, err)
252+
require.NotNil(t, config)
253+
require.NotNil(t, config.RootCAs)
254+
})
255+
187256
t.Run("Invalid CA path", func(t *testing.T) {
188257
tlsConfig := &TLS{Enable: true, Ca: "nonexistent.ca"}
189258
_, 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)