Skip to content

Commit 96eeffd

Browse files
authored
Add optional TLS certificate checking when talking to Proxmox (#336)
* Add optional TLS certificate checking when talking to Proxmox * Fix: Define behavior for containers without RootCA store * Fix: Allow all true-ish YAML values
1 parent d1dc4f7 commit 96eeffd

File tree

4 files changed

+144
-4
lines changed

4 files changed

+144
-4
lines changed

cmd/main.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import (
4848

4949
infrastructurev1alpha1 "github.com/ionos-cloud/cluster-api-provider-proxmox/api/v1alpha1"
5050
"github.com/ionos-cloud/cluster-api-provider-proxmox/internal/controller"
51+
"github.com/ionos-cloud/cluster-api-provider-proxmox/internal/tlshelper"
5152
"github.com/ionos-cloud/cluster-api-provider-proxmox/internal/webhook"
5253
capmox "github.com/ionos-cloud/cluster-api-provider-proxmox/pkg/proxmox"
5354
"github.com/ionos-cloud/cluster-api-provider-proxmox/pkg/proxmox/goproxmox"
@@ -69,6 +70,9 @@ var (
6970
ProxmoxTokenID string
7071
// ProxmoxSecret env variable that defines the Proxmox secret for the given token id.
7172
ProxmoxSecret string
73+
74+
proxmoxInsecure bool
75+
proxmoxRootCertFile string
7276
)
7377

7478
func init() {
@@ -190,9 +194,17 @@ func setupProxmoxClient(ctx context.Context, logger logr.Logger) (capmox.Client,
190194
if ProxmoxURL == "" || ProxmoxTokenID == "" || ProxmoxSecret == "" {
191195
return nil, nil
192196
}
193-
// TODO, check if we need to delete tls config
197+
198+
rootCerts, err := tlshelper.SystemRootsWithFile(proxmoxRootCertFile)
199+
if err != nil {
200+
return nil, fmt.Errorf("loading cert pool: %w", err)
201+
}
202+
194203
tr := &http.Transport{
195-
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec
204+
TLSClientConfig: &tls.Config{
205+
InsecureSkipVerify: proxmoxInsecure, //#nosec:G402 // Default retained, user can enable cert checking
206+
RootCAs: rootCerts,
207+
},
196208
}
197209

198210
httpClient := &http.Client{Transport: tr}
@@ -209,6 +221,12 @@ func initFlagsAndEnv(fs *pflag.FlagSet) {
209221
ProxmoxTokenID = env.GetString("PROXMOX_TOKEN", "")
210222
ProxmoxSecret = env.GetString("PROXMOX_SECRET", "")
211223

224+
fs.BoolVar(&proxmoxInsecure, "proxmox-insecure",
225+
env.GetString("PROXMOX_INSECURE", "true") == "true",
226+
"Skip TLS verification when connecting to Proxmox")
227+
fs.StringVar(&proxmoxRootCertFile, "proxmox-root-cert-file", "",
228+
"Root-Certificate to use to verify server TLS certificate")
229+
212230
fs.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.")
213231
fs.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
214232
fs.BoolVar(&enableLeaderElection, "leader-elect", false,

internal/tlshelper/roots.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Package tlshelper wraps loading and modifying the root-CA store for
2+
// use in tls.Config
3+
package tlshelper
4+
5+
import (
6+
"crypto/x509"
7+
"fmt"
8+
"os"
9+
)
10+
11+
// SystemRootsWithFile reads the pemBlock for SystemRootsWithCert from
12+
// the given file and then calls SystemRootsWithCert.
13+
func SystemRootsWithFile(filepath string) (*x509.CertPool, error) {
14+
if len(filepath) == 0 {
15+
// No filename given, we fall back to default behavior: returning
16+
// nil and letting Go itself take care of everything
17+
return nil, nil
18+
}
19+
20+
pemBlock, err := os.ReadFile(filepath) //#nosec:G304 // Intended to read the given file
21+
if err != nil {
22+
return nil, fmt.Errorf("loading certificate file: %w", err)
23+
}
24+
25+
return SystemRootsWithCert(pemBlock)
26+
}
27+
28+
// SystemRootsWithCert tries to load the system root store and to
29+
// append the given certificate from a PEM encoded block into it. In
30+
// case there is no pool an empty pool is used to add the certificate.
31+
func SystemRootsWithCert(pemBlock []byte) (*x509.CertPool, error) {
32+
if len(pemBlock) == 0 {
33+
// Zero length / nil, we fall back to default behavior: returning
34+
// nil and letting Go itself take care of everything
35+
return nil, nil
36+
}
37+
38+
rootCerts, err := x509.SystemCertPool()
39+
if err != nil && !os.IsNotExist(err) {
40+
return nil, fmt.Errorf("loading system cert pool: %w", err)
41+
}
42+
43+
if os.IsNotExist(err) {
44+
// The system pool is not available but that's fine as we are
45+
// supposed to add one own cert
46+
rootCerts = x509.NewCertPool()
47+
}
48+
49+
if !rootCerts.AppendCertsFromPEM(pemBlock) {
50+
return nil, fmt.Errorf("certificate was not appended")
51+
}
52+
53+
return rootCerts, nil
54+
}

internal/tlshelper/roots_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package tlshelper
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
// This block contains a 100Y valid self-signed ECDSA CA with key no
11+
// longer existent.
12+
const exampleRootCA = `-----BEGIN CERTIFICATE-----
13+
MIIB6DCCAY+gAwIBAgIUIgQxz/rn8WLH3zyHICHh8Us552AwCgYIKoZIzj0EAwIw
14+
STELMAkGA1UEBhMCREUxEDAOBgNVBAgMB0hhbWJ1cmcxEzARBgNVBAoMCkV4YW1w
15+
bGUgQ0ExEzARBgNVBAMMCkV4YW1wbGUgQ0EwIBcNMjQxMTI4MTExMzM3WhgPMjEy
16+
NDExMDQxMTEzMzdaMEkxCzAJBgNVBAYTAkRFMRAwDgYDVQQIDAdIYW1idXJnMRMw
17+
EQYDVQQKDApFeGFtcGxlIENBMRMwEQYDVQQDDApFeGFtcGxlIENBMFkwEwYHKoZI
18+
zj0CAQYIKoZIzj0DAQcDQgAEvQIYmiX4z2sv8sVqyC/eQ7e2JH1WQgIRuSWYVEOL
19+
YKEQDHwMg1KKu3+McgHXLiZ0af0JDyd00em/g7k39RzNhqNTMFEwHQYDVR0OBBYE
20+
FPDaKA2+m5nRhWESuw+61HdvmZiGMB8GA1UdIwQYMBaAFPDaKA2+m5nRhWESuw+6
21+
1HdvmZiGMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDRwAwRAIgQtj0ZhXK
22+
RYrWwHMzIHXqggsU3NhnOUa/yeeQOExDVbMCIAFDdaz3v5jOmWm7LR5BwQwLRWhO
23+
37ky2VQr/1GhU42q
24+
-----END CERTIFICATE-----`
25+
26+
func TestRootPoolLoading(t *testing.T) {
27+
// Empty file
28+
roots, err := SystemRootsWithFile("")
29+
require.NoError(t, err)
30+
assert.Nil(t, roots)
31+
32+
// Empty pemBlock
33+
roots, err = SystemRootsWithCert(nil)
34+
require.NoError(t, err)
35+
assert.Nil(t, roots)
36+
37+
// CA from system plus PEM
38+
roots, err = SystemRootsWithCert([]byte(exampleRootCA))
39+
require.NoError(t, err)
40+
assert.NotNil(t, roots)
41+
42+
// Error from broken file
43+
_, err = SystemRootsWithFile("/tmp/this/file/will/never/exist.notexist")
44+
require.Error(t, err)
45+
46+
// Error from broken PEM
47+
_, err = SystemRootsWithCert([]byte("I'm certainly not a PEM block"))
48+
require.Error(t, err)
49+
}

pkg/scope/cluster.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ package scope
2020
import (
2121
"context"
2222
"crypto/tls"
23+
"fmt"
2324
"net/http"
25+
"slices"
26+
"strings"
2427

2528
"github.com/go-logr/logr"
2629
"github.com/luthermonson/go-proxmox"
@@ -36,6 +39,7 @@ import (
3639
"sigs.k8s.io/controller-runtime/pkg/log"
3740

3841
infrav1alpha1 "github.com/ionos-cloud/cluster-api-provider-proxmox/api/v1alpha1"
42+
"github.com/ionos-cloud/cluster-api-provider-proxmox/internal/tlshelper"
3943
"github.com/ionos-cloud/cluster-api-provider-proxmox/pkg/kubernetes/ipam"
4044
capmox "github.com/ionos-cloud/cluster-api-provider-proxmox/pkg/proxmox"
4145
"github.com/ionos-cloud/cluster-api-provider-proxmox/pkg/proxmox/goproxmox"
@@ -151,9 +155,24 @@ func (s *ClusterScope) setupProxmoxClient(ctx context.Context) (capmox.Client, e
151155
tokenSecret := string(secret.Data["secret"])
152156
url := string(secret.Data["url"])
153157

154-
// TODO, check if we need to delete tls config
158+
tlsInsecure, tlsInsecureSet := secret.Data["insecure"]
159+
tlsRootCA := secret.Data["root_ca"]
160+
161+
rootCerts, err := tlshelper.SystemRootsWithCert(tlsRootCA)
162+
if err != nil {
163+
return nil, fmt.Errorf("loading cert pool: %w", err)
164+
}
165+
155166
tr := &http.Transport{
156-
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec
167+
TLSClientConfig: &tls.Config{
168+
// When "insecure" is unset we retain the pre-v0.7 behavior of
169+
// setting the connection insecure. If it is set we compare
170+
// against YAML true-ish values.
171+
//
172+
//#nosec:G402 // Intended to enable insecure mode for unknown CAs
173+
InsecureSkipVerify: !tlsInsecureSet || slices.Contains([]string{"1", "on", "true", "yes", "y"}, strings.ToLower(string(tlsInsecure))),
174+
RootCAs: rootCerts,
175+
},
157176
}
158177

159178
httpClient := &http.Client{Transport: tr}

0 commit comments

Comments
 (0)