Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions api/grpc/mpi/v1/command.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions api/grpc/mpi/v1/command.pb.validate.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions api/grpc/mpi/v1/command.proto
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,8 @@ message APIDetails {
string location = 1;
// the API listen directive
string listen = 2;
// the API Ca directive
string Ca = 3;
}

// A set of runtime NGINX App Protect settings
Expand Down
1 change: 1 addition & 0 deletions docs/proto/protos.md
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,7 @@ Perform an associated API action on an instance
| ----- | ---- | ----- | ----------- |
| location | [string](#string) | | the API location directive |
| listen | [string](#string) | | the API listen directive |
| Ca | [string](#string) | | the API Ca directive |



Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type APIDetails struct {
URL string `mapstructure:"url"`
Listen string `mapstructure:"listen"`
Location string `mapstructure:"location"`
Ca string `mapstructure:"ca"`
}

type AccessLog struct {
Expand All @@ -56,6 +57,7 @@ func CreateDefaultConfig() component.Config {
URL: "http://localhost:80/status",
Listen: "localhost:80",
Location: "status",
Ca: "",
},
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ package stubstatus

import (
"context"
"crypto/tls"
"crypto/x509"
"net"
"net/http"
"os"
"strings"
"sync"
"time"
Expand Down Expand Up @@ -63,6 +66,28 @@ func (s *NginxStubStatusScraper) ID() component.ID {
func (s *NginxStubStatusScraper) Start(_ context.Context, _ component.Host) error {
s.logger.Info("Starting NGINX stub status scraper")
httpClient := http.DefaultClient
caCertLocation := s.cfg.APIDetails.Ca
if caCertLocation != "" {
s.settings.Logger.Debug("Reading from Location for Ca Cert : ", zap.Any(caCertLocation, caCertLocation))
caCert, err := os.ReadFile(caCertLocation)
if err != nil {
s.settings.Logger.Error("Error starting NGINX stub scraper. "+
"Failed to read CA certificate : ", zap.Error(err))

return nil
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)

httpClient = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: caCertPool,
MinVersion: tls.VersionTLS13,
},
},
}
}
httpClient.Timeout = s.cfg.ClientConfig.Timeout

if strings.HasPrefix(s.cfg.APIDetails.Listen, "unix:") {
Expand Down
2 changes: 2 additions & 0 deletions internal/collector/nginxplusreceiver/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type APIDetails struct {
URL string `mapstructure:"url"`
Listen string `mapstructure:"listen"`
Location string `mapstructure:"location"`
Ca string `mapstructure:"ca"`
}

// Validate checks if the receiver configuration is valid
Expand Down Expand Up @@ -59,6 +60,7 @@ func createDefaultConfig() component.Config {
URL: "http://localhost:80/api",
Listen: "localhost:80",
Location: "/api",
Ca: "",
},
MetricsBuilderConfig: metadata.DefaultMetricsBuilderConfig(),
}
Expand Down
23 changes: 23 additions & 0 deletions internal/collector/nginxplusreceiver/scraper.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ package nginxplusreceiver

import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"net"
"net/http"
"os"
"strconv"
"strings"
"sync"
Expand Down Expand Up @@ -82,6 +85,26 @@ func (nps *NginxPlusScraper) ID() component.ID {
func (nps *NginxPlusScraper) Start(_ context.Context, _ component.Host) error {
endpoint := strings.TrimPrefix(nps.cfg.APIDetails.URL, "unix:")
httpClient := http.DefaultClient
caCertLocation := nps.cfg.APIDetails.Ca
if caCertLocation != "" {
nps.logger.Debug("Reading from Location for Ca Cert : ", zap.Any(caCertLocation, caCertLocation))
caCert, err := os.ReadFile(caCertLocation)
if err != nil {
nps.logger.Error("Unable to start NGINX Plus scraper. Failed to read CA certificate: %v", zap.Error(err))
return err
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)

httpClient = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: caCertPool,
MinVersion: tls.VersionTLS13,
},
},
}
}
httpClient.Timeout = nps.cfg.ClientConfig.Timeout

if strings.HasPrefix(nps.cfg.APIDetails.Listen, "unix:") {
Expand Down
1 change: 1 addition & 0 deletions internal/collector/otel_collector_plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,7 @@ func (oc *Collector) checkForNewReceivers(ctx context.Context, nginxConfigContex
URL: nginxConfigContext.PlusAPI.URL,
Listen: nginxConfigContext.PlusAPI.Listen,
Location: nginxConfigContext.PlusAPI.Location,
Ca: nginxConfigContext.PlusAPI.Ca,
},
CollectionInterval: defaultCollectionInterval,
},
Expand Down
2 changes: 2 additions & 0 deletions internal/collector/otelcol.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ receivers:
url: "{{- .StubStatus.URL -}}"
listen: "{{- .StubStatus.Listen -}}"
location: "{{- .StubStatus.Location -}}"
ca: "{{- .StubStatus.Ca -}}"
{{- if .CollectionInterval }}
collection_interval: {{ .CollectionInterval }}
{{- end }}
Expand All @@ -98,6 +99,7 @@ receivers:
url: "{{- .PlusAPI.URL -}}"
listen: "{{- .PlusAPI.Listen -}}"
location: "{{- .PlusAPI.Location -}}"
ca: "{{- .PlusAPI.Ca -}}"
{{- if .CollectionInterval }}
collection_interval: {{ .CollectionInterval }}
{{- end }}
Expand Down
7 changes: 7 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,12 @@ func registerFlags() {
"Warning messages in the NGINX errors logs after a NGINX reload will be treated as an error.",
)

fs.String(
NginxApiTlsCa,
DefNginxApiTlsCa,
"The NGINX Plus CA certificate file location needed to call the NGINX Plus API if SSL is enabled.",
)

fs.StringSlice(
NginxExcludeLogsKey, []string{},
"A comma-separated list of one or more NGINX log paths that you want to exclude from metrics "+
Expand Down Expand Up @@ -786,6 +792,7 @@ func resolveDataPlaneConfig() *DataPlaneConfig {
ReloadMonitoringPeriod: viperInstance.GetDuration(NginxReloadMonitoringPeriodKey),
TreatWarningsAsErrors: viperInstance.GetBool(NginxTreatWarningsAsErrorsKey),
ExcludeLogs: viperInstance.GetStringSlice(NginxExcludeLogsKey),
ApiTls: TLSConfig{Ca: viperInstance.GetString(NginxApiTlsCa)},
},
}
}
Expand Down
1 change: 1 addition & 0 deletions internal/config/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const (
DefGracefulShutdownPeriod = 5 * time.Second
DefNginxReloadMonitoringPeriod = 10 * time.Second
DefTreatErrorsAsWarnings = false
DefNginxApiTlsCa = ""

DefCommandServerHostKey = ""
DefCommandServerPortKey = 0
Expand Down
1 change: 1 addition & 0 deletions internal/config/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ var (
NginxReloadMonitoringPeriodKey = pre(DataPlaneConfigRootKey, "nginx") + "reload_monitoring_period"
NginxTreatWarningsAsErrorsKey = pre(DataPlaneConfigRootKey, "nginx") + "treat_warnings_as_errors"
NginxExcludeLogsKey = pre(DataPlaneConfigRootKey, "nginx") + "exclude_logs"
NginxApiTlsCa = pre(DataPlaneConfigRootKey, "nginx") + "api_tls_ca"

FileWatcherMonitoringFrequencyKey = pre(FileWatcherKey) + "monitoring_frequency"
NginxExcludeFilesKey = pre(FileWatcherKey) + "exclude_files"
Expand Down
2 changes: 2 additions & 0 deletions internal/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ type (
}

NginxDataPlaneConfig struct {
ApiTls TLSConfig `yaml:"api_tls" mapstructure:"api_tls"`
ExcludeLogs []string `yaml:"exclude_logs" mapstructure:"exclude_logs"`
ReloadMonitoringPeriod time.Duration `yaml:"reload_monitoring_period" mapstructure:"reload_monitoring_period"`
TreatWarningsAsErrors bool `yaml:"treat_warnings_as_errors" mapstructure:"treat_warnings_as_errors"`
Expand Down Expand Up @@ -230,6 +231,7 @@ type (
URL string `yaml:"url" mapstructure:"url"`
Listen string `yaml:"listen" mapstructure:"listen"`
Location string `yaml:"location" mapstructure:"location"`
Ca string `yaml:"ca" mapstructure:"ca"`
}

AccessLog struct {
Expand Down
92 changes: 90 additions & 2 deletions internal/datasource/config/nginx_config_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ package config

import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"io"
Expand Down Expand Up @@ -526,7 +528,11 @@ func (ncp *NginxConfigParser) apiCallback(ctx context.Context, parent,
func (ncp *NginxConfigParser) pingAPIEndpoint(ctx context.Context, statusAPIDetail *model.APIDetails,
apiType string,
) bool {
httpClient := http.DefaultClient
httpClient, err := ncp.prepareHTTPClient(ctx)
if err != nil {
slog.ErrorContext(ctx, "Failed to prepare HTTP client", "error", err)
return false
}
listen := statusAPIDetail.Listen
statusAPI := statusAPIDetail.URL

Expand Down Expand Up @@ -592,6 +598,13 @@ func (ncp *NginxConfigParser) urlsForLocationDirectiveAPIDetails(
locationDirectiveName string,
) []*model.APIDetails {
var urls []*model.APIDetails
// Check if SSL is enabled in the server block
isSSL := ncp.isSSLEnabled(parent)
caCertLocation := ""
// If SSl is enabled, check if CA cert is provided and the location is allowed
if isSSL {
caCertLocation = ncp.getCACertLocation()
}
// process from the location block
if current.Directive != locationDirective {
return urls
Expand Down Expand Up @@ -619,12 +632,15 @@ func (ncp *NginxConfigParser) urlsForLocationDirectiveAPIDetails(
URL: fmt.Sprintf(format, path),
Listen: address,
Location: path,
Ca: caCertLocation,
})
} else {
urls = append(urls, &model.APIDetails{
URL: fmt.Sprintf(apiFormat, address, path),
URL: fmt.Sprintf("%s://%s%s", map[bool]string{true: "https", false: "http"}[isSSL],
address, path),
Listen: address,
Location: path,
Ca: caCertLocation,
})
}
}
Expand Down Expand Up @@ -724,6 +740,37 @@ func (ncp *NginxConfigParser) isPort(value string) bool {
return err == nil && port >= 1 && port <= 65535
}

// hasSSLArgument checks if any of the arguments contain "ssl".
func (ncp *NginxConfigParser) hasSSLArgument(args []string) bool {
for i := 1; i < len(args); i++ {
if args[i] == "ssl" {
return true
}
}

return false
}

// isSSLListenDirective checks if a directive is a listen directive with ssl enabled.
func (ncp *NginxConfigParser) isSSLListenDirective(dir *crossplane.Directive) bool {
return dir.Directive == "listen" && ncp.hasSSLArgument(dir.Args)
}

// isSSLEnabled checks if SSL is enabled for a given server block.
func (ncp *NginxConfigParser) isSSLEnabled(serverBlock *crossplane.Directive) bool {
if serverBlock == nil {
return false
}

for _, dir := range serverBlock.Block {
if ncp.isSSLListenDirective(dir) {
return true
}
}

return false
}

func (ncp *NginxConfigParser) socketClient(socketPath string) *http.Client {
return &http.Client{
Timeout: ncp.agentConfig.Client.Grpc.KeepAlive.Timeout,
Expand All @@ -735,6 +782,47 @@ func (ncp *NginxConfigParser) socketClient(socketPath string) *http.Client {
}
}

// New helper: prepareHTTPClient handles TLS config
func (ncp *NginxConfigParser) prepareHTTPClient(ctx context.Context) (*http.Client, error) {
httpClient := http.DefaultClient
caCertLocation := ncp.agentConfig.DataPlaneConfig.Nginx.ApiTls.Ca

if caCertLocation != "" && ncp.agentConfig.IsDirectoryAllowed(caCertLocation) {
slog.DebugContext(ctx, "Reading from Location for Ca Cert : ", "cacertlocation", caCertLocation)
caCert, err := os.ReadFile(caCertLocation)
if err != nil {
slog.ErrorContext(ctx, "Failed to read CA certificate", "error", err)
return nil, err
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)

httpClient = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: caCertPool,
MinVersion: tls.VersionTLS13,
},
},
}
}

return httpClient, nil
}

// New helper: Populate the CA cert location based ondirectory allowance.
func (ncp *NginxConfigParser) getCACertLocation() string {
caCertLocation := ncp.agentConfig.DataPlaneConfig.Nginx.ApiTls.Ca

if caCertLocation != "" && !ncp.agentConfig.IsDirectoryAllowed(caCertLocation) {
// If SSL is enabled but CA cert is provided and not allowed, treat it as if no CA cert
slog.Warn("CA certificate location is not allowed, treating as if no CA cert provided.")
return ""
}

return caCertLocation
}

func (ncp *NginxConfigParser) isDuplicateFile(nginxConfigContextFiles []*mpi.File, newFile *mpi.File) bool {
for _, nginxConfigContextFile := range nginxConfigContextFiles {
if nginxConfigContextFile.GetFileMeta().GetName() == newFile.GetFileMeta().GetName() {
Expand Down
Loading
Loading