From 25564021e89ca9bbe451bcbd9240ee7ea188b604 Mon Sep 17 00:00:00 2001 From: Shankar Date: Fri, 17 Apr 2026 01:40:58 +0000 Subject: [PATCH] security(globalsign): remove InsecureSkipVerify and pin CA pool (H-5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The GlobalSign Atlas HVCA connector previously used InsecureSkipVerify:true on its mTLS TLS config, disabling server certificate validation and defeating the purpose of the client-side mTLS handshake. This was a CWE-295 Improper Certificate Validation vulnerability silently degrading trust on every production call to GlobalSign's signing API. Remediation (per H-5 audit finding, Lens 4.4): - Remove InsecureSkipVerify from all three http.Client construction sites (ValidateConfig, getHTTPClient, and legacy initialisation path). - Introduce buildServerTLSConfig() helper that constructs tls.Config with MinVersion: tls.VersionTLS12 (addresses adjacent L-1 recommendation). - New optional config field `server_ca_path` (env: CERTCTL_GLOBALSIGN_SERVER_CA_PATH). When unset the connector trusts the system root CA bundle (correct default for GlobalSign's publicly-trusted HVCA endpoints). When set the bundle is loaded via x509.NewCertPool() + AppendCertsFromPEM, and only those roots are trusted (supports private HVCA deployments and defence-in-depth root pinning). - Error wrapping chain: "failed to read server CA bundle at %s" and "no valid PEM certificates found in server CA bundle at %s" surface config problems at ValidateConfig time instead of silently failing at request time. Docs, config, service env-seed, and GUI issuer type definition updated to expose the new field. Tests: 9 dead `InsecureSkipVerify: true` client TLSClientConfig blocks (no-ops against httptest.NewServer plain-HTTP) replaced with bare http.Client; new TestGlobalSign_ServerTLSConfig covers pinned-CA trust, untrusted-server rejection, missing-file and invalid-PEM error paths. Verification: - go build ./... clean - go vet ./... clean - go test -race ./internal/connector/issuer/globalsign/... ./internal/config/... ./internal/service/... ok - go test ./... (excluding testcontainers-gated repo layer) ok - golangci-lint run ./... 0 issues - govulncheck ./... 0 reachable vulns - Per-layer coverage: service 68.7% (≥55), handler 83.6% (≥60), domain 82.0% (≥40), middleware 63.8% (≥30) - globalsign package coverage: 75.9% - Invariant sweep: 0 InsecureSkipVerify references remain in globalsign package (only a test-file comment documenting the removal). --- docs/connectors.md | 3 + internal/config/config.go | 9 + .../connector/issuer/globalsign/globalsign.go | 61 ++++- .../issuer/globalsign/globalsign_test.go | 226 ++++++++++++++---- internal/service/issuer.go | 24 +- web/src/config/issuerTypes.ts | 1 + 6 files changed, 257 insertions(+), 67 deletions(-) diff --git a/docs/connectors.md b/docs/connectors.md index 0c88a93..fb70209 100644 --- a/docs/connectors.md +++ b/docs/connectors.md @@ -465,9 +465,12 @@ GlobalSign Atlas High Volume CA REST API with dual authentication: mTLS for the | `CERTCTL_GLOBALSIGN_API_SECRET` | Yes | — | API secret for request authentication | | `CERTCTL_GLOBALSIGN_CLIENT_CERT_PATH` | Yes | — | Path to mTLS client certificate PEM | | `CERTCTL_GLOBALSIGN_CLIENT_KEY_PATH` | Yes | — | Path to mTLS client private key PEM | +| `CERTCTL_GLOBALSIGN_SERVER_CA_PATH` | No | system trust store | PEM bundle used to verify the Atlas API server certificate. Set this for private/lab Atlas deployments whose server TLS chain is not in the host's default trust bundle. | **Authentication:** Dual — mTLS client certificate for TLS handshake plus `X-API-Key` and `X-API-Secret` headers on every request. +**TLS verification:** The connector always verifies the server certificate. When `server_ca_path` is set, the PEM bundle at that path is used as the trust anchor; otherwise the host's system trust store is used. TLS 1.2 is the minimum protocol version. + **Issuance model:** `POST /v2/certificates` returns a serial number. Certificate PEM is available after validation completes. Typically resolves within seconds for DV. `GetOrderStatus` polls the certificate endpoint. **Note:** CRL and OCSP are managed by GlobalSign. certctl records revocations locally and notifies GlobalSign via `PUT /v2/certificates/{serial}/revoke`. diff --git a/internal/config/config.go b/internal/config/config.go index 217f8dc..f9663ad 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -116,6 +116,14 @@ type GlobalSignConfig struct { // ClientKeyPath is the path to the mTLS client private key PEM file. // Setting: CERTCTL_GLOBALSIGN_CLIENT_KEY_PATH environment variable. ClientKeyPath string + + // ServerCAPath is the optional path to a PEM file containing the CA + // certificate(s) used to verify the GlobalSign Atlas HVCA API server + // certificate. If empty, the system trust store is used. Set this + // for private/lab Atlas deployments whose server TLS chain is not + // present in the host's default trust bundle. + // Setting: CERTCTL_GLOBALSIGN_SERVER_CA_PATH environment variable. + ServerCAPath string } // EJBCAConfig contains EJBCA (Keyfactor) issuer connector configuration. @@ -887,6 +895,7 @@ func Load() (*Config, error) { APISecret: getEnv("CERTCTL_GLOBALSIGN_API_SECRET", ""), ClientCertPath: getEnv("CERTCTL_GLOBALSIGN_CLIENT_CERT_PATH", ""), ClientKeyPath: getEnv("CERTCTL_GLOBALSIGN_CLIENT_KEY_PATH", ""), + ServerCAPath: getEnv("CERTCTL_GLOBALSIGN_SERVER_CA_PATH", ""), }, EJBCA: EJBCAConfig{ APIUrl: getEnv("CERTCTL_EJBCA_API_URL", ""), diff --git a/internal/connector/issuer/globalsign/globalsign.go b/internal/connector/issuer/globalsign/globalsign.go index 8da0437..ae0b913 100644 --- a/internal/connector/issuer/globalsign/globalsign.go +++ b/internal/connector/issuer/globalsign/globalsign.go @@ -34,6 +34,7 @@ import ( "io" "log/slog" "net/http" + "os" "strings" "time" @@ -64,6 +65,14 @@ type Config struct { // Must match the certificate in ClientCertPath. // Required. Set via CERTCTL_GLOBALSIGN_CLIENT_KEY_PATH environment variable. ClientKeyPath string `json:"client_key_path"` + + // ServerCAPath is the filesystem path to a PEM file containing the CA + // certificate(s) used to verify the GlobalSign Atlas HVCA API server certificate. + // Optional. If empty, the system trust store is used. This option exists for + // private/lab deployments of GlobalSign Atlas that terminate TLS with an + // internal CA not present in the host's default trust bundle. + // Set via CERTCTL_GLOBALSIGN_SERVER_CA_PATH environment variable. + ServerCAPath string `json:"server_ca_path,omitempty"` } // Connector implements the issuer.Connector interface for GlobalSign Atlas HVCA. @@ -153,14 +162,12 @@ func (c *Connector) ValidateConfig(ctx context.Context, rawConfig json.RawMessag return fmt.Errorf("failed to load GlobalSign client certificate: %w", err) } - // Create an mTLS client for validation - tlsConfig := &tls.Config{ - Certificates: []tls.Certificate{cert}, - // InsecureSkipVerify=true allows testing against self-signed server certs. - // In production, GlobalSign's API uses a proper certificate chain. - // This matches the pattern used by other connectors (F5, network scanner, etc.) - // that also need to bypass hostname verification for internal/lab environments. - InsecureSkipVerify: true, + // Build a verifying mTLS TLS config. If ServerCAPath is set, that PEM + // bundle is used as the trust anchor for the server certificate; + // otherwise the system trust store is used. TLS 1.2 is the minimum. + tlsConfig, err := buildServerTLSConfig(&cfg, cert) + if err != nil { + return fmt.Errorf("failed to build GlobalSign TLS config: %w", err) } validationClient := &http.Client{ @@ -225,9 +232,9 @@ func (c *Connector) getHTTPClient(ctx context.Context) (*http.Client, error) { return nil, fmt.Errorf("failed to load GlobalSign client certificate: %w", err) } - tlsConfig := &tls.Config{ - Certificates: []tls.Certificate{cert}, - InsecureSkipVerify: true, + tlsConfig, err := buildServerTLSConfig(c.config, cert) + if err != nil { + return nil, fmt.Errorf("failed to build GlobalSign TLS config: %w", err) } return &http.Client{ @@ -238,6 +245,38 @@ func (c *Connector) getHTTPClient(ctx context.Context) (*http.Client, error) { }, nil } +// buildServerTLSConfig returns a TLS configuration for the GlobalSign Atlas +// HVCA API client. It always verifies the server certificate. When +// cfg.ServerCAPath is set, the PEM bundle at that path is used as the +// trust anchor (enables pinning a private/lab CA); otherwise the host's +// system trust store is used. TLS 1.2 is the minimum protocol version. +// +// This helper is the single source of truth for both the ValidateConfig +// probe client and the steady-state getHTTPClient production client, so +// any future TLS policy change applies uniformly. +func buildServerTLSConfig(cfg *Config, clientCert tls.Certificate) (*tls.Config, error) { + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{clientCert}, + MinVersion: tls.VersionTLS12, + } + + if cfg.ServerCAPath != "" { + caPEM, err := os.ReadFile(cfg.ServerCAPath) + if err != nil { + return nil, fmt.Errorf("failed to read server CA bundle at %s: %w", cfg.ServerCAPath, err) + } + + pool := x509.NewCertPool() + if !pool.AppendCertsFromPEM(caPEM) { + return nil, fmt.Errorf("no valid PEM certificates found in server CA bundle at %s", cfg.ServerCAPath) + } + + tlsConfig.RootCAs = pool + } + + return tlsConfig, nil +} + // IssueCertificate submits a certificate order to GlobalSign Atlas HVCA. // Returns the serial number immediately; typically the cert is available within seconds (DV) to minutes (OV). func (c *Connector) IssueCertificate(ctx context.Context, request issuer.IssuanceRequest) (*issuer.IssuanceResult, error) { diff --git a/internal/connector/issuer/globalsign/globalsign_test.go b/internal/connector/issuer/globalsign/globalsign_test.go index b80869a..6676c82 100644 --- a/internal/connector/issuer/globalsign/globalsign_test.go +++ b/internal/connector/issuer/globalsign/globalsign_test.go @@ -4,7 +4,6 @@ import ( "context" "crypto/rand" "crypto/rsa" - "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/json" @@ -161,11 +160,7 @@ func TestGlobalSignConnector(t *testing.T) { testCertPEM, _ := generateTestCert(t) testChainPEM, _ := generateTestCert(t) - httpClient := &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - }, - } + httpClient := &http.Client{} mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/v2/certificates" && r.Method == http.MethodPost { @@ -223,11 +218,7 @@ func TestGlobalSignConnector(t *testing.T) { }) t.Run("IssueCertificate_Pending", func(t *testing.T) { - httpClient := &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - }, - } + httpClient := &http.Client{} mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/v2/certificates" && r.Method == http.MethodPost { @@ -271,11 +262,7 @@ func TestGlobalSignConnector(t *testing.T) { }) t.Run("IssueCertificate_Error", func(t *testing.T) { - httpClient := &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - }, - } + httpClient := &http.Client{} mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/v2/certificates" && r.Method == http.MethodPost { @@ -312,11 +299,7 @@ func TestGlobalSignConnector(t *testing.T) { testCertPEM, _ := generateTestCert(t) testChainPEM, _ := generateTestCert(t) - httpClient := &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - }, - } + httpClient := &http.Client{} mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/v2/certificates/12345") && r.Method == http.MethodGet { @@ -356,11 +339,7 @@ func TestGlobalSignConnector(t *testing.T) { }) t.Run("GetOrderStatus_Pending", func(t *testing.T) { - httpClient := &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - }, - } + httpClient := &http.Client{} mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/v2/certificates/98765") && r.Method == http.MethodGet { @@ -401,11 +380,7 @@ func TestGlobalSignConnector(t *testing.T) { testCertPEM, _ := generateTestCert(t) testChainPEM, _ := generateTestCert(t) - httpClient := &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - }, - } + httpClient := &http.Client{} mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/v2/certificates" && r.Method == http.MethodPost { @@ -448,11 +423,7 @@ func TestGlobalSignConnector(t *testing.T) { }) t.Run("RevokeCertificate_Success", func(t *testing.T) { - httpClient := &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - }, - } + httpClient := &http.Client{} mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/v2/certificates/") && strings.HasSuffix(r.URL.Path, "/revoke") && r.Method == http.MethodPut { @@ -492,11 +463,7 @@ func TestGlobalSignConnector(t *testing.T) { }) t.Run("RevokeCertificate_Error", func(t *testing.T) { - httpClient := &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - }, - } + httpClient := &http.Client{} mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/v2/certificates/") && strings.HasSuffix(r.URL.Path, "/revoke") && r.Method == http.MethodPut { @@ -532,11 +499,7 @@ func TestGlobalSignConnector(t *testing.T) { testChainPEM, _ := generateTestCert(t) authHeadersChecked := 0 - httpClient := &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - }, - } + httpClient := &http.Client{} mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Check for auth headers on every request @@ -584,6 +547,177 @@ func TestGlobalSignConnector(t *testing.T) { }) } +// TestGlobalSign_ServerTLSConfig exercises the server-side TLS verification +// policy added by H-5. The connector must always verify the GlobalSign Atlas +// HVCA API server certificate: by default against the host's system trust +// store, and when ServerCAPath is set, against the pinned PEM bundle at that +// path. InsecureSkipVerify is no longer reachable from any production code path. +func TestGlobalSign_ServerTLSConfig(t *testing.T) { + logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) + ctx := context.Background() + + // writeClientMTLS generates a throwaway client cert+key pair and writes them + // to disk. ValidateConfig requires valid ClientCertPath / ClientKeyPath files + // before it reaches the server-CA validation path under test. + writeClientMTLS := func(t *testing.T) (certPath, keyPath string) { + t.Helper() + certPEM, keyPEM := generateTestCert(t) + dir := t.TempDir() + certPath = dir + "/client-cert.pem" + keyPath = dir + "/client-key.pem" + if err := os.WriteFile(certPath, []byte(certPEM), 0600); err != nil { + t.Fatalf("failed to write client cert: %v", err) + } + if err := os.WriteFile(keyPath, []byte(keyPEM), 0600); err != nil { + t.Fatalf("failed to write client key: %v", err) + } + return certPath, keyPath + } + + // certToPEM re-encodes a parsed certificate as a PEM block for trust-store + // pinning. httptest.NewTLSServer.Certificate() returns the server's self- + // signed cert; pinning that cert trusts exactly that one server. + certToPEM := func(t *testing.T, cert *x509.Certificate) string { + t.Helper() + return string(pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: cert.Raw, + })) + } + + t.Run("PinnedCA_TrustsExpectedServer", func(t *testing.T) { + // Mock Atlas API served over HTTPS with a self-signed cert. We pin + // that cert's PEM as the client's trust anchor; the validation probe + // should succeed because the pinned pool contains the server's issuer. + srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/v2/certificates" && r.Method == http.MethodGet { + if r.Header.Get("ApiKey") == "gs-test-key" && r.Header.Get("ApiSecret") == "gs-test-secret" { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"certificates":[]}`)) + return + } + w.WriteHeader(http.StatusForbidden) + return + } + http.NotFound(w, r) + })) + defer srv.Close() + + caPEM := certToPEM(t, srv.Certificate()) + caPath := t.TempDir() + "/atlas-ca.pem" + if err := os.WriteFile(caPath, []byte(caPEM), 0600); err != nil { + t.Fatalf("failed to write pinned CA: %v", err) + } + + clientCert, clientKey := writeClientMTLS(t) + config := globalsign.Config{ + APIUrl: srv.URL, + APIKey: "gs-test-key", + APISecret: "gs-test-secret", + ClientCertPath: clientCert, + ClientKeyPath: clientKey, + ServerCAPath: caPath, + } + + connector := globalsign.New(&config, logger) + rawConfig, _ := json.Marshal(config) + if err := connector.ValidateConfig(ctx, rawConfig); err != nil { + t.Fatalf("ValidateConfig with pinned CA should succeed, got: %v", err) + } + }) + + t.Run("PinnedCA_RejectsUntrustedServer", func(t *testing.T) { + // Mock server presents its own self-signed cert; we pin an UNRELATED + // cert as the trust anchor. The TLS handshake must fail before any + // request is sent — this is exactly what H-5 remediates. + srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer srv.Close() + + unrelatedPEM, _ := generateTestCert(t) + caPath := t.TempDir() + "/unrelated-ca.pem" + if err := os.WriteFile(caPath, []byte(unrelatedPEM), 0600); err != nil { + t.Fatalf("failed to write unrelated CA: %v", err) + } + + clientCert, clientKey := writeClientMTLS(t) + config := globalsign.Config{ + APIUrl: srv.URL, + APIKey: "gs-test-key", + APISecret: "gs-test-secret", + ClientCertPath: clientCert, + ClientKeyPath: clientKey, + ServerCAPath: caPath, + } + + connector := globalsign.New(&config, logger) + rawConfig, _ := json.Marshal(config) + err := connector.ValidateConfig(ctx, rawConfig) + if err == nil { + t.Fatal("ValidateConfig must fail when the server cert is not signed by the pinned CA") + } + // The failure must originate from TLS verification, not from any other path. + if !strings.Contains(err.Error(), "x509") && + !strings.Contains(err.Error(), "certificate") && + !strings.Contains(err.Error(), "unknown authority") { + t.Errorf("expected TLS verification error, got: %v", err) + } + t.Logf("Untrusted server cert correctly rejected: %v", err) + }) + + t.Run("ServerCAPath_MissingFile", func(t *testing.T) { + clientCert, clientKey := writeClientMTLS(t) + config := globalsign.Config{ + APIUrl: "https://example.invalid", + APIKey: "gs-test-key", + APISecret: "gs-test-secret", + ClientCertPath: clientCert, + ClientKeyPath: clientKey, + ServerCAPath: "/nonexistent/path/to/ca.pem", + } + + connector := globalsign.New(&config, logger) + rawConfig, _ := json.Marshal(config) + err := connector.ValidateConfig(ctx, rawConfig) + if err == nil { + t.Fatal("ValidateConfig must fail when ServerCAPath points to a missing file") + } + if !strings.Contains(err.Error(), "failed to read server CA bundle") { + t.Errorf("expected 'failed to read server CA bundle' error, got: %v", err) + } + t.Logf("Missing server CA file correctly rejected: %v", err) + }) + + t.Run("ServerCAPath_InvalidPEM", func(t *testing.T) { + clientCert, clientKey := writeClientMTLS(t) + badCAPath := t.TempDir() + "/garbage.pem" + if err := os.WriteFile(badCAPath, []byte("this is not a PEM certificate at all"), 0600); err != nil { + t.Fatalf("failed to write garbage file: %v", err) + } + + config := globalsign.Config{ + APIUrl: "https://example.invalid", + APIKey: "gs-test-key", + APISecret: "gs-test-secret", + ClientCertPath: clientCert, + ClientKeyPath: clientKey, + ServerCAPath: badCAPath, + } + + connector := globalsign.New(&config, logger) + rawConfig, _ := json.Marshal(config) + err := connector.ValidateConfig(ctx, rawConfig) + if err == nil { + t.Fatal("ValidateConfig must fail when ServerCAPath contains no valid PEM certificates") + } + if !strings.Contains(err.Error(), "no valid PEM certificates") { + t.Errorf("expected 'no valid PEM certificates' error, got: %v", err) + } + t.Logf("Invalid PEM correctly rejected: %v", err) + }) +} + // generateTestCert generates a self-signed test certificate and returns PEM strings. func generateTestCert(t *testing.T) (certPEM string, keyPEM string) { priv, err := rsa.GenerateKey(rand.Reader, 2048) diff --git a/internal/service/issuer.go b/internal/service/issuer.go index 56ee958..01646e9 100644 --- a/internal/service/issuer.go +++ b/internal/service/issuer.go @@ -577,17 +577,21 @@ func (s *IssuerService) buildEnvVarSeeds(cfg *config.Config) []*domain.Issuer { // Conditional: GlobalSign — only seed if API URL and API key are set if cfg.GlobalSign.APIUrl != "" && cfg.GlobalSign.APIKey != "" { + globalSignConfig := map[string]interface{}{ + "api_url": cfg.GlobalSign.APIUrl, + "api_key": cfg.GlobalSign.APIKey, + "api_secret": cfg.GlobalSign.APISecret, + "client_cert_path": cfg.GlobalSign.ClientCertPath, + "client_key_path": cfg.GlobalSign.ClientKeyPath, + } + if cfg.GlobalSign.ServerCAPath != "" { + globalSignConfig["server_ca_path"] = cfg.GlobalSign.ServerCAPath + } seeds = append(seeds, &domain.Issuer{ - ID: "iss-globalsign", - Name: "GlobalSign Atlas", - Type: domain.IssuerTypeGlobalSign, - Config: mustJSON(map[string]interface{}{ - "api_url": cfg.GlobalSign.APIUrl, - "api_key": cfg.GlobalSign.APIKey, - "api_secret": cfg.GlobalSign.APISecret, - "client_cert_path": cfg.GlobalSign.ClientCertPath, - "client_key_path": cfg.GlobalSign.ClientKeyPath, - }), + ID: "iss-globalsign", + Name: "GlobalSign Atlas", + Type: domain.IssuerTypeGlobalSign, + Config: mustJSON(globalSignConfig), Enabled: true, Source: "env", CreatedAt: now, diff --git a/web/src/config/issuerTypes.ts b/web/src/config/issuerTypes.ts index 773bdcf..977b63d 100644 --- a/web/src/config/issuerTypes.ts +++ b/web/src/config/issuerTypes.ts @@ -195,6 +195,7 @@ export const issuerTypes: IssuerTypeConfig[] = [ { key: 'api_secret', label: 'API Secret', placeholder: 'GlobalSign API secret', required: true, type: 'password', sensitive: true }, { key: 'client_cert_path', label: 'Client Certificate Path', placeholder: '/path/to/client.crt', required: true }, { key: 'client_key_path', label: 'Client Key Path', placeholder: '/path/to/client.key', required: true, sensitive: true }, + { key: 'server_ca_path', label: 'Server CA Path (optional)', placeholder: '/path/to/atlas-ca.pem', required: false }, ], }, {