Skip to content

Add support for ssl in v3 #1175

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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 file path
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 file path |



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 CA certificate", zap.Any("file_path", caCertLocation))
caCert, err := os.ReadFile(caCertLocation)
if err != nil {
s.settings.Logger.Error("Error starting NGINX stub status 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// Copyright (c) F5, Inc.
//
// This source code is licensed under the Apache License, Version 2.0 license found in the
// LICENSE file in the root directory of this source tree.

package stubstatus

import (
"context"
"crypto/tls"
"crypto/x509"
"net"
"net/http"
"net/http/httptest"
"os"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/component/componenttest"
"go.opentelemetry.io/collector/receiver/receivertest"

"github.com/nginx/agent/v3/internal/collector/nginxossreceiver/internal/config"
"github.com/nginx/agent/v3/test/helpers"
)

func TestStubStatusScraperTLS(t *testing.T) {
// Generate self-signed certificate using helper
keyBytes, certBytes := helpers.GenerateSelfSignedCert(t)

// Create a temporary directory for test files
tempDir := t.TempDir()

// Save certificate to a file
certFile := helpers.WriteCertFiles(t, tempDir, helpers.Cert{
Name: "server.crt",
Type: "CERTIFICATE",
Contents: certBytes,
})

// Parse the private key
key, err := x509.ParsePKCS1PrivateKey(keyBytes)
require.NoError(t, err)

// Create a TLS config with our self-signed certificate
tlsCert := tls.Certificate{
Certificate: [][]byte{certBytes},
PrivateKey: key,
}

serverTLSConfig := &tls.Config{
MinVersion: tls.VersionTLS13,
Certificates: []tls.Certificate{tlsCert},
}

// Create a test server with our custom TLS config
server := httptest.NewUnstartedServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can httptest.NewTLSServer be used here

if req.URL.Path == "/status" {
rw.WriteHeader(http.StatusOK)
_, _ = rw.Write([]byte(`Active connections: 291
server accepts handled requests
16630948 16630946 31070465
Reading: 6 Writing: 179 Waiting: 106
`))

return
}
rw.WriteHeader(http.StatusNotFound)
}))

server.TLS = serverTLSConfig
server.StartTLS()
defer server.Close()

// Test with TLS configuration using our self-signed certificate
t.Run("with self-signed TLS", func(t *testing.T) {
cfg, ok := config.CreateDefaultConfig().(*config.Config)
require.True(t, ok)

cfg.APIDetails.URL = server.URL + "/status"
// Use the self-signed certificate for verification
cfg.APIDetails.Ca = certFile

scraper := NewScraper(receivertest.NewNopSettings(component.Type{}), cfg)

startErr := scraper.Start(context.Background(), componenttest.NewNopHost())
require.NoError(t, startErr)

_, err = scraper.Scrape(context.Background())
assert.NoError(t, err, "Scraping with self-signed certificate should succeed")
})
}

func TestStubStatusScraperUnixSocket(t *testing.T) {
// Create a test server with a Unix domain socket
handler := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if req.URL.Path == "/status" {
rw.WriteHeader(http.StatusOK)
_, _ = rw.Write([]byte(`Active connections: 291
server accepts handled requests
16630948 16630946 31070465
Reading: 6 Writing: 179 Waiting: 106
`))

return
}
rw.WriteHeader(http.StatusNotFound)
})

// Create a socket file in a temporary directory with a shorter path
socketPath := "/tmp/nginx-test.sock"

// Clean up any existing socket file
os.Remove(socketPath)

// Create a listener for the Unix socket
listener, err := net.Listen("unix", socketPath)
require.NoError(t, err, "Failed to create Unix socket listener")

// Create a test server with our custom listener
server := &httptest.Server{
Listener: listener,
Config: &http.Server{Handler: handler},
}

// Start the server
server.Start()

// Ensure cleanup of the socket file
t.Cleanup(func() {
server.Close()
os.Remove(socketPath)
})

// Test with Unix socket
t.Run("with Unix socket", func(t *testing.T) {
cfg, ok := config.CreateDefaultConfig().(*config.Config)
require.True(t, ok)

cfg.APIDetails.URL = "http://unix/status"
cfg.APIDetails.Listen = "unix:" + socketPath

scraper := NewScraper(receivertest.NewNopSettings(component.Type{}), cfg)

startErr := scraper.Start(context.Background(), componenttest.NewNopHost())
require.NoError(t, startErr)

_, err = scraper.Scrape(context.Background())
assert.NoError(t, err)
})
}
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
25 changes: 25 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,28 @@ 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 CA certificate", zap.Any("file_path", caCertLocation))
caCert, err := os.ReadFile(caCertLocation)
if err != nil {
nps.logger.Error("Error starting NGINX stub status scraper. "+
"Failed to read CA certificate", 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
Loading