Skip to content

Commit f68c807

Browse files
authored
ROX-33250: Add support for self-singed CA (#121)
1 parent 2eb46f7 commit f68c807

10 files changed

Lines changed: 666 additions & 4 deletions

File tree

charts/stackrox-mcp/templates/_helpers.tpl

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,21 @@ TLS Secret name - returns existingSecretName if set, otherwise generates name
8080
{{- include "stackrox-mcp.fullname" . }}-tls
8181
{{- end }}
8282
{{- end }}
83+
84+
{{/*
85+
Central CA Secret name - returns existingSecretName if set, otherwise generates name
86+
*/}}
87+
{{- define "stackrox-mcp.centralCASecretName" -}}
88+
{{- if .Values.centralCACert.existingSecretName }}
89+
{{- .Values.centralCACert.existingSecretName }}
90+
{{- else }}
91+
{{- include "stackrox-mcp.fullname" . }}-central-ca
92+
{{- end }}
93+
{{- end }}
94+
95+
{{/*
96+
Central CA enabled - returns "true" if either cert or existingSecretName is set
97+
*/}}
98+
{{- define "stackrox-mcp.centralCAEnabled" -}}
99+
{{- if or .Values.centralCACert.cert .Values.centralCACert.existingSecretName }}true{{- end }}
100+
{{- end }}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{{- if and .Values.centralCACert.cert .Values.centralCACert.existingSecretName }}
2+
{{- fail "centralCACert: cannot set both 'cert' and 'existingSecretName' — use one or the other" }}
3+
{{- end }}
4+
{{- if and .Values.centralCACert.cert (not .Values.centralCACert.existingSecretName) }}
5+
apiVersion: v1
6+
kind: Secret
7+
metadata:
8+
name: {{ include "stackrox-mcp.fullname" . }}-central-ca
9+
labels:
10+
{{- include "stackrox-mcp.labels" . | nindent 4 }}
11+
type: Opaque
12+
data:
13+
ca.crt: {{ .Values.centralCACert.cert | b64enc }}
14+
{{- end }}

charts/stackrox-mcp/templates/configmap.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ data:
1313
auth_type: "passthrough"
1414
insecure_skip_tls_verify: {{ .Values.config.central.insecureSkipTLSVerify }}
1515
force_http1: {{ .Values.config.central.forceHTTP1 }}
16+
{{- if include "stackrox-mcp.centralCAEnabled" . }}
17+
ca_cert_path: "/central-ca/ca.crt"
18+
{{- end }}
1619
request_timeout: {{ .Values.config.central.requestTimeout | quote }}
1720
max_retries: {{ .Values.config.central.maxRetries }}
1821
initial_backoff: {{ .Values.config.central.initialBackoff | quote }}

charts/stackrox-mcp/templates/deployment.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ spec:
111111
mountPath: /certs
112112
readOnly: true
113113
{{- end }}
114+
{{- if include "stackrox-mcp.centralCAEnabled" . }}
115+
- name: central-ca
116+
mountPath: /central-ca
117+
readOnly: true
118+
{{- end }}
114119
volumes:
115120
- name: config
116121
configMap:
@@ -121,6 +126,12 @@ spec:
121126
secretName: {{ include "stackrox-mcp.tlsSecretName" . }}
122127
defaultMode: 0440
123128
{{- end }}
129+
{{- if include "stackrox-mcp.centralCAEnabled" . }}
130+
- name: central-ca
131+
secret:
132+
secretName: {{ include "stackrox-mcp.centralCASecretName" . }}
133+
defaultMode: 0440
134+
{{- end }}
124135
{{- with .Values.nodeSelector }}
125136
nodeSelector:
126137
{{- toYaml . | nindent 8 }}

charts/stackrox-mcp/values.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ tlsSecret:
6969
# Server TLS Private Key (PEM format)
7070
key: ""
7171

72+
# CA certificate for verifying Central's TLS certificate (e.g., self-signed)
73+
# Only one of cert or existingSecretName should be set.
74+
centralCACert:
75+
existingSecretName: ""
76+
cert: ""
77+
7278
# Resource limits and requests
7379
resources:
7480
limits:

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ require (
1111
github.com/stretchr/testify v1.11.1
1212
golang.stackrox.io/grpc-http1 v0.5.1
1313
google.golang.org/grpc v1.81.1
14+
google.golang.org/protobuf v1.36.11
1415
)
1516

1617
require (
@@ -40,7 +41,6 @@ require (
4041
golang.org/x/text v0.35.0 // indirect
4142
google.golang.org/genproto/googleapis/api v0.0.0-20260316180232-0b37fe3546d5 // indirect
4243
google.golang.org/genproto/googleapis/rpc v0.0.0-20260316180232-0b37fe3546d5 // indirect
43-
google.golang.org/protobuf v1.36.11 // indirect
4444
gopkg.in/yaml.v3 v3.0.1 // indirect
4545
)
4646

internal/client/client.go

Lines changed: 111 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ package client
44
import (
55
"context"
66
"crypto/tls"
7+
"crypto/x509"
8+
"encoding/pem"
79
"fmt"
10+
"log/slog"
11+
"os"
812
"sync"
913
"testing"
1014
"time"
@@ -23,6 +27,7 @@ import (
2327
const (
2428
minConnectTimeout = 5 * time.Second
2529
backoffJitter = 0.2
30+
maxCACertFileSize = 1 << 20 // 1MB
2631
)
2732

2833
// Client provides gRPC connection to StackRox Central API.
@@ -65,7 +70,9 @@ func (c *Client) Connect(ctx context.Context) error {
6570

6671
tlsConfig, err := c.tlsConfig()
6772
if err != nil {
68-
return err
73+
slog.Error("TLS configuration failed", "error", err)
74+
75+
return errors.New("invalid TLS configuration: verify CA certificate configuration, check server logs for details")
6976
}
7077

7178
var conn *grpc.ClientConn
@@ -229,11 +236,112 @@ func (c *Client) tlsConfig() (*tls.Config, error) {
229236
return nil, errors.Wrap(err, "failed to get central URL hostname")
230237
}
231238

232-
return &tls.Config{
239+
tlsCfg := &tls.Config{
233240
InsecureSkipVerify: c.config.InsecureSkipTLSVerify, //nolint:gosec
234241
MinVersion: tls.VersionTLS12,
235242
ServerName: hostname,
236-
}, nil
243+
}
244+
245+
// There is no reason to load certificates if we allow InsecureSkipTLSVerify.
246+
if !c.config.InsecureSkipTLSVerify && c.config.CACertPath != "" {
247+
certPool, err := loadCACertPool(c.config.CACertPath)
248+
if err != nil {
249+
return nil, err
250+
}
251+
252+
tlsCfg.RootCAs = certPool
253+
}
254+
255+
return tlsCfg, nil
256+
}
257+
258+
func loadCACertPool(caCertPath string) (*x509.CertPool, error) {
259+
// File size guard
260+
fileInfo, err := os.Stat(caCertPath)
261+
if err != nil {
262+
return nil, errors.Wrapf(err, "failed to access CA certificate at %s", caCertPath)
263+
}
264+
265+
if !fileInfo.Mode().IsRegular() {
266+
return nil, errors.Errorf("CA certificate path %s is not a regular file", caCertPath)
267+
}
268+
269+
if fileInfo.Size() == 0 {
270+
return nil, errors.Errorf("CA certificate file %s is empty", caCertPath)
271+
}
272+
273+
if fileInfo.Size() > maxCACertFileSize {
274+
return nil, errors.Errorf(
275+
"CA certificate file %s is too large (%d bytes, max %d)",
276+
caCertPath, fileInfo.Size(),
277+
maxCACertFileSize,
278+
)
279+
}
280+
281+
caCert, err := os.ReadFile(caCertPath) //nolint:gosec
282+
if err != nil {
283+
return nil, errors.Wrapf(err, "failed to read CA certificate from %s", caCertPath)
284+
}
285+
286+
// Get system cert pool, warn on fallback
287+
certPool, err := x509.SystemCertPool()
288+
if err != nil {
289+
slog.Warn("Failed to load system CA pool, using custom CA only", "error", err)
290+
291+
certPool = x509.NewCertPool()
292+
}
293+
294+
if !certPool.AppendCertsFromPEM(caCert) {
295+
return nil, errors.Errorf("failed to parse CA certificate from %s: no valid PEM data found", caCertPath)
296+
}
297+
298+
showCertInfo(caCert)
299+
300+
return certPool, nil
301+
}
302+
303+
// showCertInfo parses and logs certificate metadata.
304+
func showCertInfo(caCert []byte) {
305+
block, _ := pem.Decode(caCert)
306+
if block == nil {
307+
slog.Warn("Unable to decode CA certificate")
308+
309+
return
310+
}
311+
312+
cert, err := x509.ParseCertificate(block.Bytes)
313+
if err != nil {
314+
slog.Warn("Failed to parse CA certificate", "error", err)
315+
316+
return
317+
}
318+
319+
slog.Info("Loaded CA certificate",
320+
"subject", cert.Subject.CommonName,
321+
"issuer", cert.Issuer.CommonName,
322+
"notAfter", cert.NotAfter,
323+
"isCA", cert.IsCA,
324+
)
325+
326+
if !cert.IsCA {
327+
slog.Warn("Provided certificate does not have the CA basic constraint set — TLS verification may fail",
328+
"subject", cert.Subject.CommonName,
329+
)
330+
}
331+
332+
if time.Now().After(cert.NotAfter) {
333+
slog.Warn("CA certificate is expired — TLS verification will fail",
334+
"subject", cert.Subject.CommonName,
335+
"expiredAt", cert.NotAfter,
336+
)
337+
}
338+
339+
if time.Now().Before(cert.NotBefore) {
340+
slog.Warn("CA certificate is not yet valid",
341+
"subject", cert.Subject.CommonName,
342+
"validFrom", cert.NotBefore,
343+
)
344+
}
237345
}
238346

239347
func (c *Client) connectHTTP1(

0 commit comments

Comments
 (0)